`import defer`, six new subcommands (`deno transpile`, `deno pack`, `deno bump-version`, `deno ci`, `deno why`, `deno audit fix`), network debugging in Chrome DevTools, framework-aware `deno compile`, and 3.66x faster cold npm installs.
Deno Changelog
Why we needed an agent firewall that speaks more than HTTP.
Fresh 2.3: Zero JavaScript by default, View Transitions, and Temporal support
Fresh 2.3 is out, with over 100 commits from 20 contributors. This release makes the "zero JavaScript by default" promise actually hold, adds View Transitions support, pre-compiles middleware chains, and rounds out a long list of Vite integration fixes.
You can start a new project with:
deno create @fresh/init
or update an existing one with:
deno run -Ar jsr:@fresh/update
Zero JavaScript by default
Fresh has always said that pages ship no JavaScript unless they need to, but that wasn't strictly true. Every page ended up with a small client-entry script to bootstrap the island reviver and partials engine, even when neither was used.
Thanks to Jeroen Akkerman in #3696, Fresh now checks whether the page actually uses islands or partials (f-client-nav) before injecting anything. If it doesn't, the page ships with no <script> tag, no module preload headers, and no client-side bundle at all.
| Fresh 2.2 | Fresh 2.3 | |
|---|---|---|
| JavaScript (raw) | ~14–22 KB | 0 KB |
| JavaScript (gzip) | ~5–9 KB | 0 KB |
| Module preload headers | 1+ | 0 |
Inline boot <script> | 1 | 0 |
There is nothing to configure. Static pages will just stop shipping JavaScript after upgrading.
View Transitions
The View Transitions API lets browsers animate between DOM states natively. Fresh 2.3 wires it up to the existing partials system, so you can opt in by adding one attribute:
<body f-client-nav f-view-transition>
<!-- your app -->
</body>
Partial navigations will then be wrapped in document.startViewTransition() and you can customize the animation with regular CSS:
::view-transition-old(root) {
animation: fade-out 0.2s ease-in;
}
::view-transition-new(root) {
animation: fade-in 0.2s ease-out;
}
Or target individual elements:
.sidebar {
view-transition-name: sidebar;
}
View Transitions between two pages in a Fresh app.
Browsers without support fall back to normal partial updates. See the View Transitions docs for more.
First-class WebSocket support
Fresh now has built-in WebSocket support (#3774). The quickest way to add a WebSocket endpoint is app.ws():
main.ts
const app = new App()
.ws("/ws", {
open(socket) {
console.log("Client connected");
},
message(socket, event) {
socket.send(`Echo: ${event.data}`);
},
close(socket) {
console.log("Client disconnected");
},
});
For file-based routes, use ctx.upgrade() inside a GET handler. In managed mode, pass handlers and get the response back directly:
routes/api/ws.ts
export const handlers = define.handlers({
GET(ctx) {
return ctx.upgrade({
message(socket, event) {
socket.send(`Echo: ${event.data}`);
},
});
},
});
There's also a bare mode. Call ctx.upgrade() without arguments to get the raw WebSocket object, useful when you need to store sockets in a shared structure like a chat room:
routes/api/chat.ts
const clients = new Set<WebSocket>();
export const handlers = define.handlers({
GET(ctx) {
const { socket, response } = ctx.upgrade();
socket.onopen = () => clients.add(socket);
socket.onmessage = (event) => {
for (const client of clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(event.data);
}
}
};
socket.onclose = () => clients.delete(socket);
return response;
},
});
Non-WebSocket requests to a WebSocket route automatically get a 400 response. See the WebSocket documentation for the full API including idleTimeout and protocol options.
Vite integration improvements
A lot of this cycle went into making the Vite integration more robust, especially around npm package compatibility.
- CJS-to-ESM transforms and
process.envreplacements are now handled by Vite directly, so we could drop two Babel passes from the build. - CJS handling in SSR dev mode has been improved, React compat aliasing works properly now, and
resolve.aliasis applied before Deno resolution. Packages like Radix UI work out of the box. optimizeDeps.excludeis set up so Vite no longer creates a duplicate Preact instance during pre-bundling.- Vite asset URLs now include a cache-bust query param so immutable caching does the right thing (#3761).
- Temp files are ignored by the Vite watcher, so editor swap files no longer crash the dev server (#3763).
Work in this area is continuing. #3767 removes the rest of the Babel transforms (~2,050 lines) and lets Vite handle CJS packages natively end to end. That should land shortly after 2.3.
CSP nonces and IP filtering
Two new security middleware ship with this release.
CSP nonce injection (#3709) generates a unique nonce per request and adds it to every inline <script> and <style> tag. The corresponding Content-Security-Policy header uses 'nonce-{value}' instead of 'unsafe-inline', so only scripts and styles that Fresh rendered are allowed to execute.
main.ts
import { csp } from "fresh";
app.use(csp({ useNonce: true }));
User-supplied CSP directives now override the defaults rather than duplicating them (#3724). See the CSP documentation for the full list of options.
IP filter middleware (#3035, thanks to Octo8080X) adds built-in IP-based allow/deny lists with CIDR support:
main.ts
import { ipFilter } from "fresh";
app.use(ipFilter({
denyList: ["192.168.1.10"],
allowList: ["192.168.1.0/24"],
}));
See the IP filter documentation for custom response handling and more examples.
OpenTelemetry: server-to-browser trace propagation
Fresh already instruments middleware, route handlers, and rendering with OpenTelemetry spans. In 2.3, it also injects a W3C traceparent meta tag into the HTML response (#3729), so browser-side telemetry SDKs can connect client spans to the server trace.
Enable tracing with Deno's built-in OpenTelemetry support:
OTEL_DENO=true deno task start
Fresh will then automatically add the meta tag to every rendered page:
<meta
name="traceparent"
content="00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
/>
No code changes required. See the OpenTelemetry documentation for exporter configuration and the full list of instrumented spans.
Temporal API in islands
The Temporal API is landing in JavaScript engines, and Fresh now supports passing all eight Temporal types as island props:
Temporal.InstantTemporal.ZonedDateTimeTemporal.PlainDate,PlainTime,PlainDateTimeTemporal.PlainYearMonth,PlainMonthDayTemporal.Duration
You can pass a Temporal value from a route straight through to an island:
routes/event.tsx
export default function EventPage() {
const date = Temporal.PlainDate.from("2026-04-24");
return <Countdown target={date} />;
}
islands/Countdown.tsx
export default function Countdown(props: { target: Temporal.PlainDate }) {
const today = Temporal.Now.plainDateISO();
const days = today.until(props.target).days;
return <p>{days} days to go</p>;
}
Passing Temporal values from a route to an island.
Multiple static directories
The staticDir option now accepts an array (#3759). When the same filename exists in multiple directories, the first entry wins. This is useful when a build step generates assets into a separate directory and you want to keep those separate from hand-authored files.
vite.config.ts
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
export default defineConfig({
plugins: [
fresh({
staticDir: ["static", "generated"],
}),
],
});
See the static files documentation for more.
Loading indicators on form submissions
Loading indicators used to only work for link clicks. In 2.3 they work for form submissions too (#3753). Fresh checks the submitter element first (for example, the clicked button) and falls back to the form, so you can have per-button indicators when a form has multiple submit buttons:
import { useSignal } from "@preact/signals";
function MyForm() {
const saving = useSignal(false);
return (
<form action="/save" f-partial="/partials/save">
<button
type="submit"
ref={(el) => {
if (el) el._freshIndicator = saving;
}}
>
{saving.value ? "Saving..." : "Save"}
</button>
</form>
);
}
Per-button loading indicators on a form submission.
Reverse proxy support
Apps behind nginx, Caddy, or a cloud load balancer can now opt into trustProxy so that ctx.url reflects the actual client-facing URL (#3757):
const app = new App({ trustProxy: true });
With this enabled, Fresh reads the X-Forwarded-Proto and X-Forwarded-Host headers and rewrites ctx.url accordingly. If your proxy terminates TLS and forwards X-Forwarded-Proto: https, ctx.url.protocol will be https: instead of http:. See the reverse proxy documentation for details.
deno create support
With Deno 2.7+, you can scaffold a new Fresh project using deno create (#3706):
deno create @fresh/init
The old deno run -Ar jsr:@fresh/init form still works but now shows a deprecation warning.
Bug fixes
A non-exhaustive list of the most impactful fixes in this release:
HttpErroris now exposed for client-side code, with an optional message to keep bundle sizes down (#3080).- Routing: optional parameter routes no longer 404 (#2798), trailing slash mismatches no longer break static routes (#3721), middleware matches optional parameters in fs routing (#3726), and layouts apply correctly to index routes in programmatic routing (#3725).
- Partials: forms without an explicit
f-partialinsidef-client-navare no longer intercepted (#3722), search params are preserved through redirects (#3715), and data script tags are appended to<head>during partial navigation (#3720). - The
<Head>component now works correctly when rendered on the client (#3252). - Active links now consider query parameters and respect existing
aria-currentattributes (#3755). - Better error messages for missing exports in file routes (#3718), warnings instead of crashes on invalid HTML nesting around islands (#3762), and warnings for Partials with
append/prependmode missing akeyprop (#3738). - Pre-compiled middleware (#3104): middleware chains are now compiled once at startup instead of being assembled on every request.
- Windows: paths are normalized in generated snapshot and server entry files (#3727).
What's next
We're continuing to improve Vite support: upgrading to Vite 8 with Rolldown (#3760) and removing the remaining Babel transforms entirely (#3767).
The other big focus is build-time prerendering (#3766). Mark a route with prerender: true and Fresh renders it to static HTML during the build. Dynamic routes can enumerate their paths too, effectively turning Fresh into a static site generator for pages that don't need a server.
Follow along on GitHub.
Deno 2.7 stabilizes the Temporal API, adds Windows on ARM builds, npm overrides in package.json, brotli compression streams, self-extracting compiled binaries, deno create, and dozens of Node.js compatibility improvements.
Add structured logging, distributed traces, and game analytics to your Deno game and learn how to use Deno Deploy's built-in logs, traces, and metrics dashboards.
Capture player identities, add a customization modal, and persist those preferences via Oak + PostgreSQL.
Deno Deploy is now generally available, plus some highlights of new features and tools.
Instant Linux microVMs with defense-in-depth security for running untrusted code.
Building a leaderboard with database integration for our example browser-based game.
Adding obstacles, collision detection and game mechanics to our example browser-based game.
This series of posts will guide you through building a simple game using Deno. This post sets up the game loop, user controls and basic game physics.
A high severity Denial-of-Service (DoS) vulnerability has been found in React Server Components and Next.js. Deno has implemented mitigations in Deno Deploy. Immediate upgrades are required for other users.
This release includes dx for running package binaries, more granular permissions, source phase imports, faster type checking with tsgo, native source maps, deno audit, and much more.
This series of posts will guide you through building a simple game using Deno. Each post corresponds to a stage in the development process, gradually introducing new features and concepts.
A critical Remote Code Execution (RCE) vulnerability has been found in React Server Functions and Next.js. Deno has implemented mitigations in Deno Deploy. Immediate upgrades are required for other users.
Highlights from the new version of Deno Deploy.
Here’s a roundup of some of our popular open source libraries and how we use them in Deno.
Recent supply chain attacks on npm is a reminder that Node and npm grants unfettered access to your systems. Here's how Deno, with an opt-in security model, protects against these vulnerabilities.
Our legal battle over Oracle's claim on the word "JavaScript" is entering the discovery phase. Here's how you can help.
Simpler permission management with permission sets, new Deno.test APIs for setting up and tearing down test cases, specifying custom headers in WebSocket connections, runtime APIs in deno bundle, and more.
