From c47329ec698a2f71421f29b4ed9353e33b2467b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 19:59:27 +0100
Subject: [PATCH 01/59] docs: remove TODO placeholders and fill in missing
content
Fill in documentation for `.route()`, `.onError()` route, and `.notFound()`
route examples in concepts/app.md. Replace TODO in plugins/index.md with a
list of built-in plugins.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/app.md | 29 +++++++++++++++++++++++++----
docs/latest/plugins/index.md | 9 ++++++++-
2 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/docs/latest/concepts/app.md b/docs/latest/concepts/app.md
index 5c9c405e661..51b44993279 100644
--- a/docs/latest/concepts/app.md
+++ b/docs/latest/concepts/app.md
@@ -257,7 +257,18 @@ app.fsRoutes("/foo/bar");
## `.route()`
-TODO
+Register a route with a component and optional handlers for data loading.
+
+```tsx
+app.route("/about", {
+ component: (ctx) =>
);
+});
+```
## `.mountApp()`
diff --git a/docs/latest/plugins/index.md b/docs/latest/plugins/index.md
index 02e0b9218a8..abdec2ff4f8 100644
--- a/docs/latest/plugins/index.md
+++ b/docs/latest/plugins/index.md
@@ -22,4 +22,11 @@ const addXFreshHeader = define.middleware(async (ctx) => {
Learn more about [middlewares](/docs/concepts/middleware).
-TODO: Show more ways
+## Built-in plugins
+
+Fresh ships with the following plugins:
+
+- [cors()](/docs/plugins/cors) - Set CORS HTTP headers
+- [csrf()](/docs/plugins/csrf) - CSRF protection
+- [csp()](/docs/plugins/csp) - Content Security Policy headers
+- [trailingSlashes()](/docs/plugins/trailing-slashes) - Enforce trailing slash behavior
From daead8bde78e1077f5a547207f6d737c2c2f7aaa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 19:59:41 +0100
Subject: [PATCH 02/59] docs: fix broken code examples
- Fix syntax error in concepts/layouts.md (missing parens around arrow fn params)
- Add missing `ctx` parameter in concepts/routing.md blog handler
- Fix wrong import `app` -> `App` in plugins/csrf.md
- Remove unused `target` prop from Countdown example in getting-started
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/layouts.md | 2 +-
docs/latest/concepts/routing.md | 2 +-
docs/latest/getting-started/index.md | 2 +-
docs/latest/plugins/csrf.md | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/latest/concepts/layouts.md b/docs/latest/concepts/layouts.md
index 53110a10ea7..f9dbe8bdb59 100644
--- a/docs/latest/concepts/layouts.md
+++ b/docs/latest/concepts/layouts.md
@@ -28,7 +28,7 @@ set by middleware is available via `props.state`.
```tsx routes/sub/_layout.tsx
import { define } from "../../utils.ts";
-export default define.layout({ Component, state }) => {
+export default define.layout(({ Component, state }) => {
// do something with state here
return (
diff --git a/docs/latest/concepts/routing.md b/docs/latest/concepts/routing.md
index da5f1850114..920dad57dd9 100644
--- a/docs/latest/concepts/routing.md
+++ b/docs/latest/concepts/routing.md
@@ -16,7 +16,7 @@ const app = new App()
const id = ctx.params.id;
return new Response(`Book id: ${id}`);
})
- .get("/blog/:post/comments", () => {
+ .get("/blog/:post/comments", (ctx) => {
// Responds to: GET /blog/my-post/comments, /blog/hello/comments, etc
const post = ctx.params.post;
return new Response(`Blog post comments for post: ${post}`);
diff --git a/docs/latest/getting-started/index.md b/docs/latest/getting-started/index.md
index 9b85c9c471f..5c03981a944 100644
--- a/docs/latest/getting-started/index.md
+++ b/docs/latest/getting-started/index.md
@@ -113,7 +113,7 @@ Create a new file at `islands/Countdown.tsx`
import { useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
-export function Countdown(props: { target: string }) {
+export function Countdown() {
const count = useSignal(10);
useEffect(() => {
diff --git a/docs/latest/plugins/csrf.md b/docs/latest/plugins/csrf.md
index 32613721f25..f89fe7b0e0f 100644
--- a/docs/latest/plugins/csrf.md
+++ b/docs/latest/plugins/csrf.md
@@ -14,7 +14,7 @@ header. to HTTP requests. These allow the server to indicate which origins
from.
```ts main.ts
-import { app, csrf } from "fresh";
+import { App, csrf } from "fresh";
const app = new App();
From f1fe5faadbcdd2f97164b106a60615a22f2198cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 19:59:56 +0100
Subject: [PATCH 03/59] docs: fix copy-pasted frontmatter descriptions
- concepts/app.md: was "Add a global app wrapper..." (from app-wrapper page)
- concepts/context.md: was "Plugins can add..." (from plugins page)
- advanced/environment-variables.md: was "Error pages can be used..." (from error-handling page)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/environment-variables.md | 2 +-
docs/latest/concepts/app.md | 2 +-
docs/latest/concepts/context.md | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/latest/advanced/environment-variables.md b/docs/latest/advanced/environment-variables.md
index 2d46eeaeaac..463fbf39aff 100644
--- a/docs/latest/advanced/environment-variables.md
+++ b/docs/latest/advanced/environment-variables.md
@@ -1,6 +1,6 @@
---
description: |
- Error pages can be used to customize the page that is shown when an error occurs in the application.
+ How to use environment variables in Fresh, including public variables that are inlined into island bundles.
---
Environment variables in Deno are typically read via `Deno.env.get()` or
diff --git a/docs/latest/concepts/app.md b/docs/latest/concepts/app.md
index 51b44993279..d908313819f 100644
--- a/docs/latest/concepts/app.md
+++ b/docs/latest/concepts/app.md
@@ -1,6 +1,6 @@
---
description: |
- Add a global app wrapper to provide common meta tags or context for application routes.
+ The App class is the heart of Fresh, used to define routes, middlewares, layouts and more.
---
The `App` class is the heart of Fresh and routes incoming requests to the
diff --git a/docs/latest/concepts/context.md b/docs/latest/concepts/context.md
index 473966611d4..a5c75636455 100644
--- a/docs/latest/concepts/context.md
+++ b/docs/latest/concepts/context.md
@@ -1,5 +1,5 @@
---
-description: Plugins can add new functionality to Fresh without requiring significant complexity.
+description: The Context object is shared across all middlewares and provides access to the request, URL, params, state, and response helpers.
---
The `Context` instance is shared across all middlewares in Fresh. Use it to
From 1b226394b6d48aae7d9fecd2f0870f392c064323 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:00:09 +0100
Subject: [PATCH 04/59] docs: fix CSRF page using CORS description
The CSRF plugin page had copy-pasted text describing CORS behavior
("which origins...is permitted to load resources from"). Replaced with
an accurate description of CSRF protection mechanics.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/plugins/csrf.md | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/docs/latest/plugins/csrf.md b/docs/latest/plugins/csrf.md
index f89fe7b0e0f..f4a6e522bba 100644
--- a/docs/latest/plugins/csrf.md
+++ b/docs/latest/plugins/csrf.md
@@ -2,16 +2,14 @@
description: "Prevent Cross-Site Request Forgery with this middleware"
---
-The `csrf()` middleware can be used to add safeguard against
+The `csrf()` middleware can be used to safeguard against
[Cross-Site Request Forgery vulnerabilities](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF).
-It checks if the user is allowed to load the requested URL based on the values
-in the
+It verifies that state-changing requests (POST, PUT, DELETE, etc.) originate
+from your own site by checking the
[`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site)
-header and
+and
[`Origin`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin)
-header. to HTTP requests. These allow the server to indicate which origins
-(domains, scheme or port) other than its own is permitted to load resources
-from.
+headers. Requests from untrusted origins are rejected.
```ts main.ts
import { App, csrf } from "fresh";
From 1fbed752b99144045327c224d2eceef5d9bf8444 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:01:07 +0100
Subject: [PATCH 05/59] docs: fix grammar errors and typos across documentation
- "it's subfolders" -> "its subfolders" (middleware.md)
- "other's like" -> "others like" (define.md)
- "than this is likely" -> "then this is likely" (troubleshooting.md)
- "as well has collect" -> "as well as collect" (deno-deploy.md)
- "the the correct" -> "the correct" (cloudflare-workers.md)
- "host name number" -> "host name" (deno-compile.md)
- "While this generally desired" -> "While this is generally desired" (rendering-raw-html.md)
- "If can't update" -> "If you can't update" (builder.md)
- Missing closing quotes in code comments (context.md, app.md)
- Image filename typo "cotuntdown" -> "countdown" (getting-started)
- "_layout_.tsx" -> "_layout.tsx" (layouts.md)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/builder.md | 2 +-
docs/latest/advanced/define.md | 2 +-
docs/latest/advanced/troubleshooting.md | 2 +-
docs/latest/concepts/app.md | 2 +-
docs/latest/concepts/context.md | 2 +-
docs/latest/concepts/layouts.md | 2 +-
docs/latest/concepts/middleware.md | 2 +-
docs/latest/deployment/cloudflare-workers.md | 2 +-
docs/latest/deployment/deno-compile.md | 2 +-
docs/latest/deployment/deno-deploy.md | 2 +-
docs/latest/examples/rendering-raw-html.md | 2 +-
docs/latest/getting-started/index.md | 2 +-
12 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/docs/latest/advanced/builder.md b/docs/latest/advanced/builder.md
index a4c4d5d7864..06cec0e3ac4 100644
--- a/docs/latest/advanced/builder.md
+++ b/docs/latest/advanced/builder.md
@@ -183,7 +183,7 @@ tailwind(builder, app, {
### Tailwindcss v3
-If can't update to the current version of tailwindcss we have a dedicated
+If you can't update to the current version of tailwindcss we have a dedicated
`@fresh/plugin-tailwindcss-v3` plugin that uses tailwindcss v3. That way you can
decided on your own when it's best to update to v4.
diff --git a/docs/latest/advanced/define.md b/docs/latest/advanced/define.md
index 2015ebfa9cc..73a9d2eefa2 100644
--- a/docs/latest/advanced/define.md
+++ b/docs/latest/advanced/define.md
@@ -5,7 +5,7 @@ description: |
Define helpers can be used to shorten the amount of types you have to type
yourself in code. They are entirely optional as some developers prefer the
-explicitness of types, other's like the convenience of `define.*` helpers.
+explicitness of types, others like the convenience of `define.*` helpers.
Without define helpers:
diff --git a/docs/latest/advanced/troubleshooting.md b/docs/latest/advanced/troubleshooting.md
index 93972cee73d..56058b316c0 100644
--- a/docs/latest/advanced/troubleshooting.md
+++ b/docs/latest/advanced/troubleshooting.md
@@ -55,7 +55,7 @@ anymore and you should use the relevant npm package directly from npm.
> [info]: Not using `esm.sh` solves many issues and footguns with duplicate
> Preact versions in your app. If you're seeing strange JavaScript errors in the
-> browser in your app than this is likely the cause.
+> browser in your app then this is likely the cause.
## Attach a debugger
diff --git a/docs/latest/concepts/app.md b/docs/latest/concepts/app.md
index d908313819f..e36ad5af8ae 100644
--- a/docs/latest/concepts/app.md
+++ b/docs/latest/concepts/app.md
@@ -28,7 +28,7 @@ const app = new App()
})
.get("/", () => new Response("hello"))
.use((ctx) => {
- // Will only be called for `/about
+ // Will only be called for `/about`
return ctx.next();
})
.get("/about", (ctx) => ctx.render(
About me
));
diff --git a/docs/latest/concepts/context.md b/docs/latest/concepts/context.md
index a5c75636455..726d08e78f4 100644
--- a/docs/latest/concepts/context.md
+++ b/docs/latest/concepts/context.md
@@ -56,7 +56,7 @@ matched.
```ts
app.get("/foo/:id", (ctx) => {
- console.log(ctx.route); // Logs: "/foo/:id
+ console.log(ctx.route); // Logs: "/foo/:id"
// ...
});
```
diff --git a/docs/latest/concepts/layouts.md b/docs/latest/concepts/layouts.md
index f9dbe8bdb59..cfb598eb128 100644
--- a/docs/latest/concepts/layouts.md
+++ b/docs/latest/concepts/layouts.md
@@ -68,7 +68,7 @@ structure like this:
```txt-files Project structure
└── /routes
├── sub
- │ ├── _layout_.tsx
+ │ ├── _layout.tsx
│ ├── special.tsx # should not inherit layouts
│ └── index.tsx
└── _layout.tsx
diff --git a/docs/latest/concepts/middleware.md b/docs/latest/concepts/middleware.md
index 3322f98d713..15535a8f7b1 100644
--- a/docs/latest/concepts/middleware.md
+++ b/docs/latest/concepts/middleware.md
@@ -58,7 +58,7 @@ Fresh ships with the following middlewares built-in:
## Filesystem-based middlewares
With file system based routing you can define a middleware in a `_middleware.ts`
-file inside the `routes/` folder or any of it's subfolders.
+file inside the `routes/` folder or any of its subfolders.
```ts routes/_middleware.ts
import { define } from "../utils.ts";
diff --git a/docs/latest/deployment/cloudflare-workers.md b/docs/latest/deployment/cloudflare-workers.md
index 9e70fe23a38..6fb901328f2 100644
--- a/docs/latest/deployment/cloudflare-workers.md
+++ b/docs/latest/deployment/cloudflare-workers.md
@@ -36,5 +36,5 @@ Check out the
[Cloudflare Documentation](https://developers.cloudflare.com/workers/vite-plugin/)
for further information.
-> [info]: Make sure that you set the the correct entrypoint in your
+> [info]: Make sure that you set the correct entrypoint in your
> `wrangler.jsonc` file. It should point to `"main": "./server.js"`
diff --git a/docs/latest/deployment/deno-compile.md b/docs/latest/deployment/deno-compile.md
index 45661e9fd00..5b116c7f171 100644
--- a/docs/latest/deployment/deno-compile.md
+++ b/docs/latest/deployment/deno-compile.md
@@ -17,4 +17,4 @@ deno compile --output my-app --include _fresh -A _fresh/compiled-entry.js
The compiled entry supports two environment variables out of the box:
- `PORT` to set the port number (`PORT=4000 my-app`)
-- `HOSTNAME` to set the host name number (`HOSTNAME=0.0.0.0 my-app`)
+- `HOSTNAME` to set the host name (`HOSTNAME=0.0.0.0 my-app`)
diff --git a/docs/latest/deployment/deno-deploy.md b/docs/latest/deployment/deno-deploy.md
index dce3d4e0468..e8b3881a2f7 100644
--- a/docs/latest/deployment/deno-deploy.md
+++ b/docs/latest/deployment/deno-deploy.md
@@ -4,7 +4,7 @@ description: "Deploy Fresh on Deno Deploy"
The recommended way to deploy Fresh is by using
[Deno Deploy](https://deno.com/deploy). It will automatically create branch
-previews for pull requests, collect request and HTTP metrics, as well has
+previews for pull requests, collect request and HTTP metrics, as well as
collect traces for you out of the box.
1. Log in to [Deno Deploy](https://deno.com/deploy)
diff --git a/docs/latest/examples/rendering-raw-html.md b/docs/latest/examples/rendering-raw-html.md
index e4ab9643b35..fbd107bc1f0 100644
--- a/docs/latest/examples/rendering-raw-html.md
+++ b/docs/latest/examples/rendering-raw-html.md
@@ -4,7 +4,7 @@ description: |
---
Text content in Fresh is always escaped, whether serverside rendered or rendered
-in islands. While this generally desired, it can create issues in certain
+in islands. While this is generally desired, it can create issues in certain
situations.
To address this you can render raw HTML via Preact's `dangerouslySetInnerHTML`
diff --git a/docs/latest/getting-started/index.md b/docs/latest/getting-started/index.md
index 5c03981a944..db7465a2fc5 100644
--- a/docs/latest/getting-started/index.md
+++ b/docs/latest/getting-started/index.md
@@ -155,4 +155,4 @@ export default define.page(() => {
Now, we can see our countdown in action:
-
+
From 066622cc6710867fdeda09c33b5a24a563f481fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:01:21 +0100
Subject: [PATCH 06/59] docs: add navigation links to advanced index page
The advanced section index was a single sentence with no links to its
sub-pages. Added a complete list of all advanced topics with brief
descriptions.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/index.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/docs/latest/advanced/index.md b/docs/latest/advanced/index.md
index f94ceab7521..5ae2b1aae6a 100644
--- a/docs/latest/advanced/index.md
+++ b/docs/latest/advanced/index.md
@@ -4,3 +4,15 @@ description: |
---
This section of the documentation describes advanced functionality of Fresh.
+
+- [App wrapper](/docs/advanced/app-wrapper) - Customize the outer HTML structure
+- [Layouts](/docs/advanced/layouts) - Define programmatic layouts
+- [Error handling](/docs/advanced/error-handling) - Custom error and 404 pages
+- [Partials](/docs/advanced/partials) - Client-side partial page updates
+- [Forms](/docs/advanced/forms) - Handle form submissions
+- [Define helpers](/docs/advanced/define) - Type-safe route and middleware helpers
+- [Environment variables](/docs/advanced/environment-variables) - Use env vars in islands
+- [Modifying <head>](/docs/advanced/head) - Manage meta tags, titles, and styles
+- [Vite plugin options](/docs/advanced/vite) - Configure the Fresh Vite plugin
+- [Troubleshooting](/docs/advanced/troubleshooting) - Common issues and solutions
+- [Builder (Legacy)](/docs/advanced/builder) - Pre-Vite build system
From d08b2935c03829c2f2b6603589996e78e0491e2b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:06:25 +0100
Subject: [PATCH 07/59] docs: expand routing page with priority, methods, and
file handlers
The routing page was ~100 words with only basic examples (one broken).
Now covers route matching priority (static-first, then registration
order), HTTP method handlers, HEAD fallback behavior, and file-based
route handler patterns.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/routing.md | 70 ++++++++++++++++++++++++++++++++-
1 file changed, 69 insertions(+), 1 deletion(-)
diff --git a/docs/latest/concepts/routing.md b/docs/latest/concepts/routing.md
index 920dad57dd9..d232a8b6812 100644
--- a/docs/latest/concepts/routing.md
+++ b/docs/latest/concepts/routing.md
@@ -1,6 +1,6 @@
---
description: |
- File based routing is the simplest way to do routing in Fresh apps. Additionally custom patterns can be configured per route.
+ How routing works in Fresh, including route patterns, matching priority, method-specific handlers, and URLPattern support.
---
Routing defines which middlewares and routes should respond to a particular
@@ -30,3 +30,71 @@ const app = new App()
Fresh supports the full
[`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API)
syntax for setting pathnames.
+
+## Route matching priority
+
+Routes are matched in the following order:
+
+1. **Static routes** (exact path match like `/about`) are checked first and
+ always take precedence.
+2. **Dynamic routes** (patterns like `/posts/:id`) are checked in the order they
+ were registered. The first matching route wins.
+
+This means the registration order matters for dynamic routes:
+
+```ts main.ts
+const app = new App()
+ // This is checked first since it's registered first
+ .get("/posts/featured", () => new Response("Featured posts"))
+ // This is checked second — won't match "/posts/featured" because it's
+ // already handled above
+ .get("/posts/:id", (ctx) => new Response(`Post: ${ctx.params.id}`));
+```
+
+## HTTP method handlers
+
+Fresh provides method-specific route registration via `.get()`, `.post()`,
+`.put()`, `.delete()`, `.head()`, `.patch()`, and `.options()`. Each method
+only responds to its matching HTTP verb.
+
+Use `.all()` to respond to any HTTP method:
+
+```ts main.ts
+app.all("/api/health", () => new Response("ok"));
+```
+
+If a route is registered for `GET` but receives a `POST` request, Fresh returns
+a `405 Method Not Allowed` response. `HEAD` requests automatically fall back to
+the `GET` handler if no dedicated `HEAD` handler is defined.
+
+## File-based route handlers
+
+In file-based routes, export a `handlers` object with method-specific functions:
+
+```ts routes/api/users.ts
+import { define } from "@/utils.ts";
+
+export const handlers = define.handlers({
+ GET(ctx) {
+ return new Response(JSON.stringify({ users: [] }), {
+ headers: { "Content-Type": "application/json" },
+ });
+ },
+ POST(ctx) {
+ return new Response("Created", { status: 201 });
+ },
+});
+```
+
+To handle all methods, export a single function instead:
+
+```ts routes/api/health.ts
+import { define } from "@/utils.ts";
+
+export const handlers = define.handlers((ctx) => {
+ return new Response(`Received a ${ctx.req.method} request`);
+});
+```
+
+See [File routing](/docs/concepts/file-routing) for more on the file-based
+routing convention.
From 12931a1ac67674674fa53319461727fa7fc145fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:06:49 +0100
Subject: [PATCH 08/59] docs: expand Vite plugin page with HMR, plugin
composition, and internals
The Vite page was ~100 words showing only the config options. Now explains
what the plugin does (JSX, aliasing, HMR, code splitting), how to add
other Vite plugins alongside Fresh, how HMR works, and debugging tips.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/vite.md | 79 ++++++++++++++++++++++++++++++++++--
1 file changed, 76 insertions(+), 3 deletions(-)
diff --git a/docs/latest/advanced/vite.md b/docs/latest/advanced/vite.md
index 8cf003129a8..e3abd987cc5 100644
--- a/docs/latest/advanced/vite.md
+++ b/docs/latest/advanced/vite.md
@@ -1,12 +1,18 @@
---
description: |
- Configure the fresh vite plugin.
+ Configure the Fresh Vite plugin, add other Vite plugins, and understand how Fresh integrates with Vite.
---
-The Fresh vite plugin can be optionally configured in the following ways:
+Fresh 2 uses [Vite](https://vite.dev/) for development and production builds.
+The Fresh Vite plugin handles JSX configuration, Hot Module Replacement (HMR),
+island discovery, client/server code splitting, and React-to-Preact aliasing.
+
+## Configuration
+
+The Fresh Vite plugin can be configured in `vite.config.ts`:
```ts vite.config.ts
-import { defineConfig, type Plugin } from "vite";
+import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
export default defineConfig({
@@ -30,3 +36,70 @@ export default defineConfig({
],
});
```
+
+## Adding other Vite plugins
+
+You can use any Vite-compatible plugin alongside Fresh. The Fresh plugin should
+generally come first:
+
+```ts vite.config.ts
+import { defineConfig } from "vite";
+import { fresh } from "@fresh/plugin-vite";
+import tailwindcss from "@tailwindcss/vite";
+
+export default defineConfig({
+ plugins: [
+ fresh(),
+ tailwindcss(),
+ // Add any other Vite plugins here
+ ],
+});
+```
+
+## What the plugin does
+
+Behind the scenes, the Fresh Vite plugin:
+
+- **Configures JSX** for Preact automatically (`jsxImportSource: "preact"`)
+- **Aliases React to Preact** so npm packages that depend on React work out of
+ the box
+- **Enables HMR** via [Prefresh](https://github.com/preactjs/prefresh) for fast
+ component reloading during development
+- **Discovers islands** by scanning the islands directory and any
+ `islandSpecifiers`
+- **Builds separate client and server bundles** using Vite's Environments
+ feature
+- **Generates a server entry** (`_fresh/server.js`) for production deployment
+- **Validates imports** to catch mistakes like importing Node.js-only modules in
+ browser code
+
+## Hot Module Replacement
+
+During development (`deno task dev`), the Fresh Vite plugin enables HMR so that
+changes to components, islands, and CSS are reflected in the browser instantly
+without a full page reload. This is powered by Prefresh, Preact's fast refresh
+implementation.
+
+## Debugging
+
+To debug Vite resolution issues, run Vite with the `--debug` flag:
+
+```sh Terminal
+deno run -A npm:vite --debug
+```
+
+To inspect plugin transformations, use
+[`vite-plugin-inspect`](https://github.com/antfu-collective/vite-plugin-inspect):
+
+```ts vite.config.ts
+import { defineConfig } from "vite";
+import { fresh } from "@fresh/plugin-vite";
+import inspect from "vite-plugin-inspect";
+
+export default defineConfig({
+ plugins: [
+ fresh(),
+ inspect(), // Opens a UI at /__inspect to view all transformations
+ ],
+});
+```
From 36d3d2fd479dbf7cc21852e6001803b0fcdb9cc0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:07:29 +0100
Subject: [PATCH 09/59] docs: add Signals concept page
Signals are a core reactivity feature with zero dedicated documentation.
New page covers: creating signals with useSignal, computed signals,
passing signals as island props, shared state via module-level signals,
and serialization behavior.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/signals.md | 133 ++++++++++++++++++++++++++++++++
docs/toc.ts | 1 +
2 files changed, 134 insertions(+)
create mode 100644 docs/latest/concepts/signals.md
diff --git a/docs/latest/concepts/signals.md b/docs/latest/concepts/signals.md
new file mode 100644
index 00000000000..6ea1174c4d9
--- /dev/null
+++ b/docs/latest/concepts/signals.md
@@ -0,0 +1,133 @@
+---
+description: |
+ Signals provide reactive state management in Fresh islands using @preact/signals.
+---
+
+[Signals](https://preactjs.com/guide/v10/signals/) are Preact's reactive
+primitive for managing state in islands. When a signal's value changes, any
+component that reads it re-renders automatically — no need for `setState` or
+manual subscriptions.
+
+## Creating signals
+
+Use `useSignal` inside a component for local state, or `signal` at module level
+for shared state:
+
+```tsx islands/Counter.tsx
+import { useSignal } from "@preact/signals";
+
+export default function Counter() {
+ const count = useSignal(0);
+
+ return (
+
+
Count: {count}
+
+
+ );
+}
+```
+
+> [info]: Signals can be rendered directly in JSX (`{count}`) without accessing
+> `.value`. Preact detects the signal and subscribes to updates automatically.
+
+## Computed signals
+
+Use `computed` to derive values from other signals. Computed signals update
+automatically when their dependencies change:
+
+```tsx islands/TemperatureConverter.tsx
+import { useSignal, useComputed } from "@preact/signals";
+
+export default function TemperatureConverter() {
+ const celsius = useSignal(20);
+ const fahrenheit = useComputed(() => celsius.value * 9 / 5 + 32);
+
+ return (
+
+ );
+}
+```
+
+## Passing signals as props
+
+Signals can be passed as props to islands. Fresh automatically serializes them
+on the server and reconstructs them as live signals on the client:
+
+```tsx routes/index.tsx
+import { useSignal } from "@preact/signals";
+import Slider from "@/islands/Slider.tsx";
+
+export default function Home() {
+ const value = useSignal(50);
+ return (
+
+
+
+
+ );
+}
+```
+
+Both sliders share the same signal — moving one updates the other. When the
+same signal object is passed to multiple islands, Fresh preserves the reference
+so they stay synchronized.
+
+## Shared state across islands
+
+For state that needs to be shared between unrelated islands, create a signal in
+a separate module:
+
+```ts utils/cart.ts
+import { signal } from "@preact/signals";
+
+export const cart = signal([]);
+```
+
+```tsx islands/AddToCart.tsx
+import { cart } from "@/utils/cart.ts";
+
+export default function AddToCart(props: { product: string }) {
+ return (
+
+ );
+}
+```
+
+```tsx islands/CartCount.tsx
+import { cart } from "@/utils/cart.ts";
+
+export default function CartCount() {
+ return Items in cart: {cart.value.length};
+}
+```
+
+Since both islands import the same module-level signal, they share the same
+state automatically. See
+[Sharing state between islands](/docs/examples/sharing-state-between-islands)
+for more patterns.
+
+## Serialization
+
+When signals are passed as island props, Fresh handles serialization
+automatically:
+
+- The signal's current value is extracted on the server via `.peek()`
+- On the client, the value is wrapped back into a live `signal()` or
+ `computed()`
+- Circular references and duplicate signal references are preserved
+
+The signal's inner value must itself be serializable (see
+[Islands — Passing props](/docs/concepts/islands#passing-props-to-islands) for
+the full list of supported types).
diff --git a/docs/toc.ts b/docs/toc.ts
index 2decfcabf1d..9524a33ccb2 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -40,6 +40,7 @@ const toc: RawTableOfContents = {
["routing", "Routing", "link:latest"],
["islands", "Islands", "link:latest"],
+ ["signals", "Signals", "link:latest"],
["static-files", "Static files", "link:latest"],
["file-routing", "File routing", "link:latest"],
From be611a3442001d18233f7add1667f9ed2ad2aa75 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:07:51 +0100
Subject: [PATCH 10/59] docs: add API Routes example page
Handler-only routes (JSON APIs) are a very common pattern with no
documentation. New page covers: basic JSON responses, method-specific
handlers with 405 fallback, catch-all handlers, and programmatic routes.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/examples/api-routes.md | 81 ++++++++++++++++++++++++++++++
docs/toc.ts | 1 +
2 files changed, 82 insertions(+)
create mode 100644 docs/latest/examples/api-routes.md
diff --git a/docs/latest/examples/api-routes.md b/docs/latest/examples/api-routes.md
new file mode 100644
index 00000000000..15e4cc32e12
--- /dev/null
+++ b/docs/latest/examples/api-routes.md
@@ -0,0 +1,81 @@
+---
+description: |
+ Create JSON API endpoints by defining handler-only routes without a page component.
+---
+
+A route that exports only `handlers` (no default component export) becomes an
+API endpoint — it returns responses directly instead of rendering HTML.
+
+## Basic JSON API
+
+```ts routes/api/users.ts
+import { define } from "@/utils.ts";
+
+export const handlers = define.handlers({
+ GET(ctx) {
+ const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
+ return Response.json(users);
+ },
+});
+```
+
+A `GET /api/users` request returns:
+
+```json
+[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
+```
+
+## Method-specific handlers
+
+Define different logic per HTTP method. Methods you don't define will
+automatically return `405 Method Not Allowed`:
+
+```ts routes/api/posts/[id].ts
+import { define } from "@/utils.ts";
+
+export const handlers = define.handlers({
+ async GET(ctx) {
+ const post = await db.posts.find(ctx.params.id);
+ if (!post) {
+ return Response.json({ error: "Not found" }, { status: 404 });
+ }
+ return Response.json(post);
+ },
+
+ async PUT(ctx) {
+ const body = await ctx.req.json();
+ const post = await db.posts.update(ctx.params.id, body);
+ return Response.json(post);
+ },
+
+ async DELETE(ctx) {
+ await db.posts.delete(ctx.params.id);
+ return new Response(null, { status: 204 });
+ },
+});
+```
+
+## Catch-all handler
+
+Export a single function instead of a method object to handle all HTTP methods:
+
+```ts routes/api/health.ts
+import { define } from "@/utils.ts";
+
+export const handlers = define.handlers((ctx) => {
+ return Response.json({ status: "ok", method: ctx.req.method });
+});
+```
+
+## Programmatic API routes
+
+API routes can also be defined directly on the app without file-based routing:
+
+```ts main.ts
+const app = new App()
+ .get("/api/time", () => Response.json({ time: new Date().toISOString() }))
+ .post("/api/echo", async (ctx) => {
+ const body = await ctx.req.text();
+ return new Response(body);
+ });
+```
diff --git a/docs/toc.ts b/docs/toc.ts
index 9524a33ccb2..25ef0ff360c 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -91,6 +91,7 @@ const toc: RawTableOfContents = {
title: "Examples",
link: "latest",
pages: [
+ ["api-routes", "API Routes", "link:latest"],
["migration-guide", "Migration Guide", "link:latest"],
["daisyui", "daisyUI", "link:latest"],
["markdown", "Rendering Markdown", "link:latest"],
From 04aceb6beb2ea96257993b19e0f8be8b771b5509 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:08:05 +0100
Subject: [PATCH 11/59] docs: cross-link the two Layouts pages
There are two layout docs (file-based in concepts/, programmatic in
advanced/) with no cross-references. Added a note at the top of each
page pointing to the other with a clear distinction.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/layouts.md | 4 ++++
docs/latest/concepts/layouts.md | 4 ++++
2 files changed, 8 insertions(+)
diff --git a/docs/latest/advanced/layouts.md b/docs/latest/advanced/layouts.md
index 67e3050ebef..fa043a6a956 100644
--- a/docs/latest/advanced/layouts.md
+++ b/docs/latest/advanced/layouts.md
@@ -2,6 +2,10 @@
description: "Create re-usable layouts across routes"
---
+This page covers **programmatic layouts** defined via `app.layout()`. If you're
+using file-based routing, see [Layouts (file-based)](/docs/concepts/layouts)
+instead.
+
Layouts are plain Preact components that are inherited based on the matching
pattern. When you have a section on your site where all pages share the same
HTML structure and only the content changes, a layout is a neat way to abstract
diff --git a/docs/latest/concepts/layouts.md b/docs/latest/concepts/layouts.md
index cfb598eb128..78d497bbfca 100644
--- a/docs/latest/concepts/layouts.md
+++ b/docs/latest/concepts/layouts.md
@@ -3,6 +3,10 @@ description: |
Add a layout to provide common meta tags, context for application sub routes, and common layout.
---
+This page covers **file-based layouts** using `_layout.tsx` files. If you're
+defining routes programmatically with `new App()`, see
+[Layouts (programmatic)](/docs/advanced/layouts) instead.
+
A layout is defined in a `_layout.tsx` file in any sub directory (at any level)
under the `routes/` folder. It must contain a default export that is a regular
Preact component. Only one such layout is allowed per sub directory.
From ecb72051b7bfd368779b7fddc6d5b3e7b4d7460f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:08:22 +0100
Subject: [PATCH 12/59] docs: resolve contradictory build commands in
deployment page
The deployment index listed both 'deno task build' and the legacy
'deno run -A dev.ts build'. The latter was removed by the Fresh 2
migration (dev.ts is replaced by vite.config.ts). Now shows only
the current command with a note linking to the migration guide.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/deployment/index.md | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/docs/latest/deployment/index.md b/docs/latest/deployment/index.md
index 37955455f50..59d7780595f 100644
--- a/docs/latest/deployment/index.md
+++ b/docs/latest/deployment/index.md
@@ -7,10 +7,12 @@ assets for consumption in the browser. This step can be invoked by running:
```sh Terminal
deno task build
-# or
-deno run -A dev.ts build
```
+> [info]: This runs `vite build` under the hood. If you're migrating from
+> Fresh 1.x and still have a `dev.ts` file, see the
+> [migration guide](/docs/examples/migration-guide) for updating your tasks.
+
Once completed, it will have created a `_fresh` folder in the project directory
which contains the optimized assets.
@@ -28,8 +30,8 @@ To run Fresh in production mode, run the `start` task:
```sh Terminal
deno task start
-# or
-deno serve -A _fresh/server.js
```
-Fresh will automatically pick up the optimized assets in the `_fresh` directory.
+This runs `deno serve -A _fresh/server.js`, which serves the built assets
+directly. Fresh will automatically pick up the optimized assets in the `_fresh`
+directory.
From 8731665b7beaf973ebe24828c2d7038374d96b52 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:08:52 +0100
Subject: [PATCH 13/59] docs: expand Deno Deploy and deno compile deployment
pages
Deno Deploy page was 4 numbered steps (~70 words). Now covers build step
configuration, environment variables, custom domains, and troubleshooting.
deno compile page was ~80 words. Now covers cross-compilation, the
--include flag, and known limitations.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/deployment/deno-compile.md | 35 +++++++++++++++++---
docs/latest/deployment/deno-deploy.md | 45 ++++++++++++++++++++++++++
2 files changed, 76 insertions(+), 4 deletions(-)
diff --git a/docs/latest/deployment/deno-compile.md b/docs/latest/deployment/deno-compile.md
index 5b116c7f171..a321e8e0e9f 100644
--- a/docs/latest/deployment/deno-compile.md
+++ b/docs/latest/deployment/deno-compile.md
@@ -7,14 +7,41 @@ You can create a self-contained executable out of your app with the
It will include all assets and dependencies. This executable can run on any
platform without requiring Deno to be installed.
-```sh
+## Building the executable
+
+```sh Terminal
# Build your app first
-$ deno task build
+deno task build
# Generate self-contained executable
deno compile --output my-app --include _fresh -A _fresh/compiled-entry.js
```
+The `--include _fresh` flag ensures that all built assets (JavaScript bundles,
+CSS, static files) are embedded in the binary.
+
+## Configuration
+
The compiled entry supports two environment variables out of the box:
-- `PORT` to set the port number (`PORT=4000 my-app`)
-- `HOSTNAME` to set the host name (`HOSTNAME=0.0.0.0 my-app`)
+- `PORT` to set the port number (`PORT=4000 ./my-app`)
+- `HOSTNAME` to set the host name (`HOSTNAME=0.0.0.0 ./my-app`)
+
+## Cross-compilation
+
+You can compile for a different platform using the `--target` flag:
+
+```sh Terminal
+deno compile --target x86_64-unknown-linux-gnu --output my-app --include _fresh -A _fresh/compiled-entry.js
+```
+
+See the
+[`deno compile` documentation](https://docs.deno.com/runtime/reference/cli/compile/)
+for a full list of supported targets.
+
+## Limitations
+
+- The executable size includes the Deno runtime (~50-130 MB depending on
+ platform)
+- Dynamic imports that aren't statically analyzable may not be included
+- Native npm packages with platform-specific binaries need to match the target
+ platform
diff --git a/docs/latest/deployment/deno-deploy.md b/docs/latest/deployment/deno-deploy.md
index e8b3881a2f7..601b48b04cf 100644
--- a/docs/latest/deployment/deno-deploy.md
+++ b/docs/latest/deployment/deno-deploy.md
@@ -7,6 +7,8 @@ The recommended way to deploy Fresh is by using
previews for pull requests, collect request and HTTP metrics, as well as
collect traces for you out of the box.
+## Setup
+
1. Log in to [Deno Deploy](https://deno.com/deploy)
1. Create a new app
1. Link your GitHub repository
@@ -14,3 +16,46 @@ collect traces for you out of the box.
Every time you merge into the `main` branch a new production deployment will be
created.
+
+## Build step
+
+Deno Deploy runs `deno task build` automatically during deployment when the
+Fresh preset is selected. Make sure your `deno.json` has the correct build task:
+
+```json deno.json
+{
+ "tasks": {
+ "build": "vite build",
+ "start": "deno serve -A _fresh/server.js"
+ }
+}
+```
+
+## Environment variables
+
+You can set environment variables in the Deno Deploy dashboard under your
+project's **Settings > Environment Variables** section. These are available at
+runtime via `Deno.env.get()`.
+
+For variables that need to be available in island code (client-side), prefix
+them with `FRESH_PUBLIC_` — see
+[Environment Variables](/docs/advanced/environment-variables).
+
+## Custom domains
+
+Custom domains can be configured in the Deno Deploy dashboard under your
+project's **Settings > Domains** section. Deno Deploy automatically provisions
+TLS certificates for your domains.
+
+## Troubleshooting
+
+If your deployment fails to start:
+
+1. Ensure `deno task build` has been run (check that the Fresh preset is
+ selected)
+2. Verify your entry point is `_fresh/server.js`, not `main.ts` — Fresh 2
+ generates the server entry during the build step
+3. Check the deployment logs in the Deno Deploy dashboard for specific errors
+
+See the [Deno Deploy documentation](https://docs.deno.com/deploy/manual/) for
+more details.
From 6398da6c27dfcd00ab4fc4728c1e49da8353ac13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:25:36 +0100
Subject: [PATCH 14/59] docs: add Data Fetching concept page
Fresh 1.x had a dedicated data fetching page that was dropped in 2.x.
New page covers: handler-to-component data flow with page(), type-safe
data passing with define.handlers/define.page, async page components,
middleware state, response customization, and a PageProps reference table.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/data-fetching.md | 123 ++++++++++++++++++++++++++
docs/toc.ts | 1 +
2 files changed, 124 insertions(+)
create mode 100644 docs/latest/concepts/data-fetching.md
diff --git a/docs/latest/concepts/data-fetching.md b/docs/latest/concepts/data-fetching.md
new file mode 100644
index 00000000000..ca9e75f67c2
--- /dev/null
+++ b/docs/latest/concepts/data-fetching.md
@@ -0,0 +1,123 @@
+---
+description: |
+ Load data on the server in handlers and pass it to page components with full type safety.
+---
+
+Data fetching in Fresh happens on the server. Handlers load data and pass it to
+page components via the `page()` helper. This keeps API keys, database
+connections, and sensitive logic out of the browser.
+
+## Handlers and page components
+
+A handler fetches data and returns it with `page()`. The page component receives
+it in `props.data`:
+
+```tsx routes/projects/[id].tsx
+import { page } from "fresh";
+import { define } from "@/utils.ts";
+
+interface Data {
+ project: { name: string; stars: number };
+}
+
+export const handlers = define.handlers({
+ async GET(ctx) {
+ const project = await db.projects.findOne(ctx.params.id);
+ if (!project) {
+ throw new HttpError(404);
+ }
+ return page({ project });
+ },
+});
+
+export default define.page(({ data }) => {
+ return (
+
+
{data.project.name}
+
{data.project.stars} stars
+
+ );
+});
+```
+
+The `define.page` generic links the handler's return type to
+the component's props, giving you full autocompletion on `data`.
+
+## Setting response headers and status
+
+Pass options to `page()` to customize the HTTP response:
+
+```ts
+return page(data, {
+ status: 201,
+ headers: { "Cache-Control": "public, max-age=3600" },
+});
+```
+
+## Async page components
+
+For simpler cases, you can fetch data directly in an async component without a
+separate handler:
+
+```tsx routes/projects/[id].tsx
+import { define } from "@/utils.ts";
+
+export default define.page(async (ctx) => {
+ const project = await db.projects.findOne(ctx.params.id);
+ if (!project) {
+ throw new HttpError(404);
+ }
+
+ return (
+
+
{project.name}
+
{project.stars} stars
+
+ );
+});
+```
+
+This is convenient for pages where you don't need the type-safe data bridge
+between handler and component.
+
+## Passing state from middleware
+
+Middleware can set values on `ctx.state` that are available to all downstream
+handlers and components:
+
+```ts routes/_middleware.ts
+import { define } from "@/utils.ts";
+
+export default define.middleware(async (ctx) => {
+ const session = await getSession(ctx.req);
+ ctx.state.user = session?.user ?? null;
+ return ctx.next();
+});
+```
+
+```tsx routes/dashboard.tsx
+import { define } from "@/utils.ts";
+
+export default define.page((ctx) => {
+ if (!ctx.state.user) {
+ return ctx.redirect("/login");
+ }
+ return
Welcome, {ctx.state.user.name}
;
+});
+```
+
+## What's available in page props
+
+Page components receive these properties:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `data` | `Data` | Data returned by the handler via `page()` |
+| `url` | `URL` | The request URL |
+| `params` | `Record` | Route parameters (e.g. `:id`) |
+| `req` | `Request` | The original HTTP request |
+| `state` | `State` | Shared state set by middleware |
+| `route` | `string \| null` | The matched route pattern |
+| `error` | `unknown \| null` | Caught error (on error pages) |
+| `isPartial` | `boolean` | Whether this is a partial request |
+| `Component` | `FunctionComponent` | Child component (in layouts) |
diff --git a/docs/toc.ts b/docs/toc.ts
index 25ef0ff360c..2bca6e0e1e9 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -38,6 +38,7 @@ const toc: RawTableOfContents = {
["context", "Context", "link:latest"],
["routing", "Routing", "link:latest"],
+ ["data-fetching", "Data Fetching", "link:latest"],
["islands", "Islands", "link:latest"],
["signals", "Signals", "link:latest"],
From 40e1b4758cda82c21837c567c7d2a9b8aa102833 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:26:18 +0100
Subject: [PATCH 15/59] docs: add Serialization reference page
Documents what types can be passed as island props, what cannot be
serialized (functions, class instances, symbols), circular reference
handling, signal serialization mechanics, and common pitfalls like
large props and accidental non-serializable values.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/index.md | 1 +
docs/latest/advanced/serialization.md | 100 ++++++++++++++++++++++++++
docs/toc.ts | 1 +
3 files changed, 102 insertions(+)
create mode 100644 docs/latest/advanced/serialization.md
diff --git a/docs/latest/advanced/index.md b/docs/latest/advanced/index.md
index 5ae2b1aae6a..d63ed8d761b 100644
--- a/docs/latest/advanced/index.md
+++ b/docs/latest/advanced/index.md
@@ -11,6 +11,7 @@ This section of the documentation describes advanced functionality of Fresh.
- [Partials](/docs/advanced/partials) - Client-side partial page updates
- [Forms](/docs/advanced/forms) - Handle form submissions
- [Define helpers](/docs/advanced/define) - Type-safe route and middleware helpers
+- [Serialization](/docs/advanced/serialization) - What types can be passed as island props
- [Environment variables](/docs/advanced/environment-variables) - Use env vars in islands
- [Modifying <head>](/docs/advanced/head) - Manage meta tags, titles, and styles
- [Vite plugin options](/docs/advanced/vite) - Configure the Fresh Vite plugin
diff --git a/docs/latest/advanced/serialization.md b/docs/latest/advanced/serialization.md
new file mode 100644
index 00000000000..df31d7168f2
--- /dev/null
+++ b/docs/latest/advanced/serialization.md
@@ -0,0 +1,100 @@
+---
+description: |
+ What types can be passed as island props, how Fresh serializes data between server and client, and common pitfalls.
+---
+
+When Fresh renders a page on the server, island props must be serialized to
+JSON and sent to the browser for hydration. Fresh uses a custom serialization
+system that supports more types than standard `JSON.stringify`.
+
+## Supported types
+
+The following types can be passed as island props:
+
+| Type | Notes |
+|------|-------|
+| `string`, `number`, `boolean` | Primitive types |
+| `null`, `undefined` | |
+| `bigint` | |
+| `NaN`, `Infinity`, `-Infinity`, `-0` | Special numeric values |
+| `Array` | Including sparse arrays |
+| Plain objects | Objects with string keys and serializable values |
+| `Date` | |
+| `URL` | |
+| `RegExp` | Including flags |
+| `Set` | Values must be serializable |
+| `Map` | Keys and values must be serializable |
+| `Uint8Array` | Binary data |
+| `Signal` | From `@preact/signals` — see [Signals](/docs/concepts/signals) |
+| `Computed Signal` | Read-only signals |
+| JSX Elements | Server-rendered JSX passed to islands |
+
+## Not serializable
+
+The following **cannot** be passed as island props:
+
+- **Functions and closures** — there is no way to transfer executable code
+- **Class instances** — only plain objects are supported (no custom prototypes)
+- **Symbols** — not representable in JSON
+- **WeakMap / WeakSet** — cannot be enumerated
+- **Streams, Promises** — async values cannot be frozen for transfer
+
+```tsx
+// WRONG — functions cannot be serialized
+ console.log("clicked")} />
+
+// WRONG — class instance loses its prototype
+
+```
+
+## Circular references
+
+Fresh handles circular references automatically. If the same object or signal
+appears multiple times in the props tree, it is serialized once and all
+references are restored on the client:
+
+```tsx
+const shared = { value: 42 };
+const data = { a: shared, b: shared };
+
+// `data.a` and `data.b` will reference the same object on the client
+
+```
+
+## How signals are serialized
+
+When a `Signal` is detected in island props:
+
+1. **Server**: the signal's current value is read via `.peek()` and serialized
+2. **Client**: the value is wrapped in a new `signal()` call, creating a live
+ reactive signal
+
+If the same signal object is passed to multiple islands, it is serialized once
+and all islands receive the same signal instance on the client — keeping them
+synchronized.
+
+Computed signals are serialized by reading their current value and wrapping it
+in `computed(() => value)` on the client. Since the original computation
+function cannot be transferred, the client-side computed signal holds a static
+value.
+
+## Common pitfalls
+
+### Accidentally passing non-serializable props
+
+If you pass a function or class instance as a prop to an island, you'll get a
+runtime error during serialization. Keep island props to plain data:
+
+```tsx
+// Instead of passing a callback...
+
+
+// ...pass data and handle events inside the island
+
+```
+
+### Large props
+
+Every byte of serialized props is embedded in the HTML and parsed on the
+client. Keep island props small — pass IDs or minimal data, and fetch the rest
+client-side if needed.
diff --git a/docs/toc.ts b/docs/toc.ts
index 2bca6e0e1e9..dff83649750 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -57,6 +57,7 @@ const toc: RawTableOfContents = {
["partials", "Partials", "link:latest"],
["forms", "Forms", "link:latest"],
["define", "Define Helpers", "link:latest"],
+ ["serialization", "Serialization", "link:latest"],
["environment-variables", "Environment Variables", "link:latest"],
["head", "Modifying ", "link:latest"],
["vite", "Vite Plugin Options", "link:latest"],
From 7b69ec6e14aecedaecd0dea69ff72a0bbed33b6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:28:20 +0100
Subject: [PATCH 16/59] docs: add Common Patterns recipes page
Collects frequently-needed patterns in one place: protected routes,
URL redirects, content negotiation, cookies, query parameters, response
headers, streaming, WebSockets, and timing middleware.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/examples/common-patterns.md | 202 ++++++++++++++++++++++++
docs/toc.ts | 1 +
2 files changed, 203 insertions(+)
create mode 100644 docs/latest/examples/common-patterns.md
diff --git a/docs/latest/examples/common-patterns.md b/docs/latest/examples/common-patterns.md
new file mode 100644
index 00000000000..c6f11a2a517
--- /dev/null
+++ b/docs/latest/examples/common-patterns.md
@@ -0,0 +1,202 @@
+---
+description: |
+ Common patterns and recipes for Fresh applications: authentication, redirects, content negotiation, cookies, and more.
+---
+
+This page collects common patterns you'll encounter when building Fresh apps.
+
+## Protected routes
+
+Use middleware to check authentication and redirect unauthenticated users:
+
+```ts routes/dashboard/_middleware.ts
+import { define } from "@/utils.ts";
+
+export default define.middleware(async (ctx) => {
+ const session = await getSession(ctx.req);
+ if (!session) {
+ return ctx.redirect("/login");
+ }
+ ctx.state.user = session.user;
+ return ctx.next();
+});
+```
+
+All routes under `routes/dashboard/` are now protected. The user data is
+available in any downstream handler or component via `ctx.state.user`.
+
+## Redirect old URLs
+
+Handle URL migrations with middleware:
+
+```ts routes/_middleware.ts
+const REDIRECTS: Record = {
+ "/old-page": "/new-page",
+ "/blog/old-slug": "/blog/new-slug",
+};
+
+export default function handler(ctx) {
+ const redirect = REDIRECTS[ctx.url.pathname];
+ if (redirect) {
+ return ctx.redirect(redirect, 301);
+ }
+ return ctx.next();
+}
+```
+
+> [info]: `ctx.redirect()` includes protection against open redirect attacks.
+> Protocol-relative URLs like `//evil.com` are rejected.
+
+## Content negotiation
+
+Return different formats based on the `Accept` header:
+
+```ts routes/api/users/[id].ts
+import { define } from "@/utils.ts";
+
+export const handlers = define.handlers({
+ async GET(ctx) {
+ const user = await db.users.find(ctx.params.id);
+ if (!user) {
+ throw new HttpError(404);
+ }
+
+ const accept = ctx.req.headers.get("Accept") ?? "";
+ if (accept.includes("text/html")) {
+ return ctx.render();
+ }
+ return Response.json(user);
+ },
+});
+```
+
+## Setting cookies
+
+Use the `@std/http` cookie utilities:
+
+```ts routes/_middleware.ts
+import { getCookies, setCookie } from "@std/http";
+
+export default async function handler(ctx) {
+ const cookies = getCookies(ctx.req.headers);
+ ctx.state.theme = cookies["theme"] ?? "light";
+
+ const response = await ctx.next();
+
+ // Set a cookie on the response
+ setCookie(response.headers, {
+ name: "theme",
+ value: ctx.state.theme,
+ path: "/",
+ httpOnly: true,
+ sameSite: "Lax",
+ maxAge: 60 * 60 * 24 * 365, // 1 year
+ });
+
+ return response;
+}
+```
+
+See [Session management](/docs/examples/session-management) for a complete
+session example.
+
+## Reading query parameters
+
+Access URL search params from the context:
+
+```ts routes/search.tsx
+import { define } from "@/utils.ts";
+
+export const handlers = define.handlers({
+ GET(ctx) {
+ const query = ctx.url.searchParams.get("q") ?? "";
+ const page = Number(ctx.url.searchParams.get("page") ?? "1");
+ const results = search(query, page);
+ return page({ query, results });
+ },
+});
+```
+
+## Adding response headers
+
+Set custom headers in middleware or handlers:
+
+```ts routes/_middleware.ts
+export default async function handler(ctx) {
+ const response = await ctx.next();
+ response.headers.set("X-Frame-Options", "DENY");
+ response.headers.set("X-Content-Type-Options", "nosniff");
+ return response;
+}
+```
+
+Or set headers on a specific route using `page()`:
+
+```ts
+import { page } from "fresh";
+
+return page(data, {
+ headers: { "Cache-Control": "public, max-age=3600" },
+});
+```
+
+## Streaming responses
+
+Return a streaming response from a handler:
+
+```ts routes/api/stream.ts
+export const handlers = define.handlers({
+ GET() {
+ const body = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode("Hello "));
+ setTimeout(() => {
+ controller.enqueue(new TextEncoder().encode("World!"));
+ controller.close();
+ }, 1000);
+ },
+ });
+ return new Response(body, {
+ headers: { "Content-Type": "text/plain" },
+ });
+ },
+});
+```
+
+## WebSockets
+
+Fresh runs on Deno, so you can upgrade HTTP connections to WebSockets directly:
+
+```ts routes/api/ws.ts
+export const handlers = define.handlers({
+ GET(ctx) {
+ const { socket, response } = Deno.upgradeWebSocket(ctx.req);
+
+ socket.onopen = () => {
+ console.log("Client connected");
+ };
+ socket.onmessage = (event) => {
+ socket.send(`Echo: ${event.data}`);
+ };
+ socket.onclose = () => {
+ console.log("Client disconnected");
+ };
+
+ return response;
+ },
+});
+```
+
+## Timing middleware
+
+Measure how long request processing takes:
+
+```ts routes/_middleware.ts
+export default async function handler(ctx) {
+ const start = performance.now();
+ const response = await ctx.next();
+ const duration = performance.now() - start;
+ response.headers.set("Server-Timing", `total;dur=${duration.toFixed(1)}`);
+ return response;
+}
+```
diff --git a/docs/toc.ts b/docs/toc.ts
index dff83649750..047a0a42405 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -105,6 +105,7 @@ const toc: RawTableOfContents = {
],
["active-links", "Active links", "link:latest"],
["session-management", "Session management", "link:latest"],
+ ["common-patterns", "Common Patterns", "link:latest"],
],
},
contributing: {
From 00a6228e23518f9365a1c02ef1cc96ff18034a28 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 20:28:57 +0100
Subject: [PATCH 17/59] docs: add Architecture overview with request lifecycle
diagram
Fresh 1.x had a dedicated architecture page that was dropped in 2.x.
New page includes an ASCII request flow diagram covering static files,
middleware chain, routing, handlers, layouts, page rendering, and island
hydration. Also explains server-first rendering, islands architecture,
middleware onion pattern, layout inheritance, and build/deploy flow.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/architecture.md | 136 +++++++++++++++++++++++++++
docs/toc.ts | 1 +
2 files changed, 137 insertions(+)
create mode 100644 docs/latest/concepts/architecture.md
diff --git a/docs/latest/concepts/architecture.md b/docs/latest/concepts/architecture.md
new file mode 100644
index 00000000000..04f79c1a2a3
--- /dev/null
+++ b/docs/latest/concepts/architecture.md
@@ -0,0 +1,136 @@
+---
+description: |
+ How Fresh processes requests: the flow from incoming request through middleware, routing, handlers, layouts, and island hydration.
+---
+
+Fresh is a server-first web framework. Pages are rendered on the server and only
+the interactive parts (islands) ship JavaScript to the browser. This page
+explains how a request flows through the framework.
+
+## Request lifecycle
+
+```
+ Browser Request
+ │
+ ▼
+ ┌─────────────┐
+ │ Static Files │──▶ Serve file directly (if match)
+ └─────┬───────┘
+ │ (no match)
+ ▼
+ ┌─────────────┐
+ │ Middlewares │──▶ Run in registration order
+ │ (global) │ Can modify request/response, set state,
+ └─────┬───────┘ or short-circuit with a Response
+ │
+ ▼
+ ┌─────────────┐
+ │ Router │──▶ Match URL pattern + HTTP method
+ └─────┬───────┘ Static routes checked first, then dynamic
+ │
+ ▼
+ ┌─────────────┐
+ │ Middlewares │──▶ Path-specific middlewares
+ │ (scoped) │
+ └─────┬───────┘
+ │
+ ▼
+ ┌─────────────┐ ┌────────────┐
+ │ Handler │────▶│ API route │──▶ Return Response (JSON, etc.)
+ └─────┬───────┘ └────────────┘
+ │ (has component)
+ ▼
+ ┌─────────────┐
+ │ App Wrapper │──▶ Outer // structure
+ └─────┬───────┘
+ │
+ ▼
+ ┌─────────────┐
+ │ Layouts │──▶ Nested layout components (inherited)
+ └─────┬───────┘
+ │
+ ▼
+ ┌─────────────┐
+ │ Page │──▶ Route component with props.data
+ │ Component │
+ └─────┬───────┘
+ │
+ ▼
+ ┌─────────────┐
+ │ HTML + JS │──▶ Server-rendered HTML sent to browser
+ │ Response │ Island props serialized inline
+ └─────────────┘
+
+ Browser
+ │
+ ▼
+ ┌─────────────┐
+ │ Hydration │──▶ Only islands receive JavaScript
+ │ (Islands) │ Rest of the page stays static HTML
+ └─────────────┘
+```
+
+## Key concepts
+
+### Server-first rendering
+
+Every page is fully rendered to HTML on the server before being sent to the
+browser. This means:
+
+- Pages are visible immediately — no blank loading screens
+- Search engines see complete content
+- Pages work without JavaScript enabled
+
+### Islands architecture
+
+Fresh uses the
+[islands architecture](https://jasonformat.com/islands-architecture/). Only
+components in the `islands/` directory are hydrated in the browser. Everything
+else is static HTML that never runs JavaScript on the client.
+
+This means a page with a single interactive button only ships the JavaScript for
+that button — not for the entire page.
+
+### Middleware chain
+
+Middlewares execute in registration order, wrapping the handler. Each middleware
+calls `ctx.next()` to pass control to the next middleware (or handler). This
+creates an onion-like pattern where middlewares can act on both the request
+(before `ctx.next()`) and the response (after `ctx.next()`):
+
+```ts
+app.use(async (ctx) => {
+ // Before: runs on the way in
+ console.log("Request:", ctx.url.pathname);
+
+ const response = await ctx.next();
+
+ // After: runs on the way out
+ console.log("Status:", response.status);
+ return response;
+});
+```
+
+### Layout inheritance
+
+Layouts wrap page components and are inherited from parent directories. A page
+at `routes/blog/post.tsx` inherits layouts from:
+
+1. `routes/_layout.tsx` (root layout)
+2. `routes/blog/_layout.tsx` (section layout)
+
+The innermost layout wraps the page component, and each outer layout wraps the
+next. The app wrapper (`_app.tsx`) wraps everything.
+
+### Build and deploy
+
+Fresh uses [Vite](https://vite.dev/) to bundle island JavaScript for
+production. The `deno task build` command:
+
+1. Discovers all islands and their dependencies
+2. Bundles client-side JavaScript with code splitting
+3. Generates a server entry point (`_fresh/server.js`)
+4. Hashes assets for cache busting
+
+In production, `_fresh/server.js` serves the pre-built assets. In development,
+Vite provides Hot Module Replacement for instant feedback.
diff --git a/docs/toc.ts b/docs/toc.ts
index 047a0a42405..8bf1a102fec 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -33,6 +33,7 @@ const toc: RawTableOfContents = {
title: "Concepts",
link: "latest",
pages: [
+ ["architecture", "Architecture", "link:latest"],
["app", "App", "link:latest"],
["middleware", "Middlewares", "link:latest"],
["context", "Context", "link:latest"],
From b13260df1d433185e129eededd86ac9a3431ae60 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 21:30:50 +0100
Subject: [PATCH 18/59] docs: fix remaining code and content errors
- islands.md: fix "route/index.tsx" typo -> "routes/index.tsx"
- middleware.md: add missing csp() to included middlewares list, fix
trailingSlash -> trailingSlashes
- layouts.md: fix line 88 using define.layout() for a page example ->
define.page()
- migration-guide.md: add missing ctx parameter in ctx.render() example
- markdown.md: remove unused CONTENT variable, fix step numbering (3 not 4)
- builder.md: fix inconsistent tailwind() args (remove extra app param)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/builder.md | 2 +-
docs/latest/concepts/islands.md | 2 +-
docs/latest/concepts/layouts.md | 2 +-
docs/latest/concepts/middleware.md | 5 +++--
docs/latest/examples/markdown.md | 9 +--------
docs/latest/examples/migration-guide.md | 2 +-
6 files changed, 8 insertions(+), 14 deletions(-)
diff --git a/docs/latest/advanced/builder.md b/docs/latest/advanced/builder.md
index 06cec0e3ac4..9d5c75b20f4 100644
--- a/docs/latest/advanced/builder.md
+++ b/docs/latest/advanced/builder.md
@@ -171,7 +171,7 @@ For more information on how to use tailwindcss, check out
You can customize the tailwind plugin via the following options:
```ts dev.ts
-tailwind(builder, app, {
+tailwind(builder, {
// Exclude certain files from processing
exclude: ["/admin/**", "*.temp.css"],
// Force optimization (defaults to production mode)
diff --git a/docs/latest/concepts/islands.md b/docs/latest/concepts/islands.md
index 2b787453820..92db7f79194 100644
--- a/docs/latest/concepts/islands.md
+++ b/docs/latest/concepts/islands.md
@@ -102,7 +102,7 @@ that is needed for the islands to the browser.
export default (props: { foo: string }) => <>{props.foo}>;
```
-```tsx route/index.tsx
+```tsx routes/index.tsx
import MyIsland from "../islands/my-island.tsx";
import OtherIsland from "../islands/other-island.tsx";
diff --git a/docs/latest/concepts/layouts.md b/docs/latest/concepts/layouts.md
index 78d497bbfca..3febf542d1e 100644
--- a/docs/latest/concepts/layouts.md
+++ b/docs/latest/concepts/layouts.md
@@ -89,7 +89,7 @@ export const config: RouteConfig = {
skipInheritedLayouts: true, // Skip already inherited layouts
};
-export default define.layout(() => {
+export default define.page(() => {
return
Hello world
;
});
```
diff --git a/docs/latest/concepts/middleware.md b/docs/latest/concepts/middleware.md
index 15535a8f7b1..fa65d75cd4a 100644
--- a/docs/latest/concepts/middleware.md
+++ b/docs/latest/concepts/middleware.md
@@ -52,8 +52,9 @@ const middleware = define.middleware(async (ctx) => {
Fresh ships with the following middlewares built-in:
- [cors()](/docs/plugins/cors) - Set CORS HTTP headers
-- [csrf()](/docs/plugins/csrf) - Set CSRF HTTP headers
-- [trailingSlash()](/docs/plugins/trailing-slashes) - Enforce trailing slashes
+- [csrf()](/docs/plugins/csrf) - CSRF protection
+- [csp()](/docs/plugins/csp) - Content Security Policy headers
+- [trailingSlashes()](/docs/plugins/trailing-slashes) - Enforce trailing slashes
## Filesystem-based middlewares
diff --git a/docs/latest/examples/markdown.md b/docs/latest/examples/markdown.md
index ba7d18e928c..ad889b11227 100644
--- a/docs/latest/examples/markdown.md
+++ b/docs/latest/examples/markdown.md
@@ -22,18 +22,11 @@ and some interesting text here
> oh look a blockquote
```
-4. Add a route that renders that file
+3. Add a route that renders that file
```tsx main.ts
import { CSS, render as renderMarkdown } from "@deno/gfm";
-const CONTENT = `## some heading
-
-and some interesting text here
-
-> oh look a blockquote
-`;
-
const app = new App();
app.get("/", async (ctx) => {
diff --git a/docs/latest/examples/migration-guide.md b/docs/latest/examples/migration-guide.md
index fa43d59a471..de52a6a492b 100644
--- a/docs/latest/examples/migration-guide.md
+++ b/docs/latest/examples/migration-guide.md
@@ -292,7 +292,7 @@ To render JSX in general, use the `ctx.render()` function:
```tsx
const app = new App()
- .get("/", () => ctx.render(
hello
));
+ .get("/", (ctx) => ctx.render(
hello
));
```
## `createHandler`
From 033022a9bb9fe393b1758be4721755a42af4ec19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 21:31:12 +0100
Subject: [PATCH 19/59] docs: update concepts index with all sub-pages
Was missing Architecture, Data Fetching, Signals, Static Files, and
File Routing links. Now lists every concept sub-page with descriptions.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/index.md | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/docs/latest/concepts/index.md b/docs/latest/concepts/index.md
index 46f8dff28b1..533b6fb0b20 100644
--- a/docs/latest/concepts/index.md
+++ b/docs/latest/concepts/index.md
@@ -13,6 +13,8 @@ Fresh will boot them up in the browser and execute the relevant JavaScript.
Here is an overview of the basic concepts in Fresh:
+- [**Architecture**](/docs/concepts/architecture) - How requests flow through
+ Fresh, from middleware to islands
- [**App**](/docs/concepts/app) - Holds all the information about your app, like
routes, etc
- [**Middleware**](/docs/concepts/middleware) - Respond to a request and return
@@ -21,10 +23,17 @@ Here is an overview of the basic concepts in Fresh:
called a "handler".
- [**Context**](/docs/concepts/context) - Passed through every middleware. Use
this to share state, trigger redirects or render HTML.
-- [**Routes**](/docs/concepts/routing) - Responds to a particular URL and runs
+- [**Routing**](/docs/concepts/routing) - Responds to a particular URL and runs
as chain of middlewares if it matches
+- [**Data Fetching**](/docs/concepts/data-fetching) - Load data on the server
+ and pass it to page components
- [**Islands**](/docs/concepts/islands) - Render interactive Preact components
on the client
+- [**Signals**](/docs/concepts/signals) - Reactive state management for islands
+- [**Static Files**](/docs/concepts/static-files) - Serve images, CSS, and
+ other assets
+- [**File Routing**](/docs/concepts/file-routing) - Convention-based routing
+ from the filesystem
Advanced concepts:
From b3d32a25301fd86c0d8b8f08d7d56d5cbd883edb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 21:31:25 +0100
Subject: [PATCH 20/59] docs: add next steps section to getting started page
The getting started page ended abruptly after the Countdown example
with no guidance on where to go next. Added links to routing, data
fetching, islands, middleware, and architecture.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/getting-started/index.md | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/docs/latest/getting-started/index.md b/docs/latest/getting-started/index.md
index db7465a2fc5..1abd5a99992 100644
--- a/docs/latest/getting-started/index.md
+++ b/docs/latest/getting-started/index.md
@@ -156,3 +156,18 @@ export default define.page(() => {
Now, we can see our countdown in action:

+
+## Next steps
+
+Now that you have a working Fresh project, here are some things to explore:
+
+- [**Routing**](/docs/concepts/routing) - Learn about route patterns, dynamic
+ parameters, and method-specific handlers
+- [**Data Fetching**](/docs/concepts/data-fetching) - Load data on the server
+ and pass it to page components
+- [**Islands**](/docs/concepts/islands) - Understand how Fresh's partial
+ hydration works and what can be passed as props
+- [**Middleware**](/docs/concepts/middleware) - Add authentication, logging, or
+ custom headers to your routes
+- [**Architecture**](/docs/concepts/architecture) - See how requests flow
+ through the entire framework
From a65125067386007e61e82f8a42c16daacff7f5a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 21:31:40 +0100
Subject: [PATCH 21/59] docs: explain (_islands) and (_components) directories
in file routing
These directories were shown in the project structure tree but never
explained. Added a section describing what each does and why the
parentheses matter.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/file-routing.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/latest/concepts/file-routing.md b/docs/latest/concepts/file-routing.md
index 94faad1d0ae..c94d301fce4 100644
--- a/docs/latest/concepts/file-routing.md
+++ b/docs/latest/concepts/file-routing.md
@@ -46,6 +46,15 @@ Example project structure:
└── index.tsx
```
+**Special directories inside `routes/`:**
+
+- **`(_islands)`** — Files in this directory are treated as
+ [islands](/docs/concepts/islands), just like files in the top-level `islands/`
+ folder. This lets you co-locate islands next to the routes that use them.
+- **`(_components)`** — A conventional directory for non-island components that
+ are only used by nearby routes. Fresh does not treat these files specially —
+ the parentheses just prevent them from becoming routes.
+
File names are mapped to route patterns as follows:
- File extensions are ignored.
From ff6259545625357344fc721d4f5bfac98da3dc04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 21:32:44 +0100
Subject: [PATCH 22/59] docs: document previously undocumented features
- static-files.md: add assetSrcSet() usage example
- error-handling.md: expand HttpError docs with message param, import
path, and browser availability
- app.md: add Configuration section documenting basePath option
- islands.md: clarify IS_BROWSER import and behavior
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/error-handling.md | 19 +++++++++++++++++--
docs/latest/concepts/app.md | 17 +++++++++++++++++
docs/latest/concepts/islands.md | 5 +++--
docs/latest/concepts/static-files.md | 15 +++++++++++++++
4 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/docs/latest/advanced/error-handling.md b/docs/latest/advanced/error-handling.md
index c9af64f62c0..f37f2fcdd50 100644
--- a/docs/latest/advanced/error-handling.md
+++ b/docs/latest/advanced/error-handling.md
@@ -71,7 +71,13 @@ middleware. Contrary to generic error pages this handler cannot be nested.
## Throwing HTTP errors
If you need to bail out of execution and need to respond with a particular HTTP
-error code, you can use Fresh's `HttpError` class.
+error code, you can use Fresh's `HttpError` class:
+
+```ts
+import { HttpError } from "fresh";
+```
+
+`HttpError` takes a status code and an optional message:
```ts middleware/auth.ts
import { HttpError } from "fresh";
@@ -84,11 +90,17 @@ async function authMiddleware(ctx) {
throw new HttpError(404);
}
+ // Forbidden with a custom message
+ if (!isAdmin(user)) {
+ throw new HttpError(403, "Admin access required");
+ }
+
return await ctx.next();
}
```
-You can check the status code of the thrown `HttpError` in your error handler:
+When an `HttpError` is thrown, Fresh catches it and invokes the error handler.
+You can check the status code in your error handler:
```ts main.ts
app.onError((ctx) => {
@@ -100,3 +112,6 @@ app.onError((ctx) => {
// ...
});
```
+
+`HttpError` is also available in the browser via `fresh/runtime` for use in
+island code.
diff --git a/docs/latest/concepts/app.md b/docs/latest/concepts/app.md
index e36ad5af8ae..a6dafb18da3 100644
--- a/docs/latest/concepts/app.md
+++ b/docs/latest/concepts/app.md
@@ -17,6 +17,23 @@ const app = new App()
app.listen();
```
+## Configuration
+
+The `App` constructor accepts an options object:
+
+```ts
+const app = new App({
+ // Serve the app from a sub-path instead of root.
+ // All routes will be prefixed with this path.
+ basePath: "/my-app",
+});
+```
+
+With `basePath: "/my-app"`, a route registered at `/about` will respond to
+`/my-app/about`. This is useful when Fresh runs behind a reverse proxy or is
+mounted alongside other apps. The base path is available in handlers via
+`ctx.config.basePath`.
+
All items are applied from top to bottom. This means that when you defined a
middleware _after_ a `.get()` handler, it won't be included.
diff --git a/docs/latest/concepts/islands.md b/docs/latest/concepts/islands.md
index 92db7f79194..eaf74d4e997 100644
--- a/docs/latest/concepts/islands.md
+++ b/docs/latest/concepts/islands.md
@@ -118,8 +118,9 @@ import OtherIsland from "../islands/other-island.tsx";
## Rendering islands on client only
When using client-only APIs, like `EventSource` or `navigator.getUserMedia`,
-this component will not run on the server as it will produce an error. To fix
-this use the `IS_BROWSER` flag as a guard:
+the component would error during server-side rendering. Use the `IS_BROWSER`
+constant from `fresh/runtime` to guard browser-only code. It is `false` on the
+server and `true` in the browser:
```tsx islands/my-island.tsx
import { IS_BROWSER } from "fresh/runtime";
diff --git a/docs/latest/concepts/static-files.md b/docs/latest/concepts/static-files.md
index aabb6d076e1..c22e9a1c266 100644
--- a/docs/latest/concepts/static-files.md
+++ b/docs/latest/concepts/static-files.md
@@ -76,3 +76,18 @@ export default function About() {
return View brochure;
}
```
+
+For `` tags with a `srcset` attribute, use `assetSrcSet()`:
+
+```tsx routes/gallery.tsx
+import { assetSrcSet } from "fresh/runtime";
+
+export default function Gallery() {
+ return (
+
+ );
+}
+```
From 5a035f90fc168bd7c85b0ff4cc596cc086466996 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 21:33:49 +0100
Subject: [PATCH 23/59] docs: add API Reference page listing all public exports
Manual reference covering all exports from fresh, fresh/runtime, and
fresh/dev entry points. Each export includes its kind (class, function,
component, constant, type) and links to the relevant docs page.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/api-reference.md | 85 +++++++++++++++++++++++++++
docs/latest/advanced/index.md | 1 +
docs/toc.ts | 1 +
3 files changed, 87 insertions(+)
create mode 100644 docs/latest/advanced/api-reference.md
diff --git a/docs/latest/advanced/api-reference.md b/docs/latest/advanced/api-reference.md
new file mode 100644
index 00000000000..390100035eb
--- /dev/null
+++ b/docs/latest/advanced/api-reference.md
@@ -0,0 +1,85 @@
+---
+description: |
+ Quick reference for all public exports from Fresh's entry points: fresh, fresh/runtime, and fresh/dev.
+---
+
+This page lists all public exports from Fresh's entry points.
+
+## `fresh`
+
+The main entry point for server-side code.
+
+```ts
+import { App, staticFiles, createDefine, HttpError, page } from "fresh";
+```
+
+| Export | Kind | Description |
+|--------|------|-------------|
+| `App` | Class | The main application class. See [App](/docs/concepts/app). |
+| `staticFiles` | Function | Middleware for serving static files. See [Static Files](/docs/concepts/static-files). |
+| `createDefine` | Function | Create type-safe `define.*` helpers. See [Define Helpers](/docs/advanced/define). |
+| `page` | Function | Return data from a handler to a page component. See [Data Fetching](/docs/concepts/data-fetching). |
+| `HttpError` | Class | Throw HTTP errors with status codes. See [Error Handling](/docs/advanced/error-handling). |
+| `cors` | Function | CORS middleware. See [cors](/docs/plugins/cors). |
+| `csrf` | Function | CSRF protection middleware. See [csrf](/docs/plugins/csrf). |
+| `csp` | Function | Content Security Policy middleware. See [csp](/docs/plugins/csp). |
+| `trailingSlashes` | Function | Trailing slash enforcement middleware. See [trailingSlashes](/docs/plugins/trailing-slashes). |
+
+**Types:**
+
+| Export | Description |
+|--------|-------------|
+| `Context` / `FreshContext` | The request context passed to all middlewares and handlers. |
+| `PageProps` | Props received by page components (includes `data`, `url`, `params`, `state`, etc.). |
+| `Middleware` / `MiddlewareFn` | Middleware function type. |
+| `HandlerFn` | Single handler function type. |
+| `HandlerByMethod` | Object with per-method handler functions. |
+| `RouteHandler` | Union of `HandlerFn` and `HandlerByMethod`. |
+| `PageResponse` | Return type of `page()`. |
+| `RouteConfig` | Route configuration (`routeOverride`, `skipInheritedLayouts`, etc.). |
+| `LayoutConfig` | Layout configuration (`skipInheritedLayouts`, `skipAppWrapper`). |
+| `Define` | Type of the object returned by `createDefine()`. |
+| `FreshConfig` / `ResolvedFreshConfig` | App configuration types. |
+| `ListenOptions` | Options for `app.listen()`. |
+| `Island` | Island component type. |
+| `Method` | HTTP method union type. |
+| `CORSOptions` | Options for `cors()`. |
+| `CsrfOptions` | Options for `csrf()`. |
+| `CSPOptions` | Options for `csp()`. |
+
+## `fresh/runtime`
+
+Shared runtime utilities for both server and client code. Safe to import in
+islands.
+
+```ts
+import { IS_BROWSER, asset, assetSrcSet, Partial, Head, HttpError } from "fresh/runtime";
+```
+
+| Export | Kind | Description |
+|--------|------|-------------|
+| `IS_BROWSER` | Constant | `true` in the browser, `false` on the server. Use to guard browser-only code. |
+| `asset` | Function | Add cache-busting query params to asset URLs. See [Static Files](/docs/concepts/static-files). |
+| `assetSrcSet` | Function | Apply `asset()` to all URLs in a `srcset` string. |
+| `Partial` | Component | Mark a region for partial updates. See [Partials](/docs/advanced/partials). |
+| `Head` | Component | Add elements to the document ``. See [Modifying head](/docs/advanced/head). |
+| `HttpError` | Class | HTTP error class (re-exported from `fresh`). |
+
+## `fresh/dev`
+
+Development and build tools. Only used in `dev.ts` (legacy) or build scripts.
+
+```ts
+import { Builder } from "fresh/dev";
+```
+
+| Export | Kind | Description |
+|--------|------|-------------|
+| `Builder` | Class | Pre-Vite build system (legacy). See [Builder](/docs/advanced/builder). |
+
+**Types:**
+
+| Export | Description |
+|--------|-------------|
+| `BuildOptions` | Options for `new Builder()`. |
+| `OnTransformArgs` / `OnTransformOptions` / `TransformFn` | Build plugin hook types. |
diff --git a/docs/latest/advanced/index.md b/docs/latest/advanced/index.md
index d63ed8d761b..ffd4dc22c46 100644
--- a/docs/latest/advanced/index.md
+++ b/docs/latest/advanced/index.md
@@ -15,5 +15,6 @@ This section of the documentation describes advanced functionality of Fresh.
- [Environment variables](/docs/advanced/environment-variables) - Use env vars in islands
- [Modifying <head>](/docs/advanced/head) - Manage meta tags, titles, and styles
- [Vite plugin options](/docs/advanced/vite) - Configure the Fresh Vite plugin
+- [API Reference](/docs/advanced/api-reference) - All public exports from Fresh
- [Troubleshooting](/docs/advanced/troubleshooting) - Common issues and solutions
- [Builder (Legacy)](/docs/advanced/builder) - Pre-Vite build system
diff --git a/docs/toc.ts b/docs/toc.ts
index 8bf1a102fec..b3e84206c96 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -62,6 +62,7 @@ const toc: RawTableOfContents = {
["environment-variables", "Environment Variables", "link:latest"],
["head", "Modifying ", "link:latest"],
["vite", "Vite Plugin Options", "link:latest"],
+ ["api-reference", "API Reference", "link:latest"],
["troubleshooting", "Troubleshooting", "link:latest"],
["builder", "Builder (Legacy)", "link:latest"],
],
From ebf194e1710d8ed5695c7a5a37031ebc2fa10418 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Fri, 27 Mar 2026 21:34:13 +0100
Subject: [PATCH 24/59] docs: update examples index and fix migration guide
grammar
- examples/index.md: add API Routes and Common Patterns to listing,
match order to TOC
- migration-guide.md: fix awkward phrasing "automatically update like"
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/examples/index.md | 10 ++++++----
docs/latest/examples/migration-guide.md | 2 +-
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/docs/latest/examples/index.md b/docs/latest/examples/index.md
index 819c104dcd9..8653ca819e8 100644
--- a/docs/latest/examples/index.md
+++ b/docs/latest/examples/index.md
@@ -6,10 +6,12 @@ description: |
In this chapter of the Fresh documentation, you can find examples of features
that you may like in your Fresh project.
-- [Active links](./examples/active-links)
+- [API Routes](./examples/api-routes)
+- [Migration Guide](./examples/migration-guide)
- [DaisyUI](./examples/daisyui)
-- [Markdown](./examples/markdown)
-- [Migration guide](./examples/migration-guide)
+- [Rendering Markdown](./examples/markdown)
- [Rendering raw HTML](./examples/rendering-raw-html)
-- [Session management](./examples/session-management)
- [Sharing state between islands](./examples/sharing-state-between-islands)
+- [Active links](./examples/active-links)
+- [Session management](./examples/session-management)
+- [Common Patterns](./examples/common-patterns)
diff --git a/docs/latest/examples/migration-guide.md b/docs/latest/examples/migration-guide.md
index de52a6a492b..0e8c87cb0f1 100644
--- a/docs/latest/examples/migration-guide.md
+++ b/docs/latest/examples/migration-guide.md
@@ -20,7 +20,7 @@ update by running it in your project directory:
deno run -Ar jsr:@fresh/update
```
-This will apply most API changes made in Fresh 2 automatically update like
+This will apply most API changes made in Fresh 2 automatically, like
changing `$fresh/server.ts` imports to `fresh`.
## Getting `main.ts` and `dev.ts` ready
From 025b3c610b62fce6475c29bd7e1ec73bb4936096 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:35:42 +0100
Subject: [PATCH 25/59] fmt
---
docs/latest/advanced/api-reference.md | 99 +++++++++++---------
docs/latest/advanced/index.md | 15 ++-
docs/latest/advanced/serialization.md | 46 ++++-----
docs/latest/concepts/architecture.md | 4 +-
docs/latest/concepts/data-fetching.md | 22 ++---
docs/latest/concepts/index.md | 4 +-
docs/latest/concepts/islands.md | 4 +-
docs/latest/concepts/layouts.md | 2 +-
docs/latest/concepts/routing.md | 4 +-
docs/latest/concepts/signals.md | 8 +-
docs/latest/deployment/cloudflare-workers.md | 4 +-
docs/latest/deployment/deno-deploy.md | 4 +-
docs/latest/deployment/index.md | 4 +-
docs/latest/examples/api-routes.md | 2 +-
docs/latest/examples/migration-guide.md | 4 +-
docs/latest/plugins/index.md | 3 +-
16 files changed, 121 insertions(+), 108 deletions(-)
diff --git a/docs/latest/advanced/api-reference.md b/docs/latest/advanced/api-reference.md
index 390100035eb..1a6d30db140 100644
--- a/docs/latest/advanced/api-reference.md
+++ b/docs/latest/advanced/api-reference.md
@@ -10,42 +10,42 @@ This page lists all public exports from Fresh's entry points.
The main entry point for server-side code.
```ts
-import { App, staticFiles, createDefine, HttpError, page } from "fresh";
+import { App, createDefine, HttpError, page, staticFiles } from "fresh";
```
-| Export | Kind | Description |
-|--------|------|-------------|
-| `App` | Class | The main application class. See [App](/docs/concepts/app). |
-| `staticFiles` | Function | Middleware for serving static files. See [Static Files](/docs/concepts/static-files). |
-| `createDefine` | Function | Create type-safe `define.*` helpers. See [Define Helpers](/docs/advanced/define). |
-| `page` | Function | Return data from a handler to a page component. See [Data Fetching](/docs/concepts/data-fetching). |
-| `HttpError` | Class | Throw HTTP errors with status codes. See [Error Handling](/docs/advanced/error-handling). |
-| `cors` | Function | CORS middleware. See [cors](/docs/plugins/cors). |
-| `csrf` | Function | CSRF protection middleware. See [csrf](/docs/plugins/csrf). |
-| `csp` | Function | Content Security Policy middleware. See [csp](/docs/plugins/csp). |
-| `trailingSlashes` | Function | Trailing slash enforcement middleware. See [trailingSlashes](/docs/plugins/trailing-slashes). |
+| Export | Kind | Description |
+| ----------------- | -------- | -------------------------------------------------------------------------------------------------- |
+| `App` | Class | The main application class. See [App](/docs/concepts/app). |
+| `staticFiles` | Function | Middleware for serving static files. See [Static Files](/docs/concepts/static-files). |
+| `createDefine` | Function | Create type-safe `define.*` helpers. See [Define Helpers](/docs/advanced/define). |
+| `page` | Function | Return data from a handler to a page component. See [Data Fetching](/docs/concepts/data-fetching). |
+| `HttpError` | Class | Throw HTTP errors with status codes. See [Error Handling](/docs/advanced/error-handling). |
+| `cors` | Function | CORS middleware. See [cors](/docs/plugins/cors). |
+| `csrf` | Function | CSRF protection middleware. See [csrf](/docs/plugins/csrf). |
+| `csp` | Function | Content Security Policy middleware. See [csp](/docs/plugins/csp). |
+| `trailingSlashes` | Function | Trailing slash enforcement middleware. See [trailingSlashes](/docs/plugins/trailing-slashes). |
**Types:**
-| Export | Description |
-|--------|-------------|
-| `Context` / `FreshContext` | The request context passed to all middlewares and handlers. |
-| `PageProps` | Props received by page components (includes `data`, `url`, `params`, `state`, etc.). |
-| `Middleware` / `MiddlewareFn` | Middleware function type. |
-| `HandlerFn` | Single handler function type. |
-| `HandlerByMethod` | Object with per-method handler functions. |
-| `RouteHandler` | Union of `HandlerFn` and `HandlerByMethod`. |
-| `PageResponse` | Return type of `page()`. |
-| `RouteConfig` | Route configuration (`routeOverride`, `skipInheritedLayouts`, etc.). |
-| `LayoutConfig` | Layout configuration (`skipInheritedLayouts`, `skipAppWrapper`). |
-| `Define` | Type of the object returned by `createDefine()`. |
-| `FreshConfig` / `ResolvedFreshConfig` | App configuration types. |
-| `ListenOptions` | Options for `app.listen()`. |
-| `Island` | Island component type. |
-| `Method` | HTTP method union type. |
-| `CORSOptions` | Options for `cors()`. |
-| `CsrfOptions` | Options for `csrf()`. |
-| `CSPOptions` | Options for `csp()`. |
+| Export | Description |
+| ------------------------------------- | ------------------------------------------------------------------------------------ |
+| `Context` / `FreshContext` | The request context passed to all middlewares and handlers. |
+| `PageProps` | Props received by page components (includes `data`, `url`, `params`, `state`, etc.). |
+| `Middleware` / `MiddlewareFn` | Middleware function type. |
+| `HandlerFn` | Single handler function type. |
+| `HandlerByMethod` | Object with per-method handler functions. |
+| `RouteHandler` | Union of `HandlerFn` and `HandlerByMethod`. |
+| `PageResponse` | Return type of `page()`. |
+| `RouteConfig` | Route configuration (`routeOverride`, `skipInheritedLayouts`, etc.). |
+| `LayoutConfig` | Layout configuration (`skipInheritedLayouts`, `skipAppWrapper`). |
+| `Define` | Type of the object returned by `createDefine()`. |
+| `FreshConfig` / `ResolvedFreshConfig` | App configuration types. |
+| `ListenOptions` | Options for `app.listen()`. |
+| `Island` | Island component type. |
+| `Method` | HTTP method union type. |
+| `CORSOptions` | Options for `cors()`. |
+| `CsrfOptions` | Options for `csrf()`. |
+| `CSPOptions` | Options for `csp()`. |
## `fresh/runtime`
@@ -53,17 +53,24 @@ Shared runtime utilities for both server and client code. Safe to import in
islands.
```ts
-import { IS_BROWSER, asset, assetSrcSet, Partial, Head, HttpError } from "fresh/runtime";
+import {
+ asset,
+ assetSrcSet,
+ Head,
+ HttpError,
+ IS_BROWSER,
+ Partial,
+} from "fresh/runtime";
```
-| Export | Kind | Description |
-|--------|------|-------------|
-| `IS_BROWSER` | Constant | `true` in the browser, `false` on the server. Use to guard browser-only code. |
-| `asset` | Function | Add cache-busting query params to asset URLs. See [Static Files](/docs/concepts/static-files). |
-| `assetSrcSet` | Function | Apply `asset()` to all URLs in a `srcset` string. |
-| `Partial` | Component | Mark a region for partial updates. See [Partials](/docs/advanced/partials). |
-| `Head` | Component | Add elements to the document ``. See [Modifying head](/docs/advanced/head). |
-| `HttpError` | Class | HTTP error class (re-exported from `fresh`). |
+| Export | Kind | Description |
+| ------------- | --------- | ---------------------------------------------------------------------------------------------- |
+| `IS_BROWSER` | Constant | `true` in the browser, `false` on the server. Use to guard browser-only code. |
+| `asset` | Function | Add cache-busting query params to asset URLs. See [Static Files](/docs/concepts/static-files). |
+| `assetSrcSet` | Function | Apply `asset()` to all URLs in a `srcset` string. |
+| `Partial` | Component | Mark a region for partial updates. See [Partials](/docs/advanced/partials). |
+| `Head` | Component | Add elements to the document ``. See [Modifying head](/docs/advanced/head). |
+| `HttpError` | Class | HTTP error class (re-exported from `fresh`). |
## `fresh/dev`
@@ -73,13 +80,13 @@ Development and build tools. Only used in `dev.ts` (legacy) or build scripts.
import { Builder } from "fresh/dev";
```
-| Export | Kind | Description |
-|--------|------|-------------|
+| Export | Kind | Description |
+| --------- | ----- | ---------------------------------------------------------------------- |
| `Builder` | Class | Pre-Vite build system (legacy). See [Builder](/docs/advanced/builder). |
**Types:**
-| Export | Description |
-|--------|-------------|
-| `BuildOptions` | Options for `new Builder()`. |
-| `OnTransformArgs` / `OnTransformOptions` / `TransformFn` | Build plugin hook types. |
+| Export | Description |
+| -------------------------------------------------------- | ---------------------------- |
+| `BuildOptions` | Options for `new Builder()`. |
+| `OnTransformArgs` / `OnTransformOptions` / `TransformFn` | Build plugin hook types. |
diff --git a/docs/latest/advanced/index.md b/docs/latest/advanced/index.md
index ffd4dc22c46..542a9c6e107 100644
--- a/docs/latest/advanced/index.md
+++ b/docs/latest/advanced/index.md
@@ -10,11 +10,16 @@ This section of the documentation describes advanced functionality of Fresh.
- [Error handling](/docs/advanced/error-handling) - Custom error and 404 pages
- [Partials](/docs/advanced/partials) - Client-side partial page updates
- [Forms](/docs/advanced/forms) - Handle form submissions
-- [Define helpers](/docs/advanced/define) - Type-safe route and middleware helpers
-- [Serialization](/docs/advanced/serialization) - What types can be passed as island props
-- [Environment variables](/docs/advanced/environment-variables) - Use env vars in islands
-- [Modifying <head>](/docs/advanced/head) - Manage meta tags, titles, and styles
+- [Define helpers](/docs/advanced/define) - Type-safe route and middleware
+ helpers
+- [Serialization](/docs/advanced/serialization) - What types can be passed as
+ island props
+- [Environment variables](/docs/advanced/environment-variables) - Use env vars
+ in islands
+- [Modifying <head>](/docs/advanced/head) - Manage meta tags, titles, and
+ styles
- [Vite plugin options](/docs/advanced/vite) - Configure the Fresh Vite plugin
- [API Reference](/docs/advanced/api-reference) - All public exports from Fresh
-- [Troubleshooting](/docs/advanced/troubleshooting) - Common issues and solutions
+- [Troubleshooting](/docs/advanced/troubleshooting) - Common issues and
+ solutions
- [Builder (Legacy)](/docs/advanced/builder) - Pre-Vite build system
diff --git a/docs/latest/advanced/serialization.md b/docs/latest/advanced/serialization.md
index df31d7168f2..d74f8aeff2e 100644
--- a/docs/latest/advanced/serialization.md
+++ b/docs/latest/advanced/serialization.md
@@ -3,31 +3,31 @@ description: |
What types can be passed as island props, how Fresh serializes data between server and client, and common pitfalls.
---
-When Fresh renders a page on the server, island props must be serialized to
-JSON and sent to the browser for hydration. Fresh uses a custom serialization
-system that supports more types than standard `JSON.stringify`.
+When Fresh renders a page on the server, island props must be serialized to JSON
+and sent to the browser for hydration. Fresh uses a custom serialization system
+that supports more types than standard `JSON.stringify`.
## Supported types
The following types can be passed as island props:
-| Type | Notes |
-|------|-------|
-| `string`, `number`, `boolean` | Primitive types |
-| `null`, `undefined` | |
-| `bigint` | |
-| `NaN`, `Infinity`, `-Infinity`, `-0` | Special numeric values |
-| `Array` | Including sparse arrays |
-| Plain objects | Objects with string keys and serializable values |
-| `Date` | |
-| `URL` | |
-| `RegExp` | Including flags |
-| `Set` | Values must be serializable |
-| `Map` | Keys and values must be serializable |
-| `Uint8Array` | Binary data |
-| `Signal` | From `@preact/signals` — see [Signals](/docs/concepts/signals) |
-| `Computed Signal` | Read-only signals |
-| JSX Elements | Server-rendered JSX passed to islands |
+| Type | Notes |
+| ------------------------------------ | -------------------------------------------------------------- |
+| `string`, `number`, `boolean` | Primitive types |
+| `null`, `undefined` | |
+| `bigint` | |
+| `NaN`, `Infinity`, `-Infinity`, `-0` | Special numeric values |
+| `Array` | Including sparse arrays |
+| Plain objects | Objects with string keys and serializable values |
+| `Date` | |
+| `URL` | |
+| `RegExp` | Including flags |
+| `Set` | Values must be serializable |
+| `Map` | Keys and values must be serializable |
+| `Uint8Array` | Binary data |
+| `Signal` | From `@preact/signals` — see [Signals](/docs/concepts/signals) |
+| `Computed Signal` | Read-only signals |
+| JSX Elements | Server-rendered JSX passed to islands |
## Not serializable
@@ -58,7 +58,7 @@ const shared = { value: 42 };
const data = { a: shared, b: shared };
// `data.a` and `data.b` will reference the same object on the client
-
+;
```
## How signals are serialized
@@ -95,6 +95,6 @@ runtime error during serialization. Keep island props to plain data:
### Large props
-Every byte of serialized props is embedded in the HTML and parsed on the
-client. Keep island props small — pass IDs or minimal data, and fetch the rest
+Every byte of serialized props is embedded in the HTML and parsed on the client.
+Keep island props small — pass IDs or minimal data, and fetch the rest
client-side if needed.
diff --git a/docs/latest/concepts/architecture.md b/docs/latest/concepts/architecture.md
index 04f79c1a2a3..cbdd31f637c 100644
--- a/docs/latest/concepts/architecture.md
+++ b/docs/latest/concepts/architecture.md
@@ -124,8 +124,8 @@ next. The app wrapper (`_app.tsx`) wraps everything.
### Build and deploy
-Fresh uses [Vite](https://vite.dev/) to bundle island JavaScript for
-production. The `deno task build` command:
+Fresh uses [Vite](https://vite.dev/) to bundle island JavaScript for production.
+The `deno task build` command:
1. Discovers all islands and their dependencies
2. Bundles client-side JavaScript with code splitting
diff --git a/docs/latest/concepts/data-fetching.md b/docs/latest/concepts/data-fetching.md
index ca9e75f67c2..8e8043f6652 100644
--- a/docs/latest/concepts/data-fetching.md
+++ b/docs/latest/concepts/data-fetching.md
@@ -110,14 +110,14 @@ export default define.page((ctx) => {
Page components receive these properties:
-| Property | Type | Description |
-|----------|------|-------------|
-| `data` | `Data` | Data returned by the handler via `page()` |
-| `url` | `URL` | The request URL |
-| `params` | `Record` | Route parameters (e.g. `:id`) |
-| `req` | `Request` | The original HTTP request |
-| `state` | `State` | Shared state set by middleware |
-| `route` | `string \| null` | The matched route pattern |
-| `error` | `unknown \| null` | Caught error (on error pages) |
-| `isPartial` | `boolean` | Whether this is a partial request |
-| `Component` | `FunctionComponent` | Child component (in layouts) |
+| Property | Type | Description |
+| ----------- | ------------------------ | ----------------------------------------- |
+| `data` | `Data` | Data returned by the handler via `page()` |
+| `url` | `URL` | The request URL |
+| `params` | `Record` | Route parameters (e.g. `:id`) |
+| `req` | `Request` | The original HTTP request |
+| `state` | `State` | Shared state set by middleware |
+| `route` | `string \| null` | The matched route pattern |
+| `error` | `unknown \| null` | Caught error (on error pages) |
+| `isPartial` | `boolean` | Whether this is a partial request |
+| `Component` | `FunctionComponent` | Child component (in layouts) |
diff --git a/docs/latest/concepts/index.md b/docs/latest/concepts/index.md
index 533b6fb0b20..50ba2497ed2 100644
--- a/docs/latest/concepts/index.md
+++ b/docs/latest/concepts/index.md
@@ -30,8 +30,8 @@ Here is an overview of the basic concepts in Fresh:
- [**Islands**](/docs/concepts/islands) - Render interactive Preact components
on the client
- [**Signals**](/docs/concepts/signals) - Reactive state management for islands
-- [**Static Files**](/docs/concepts/static-files) - Serve images, CSS, and
- other assets
+- [**Static Files**](/docs/concepts/static-files) - Serve images, CSS, and other
+ assets
- [**File Routing**](/docs/concepts/file-routing) - Convention-based routing
from the filesystem
diff --git a/docs/latest/concepts/islands.md b/docs/latest/concepts/islands.md
index eaf74d4e997..43f1ac7caa6 100644
--- a/docs/latest/concepts/islands.md
+++ b/docs/latest/concepts/islands.md
@@ -117,8 +117,8 @@ import OtherIsland from "../islands/other-island.tsx";
## Rendering islands on client only
-When using client-only APIs, like `EventSource` or `navigator.getUserMedia`,
-the component would error during server-side rendering. Use the `IS_BROWSER`
+When using client-only APIs, like `EventSource` or `navigator.getUserMedia`, the
+component would error during server-side rendering. Use the `IS_BROWSER`
constant from `fresh/runtime` to guard browser-only code. It is `false` on the
server and `true` in the browser:
diff --git a/docs/latest/concepts/layouts.md b/docs/latest/concepts/layouts.md
index 3febf542d1e..1a53a128f0d 100644
--- a/docs/latest/concepts/layouts.md
+++ b/docs/latest/concepts/layouts.md
@@ -39,7 +39,7 @@ export default define.layout(({ Component, state }) => {
);
-})
+});
```
## Async layouts
diff --git a/docs/latest/concepts/routing.md b/docs/latest/concepts/routing.md
index d232a8b6812..961507b519c 100644
--- a/docs/latest/concepts/routing.md
+++ b/docs/latest/concepts/routing.md
@@ -54,8 +54,8 @@ const app = new App()
## HTTP method handlers
Fresh provides method-specific route registration via `.get()`, `.post()`,
-`.put()`, `.delete()`, `.head()`, `.patch()`, and `.options()`. Each method
-only responds to its matching HTTP verb.
+`.put()`, `.delete()`, `.head()`, `.patch()`, and `.options()`. Each method only
+responds to its matching HTTP verb.
Use `.all()` to respond to any HTTP method:
diff --git a/docs/latest/concepts/signals.md b/docs/latest/concepts/signals.md
index 6ea1174c4d9..9300f51fbc9 100644
--- a/docs/latest/concepts/signals.md
+++ b/docs/latest/concepts/signals.md
@@ -37,7 +37,7 @@ Use `computed` to derive values from other signals. Computed signals update
automatically when their dependencies change:
```tsx islands/TemperatureConverter.tsx
-import { useSignal, useComputed } from "@preact/signals";
+import { useComputed, useSignal } from "@preact/signals";
export default function TemperatureConverter() {
const celsius = useSignal(20);
@@ -78,9 +78,9 @@ export default function Home() {
}
```
-Both sliders share the same signal — moving one updates the other. When the
-same signal object is passed to multiple islands, Fresh preserves the reference
-so they stay synchronized.
+Both sliders share the same signal — moving one updates the other. When the same
+signal object is passed to multiple islands, Fresh preserves the reference so
+they stay synchronized.
## Shared state across islands
diff --git a/docs/latest/deployment/cloudflare-workers.md b/docs/latest/deployment/cloudflare-workers.md
index 6fb901328f2..de38accb487 100644
--- a/docs/latest/deployment/cloudflare-workers.md
+++ b/docs/latest/deployment/cloudflare-workers.md
@@ -36,5 +36,5 @@ Check out the
[Cloudflare Documentation](https://developers.cloudflare.com/workers/vite-plugin/)
for further information.
-> [info]: Make sure that you set the correct entrypoint in your
-> `wrangler.jsonc` file. It should point to `"main": "./server.js"`
+> [info]: Make sure that you set the correct entrypoint in your `wrangler.jsonc`
+> file. It should point to `"main": "./server.js"`
diff --git a/docs/latest/deployment/deno-deploy.md b/docs/latest/deployment/deno-deploy.md
index 601b48b04cf..f48fc85dacd 100644
--- a/docs/latest/deployment/deno-deploy.md
+++ b/docs/latest/deployment/deno-deploy.md
@@ -4,8 +4,8 @@ description: "Deploy Fresh on Deno Deploy"
The recommended way to deploy Fresh is by using
[Deno Deploy](https://deno.com/deploy). It will automatically create branch
-previews for pull requests, collect request and HTTP metrics, as well as
-collect traces for you out of the box.
+previews for pull requests, collect request and HTTP metrics, as well as collect
+traces for you out of the box.
## Setup
diff --git a/docs/latest/deployment/index.md b/docs/latest/deployment/index.md
index 59d7780595f..d3c45f0c3c8 100644
--- a/docs/latest/deployment/index.md
+++ b/docs/latest/deployment/index.md
@@ -9,8 +9,8 @@ assets for consumption in the browser. This step can be invoked by running:
deno task build
```
-> [info]: This runs `vite build` under the hood. If you're migrating from
-> Fresh 1.x and still have a `dev.ts` file, see the
+> [info]: This runs `vite build` under the hood. If you're migrating from Fresh
+> 1.x and still have a `dev.ts` file, see the
> [migration guide](/docs/examples/migration-guide) for updating your tasks.
Once completed, it will have created a `_fresh` folder in the project directory
diff --git a/docs/latest/examples/api-routes.md b/docs/latest/examples/api-routes.md
index 15e4cc32e12..aaa770b1837 100644
--- a/docs/latest/examples/api-routes.md
+++ b/docs/latest/examples/api-routes.md
@@ -22,7 +22,7 @@ export const handlers = define.handlers({
A `GET /api/users` request returns:
```json
-[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
+[{ "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" }]
```
## Method-specific handlers
diff --git a/docs/latest/examples/migration-guide.md b/docs/latest/examples/migration-guide.md
index 0e8c87cb0f1..ff5aa64d4f0 100644
--- a/docs/latest/examples/migration-guide.md
+++ b/docs/latest/examples/migration-guide.md
@@ -20,8 +20,8 @@ update by running it in your project directory:
deno run -Ar jsr:@fresh/update
```
-This will apply most API changes made in Fresh 2 automatically, like
-changing `$fresh/server.ts` imports to `fresh`.
+This will apply most API changes made in Fresh 2 automatically, like changing
+`$fresh/server.ts` imports to `fresh`.
## Getting `main.ts` and `dev.ts` ready
diff --git a/docs/latest/plugins/index.md b/docs/latest/plugins/index.md
index abdec2ff4f8..5832c6ec0f4 100644
--- a/docs/latest/plugins/index.md
+++ b/docs/latest/plugins/index.md
@@ -29,4 +29,5 @@ Fresh ships with the following plugins:
- [cors()](/docs/plugins/cors) - Set CORS HTTP headers
- [csrf()](/docs/plugins/csrf) - CSRF protection
- [csp()](/docs/plugins/csp) - Content Security Policy headers
-- [trailingSlashes()](/docs/plugins/trailing-slashes) - Enforce trailing slash behavior
+- [trailingSlashes()](/docs/plugins/trailing-slashes) - Enforce trailing slash
+ behavior
From 5b8af4c923c5c051ee83af800eeb1b85cf14840a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:39:43 +0100
Subject: [PATCH 26/59] docs: add Deno version requirements to introduction
The introduction had no mention of what Deno version is needed.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/introduction/index.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/latest/introduction/index.md b/docs/latest/introduction/index.md
index fd5f512bc05..dfd1f7b847d 100644
--- a/docs/latest/introduction/index.md
+++ b/docs/latest/introduction/index.md
@@ -19,6 +19,11 @@ const app = new App()
app.listen();
```
+## Requirements
+
+- [Deno](https://deno.com/) v2.1 or later
+- TypeScript is supported out of the box (no configuration needed)
+
## Quick Start
Create a new Fresh app by running:
From 37aaaed1b82ef90b34c47ede0f2b49be0b2b0ca7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:40:04 +0100
Subject: [PATCH 27/59] docs: add plugin authoring guidance to plugins page
The audit noted there was no explanation of how to create custom plugins.
Added a "Creating reusable plugins" section showing how to export a
middleware function as a reusable plugin.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/plugins/index.md | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/docs/latest/plugins/index.md b/docs/latest/plugins/index.md
index 5832c6ec0f4..a6f5462b6ef 100644
--- a/docs/latest/plugins/index.md
+++ b/docs/latest/plugins/index.md
@@ -22,6 +22,37 @@ const addXFreshHeader = define.middleware(async (ctx) => {
Learn more about [middlewares](/docs/concepts/middleware).
+## Creating reusable plugins
+
+Since Fresh plugins are just middlewares and route handlers, creating a
+reusable plugin is as simple as exporting a function that returns a middleware:
+
+```ts plugins/request-id.ts
+import type { MiddlewareFn } from "fresh";
+
+export function requestId(): MiddlewareFn<{ requestId: string }> {
+ return async (ctx) => {
+ ctx.state.requestId = crypto.randomUUID();
+ const res = await ctx.next();
+ res.headers.set("X-Request-Id", ctx.state.requestId);
+ return res;
+ };
+}
+```
+
+```ts main.ts
+import { App, staticFiles } from "fresh";
+import { requestId } from "./plugins/request-id.ts";
+
+const app = new App()
+ .use(staticFiles())
+ .use(requestId())
+ .fsRoutes();
+```
+
+For more complex plugins, you can combine multiple middlewares, add routes, or
+use the [`Builder`](/docs/advanced/builder) hooks for build-time processing.
+
## Built-in plugins
Fresh ships with the following plugins:
From 272342a365491d2d1f62ad707775b4924d3315f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:40:38 +0100
Subject: [PATCH 28/59] docs: add OpenTelemetry page
Fresh has built-in OTel instrumentation listed as a feature in the
introduction but with no dedicated documentation. New page covers what's
instrumented, how to enable tracing with Deno's built-in OTel support,
and the Deno Deploy integration.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/index.md | 1 +
docs/latest/advanced/opentelemetry.md | 45 +++++++++++++++++++++++++++
docs/toc.ts | 1 +
3 files changed, 47 insertions(+)
create mode 100644 docs/latest/advanced/opentelemetry.md
diff --git a/docs/latest/advanced/index.md b/docs/latest/advanced/index.md
index 542a9c6e107..149c6a9f830 100644
--- a/docs/latest/advanced/index.md
+++ b/docs/latest/advanced/index.md
@@ -19,6 +19,7 @@ This section of the documentation describes advanced functionality of Fresh.
- [Modifying <head>](/docs/advanced/head) - Manage meta tags, titles, and
styles
- [Vite plugin options](/docs/advanced/vite) - Configure the Fresh Vite plugin
+- [OpenTelemetry](/docs/advanced/opentelemetry) - Built-in tracing and observability
- [API Reference](/docs/advanced/api-reference) - All public exports from Fresh
- [Troubleshooting](/docs/advanced/troubleshooting) - Common issues and
solutions
diff --git a/docs/latest/advanced/opentelemetry.md b/docs/latest/advanced/opentelemetry.md
new file mode 100644
index 00000000000..0fdca4421c4
--- /dev/null
+++ b/docs/latest/advanced/opentelemetry.md
@@ -0,0 +1,45 @@
+---
+description: |
+ Fresh has built-in OpenTelemetry instrumentation for tracing requests through middleware, handlers, and rendering.
+---
+
+Fresh automatically instruments key operations with
+[OpenTelemetry](https://opentelemetry.io/) spans, giving you visibility into
+how requests flow through your application.
+
+## What's instrumented
+
+Fresh creates spans for:
+
+- **Middleware execution** — each middleware in the chain
+- **Route handler execution** — handler function calls
+- **Rendering** — server-side page rendering
+- **Static file serving** — file lookups and responses
+- **Lazy route loading** — dynamic imports of route modules
+
+## Enabling tracing
+
+Fresh uses the `@opentelemetry/api` package (the vendor-neutral API). Spans
+are created automatically — you just need to provide an OpenTelemetry SDK and
+exporter to collect them.
+
+If no exporter is configured, the spans are silently discarded (no performance
+overhead).
+
+### With Deno's built-in OpenTelemetry
+
+Deno has
+[built-in OpenTelemetry support](https://docs.deno.com/runtime/fundamentals/open_telemetry/).
+Enable it with the `OTEL_DENO` environment variable:
+
+```sh Terminal
+OTEL_DENO=true deno task start
+```
+
+This exports traces to an OTLP-compatible collector (configure the endpoint
+with `OTEL_EXPORTER_OTLP_ENDPOINT`).
+
+### With Deno Deploy
+
+[Deno Deploy](https://deno.com/deploy) collects Fresh traces automatically
+when using the Fresh preset — no configuration needed.
diff --git a/docs/toc.ts b/docs/toc.ts
index b3e84206c96..2939400a344 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -62,6 +62,7 @@ const toc: RawTableOfContents = {
["environment-variables", "Environment Variables", "link:latest"],
["head", "Modifying ", "link:latest"],
["vite", "Vite Plugin Options", "link:latest"],
+ ["opentelemetry", "OpenTelemetry", "link:latest"],
["api-reference", "API Reference", "link:latest"],
["troubleshooting", "Troubleshooting", "link:latest"],
["builder", "Builder (Legacy)", "link:latest"],
From 3adb4cc22e3f9859f59f2e76587f2f3d5a8376ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:40:51 +0100
Subject: [PATCH 29/59] docs: add PR guidelines to contributing page
The audit noted the contributing page could use PR submission guidelines.
Added a section covering deno task ok, focused PRs, test expectations,
and code style.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/contributing/index.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/latest/contributing/index.md b/docs/latest/contributing/index.md
index 27f1992c44a..b69b0412fe7 100644
--- a/docs/latest/contributing/index.md
+++ b/docs/latest/contributing/index.md
@@ -91,3 +91,12 @@ convention, and require the `-A` flag. Snapshot tests are stored in
Some tests may fail locally but pass in CI (`Could not find server address`,
`Text file busy (os error 26)`) — these can be safely ignored.
+
+## Pull Requests
+
+- Run `deno task ok` before submitting to catch formatting, lint, and type
+ errors early
+- Keep PRs focused — one feature or fix per PR
+- Add or update tests for any behavior changes
+- Follow existing code style — the repository uses `deno fmt` for formatting
+ and `deno lint` for linting
From 715a0feffcf61f7579bc7934fa26e411a191f5a2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:41:13 +0100
Subject: [PATCH 30/59] docs: document route css option in file routing
The RouteConfig css option for loading per-route CSS files was
undocumented. Added an example alongside the existing routeOverride
section.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/file-routing.md | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/docs/latest/concepts/file-routing.md b/docs/latest/concepts/file-routing.md
index c94d301fce4..e71dc8b1546 100644
--- a/docs/latest/concepts/file-routing.md
+++ b/docs/latest/concepts/file-routing.md
@@ -93,6 +93,16 @@ export const config: RouteConfig = {
// ...
```
+You can also load additional CSS files for a specific route:
+
+```ts routes/dashboard.tsx
+import { RouteConfig } from "fresh";
+
+export const config: RouteConfig = {
+ css: ["./assets/dashboard.css"],
+};
+```
+
## Route Groups
When working with [layouts](/docs/advanced/layouts) or
From 68bc424e8bb570af51389b32d0956139a1b3a563 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:50:00 +0100
Subject: [PATCH 31/59] docs: fix broken image references
- Rename getting-started-3-cotuntdown.png -> getting-started-3-countdown.png
to match the markdown reference fixed in an earlier commit
- Fix 1.x partials.md referencing /docs/1.x/fresh-partial-docs.png when
the file is at /docs/fresh-partial-docs.png
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/1.x/concepts/partials.md | 2 +-
...tuntdown.png => getting-started-3-countdown.png} | Bin
2 files changed, 1 insertion(+), 1 deletion(-)
rename www/static/docs/{getting-started-3-cotuntdown.png => getting-started-3-countdown.png} (100%)
diff --git a/docs/1.x/concepts/partials.md b/docs/1.x/concepts/partials.md
index aa8af04a7f6..1b697dcfd5b 100644
--- a/docs/1.x/concepts/partials.md
+++ b/docs/1.x/concepts/partials.md
@@ -76,7 +76,7 @@ Let's use a typical documentation page layout as an example. It often features a
main content area and a sidebar of links to switch between pages of the
documentation (marked green here).
-
+
The code for such a page (excluding styling) might look like this:
diff --git a/www/static/docs/getting-started-3-cotuntdown.png b/www/static/docs/getting-started-3-countdown.png
similarity index 100%
rename from www/static/docs/getting-started-3-cotuntdown.png
rename to www/static/docs/getting-started-3-countdown.png
From 8f1611011ba094c6eba612fbcca46005c7aa8897 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:50:53 +0100
Subject: [PATCH 32/59] ci: add doc image reference checker test
Walks all markdown files in docs/ and verifies that every image
reference () points to an existing file in www/static/.
Skips external URLs. Fails with a list of all broken references.
Run: deno test -A docs/check_images_test.ts
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/check_images_test.ts | 53 +++++++++++++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
create mode 100644 docs/check_images_test.ts
diff --git a/docs/check_images_test.ts b/docs/check_images_test.ts
new file mode 100644
index 00000000000..443648964d3
--- /dev/null
+++ b/docs/check_images_test.ts
@@ -0,0 +1,53 @@
+/**
+ * Verifies that all image references in documentation markdown files
+ * point to existing files in www/static/.
+ *
+ * Run: deno test -A docs/check_images_test.ts
+ */
+import { walk } from "jsr:@std/fs@1/walk";
+import { join, resolve } from "jsr:@std/path@1";
+
+const ROOT = resolve(import.meta.dirname!);
+const PROJECT_ROOT = resolve(ROOT, "..");
+const DOCS_DIR = ROOT;
+const STATIC_DIR = join(PROJECT_ROOT, "www", "static");
+
+const IMAGE_RE = /!\[.*?\]\(([^)]+)\)/g;
+
+Deno.test("all doc image references point to existing files", async () => {
+ const errors: string[] = [];
+
+ for await (
+ const entry of walk(DOCS_DIR, {
+ exts: [".md", ".mdx"],
+ includeDirs: false,
+ })
+ ) {
+ const content = await Deno.readTextFile(entry.path);
+ const relativePath = entry.path.slice(PROJECT_ROOT.length);
+
+ for (const match of content.matchAll(IMAGE_RE)) {
+ const imgPath = match[1];
+
+ // Skip external URLs
+ if (imgPath.startsWith("http://") || imgPath.startsWith("https://")) {
+ continue;
+ }
+
+ // Doc image paths like "/docs/foo.png" map to "www/static/docs/foo.png"
+ const fsPath = join(STATIC_DIR, imgPath);
+
+ try {
+ await Deno.stat(fsPath);
+ } catch {
+ errors.push(`${relativePath}: image not found: ${imgPath}`);
+ }
+ }
+ }
+
+ if (errors.length > 0) {
+ throw new Error(
+ `Found ${errors.length} broken image reference(s):\n\n${errors.join("\n")}`,
+ );
+ }
+});
From 2c0491ed2152d2f18d8ec4b6493bb8a577803d00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:55:04 +0100
Subject: [PATCH 33/59] chore: format check_images_test.ts
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/check_images_test.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/docs/check_images_test.ts b/docs/check_images_test.ts
index 443648964d3..e53c2ffa6d0 100644
--- a/docs/check_images_test.ts
+++ b/docs/check_images_test.ts
@@ -47,7 +47,9 @@ Deno.test("all doc image references point to existing files", async () => {
if (errors.length > 0) {
throw new Error(
- `Found ${errors.length} broken image reference(s):\n\n${errors.join("\n")}`,
+ `Found ${errors.length} broken image reference(s):\n\n${
+ errors.join("\n")
+ }`,
);
}
});
From 971ece96ae28c0691eff145de19afe1017a081d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 08:57:54 +0100
Subject: [PATCH 34/59] fmt
---
docs/latest/advanced/index.md | 3 ++-
docs/latest/advanced/opentelemetry.md | 16 ++++++++--------
docs/latest/contributing/index.md | 4 ++--
docs/latest/plugins/index.md | 4 ++--
4 files changed, 14 insertions(+), 13 deletions(-)
diff --git a/docs/latest/advanced/index.md b/docs/latest/advanced/index.md
index 149c6a9f830..c5fd0aab09d 100644
--- a/docs/latest/advanced/index.md
+++ b/docs/latest/advanced/index.md
@@ -19,7 +19,8 @@ This section of the documentation describes advanced functionality of Fresh.
- [Modifying <head>](/docs/advanced/head) - Manage meta tags, titles, and
styles
- [Vite plugin options](/docs/advanced/vite) - Configure the Fresh Vite plugin
-- [OpenTelemetry](/docs/advanced/opentelemetry) - Built-in tracing and observability
+- [OpenTelemetry](/docs/advanced/opentelemetry) - Built-in tracing and
+ observability
- [API Reference](/docs/advanced/api-reference) - All public exports from Fresh
- [Troubleshooting](/docs/advanced/troubleshooting) - Common issues and
solutions
diff --git a/docs/latest/advanced/opentelemetry.md b/docs/latest/advanced/opentelemetry.md
index 0fdca4421c4..846e0c72562 100644
--- a/docs/latest/advanced/opentelemetry.md
+++ b/docs/latest/advanced/opentelemetry.md
@@ -4,8 +4,8 @@ description: |
---
Fresh automatically instruments key operations with
-[OpenTelemetry](https://opentelemetry.io/) spans, giving you visibility into
-how requests flow through your application.
+[OpenTelemetry](https://opentelemetry.io/) spans, giving you visibility into how
+requests flow through your application.
## What's instrumented
@@ -19,8 +19,8 @@ Fresh creates spans for:
## Enabling tracing
-Fresh uses the `@opentelemetry/api` package (the vendor-neutral API). Spans
-are created automatically — you just need to provide an OpenTelemetry SDK and
+Fresh uses the `@opentelemetry/api` package (the vendor-neutral API). Spans are
+created automatically — you just need to provide an OpenTelemetry SDK and
exporter to collect them.
If no exporter is configured, the spans are silently discarded (no performance
@@ -36,10 +36,10 @@ Enable it with the `OTEL_DENO` environment variable:
OTEL_DENO=true deno task start
```
-This exports traces to an OTLP-compatible collector (configure the endpoint
-with `OTEL_EXPORTER_OTLP_ENDPOINT`).
+This exports traces to an OTLP-compatible collector (configure the endpoint with
+`OTEL_EXPORTER_OTLP_ENDPOINT`).
### With Deno Deploy
-[Deno Deploy](https://deno.com/deploy) collects Fresh traces automatically
-when using the Fresh preset — no configuration needed.
+[Deno Deploy](https://deno.com/deploy) collects Fresh traces automatically when
+using the Fresh preset — no configuration needed.
diff --git a/docs/latest/contributing/index.md b/docs/latest/contributing/index.md
index b69b0412fe7..404a4272050 100644
--- a/docs/latest/contributing/index.md
+++ b/docs/latest/contributing/index.md
@@ -98,5 +98,5 @@ Some tests may fail locally but pass in CI (`Could not find server address`,
errors early
- Keep PRs focused — one feature or fix per PR
- Add or update tests for any behavior changes
-- Follow existing code style — the repository uses `deno fmt` for formatting
- and `deno lint` for linting
+- Follow existing code style — the repository uses `deno fmt` for formatting and
+ `deno lint` for linting
diff --git a/docs/latest/plugins/index.md b/docs/latest/plugins/index.md
index a6f5462b6ef..336ed8edc48 100644
--- a/docs/latest/plugins/index.md
+++ b/docs/latest/plugins/index.md
@@ -24,8 +24,8 @@ Learn more about [middlewares](/docs/concepts/middleware).
## Creating reusable plugins
-Since Fresh plugins are just middlewares and route handlers, creating a
-reusable plugin is as simple as exporting a function that returns a middleware:
+Since Fresh plugins are just middlewares and route handlers, creating a reusable
+plugin is as simple as exporting a function that returns a middleware:
```ts plugins/request-id.ts
import type { MiddlewareFn } from "fresh";
From 542a0827677b301f9559a2d50e392b692f4f7869 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 09:09:55 +0100
Subject: [PATCH 35/59] docs: add layout overriding and multiple layouts to
programmatic layouts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Addresses #3695 — documents how to register multiple layouts for
different paths and how to use skipInheritedLayouts to override
inherited layouts for specific routes.
Closes #3695
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/layouts.md | 29 ++++++++++++++++++++++++++---
1 file changed, 26 insertions(+), 3 deletions(-)
diff --git a/docs/latest/advanced/layouts.md b/docs/latest/advanced/layouts.md
index fa043a6a956..fef66e5bfbc 100644
--- a/docs/latest/advanced/layouts.md
+++ b/docs/latest/advanced/layouts.md
@@ -36,14 +36,37 @@ If you browse to the `/` route, Fresh will render the following HTML
```
-## Options
+## Multiple layouts
-Add a layout and ignore all previously inherited ones.
+You can register multiple layouts for different paths. Layouts are inherited
+from parent paths — a layout at `"*"` applies to all routes, and more specific
+layouts are added on top:
```ts main.ts
-app.layout("/foo/bar", MyComponent, { skipInheritedLayouts: true });
+const app = new App()
+ .layout("*", MainLayout) // Applied to all routes
+ .layout("/admin/*", AdminLayout) // Added on top for /admin/* routes
+ .get("/", (ctx) => ctx.render(
));
```
+For `/admin/dashboard`, both `MainLayout` and `AdminLayout` will wrap the page
+component (MainLayout as the outer wrapper, AdminLayout as the inner).
+
+## Overriding layouts
+
+Use `skipInheritedLayouts` to replace all inherited layouts with a single one:
+
+```ts main.ts
+const app = new App()
+ .layout("*", MainLayout)
+ .layout("/landing", LandingLayout, { skipInheritedLayouts: true })
+ .get("/", (ctx) => ctx.render(
)); // Uses only LandingLayout
+```
+
+## Options
+
Ignore the app wrapper component:
```ts main.ts
From 09d1332d2f9f4c84ab5ef70f6a2946d2db67e06f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:03:30 +0100
Subject: [PATCH 36/59] docs: clarify .listen() behavior with default project
setup
Explain that .listen() conflicts with deno serve / Vite dev server
and document how to customize the port in the default setup.
Closes #3548
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/app.md | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/docs/latest/concepts/app.md b/docs/latest/concepts/app.md
index a6dafb18da3..8558eb61e9b 100644
--- a/docs/latest/concepts/app.md
+++ b/docs/latest/concepts/app.md
@@ -373,7 +373,8 @@ frameworks.
## `.listen()`
-Spawns a server and listens for incoming connections.
+Spawns a server and listens for incoming connections. This calls `Deno.serve()`
+internally.
```ts
const app = new App()
@@ -388,3 +389,15 @@ aspects.
```ts
app.listen({ port: 4000 });
```
+
+> **Important:** `.listen()` is only used when running your app directly with
+> `deno run -A main.ts`. The default project setup uses `deno task dev` (Vite
+> dev server) and `deno task start` (`deno serve`), which spawn their own
+> servers — calling `.listen()` alongside these will create a second server and
+> cause `AddrInUse` errors.
+>
+> To customize the port in the default setup:
+>
+> - **Dev:** set `server.port` in `vite.config.ts`
+> - **Prod:** pass `--port` to `deno serve` in your task, e.g.
+> `"start": "deno serve --port 4000 -A _fresh/server.js"`
From fd43eedceeb793b92db709033b2503d28dfbc1c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:21:09 +0100
Subject: [PATCH 37/59] docs: fix Docker deployment instructions for Fresh 2
- Add missing `deno install --allow-scripts` step before build
- Remove incorrect `deno cache` step (not needed with Vite build output)
- Add `deno` prefix to `serve` command in CMD
- Add install step to deployment index page
Ref #3500
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/deployment/docker.md | 9 +++++++--
docs/latest/deployment/index.md | 9 ++++++++-
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/docs/latest/deployment/docker.md b/docs/latest/deployment/docker.md
index 2054701119f..8dacda23b97 100644
--- a/docs/latest/deployment/docker.md
+++ b/docs/latest/deployment/docker.md
@@ -24,14 +24,19 @@ ENV DENO_DEPLOYMENT_ID=${GIT_REVISION}
WORKDIR /app
COPY . .
+RUN deno install --allow-scripts
RUN deno task build
-RUN deno cache _fresh/server.js
EXPOSE 8000
-CMD ["serve", "-A", "_fresh/server.js"]
+CMD ["deno", "serve", "-A", "_fresh/server.js"]
```
+> [!NOTE]
+> The `deno install --allow-scripts` step is required to populate `node_modules`
+> and run any post-install scripts needed by npm packages (e.g. Tailwind CSS).
+> This must happen before `deno task build`.
+
To build your Docker image inside of a Git repository:
```sh Terminal
diff --git a/docs/latest/deployment/index.md b/docs/latest/deployment/index.md
index d3c45f0c3c8..5043586f31a 100644
--- a/docs/latest/deployment/index.md
+++ b/docs/latest/deployment/index.md
@@ -3,7 +3,14 @@ description: "Create a production build of your app"
---
When shipping an app to production, we can run a build step that optimizes
-assets for consumption in the browser. This step can be invoked by running:
+assets for consumption in the browser. First, make sure dependencies are
+installed:
+
+```sh Terminal
+deno install --allow-scripts
+```
+
+Then run the build:
```sh Terminal
deno task build
From dc5689646c8ffa4bf988ac8fd5135ddb6d4e6e98 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:22:02 +0100
Subject: [PATCH 38/59] docs: add tip about using root-relative URLs for static
files
Ref #3482
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/static-files.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/latest/concepts/static-files.md b/docs/latest/concepts/static-files.md
index c22e9a1c266..6e03a8608cd 100644
--- a/docs/latest/concepts/static-files.md
+++ b/docs/latest/concepts/static-files.md
@@ -37,6 +37,12 @@ import "./assets/styles.css";
- Files **referenced by URL path** (favicon.ico, fonts, robots.txt, PDFs, etc.):
place in `static/`
+> [!TIP]
+> Always use root-relative URLs (starting with `/`) when referencing static
+> files in HTML. For example, use `src="/image/photo.png"` instead of
+> `src="image/photo.png"`. Relative paths resolve against the browser's current
+> URL, which breaks when navigating between routes.
+
When you import a file in your code, Vite processes it through its build
pipeline, optimizes it, and adds a content hash to the filename for cache
busting. Keeping these files outside `static/` ensures they're only included
From 7e9bff1fd6297dab6e0f02633c3f89c254ebc157 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:23:52 +0100
Subject: [PATCH 39/59] docs: clarify main.ts vs main.tsx usage
- Fix examples that showed JSX in main.ts (not valid without config)
- Add tip explaining how to enable main.tsx with serverEntry config
Closes #3476
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/app.md | 14 +++++++++-----
docs/latest/introduction/index.md | 5 ++---
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/docs/latest/concepts/app.md b/docs/latest/concepts/app.md
index 8558eb61e9b..e968df7ca60 100644
--- a/docs/latest/concepts/app.md
+++ b/docs/latest/concepts/app.md
@@ -7,16 +7,20 @@ The `App` class is the heart of Fresh and routes incoming requests to the
correct middlewares. This is where routes, middlewares, layouts and more are
defined.
-```tsx main.ts
+```ts main.ts
const app = new App()
.use(staticFiles())
- .get("/", () => new Response("hello"))
- .get("/about", (ctx) => ctx.render(
About me
));
+ .get("/", () => new Response("hello"));
// Start server
app.listen();
```
+> [!TIP]
+> To use JSX in your `main` file (e.g. with `ctx.render(
Hello
)`),
+> rename it to `main.tsx` and set `serverEntry: "main.tsx"` in the `fresh()`
+> plugin options in `vite.config.ts`.
+
## Configuration
The `App` constructor accepts an options object:
@@ -37,7 +41,7 @@ mounted alongside other apps. The base path is available in handlers via
All items are applied from top to bottom. This means that when you defined a
middleware _after_ a `.get()` handler, it won't be included.
-```tsx main.tsx
+```ts main.ts
const app = new App()
.use((ctx) => {
// Will be called for all middlewares
@@ -48,7 +52,7 @@ const app = new App()
// Will only be called for `/about`
return ctx.next();
})
- .get("/about", (ctx) => ctx.render(
About me
));
+ .get("/about", () => new Response("About me"));
```
## `.use()`
diff --git a/docs/latest/introduction/index.md b/docs/latest/introduction/index.md
index dfd1f7b847d..0a1d77795f9 100644
--- a/docs/latest/introduction/index.md
+++ b/docs/latest/introduction/index.md
@@ -9,12 +9,11 @@ Fresh is a small, fast and extensible full stack web framework built on Web
Standards. It's designed for building high-quality, performant, and personalized
web applications.
-```tsx main.tsx
+```ts main.ts
import { App } from "fresh";
const app = new App()
- .get("/", () => new Response("hello world"))
- .get("/jsx", (ctx) => ctx.render(
render JSX!
));
+ .get("/", () => new Response("hello world"));
app.listen();
```
From 7ec338cdf851296ca884d8cb5f59d5dc21a12453 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:34:15 +0100
Subject: [PATCH 40/59] docs: add catch-all redirect from /docs/canary/ to
/docs/
Replaces hardcoded canary redirects with a single catch-all since
canary docs have been merged into latest.
Closes #3375
Co-Authored-By: Claude Opus 4.6 (1M context)
---
www/routes/docs/_middleware.ts | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/www/routes/docs/_middleware.ts b/www/routes/docs/_middleware.ts
index 2a30a0ae5ed..fb49a322ac1 100644
--- a/www/routes/docs/_middleware.ts
+++ b/www/routes/docs/_middleware.ts
@@ -3,13 +3,24 @@ import type { Context } from "fresh";
const REDIRECTS: Record = {
"/docs/getting-started/fetching-data":
"/docs/getting-started/custom-handlers",
- "/docs/canary/examples/modifying-the-head": "/docs/advanced/head",
- "/docs/canary/concepts/builder": "/docs/advanced/builder",
};
export async function handler(ctx: Context) {
+ const { pathname } = ctx.url;
+
+ // Redirect /docs/canary/... to /docs/... since canary docs
+ // have been merged into latest
+ if (pathname.startsWith("/docs/canary/")) {
+ const rest = pathname.slice("/docs/canary".length);
+ const url = new URL(`/docs${rest}`, ctx.url.origin);
+ return new Response("", {
+ status: 301,
+ headers: { location: url.href },
+ });
+ }
+
// Redirect from old doc URLs to new ones
- const redirect = REDIRECTS[ctx.url.pathname];
+ const redirect = REDIRECTS[pathname];
if (redirect) {
const url = new URL(redirect, ctx.url.origin);
return new Response("", {
From 3ac8461a16b41b3f89fa9a29c62b4cfaef7bed29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:41:50 +0100
Subject: [PATCH 41/59] docs: add progressive complexity code examples to
homepage
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Shows three steps on the homepage: one-file server, adding JSX,
and adding an island — demonstrating Fresh's progressive complexity.
Ref #2755
Co-Authored-By: Claude Opus 4.6 (1M context)
---
www/components/homepage/Simple.tsx | 66 ++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/www/components/homepage/Simple.tsx b/www/components/homepage/Simple.tsx
index 07cd39c1cd0..b161d102a3b 100644
--- a/www/components/homepage/Simple.tsx
+++ b/www/components/homepage/Simple.tsx
@@ -1,5 +1,31 @@
import { PageSection } from "../../components/PageSection.tsx";
+const ONE_FILE_EXAMPLE = `import { App } from "fresh";
+
+const app = new App()
+ .get("/", () => new Response("Hello, World!"));
+
+app.listen();`;
+
+const ADD_JSX_EXAMPLE = `import { App } from "fresh";
+
+const app = new App()
+ .get("/", (ctx) => ctx.render(
Hello!
));
+
+app.listen();`;
+
+const ADD_ISLAND_EXAMPLE = `// islands/Counter.tsx
+import { useSignal } from "@preact/signals";
+
+export function Counter() {
+ const count = useSignal(0);
+ return (
+
+ );
+}`;
+
export function Simple() {
return (
@@ -13,6 +39,46 @@ export function Simple() {
tools, conventions, and web standards.
+ );
+}
From 60502ed329812e75db66d7275781a0576001ff96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:43:16 +0100
Subject: [PATCH 42/59] docs: clarify Head component dedup precedence order
Explains that the last-rendered element wins, so route
components override defaults set in _app.tsx.
Closes #2749
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/head.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/latest/advanced/head.md b/docs/latest/advanced/head.md
index 5ab4dc58c63..7a13ade001d 100644
--- a/docs/latest/advanced/head.md
+++ b/docs/latest/advanced/head.md
@@ -77,5 +77,10 @@ the matching element:
5. No matching element was found, Fresh will create a new one and append it to
``
+When multiple `` components render an element with the same key, the
+**last one rendered wins**. Since Fresh renders top-down (app wrapper → layout →
+route → page component), a route page can override defaults set in `_app.tsx`
+by using the same `key` prop.
+
> [info]: The ``-tag is automatically deduplicated, even without a `key`
> prop.
From 6e1f6601d52b69f36aa04497c0f88986663dc379 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:50:02 +0100
Subject: [PATCH 43/59] docs: add custom elements (web components) section to
islands docs
Shows how to register and use custom elements and third-party
web component libraries inside Fresh islands.
Closes #2741
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/islands.md | 64 +++++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/docs/latest/concepts/islands.md b/docs/latest/concepts/islands.md
index 43f1ac7caa6..b508236c395 100644
--- a/docs/latest/concepts/islands.md
+++ b/docs/latest/concepts/islands.md
@@ -134,3 +134,67 @@ export function MyIsland() {
return ;
}
```
+
+## Using Custom Elements (Web Components)
+
+[Custom elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)
+can be used in Fresh, but they must be registered client-side since
+`customElements.define()` is a browser API.
+
+### Registering a custom element
+
+Use an island to register and render custom elements:
+
+```tsx islands/MyElement.tsx
+import { useEffect } from "preact/hooks";
+import { IS_BROWSER } from "fresh/runtime";
+
+export function MyElement() {
+ useEffect(() => {
+ if (customElements.get("my-greeting")) return;
+
+ customElements.define(
+ "my-greeting",
+ class extends HTMLElement {
+ connectedCallback() {
+ const name = this.getAttribute("name") ?? "World";
+ this.innerHTML = `
Hello, ${name}!
`;
+ }
+ },
+ );
+ }, []);
+
+ if (!IS_BROWSER) {
+ return ;
+ }
+
+ return ;
+}
+```
+
+### Using third-party web components
+
+Third-party web component libraries work the same way — import and register
+them inside an island:
+
+```tsx islands/ThirdPartyElement.tsx
+import { useEffect } from "preact/hooks";
+import { IS_BROWSER } from "fresh/runtime";
+
+export function ShoelaceButton() {
+ useEffect(() => {
+ // Import the library's registration script
+ import("@shoelace-style/shoelace/dist/components/button/button.js");
+ }, []);
+
+ if (!IS_BROWSER) {
+ return ;
+ }
+
+ return Click me;
+}
+```
+
+> [!TIP]
+> Return a plain HTML fallback from the server-side branch (`!IS_BROWSER`) so
+> the page is usable before JavaScript loads.
From badf67072f0543f9b7413597009a9039756e37d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:55:14 +0100
Subject: [PATCH 44/59] fix: redirect direct access to recipe partial routes to
homepage
When recipe partial routes are accessed directly (not as a partial
request), redirect to /#partials instead of serving raw HTML fragments.
Closes #2559
Co-Authored-By: Claude Opus 4.6 (1M context)
---
www/components/homepage/PartialsSection.tsx | 2 +-
www/routes/recipes/lemon-honey-tea.tsx | 22 +++++++++++++++------
www/routes/recipes/lemonade.tsx | 22 +++++++++++++++------
www/routes/recipes/lemondrop.tsx | 22 +++++++++++++++------
4 files changed, 49 insertions(+), 19 deletions(-)
diff --git a/www/components/homepage/PartialsSection.tsx b/www/components/homepage/PartialsSection.tsx
index ab1db6fc683..0f7432175ac 100644
--- a/www/components/homepage/PartialsSection.tsx
+++ b/www/components/homepage/PartialsSection.tsx
@@ -33,7 +33,7 @@ export const Recipes = () => (
export function PartialsSection() {
return (
-
+ (
+export default define.page(() => (
-);
-
-export default LemonDrop;
+));
From 0c74c353391182e6e26d65388941c436029976bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 10:56:46 +0100
Subject: [PATCH 45/59] docs: rewrite cart example to avoid shared module-level
signals
Module-level signals are shared across all server requests, causing
state leaks between users. Rewrote the cart example to create signals
per-render and pass them as props, with a warning about the pitfall.
Closes #2437
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../examples/sharing-state-between-islands.md | 109 ++++++++++--------
1 file changed, 60 insertions(+), 49 deletions(-)
diff --git a/docs/latest/examples/sharing-state-between-islands.md b/docs/latest/examples/sharing-state-between-islands.md
index 15c9b590d9c..2b038425432 100644
--- a/docs/latest/examples/sharing-state-between-islands.md
+++ b/docs/latest/examples/sharing-state-between-islands.md
@@ -3,9 +3,6 @@ description: |
When you need to have state shared between islands, this page provides a few recipes.
---
-All of this content is lifted from this great
-[example](https://github.com/lucacasonato/fresh-with-signals) by Luca.
-
## Multiple Sibling Islands with Independent State
Imagine we have `Counter.tsx` like this:
@@ -87,87 +84,82 @@ export default function Home() {
they would all use the same value.
-## Independent Islands
-
-We can also create a `signal` in a utility file and export it for consumption
-across multiple places.
+## Sharing State Across Independent Islands
-```ts utils/cart.ts
-import { signal } from "@preact/signals";
-
-export const cart = signal([]);
-```
+When islands are not rendered as siblings (e.g. one in a sidebar and one in the
+main content), you can share state by creating a signal in a parent component
+and passing it as a prop to each island.
```tsx islands/AddToCart.tsx
+import { type Signal } from "@preact/signals";
import { Button } from "../components/Button.tsx";
-import { cart } from "../utils/cart.ts";
interface AddToCartProps {
+ cart: Signal;
product: string;
}
-// This island is used to add a product to the cart state.
export default function AddToCart(props: AddToCartProps) {
+ const { cart, product } = props;
return (
);
}
```
```tsx islands/Cart.tsx
+import { type Signal } from "@preact/signals";
import { Button } from "../components/Button.tsx";
-import { cart } from "../utils/cart.ts";
import * as icons from "../components/Icons.tsx";
-// This island is used to display the cart contents and remove items from it.
-export default function Cart() {
+interface CartProps {
+ cart: Signal;
+}
+
+export default function Cart(props: CartProps) {
+ const { cart } = props;
return (
-
@@ -176,13 +168,32 @@ function CartItem(props: CartItemProps) {
}
```
-Now we can add the islands to our site by doing the following:
+Then wire them together from a route, passing the same signal to both:
```tsx routes/cart.tsx
-
-
-
+import { useSignal } from "@preact/signals";
+import AddToCart from "../islands/AddToCart.tsx";
+import Cart from "../islands/Cart.tsx";
+import { define } from "../utils.ts";
+
+export default define.page(function CartPage() {
+ const cart = useSignal([]);
+ return (
+
+
+
+
+
+ );
+});
```
-What happens as a result? The `cart` signal is shared across the two `AddToCart`
-islands _and_ the `Cart` island.
+The `cart` signal is created per-render (not at module level), so each request
+gets its own independent cart. Fresh serializes the signal and passes it to both
+islands, keeping them in sync on the client.
+
+> [!CAUTION]
+> Avoid creating signals at the module level (e.g. `export const cart =
+> signal([])` in a utility file). Module-level state is shared across all
+> requests on the server, which means different users would see the same cart.
+> Always create signals inside components or handlers.
From 1b9e18463f6168d691d1a6c6d784a1c081af35ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 11:12:42 +0100
Subject: [PATCH 46/59] docs: add image optimization section to static files
docs
Covers build-time optimization with vite-imagetools, CDN image
services, and best practices for responsive images.
Closes #671
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/static-files.md | 55 ++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/docs/latest/concepts/static-files.md b/docs/latest/concepts/static-files.md
index 6e03a8608cd..57a5e4caa98 100644
--- a/docs/latest/concepts/static-files.md
+++ b/docs/latest/concepts/static-files.md
@@ -97,3 +97,58 @@ export default function Gallery() {
);
}
```
+
+## Image optimization
+
+Fresh does not include a built-in image optimization pipeline, but since Fresh 2
+uses Vite, you can use Vite plugins or external services to optimize images.
+
+### Build-time optimization with Vite
+
+[vite-imagetools](https://github.com/JonasKruckenberg/imagetools) lets you
+import images with query parameters to resize, convert formats, and generate
+`srcset` at build time:
+
+```ts
+deno add -D npm:vite-imagetools
+```
+
+```ts vite.config.ts
+import { defineConfig } from "vite";
+import { fresh } from "@fresh/plugin-vite";
+import { imagetools } from "vite-imagetools";
+
+export default defineConfig({
+ plugins: [fresh(), imagetools()],
+});
+```
+
+Then import optimized images directly:
+
+```tsx
+import heroAvif from "../static/hero.jpg?format=avif&w=800";
+
+export default function Page() {
+ return ;
+}
+```
+
+### CDN image services
+
+For dynamic optimization without a build step, use a CDN image service that
+transforms images on-the-fly:
+
+- [Cloudflare Images](https://developers.cloudflare.com/images/)
+- [imgix](https://imgix.com/)
+- [Cloudinary](https://cloudinary.com/)
+
+These services resize, compress, and convert images to modern formats (WebP,
+AVIF) based on URL parameters, with automatic caching at the edge.
+
+### Best practices
+
+- Use modern formats (WebP, AVIF) with `` fallbacks
+- Provide responsive images with `srcset` and `sizes` attributes
+- Set `width` and `height` on `` tags to prevent layout shift
+- Use `loading="lazy"` for below-the-fold images
+- Use `asset()` / `assetSrcSet()` for cache-busted URLs
From a00673e433fb1ec3b5e5501467610e204d62364d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 11:14:49 +0100
Subject: [PATCH 47/59] docs: add subdomain routing, proxying, and lazy island
patterns
- Subdomain routing via middleware + URLPattern (ref #603)
- Proxying requests to upstream servers (closes #361)
- Lazy-loading island content with Preact lazy() + Suspense (ref #565)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/examples/common-patterns.md | 77 +++++++++++++++++++++++++
1 file changed, 77 insertions(+)
diff --git a/docs/latest/examples/common-patterns.md b/docs/latest/examples/common-patterns.md
index c6f11a2a517..12066229872 100644
--- a/docs/latest/examples/common-patterns.md
+++ b/docs/latest/examples/common-patterns.md
@@ -187,6 +187,83 @@ export const handlers = define.handlers({
});
```
+## Subdomain routing
+
+Use middleware with `URLPattern` to route based on subdomains:
+
+```ts routes/_middleware.ts
+const SUBDOMAIN_PATTERN = new URLPattern({ hostname: ":sub.example.com" });
+
+export default async function handler(ctx) {
+ const match = SUBDOMAIN_PATTERN.exec(ctx.req.url);
+ if (match) {
+ const sub = match.hostname.groups["sub"];
+ ctx.state.subdomain = sub;
+
+ // Route to different handlers based on subdomain
+ if (sub === "api") {
+ return ctx.next(); // Let API routes handle it
+ }
+ if (sub !== "www") {
+ // Tenant-specific logic
+ ctx.state.tenant = await getTenant(sub);
+ }
+ }
+ return ctx.next();
+}
+```
+
+## Proxying requests
+
+Forward requests to an upstream server from a route handler:
+
+```ts routes/api/[...path].ts
+import { define } from "@/utils.ts";
+
+const UPSTREAM = "https://api.example.com";
+
+export const handlers = define.handlers({
+ async GET(ctx) {
+ const url = new URL(ctx.params.path, UPSTREAM);
+ url.search = ctx.url.search;
+
+ const response = await fetch(url, {
+ headers: ctx.req.headers,
+ });
+
+ return new Response(response.body, {
+ status: response.status,
+ headers: response.headers,
+ });
+ },
+});
+```
+
+This is useful for proxying to backend services or working around CORS
+restrictions during development.
+
+## Lazy-loading island content
+
+Use Preact's `lazy()` and `` to code-split heavy components inside
+an island, so their JavaScript is only loaded when needed:
+
+```tsx islands/HeavyFeature.tsx
+import { lazy, Suspense } from "preact/compat";
+
+const Chart = lazy(() => import("../components/Chart.tsx"));
+
+export function HeavyFeature() {
+ return (
+ Loading chart...}>
+
+
+ );
+}
+```
+
+The `Chart` component's code is split into a separate chunk and only fetched
+when `HeavyFeature` renders in the browser.
+
## Timing middleware
Measure how long request processing takes:
From 0e07166a1587d8ed1d01f322fe5ff4a17991ad75 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 11:20:14 +0100
Subject: [PATCH 48/59] chore: format docs
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/head.md | 4 ++--
docs/latest/advanced/layouts.md | 6 +++---
docs/latest/concepts/islands.md | 4 ++--
docs/latest/examples/common-patterns.md | 4 ++--
docs/latest/examples/sharing-state-between-islands.md | 9 +++++----
5 files changed, 14 insertions(+), 13 deletions(-)
diff --git a/docs/latest/advanced/head.md b/docs/latest/advanced/head.md
index 7a13ade001d..e6bdd4b6309 100644
--- a/docs/latest/advanced/head.md
+++ b/docs/latest/advanced/head.md
@@ -79,8 +79,8 @@ the matching element:
When multiple `` components render an element with the same key, the
**last one rendered wins**. Since Fresh renders top-down (app wrapper → layout →
-route → page component), a route page can override defaults set in `_app.tsx`
-by using the same `key` prop.
+route → page component), a route page can override defaults set in `_app.tsx` by
+using the same `key` prop.
> [info]: The ``-tag is automatically deduplicated, even without a `key`
> prop.
diff --git a/docs/latest/advanced/layouts.md b/docs/latest/advanced/layouts.md
index fef66e5bfbc..8042e2db15a 100644
--- a/docs/latest/advanced/layouts.md
+++ b/docs/latest/advanced/layouts.md
@@ -44,8 +44,8 @@ layouts are added on top:
```ts main.ts
const app = new App()
- .layout("*", MainLayout) // Applied to all routes
- .layout("/admin/*", AdminLayout) // Added on top for /admin/* routes
+ .layout("*", MainLayout) // Applied to all routes
+ .layout("/admin/*", AdminLayout) // Added on top for /admin/* routes
.get("/", (ctx) => ctx.render(
Home
))
.get("/admin/dashboard", (ctx) => ctx.render(
Dashboard
));
```
@@ -61,7 +61,7 @@ Use `skipInheritedLayouts` to replace all inherited layouts with a single one:
const app = new App()
.layout("*", MainLayout)
.layout("/landing", LandingLayout, { skipInheritedLayouts: true })
- .get("/", (ctx) => ctx.render(
,
});
```
diff --git a/docs/latest/concepts/architecture.md b/docs/latest/concepts/architecture.md
index cbdd31f637c..d94bab327b1 100644
--- a/docs/latest/concepts/architecture.md
+++ b/docs/latest/concepts/architecture.md
@@ -119,8 +119,9 @@ at `routes/blog/post.tsx` inherits layouts from:
1. `routes/_layout.tsx` (root layout)
2. `routes/blog/_layout.tsx` (section layout)
-The innermost layout wraps the page component, and each outer layout wraps the
-next. The app wrapper (`_app.tsx`) wraps everything.
+Layouts nest from the outside in: the root layout is outermost, each deeper
+layout wraps closer to the page, and the innermost layout directly wraps the
+page component. The app wrapper (`_app.tsx`) wraps everything.
### Build and deploy
diff --git a/docs/latest/concepts/data-fetching.md b/docs/latest/concepts/data-fetching.md
index 8e8043f6652..ffb2b966656 100644
--- a/docs/latest/concepts/data-fetching.md
+++ b/docs/latest/concepts/data-fetching.md
@@ -13,7 +13,7 @@ A handler fetches data and returns it with `page()`. The page component receives
it in `props.data`:
```tsx routes/projects/[id].tsx
-import { page } from "fresh";
+import { HttpError, page } from "fresh";
import { define } from "@/utils.ts";
interface Data {
@@ -60,6 +60,7 @@ For simpler cases, you can fetch data directly in an async component without a
separate handler:
```tsx routes/projects/[id].tsx
+import { HttpError } from "fresh";
import { define } from "@/utils.ts";
export default define.page(async (ctx) => {
@@ -96,12 +97,19 @@ export default define.middleware(async (ctx) => {
```
```tsx routes/dashboard.tsx
+import { page } from "fresh";
import { define } from "@/utils.ts";
+export const handler = define.handlers({
+ GET(ctx) {
+ if (!ctx.state.user) {
+ return ctx.redirect("/login");
+ }
+ return page();
+ },
+});
+
export default define.page((ctx) => {
- if (!ctx.state.user) {
- return ctx.redirect("/login");
- }
return
Welcome, {ctx.state.user.name}
;
});
```
@@ -117,7 +125,9 @@ Page components receive these properties:
| `params` | `Record` | Route parameters (e.g. `:id`) |
| `req` | `Request` | The original HTTP request |
| `state` | `State` | Shared state set by middleware |
+| `config` | `ResolvedFreshConfig` | The resolved Fresh configuration |
| `route` | `string \| null` | The matched route pattern |
+| `info` | `Deno.ServeHandlerInfo` | Server connection info |
| `error` | `unknown \| null` | Caught error (on error pages) |
| `isPartial` | `boolean` | Whether this is a partial request |
| `Component` | `FunctionComponent` | Child component (in layouts) |
diff --git a/docs/latest/concepts/file-routing.md b/docs/latest/concepts/file-routing.md
index e71dc8b1546..18f2f7ec8b0 100644
--- a/docs/latest/concepts/file-routing.md
+++ b/docs/latest/concepts/file-routing.md
@@ -93,14 +93,11 @@ export const config: RouteConfig = {
// ...
```
-You can also load additional CSS files for a specific route:
+You can also load additional CSS files for a specific route by exporting a `css`
+array. This is a top-level export, separate from `config`:
```ts routes/dashboard.tsx
-import { RouteConfig } from "fresh";
-
-export const config: RouteConfig = {
- css: ["./assets/dashboard.css"],
-};
+export const css = ["./assets/dashboard.css"];
```
## Route Groups
diff --git a/docs/latest/concepts/signals.md b/docs/latest/concepts/signals.md
index 9300f51fbc9..1e8b6b4680e 100644
--- a/docs/latest/concepts/signals.md
+++ b/docs/latest/concepts/signals.md
@@ -82,6 +82,12 @@ Both sliders share the same signal — moving one updates the other. When the sa
signal object is passed to multiple islands, Fresh preserves the reference so
they stay synchronized.
+> [!NOTE]
+> Using `useSignal` in a route component (not an island) is intentional here.
+> The signal is created during server rendering, serialized into the HTML, and
+> reconstructed as a live signal on the client. This is how Fresh shares
+> reactive state between multiple islands on the same page.
+
## Shared state across islands
For state that needs to be shared between unrelated islands, create a signal in
diff --git a/docs/latest/examples/common-patterns.md b/docs/latest/examples/common-patterns.md
index f54945677d8..8ac42eef7b0 100644
--- a/docs/latest/examples/common-patterns.md
+++ b/docs/latest/examples/common-patterns.md
@@ -52,6 +52,7 @@ export default function handler(ctx) {
Return different formats based on the `Accept` header:
```ts routes/api/users/[id].ts
+import { HttpError } from "fresh";
import { define } from "@/utils.ts";
export const handlers = define.handlers({
@@ -105,13 +106,14 @@ session example.
Access URL search params from the context:
```ts routes/search.tsx
+import { page } from "fresh";
import { define } from "@/utils.ts";
export const handlers = define.handlers({
GET(ctx) {
const query = ctx.url.searchParams.get("q") ?? "";
- const page = Number(ctx.url.searchParams.get("page") ?? "1");
- const results = search(query, page);
+ const pageNum = Number(ctx.url.searchParams.get("page") ?? "1");
+ const results = search(query, pageNum);
return page({ query, results });
},
});
@@ -145,6 +147,8 @@ return page(data, {
Return a streaming response from a handler:
```ts routes/api/stream.ts
+import { define } from "@/utils.ts";
+
export const handlers = define.handlers({
GET() {
const body = new ReadableStream({
From 92f074465d1cb6dc80843c37ab6ff4b9093f67a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 12:00:34 +0100
Subject: [PATCH 52/59] docs: fix remaining review issues
- Fix app.onError() missing path arg in error-handling.md
- Add missing HttpError import in error-handling.md
- Add missing define imports in WebSocket, subdomain, timing examples
- Standardize on `handler` (singular) for all export names
- Convert all untyped middleware to define.middleware()
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/error-handling.md | 4 ++-
docs/latest/concepts/data-fetching.md | 8 ++---
docs/latest/examples/common-patterns.md | 43 ++++++++++++++++---------
3 files changed, 34 insertions(+), 21 deletions(-)
diff --git a/docs/latest/advanced/error-handling.md b/docs/latest/advanced/error-handling.md
index f37f2fcdd50..94382f8c4b6 100644
--- a/docs/latest/advanced/error-handling.md
+++ b/docs/latest/advanced/error-handling.md
@@ -103,7 +103,9 @@ When an `HttpError` is thrown, Fresh catches it and invokes the error handler.
You can check the status code in your error handler:
```ts main.ts
-app.onError((ctx) => {
+import { HttpError } from "fresh";
+
+app.onError("*", (ctx) => {
if (ctx.error instanceof HttpError) {
const status = ctx.error.status;
return new Response("oops", { status });
diff --git a/docs/latest/concepts/data-fetching.md b/docs/latest/concepts/data-fetching.md
index ffb2b966656..cf49abeeaea 100644
--- a/docs/latest/concepts/data-fetching.md
+++ b/docs/latest/concepts/data-fetching.md
@@ -20,7 +20,7 @@ interface Data {
project: { name: string; stars: number };
}
-export const handlers = define.handlers({
+export const handler = define.handlers({
async GET(ctx) {
const project = await db.projects.findOne(ctx.params.id);
if (!project) {
@@ -30,7 +30,7 @@ export const handlers = define.handlers({
},
});
-export default define.page(({ data }) => {
+export default define.page(({ data }) => {
return (
{data.project.name}
@@ -40,8 +40,8 @@ export default define.page(({ data }) => {
});
```
-The `define.page` generic links the handler's return type to
-the component's props, giving you full autocompletion on `data`.
+The `define.page` generic links the handler's return type to the
+component's props, giving you full autocompletion on `data`.
## Setting response headers and status
diff --git a/docs/latest/examples/common-patterns.md b/docs/latest/examples/common-patterns.md
index 8ac42eef7b0..c60dba1e209 100644
--- a/docs/latest/examples/common-patterns.md
+++ b/docs/latest/examples/common-patterns.md
@@ -30,18 +30,20 @@ available in any downstream handler or component via `ctx.state.user`.
Handle URL migrations with middleware:
```ts routes/_middleware.ts
+import { define } from "@/utils.ts";
+
const REDIRECTS: Record = {
"/old-page": "/new-page",
"/blog/old-slug": "/blog/new-slug",
};
-export default function handler(ctx) {
+export default define.middleware((ctx) => {
const redirect = REDIRECTS[ctx.url.pathname];
if (redirect) {
return ctx.redirect(redirect, 301);
}
return ctx.next();
-}
+});
```
> [info]: `ctx.redirect()` includes protection against open redirect attacks.
@@ -55,7 +57,7 @@ Return different formats based on the `Accept` header:
import { HttpError } from "fresh";
import { define } from "@/utils.ts";
-export const handlers = define.handlers({
+export const handler = define.handlers({
async GET(ctx) {
const user = await db.users.find(ctx.params.id);
if (!user) {
@@ -77,8 +79,9 @@ Use the `@std/http` cookie utilities:
```ts routes/_middleware.ts
import { getCookies, setCookie } from "@std/http";
+import { define } from "@/utils.ts";
-export default async function handler(ctx) {
+export default define.middleware(async (ctx) => {
const cookies = getCookies(ctx.req.headers);
ctx.state.theme = cookies["theme"] ?? "light";
@@ -95,7 +98,7 @@ export default async function handler(ctx) {
});
return response;
-}
+});
```
See [Session management](/docs/examples/session-management) for a complete
@@ -109,7 +112,7 @@ Access URL search params from the context:
import { page } from "fresh";
import { define } from "@/utils.ts";
-export const handlers = define.handlers({
+export const handler = define.handlers({
GET(ctx) {
const query = ctx.url.searchParams.get("q") ?? "";
const pageNum = Number(ctx.url.searchParams.get("page") ?? "1");
@@ -121,15 +124,17 @@ export const handlers = define.handlers({
## Adding response headers
-Set custom headers in middleware or handlers:
+Set custom headers in middleware:
```ts routes/_middleware.ts
-export default async function handler(ctx) {
+import { define } from "@/utils.ts";
+
+export default define.middleware(async (ctx) => {
const response = await ctx.next();
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
return response;
-}
+});
```
Or set headers on a specific route using `page()`:
@@ -149,7 +154,7 @@ Return a streaming response from a handler:
```ts routes/api/stream.ts
import { define } from "@/utils.ts";
-export const handlers = define.handlers({
+export const handler = define.handlers({
GET() {
const body = new ReadableStream({
start(controller) {
@@ -172,7 +177,9 @@ export const handlers = define.handlers({
Fresh runs on Deno, so you can upgrade HTTP connections to WebSockets directly:
```ts routes/api/ws.ts
-export const handlers = define.handlers({
+import { define } from "@/utils.ts";
+
+export const handler = define.handlers({
GET(ctx) {
const { socket, response } = Deno.upgradeWebSocket(ctx.req);
@@ -196,9 +203,11 @@ export const handlers = define.handlers({
Use middleware with `URLPattern` to route based on subdomains:
```ts routes/_middleware.ts
+import { define } from "@/utils.ts";
+
const SUBDOMAIN_PATTERN = new URLPattern({ hostname: ":sub.example.com" });
-export default async function handler(ctx) {
+export default define.middleware(async (ctx) => {
const match = SUBDOMAIN_PATTERN.exec(ctx.req.url);
if (match) {
const sub = match.hostname.groups["sub"];
@@ -214,7 +223,7 @@ export default async function handler(ctx) {
}
}
return ctx.next();
-}
+});
```
## Proxying requests
@@ -226,7 +235,7 @@ import { define } from "@/utils.ts";
const UPSTREAM = "https://api.example.com";
-export const handlers = define.handlers({
+export const handler = define.handlers({
async GET(ctx) {
const url = new URL(ctx.params.path, UPSTREAM);
url.search = ctx.url.search;
@@ -273,11 +282,13 @@ when `HeavyFeature` renders in the browser.
Measure how long request processing takes:
```ts routes/_middleware.ts
-export default async function handler(ctx) {
+import { define } from "@/utils.ts";
+
+export default define.middleware(async (ctx) => {
const start = performance.now();
const response = await ctx.next();
const duration = performance.now() - start;
response.headers.set("Server-Timing", `total;dur=${duration.toFixed(1)}`);
return response;
-}
+});
```
From cb0127205b240fff7d45f47b0780a84443322154 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 12:03:46 +0100
Subject: [PATCH 53/59] docs: link to JSR auto-generated API docs
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/api-reference.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/latest/advanced/api-reference.md b/docs/latest/advanced/api-reference.md
index 31c91029b98..9aa76c05efb 100644
--- a/docs/latest/advanced/api-reference.md
+++ b/docs/latest/advanced/api-reference.md
@@ -5,6 +5,9 @@ description: |
This page lists all public exports from Fresh's entry points.
+> [info]: You can also explore Fresh's full API documentation on JSR:
+> [`@fresh/core`](https://jsr.io/@fresh/core/doc)
+
## `fresh`
The main entry point for server-side code.
From 16147069a7691a8dd185d46661847129afd9012c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 15:21:42 +0100
Subject: [PATCH 54/59] docs: replace ASCII art with HTML/CSS flowchart in
architecture page
The ASCII art request lifecycle diagram had broken box alignment.
Replace it with an inline HTML/CSS flowchart that renders cleanly.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/concepts/architecture.md | 145 ++++++++++++++++-----------
1 file changed, 85 insertions(+), 60 deletions(-)
diff --git a/docs/latest/concepts/architecture.md b/docs/latest/concepts/architecture.md
index d94bab327b1..66c72600a09 100644
--- a/docs/latest/concepts/architecture.md
+++ b/docs/latest/concepts/architecture.md
@@ -9,66 +9,91 @@ explains how a request flows through the framework.
## Request lifecycle
-```
- Browser Request
- │
- ▼
- ┌─────────────┐
- │ Static Files │──▶ Serve file directly (if match)
- └─────┬───────┘
- │ (no match)
- ▼
- ┌─────────────┐
- │ Middlewares │──▶ Run in registration order
- │ (global) │ Can modify request/response, set state,
- └─────┬───────┘ or short-circuit with a Response
- │
- ▼
- ┌─────────────┐
- │ Router │──▶ Match URL pattern + HTTP method
- └─────┬───────┘ Static routes checked first, then dynamic
- │
- ▼
- ┌─────────────┐
- │ Middlewares │──▶ Path-specific middlewares
- │ (scoped) │
- └─────┬───────┘
- │
- ▼
- ┌─────────────┐ ┌────────────┐
- │ Handler │────▶│ API route │──▶ Return Response (JSON, etc.)
- └─────┬───────┘ └────────────┘
- │ (has component)
- ▼
- ┌─────────────┐
- │ App Wrapper │──▶ Outer // structure
- └─────┬───────┘
- │
- ▼
- ┌─────────────┐
- │ Layouts │──▶ Nested layout components (inherited)
- └─────┬───────┘
- │
- ▼
- ┌─────────────┐
- │ Page │──▶ Route component with props.data
- │ Component │
- └─────┬───────┘
- │
- ▼
- ┌─────────────┐
- │ HTML + JS │──▶ Server-rendered HTML sent to browser
- │ Response │ Island props serialized inline
- └─────────────┘
-
- Browser
- │
- ▼
- ┌─────────────┐
- │ Hydration │──▶ Only islands receive JavaScript
- │ (Islands) │ Rest of the page stays static HTML
- └─────────────┘
-```
+
+
+
+
+
Browser Request
+
+
↓
+
+
+
Static Files
+
Serve file directly if path matches a static asset
+
+
no match
+
↓
+
+
+
Global Middlewareruns in registration order
+
Can modify request/response, set state, or short‑circuit with a Response
+
+
↓
+
+
+
Router
+
Match URL pattern + HTTP method. Static routes checked first, then dynamic.
+
+
↓
+
+
+
Scoped Middleware
+
Path‑specific middleware
+
+
↓
+
+
+
Handler
+
API route? Return Response directly (JSON, redirect, etc.)
+
+
has component
+
↓
+
+
Rendering
+
+
+
App Wrapper
+
Outer <html>/<head>/<body> structure
+
+
↓
+
+
+
Layouts
+
Nested layout components (inherited from parent directories)
+
+
↓
+
+
+
Page Component
+
Route component receives props.data from handler
+
+
↓
+
+
+
HTML + JS Responsesent to browser
+
Server‑rendered HTML with island props serialized inline
+
+
+
Client (Browser)
+
↓
+
+
+
Island Hydrationonly islands receive JS
+
Rest of the page stays static HTML. No JavaScript for non‑island components.
+
+
## Key concepts
From 6a1b5079afe439d5112e55d6457170179b611650 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?=
Date: Sat, 28 Mar 2026 16:24:03 +0100
Subject: [PATCH 55/59] docs: replace em dashes, add cross-links, update
introduction and architecture
- Replace em dashes with hyphens across all changed docs
- Add ~49 cross-links between doc pages for better discoverability
- Restore JSX example on introduction page, remove Requirements section
- Rework "Where to host" section, add AI agent mention to "When to use"
- Expand scoped middleware description in architecture diagram
Co-Authored-By: Claude Opus 4.6 (1M context)
---
docs/latest/advanced/define.md | 4 +-
docs/latest/advanced/environment-variables.md | 2 +-
docs/latest/advanced/error-handling.md | 5 ++-
docs/latest/advanced/head.md | 11 +++---
docs/latest/advanced/layouts.md | 4 +-
docs/latest/advanced/opentelemetry.md | 17 ++++----
docs/latest/advanced/serialization.md | 27 ++++++-------
docs/latest/advanced/troubleshooting.md | 2 +-
docs/latest/advanced/vite.md | 3 +-
docs/latest/concepts/app.md | 10 ++---
docs/latest/concepts/architecture.md | 39 +++++++++++++------
docs/latest/concepts/context.md | 8 ++--
docs/latest/concepts/data-fetching.md | 4 +-
docs/latest/concepts/file-routing.md | 10 ++---
docs/latest/concepts/islands.md | 9 +++--
docs/latest/concepts/layouts.md | 2 +-
docs/latest/concepts/middleware.md | 5 ++-
docs/latest/concepts/routing.md | 2 +-
docs/latest/concepts/signals.md | 14 +++----
docs/latest/concepts/static-files.md | 6 +--
docs/latest/contributing/index.md | 6 +--
docs/latest/deployment/cloudflare-workers.md | 3 +-
docs/latest/deployment/deno-deploy.md | 8 ++--
docs/latest/deployment/docker.md | 14 ++++---
docs/latest/deployment/index.md | 4 +-
docs/latest/examples/api-routes.md | 5 ++-
docs/latest/examples/common-patterns.md | 3 +-
docs/latest/examples/migration-guide.md | 9 +++--
docs/latest/examples/rendering-raw-html.md | 4 +-
.../examples/sharing-state-between-islands.md | 4 +-
docs/latest/getting-started/index.md | 6 +--
docs/latest/introduction/index.md | 30 +++++++-------
docs/latest/plugins/index.md | 4 +-
tools/check_links.ts | 4 +-
www/components/homepage/PartialsSection.tsx | 2 +-
35 files changed, 161 insertions(+), 129 deletions(-)
diff --git a/docs/latest/advanced/define.md b/docs/latest/advanced/define.md
index 73a9d2eefa2..afe587caa79 100644
--- a/docs/latest/advanced/define.md
+++ b/docs/latest/advanced/define.md
@@ -1,6 +1,6 @@
---
description: |
- Define helpers are a less TypeScripty way to declare middlewares, routes and layouts
+ Define helpers are a less TypeScripty way to declare [middlewares](/docs/concepts/middleware), routes and [layouts](/docs/concepts/layouts)
---
Define helpers can be used to shorten the amount of types you have to type
@@ -72,7 +72,7 @@ export default define.page((props) => {
});
```
-There is also a `define.layout()` helper for layouts:
+There is also a `define.layout()` helper for [layouts](/docs/concepts/layouts):
```tsx
export default define.layout((props) => {
diff --git a/docs/latest/advanced/environment-variables.md b/docs/latest/advanced/environment-variables.md
index 463fbf39aff..b5c527f78f6 100644
--- a/docs/latest/advanced/environment-variables.md
+++ b/docs/latest/advanced/environment-variables.md
@@ -9,7 +9,7 @@ see
[how to use Environment Variables in Deno](https://docs.deno.com/runtime/reference/env_variables/).
On top of that Fresh automatically inlines all environment variables whose names
-start with `FRESH_PUBLIC_` during bundling of islands.
+start with `FRESH_PUBLIC_` during bundling of [islands](/docs/concepts/islands).
> [info]: This inlining step occurs when building the app (`deno task build`).
> Environment variables inside islands cannot be read at runtime.
diff --git a/docs/latest/advanced/error-handling.md b/docs/latest/advanced/error-handling.md
index 94382f8c4b6..6056d7fd45e 100644
--- a/docs/latest/advanced/error-handling.md
+++ b/docs/latest/advanced/error-handling.md
@@ -66,7 +66,8 @@ const app = new App()
```
Accessing an unknown route like `/invalid` will trigger the `notFound`
-middleware. Contrary to generic error pages this handler cannot be nested.
+[middleware](/docs/concepts/middleware). Contrary to generic error pages this
+handler cannot be nested.
## Throwing HTTP errors
@@ -116,4 +117,4 @@ app.onError("*", (ctx) => {
```
`HttpError` is also available in the browser via `fresh/runtime` for use in
-island code.
+[island code](/docs/concepts/islands).
diff --git a/docs/latest/advanced/head.md b/docs/latest/advanced/head.md
index e6bdd4b6309..aa4ab667e88 100644
--- a/docs/latest/advanced/head.md
+++ b/docs/latest/advanced/head.md
@@ -12,12 +12,13 @@ is a crucial element in HTML to set metadata for a page. It allows you to:
- Include JavaScript code with `