























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:
or update an existing one with:
deno run -Ar jsr:@fresh/updateFresh 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.
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> </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; }
Browsers without support fall back to normal partial updates. See the View Transitions docs for more.
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.
A lot of this cycle went into making the Vite integration more robust, especially around npm package compatibility.
process.env replacements are now handled by Vite
directly, so we could drop two Babel passes from the build.resolve.alias is applied before Deno resolution. Packages
like Radix UI work out of the box.optimizeDeps.exclude is set up so Vite no longer creates a duplicate Preact
instance during pre-bundling.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.
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.
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:
terminal
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.
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.DurationYou 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>; }
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 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> ); }
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 supportWith Deno 2.7+, you can scaffold a new Fresh project using
deno create
(#3706):
The old deno run -Ar jsr:@fresh/init form still works but now shows a
deprecation warning.
A non-exhaustive list of the most impactful fixes in this release:
HttpError is now exposed for client-side code, with an optional message to
keep bundle sizes down (#3080).f-partial inside f-client-nav are no
longer intercepted (#3722),
search params are preserved through redirects
(#3715), and data script tags
are appended to <head> during partial navigation
(#3720).<Head> component now works correctly when rendered on the client
(#3252).aria-current
attributes (#3755).append/prepend mode missing a key prop
(#3738).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.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。