Skip to content

Commit 14b7827

Browse files
committed
Merge branch 'main' into v2.3.0
2 parents 70357eb + 0ab5a2a commit 14b7827

53 files changed

Lines changed: 2034 additions & 250 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
2323
test:
2424
runs-on: ${{ matrix.os }}
25-
timeout-minutes: 10
25+
timeout-minutes: 15
2626

2727
strategy:
2828
fail-fast: false
@@ -39,7 +39,7 @@ jobs:
3939

4040
steps:
4141
- name: Checkout repo
42-
uses: actions/checkout@v4
42+
uses: actions/checkout@v6
4343

4444
- name: Setup Deno
4545
uses: denoland/setup-deno@v2

.github/workflows/post_publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@ jobs:
1111
if: github.repository == 'denoland/fresh'
1212
steps:
1313
- name: Checkout repo
14-
uses: actions/checkout@v4
14+
uses: actions/checkout@v6
1515

1616
- name: Authenticate with Google Cloud
17-
uses: google-github-actions/auth@v2
17+
uses: google-github-actions/auth@v3
1818
with:
1919
project_id: denoland
2020
credentials_json: ${{ secrets.GCP_SA_KEY }}
2121
export_environment_variables: true
2222
create_credentials_file: true
2323

2424
- name: Setup gcloud
25-
uses: google-github-actions/setup-gcloud@v2
25+
uses: google-github-actions/setup-gcloud@v3
2626
with:
2727
project_id: denoland
2828

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
if: github.repository_owner == 'denoland'
1717

1818
steps:
19-
- uses: actions/checkout@v4
19+
- uses: actions/checkout@v6
2020

2121
- name: Install Deno
2222
uses: denoland/setup-deno@v2

docs/latest/advanced/builder.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ const builder = new Builder({
4040
// Where to write generated files when doing a production build.
4141
// (default: `<root>/_fresh/`)
4242
outDir?: string;
43-
// Path to static file directory. (Default: `<root>/static/`)
44-
staticDir?: string;
43+
// Path to static file directory, or an array of directories.
44+
// When multiple directories are specified they are searched in order
45+
// and the first match wins. (Default: `<root>/static/`)
46+
staticDir?: string | string[];
4547
// Path to island directory. (Default: `<root>/islands`)
4648
islandDir?: string;
4749
// Path to routes directory. (Default: `<root>/routes`)
@@ -93,8 +95,23 @@ builder.onTransformStaticFile({
9395
});
9496
```
9597

96-
> [info]: Only static files in `static/` or the value you set `staticDir` to
97-
> will be processed. The builder won't process anything else.
98+
> [info]: Only static files in `static/` or the directories you set `staticDir`
99+
> to will be processed. The builder won't process anything else.
100+
101+
### Multiple static directories
102+
103+
You can pass an array to `staticDir` to serve files from multiple directories.
104+
When the same filename exists in more than one directory, the first directory in
105+
the array takes precedence.
106+
107+
```ts dev.ts
108+
const builder = new Builder({
109+
staticDir: ["static", "generated"],
110+
});
111+
```
112+
113+
This is useful when you have a build step that generates assets into a separate
114+
directory and you want to keep them apart from hand-authored static files.
98115

99116
## Testing
100117

docs/latest/advanced/partials.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,66 @@ Partial updates can be animated using the browser's
208208
alongside `f-client-nav` to enable smooth animated transitions between pages
209209
with zero JavaScript animation code.
210210

211+
## Loading indicators
212+
213+
When a partial request is in flight, you may want to show a loading spinner or
214+
disable a button. Fresh supports this through the `_freshIndicator` property.
215+
216+
Attach an object with a `value` property to any element that triggers a partial
217+
navigation. Fresh will set `value` to `true` when the request starts and back to
218+
`false` when it completes (or fails).
219+
220+
```tsx
221+
import { useSignal } from "@preact/signals";
222+
223+
function NavLink() {
224+
const loading = useSignal(false);
225+
226+
return (
227+
<a
228+
href="/next-page"
229+
f-partial="/partials/next-page"
230+
ref={(el) => {
231+
if (el) el._freshIndicator = loading;
232+
}}
233+
>
234+
{loading.value ? "Loading..." : "Go"}
235+
</a>
236+
);
237+
}
238+
```
239+
240+
This works for links, forms, and submit buttons. For form submissions, Fresh
241+
checks the submitter element (e.g. the clicked button) first, then falls back to
242+
the form element itself. This lets you show per-button indicators when a form
243+
has multiple submit buttons.
244+
245+
```tsx
246+
import { useSignal } from "@preact/signals";
247+
248+
function MyForm() {
249+
const saving = useSignal(false);
250+
251+
return (
252+
<form action="/save" f-partial="/partials/save">
253+
{/* indicator is on the button, not the form */}
254+
<button
255+
type="submit"
256+
ref={(el) => {
257+
if (el) el._freshIndicator = saving;
258+
}}
259+
>
260+
{saving.value ? "Saving..." : "Save"}
261+
</button>
262+
</form>
263+
);
264+
}
265+
```
266+
267+
> [info]: Any object with a mutable `value` property works — Preact signals are
268+
> the most convenient choice because they automatically re-render the component
269+
> when the value changes.
270+
211271
## Bypassing or disabling Partials
212272

213273
If you want to exempt a particular element from triggering a partial request

docs/latest/advanced/vite.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export default defineConfig({
2727
islandsDir: "./islands",
2828
// Path to routes directory. Default: ./routes
2929
routeDir: "./routes",
30+
// Static file directory or directories. Default: "static"
31+
// When multiple directories are given, they are searched in
32+
// order and the first match wins.
33+
staticDir: ["static", "generated"],
3034
// Optional regex to ignore folders when crawling the routes and
3135
// island directory.
3236
ignore: [/[\\/]+some-folder[\\/]+/],

docs/latest/concepts/app.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ With `basePath: "/my-app"`, a route registered at `/about` will respond to
3737
mounted alongside other apps. The base path is available in handlers via
3838
`ctx.config.basePath`.
3939

40+
### Reverse proxy support
41+
42+
When running behind a reverse proxy (nginx, Caddy, etc.), set `trustProxy` to
43+
make `ctx.url` reflect the client-facing URL instead of the internal one:
44+
45+
```ts
46+
const app = new App({ trustProxy: true });
47+
```
48+
49+
With this enabled, Fresh reads `X-Forwarded-Proto` and `X-Forwarded-Host`
50+
headers and rewrites `ctx.url` accordingly. For example, if your proxy
51+
terminates TLS and forwards `X-Forwarded-Proto: https`, `ctx.url.protocol` will
52+
be `https:` instead of `http:`.
53+
54+
> [warn]: Only enable `trustProxy` when your app is actually behind a trusted
55+
> reverse proxy. Untrusted clients could otherwise spoof these headers.
56+
4057
All items are applied from top to bottom. This means that when you defined a
4158
middleware _after_ a `.get()` handler, it won't be included.
4259

docs/latest/concepts/architecture.md

Lines changed: 1 addition & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -9,91 +9,7 @@ browser. This page explains how a request flows through the framework.
99

1010
## Request lifecycle
1111

12-
<div style="max-width:620px;font-family:system-ui,-apple-system,sans-serif;font-size:14px;color:#1a1a1a;">
13-
<style>
14-
.flow-step{display:flex;align-items:flex-start;gap:16px}
15-
.flow-box{flex-shrink:0;width:200px;padding:10px 14px;border:1.5px solid #333;border-radius:6px;background:#f8f9fa;text-align:center}
16-
.flow-box strong{display:block;font-size:14px}
17-
.flow-box small{font-size:11px;color:#777}
18-
.flow-box.accent{background:#eef6ff}
19-
.flow-box.green{background:#f0faf0}
20-
.flow-desc{padding-top:10px;font-size:12.5px;color:#555;line-height:1.5}
21-
.flow-arrow{text-align:center;width:200px;color:#666;font-size:18px;line-height:1;padding:4px 0}
22-
.flow-arrow-label{text-align:center;width:200px;padding:2px 0}
23-
.flow-arrow-label em{font-size:11px;color:#777}
24-
.flow-divider{margin:12px 0 8px;padding-top:8px;border-top:1px dashed #e0e0e0;font-size:11px;color:#999;font-weight:600;letter-spacing:0.5px;text-transform:uppercase}
25-
</style>
26-
<!-- Browser Request -->
27-
<div class="flow-step">
28-
<div class="flow-box accent"><strong>Browser Request</strong></div>
29-
</div>
30-
<div class="flow-arrow">&#x2193;</div>
31-
<!-- Static Files -->
32-
<div class="flow-step">
33-
<div class="flow-box"><strong>Static Files</strong></div>
34-
<div class="flow-desc">Serve file directly if path matches a static asset</div>
35-
</div>
36-
<div class="flow-arrow-label"><em>no match</em></div>
37-
<div class="flow-arrow">&#x2193;</div>
38-
<!-- Global Middleware -->
39-
<div class="flow-step">
40-
<div class="flow-box"><strong>Global Middleware</strong><small>runs in registration order</small></div>
41-
<div class="flow-desc">Can modify request/response, set state, or short&#8209;circuit with a Response</div>
42-
</div>
43-
<div class="flow-arrow">&#x2193;</div>
44-
<!-- Router -->
45-
<div class="flow-step">
46-
<div class="flow-box"><strong>Router</strong></div>
47-
<div class="flow-desc">Match URL pattern + HTTP method. Static routes checked first, then dynamic.</div>
48-
</div>
49-
<div class="flow-arrow">&#x2193;</div>
50-
<!-- Scoped Middleware -->
51-
<div class="flow-step">
52-
<div class="flow-box"><strong>Scoped Middleware</strong></div>
53-
<div class="flow-desc">Middleware that only runs for matching path prefixes (e.g. /admin/*). Used for auth, logging, or other route&#8209;specific logic.</div>
54-
</div>
55-
<div class="flow-arrow">&#x2193;</div>
56-
<!-- Handler -->
57-
<div class="flow-step">
58-
<div class="flow-box"><strong>Handler</strong></div>
59-
<div class="flow-desc">API route? Return Response directly (JSON, redirect, etc.)</div>
60-
</div>
61-
<div class="flow-arrow-label"><em>has component</em></div>
62-
<div class="flow-arrow">&#x2193;</div>
63-
<!-- Rendering section -->
64-
<div class="flow-divider">Rendering</div>
65-
<!-- App Wrapper -->
66-
<div class="flow-step">
67-
<div class="flow-box"><strong>App Wrapper</strong></div>
68-
<div class="flow-desc">Outer &lt;html&gt;/&lt;head&gt;/&lt;body&gt; structure</div>
69-
</div>
70-
<div class="flow-arrow">&#x2193;</div>
71-
<!-- Layouts -->
72-
<div class="flow-step">
73-
<div class="flow-box"><strong>Layouts</strong></div>
74-
<div class="flow-desc">Nested layout components (inherited from parent directories)</div>
75-
</div>
76-
<div class="flow-arrow">&#x2193;</div>
77-
<!-- Page Component -->
78-
<div class="flow-step">
79-
<div class="flow-box"><strong>Page Component</strong></div>
80-
<div class="flow-desc">Route component receives props.data from handler</div>
81-
</div>
82-
<div class="flow-arrow">&#x2193;</div>
83-
<!-- HTML Response -->
84-
<div class="flow-step">
85-
<div class="flow-box accent"><strong>HTML + JS Response</strong><small>sent to browser</small></div>
86-
<div class="flow-desc">Server&#8209;rendered HTML with island props <a href="/docs/advanced/serialization">serialized</a> inline</div>
87-
</div>
88-
<!-- Client section -->
89-
<div class="flow-divider">Client (Browser)</div>
90-
<div class="flow-arrow">&#x2193;</div>
91-
<!-- Hydration -->
92-
<div class="flow-step">
93-
<div class="flow-box green"><strong>Island Hydration</strong><small>only islands receive JS</small></div>
94-
<div class="flow-desc">Rest of the page stays static HTML. No JavaScript for non&#8209;island components.</div>
95-
</div>
96-
</div>
12+
![Request lifecycle flow diagram](/docs/architecture-flow-v2.svg)
9713

9814
## Key concepts
9915

docs/latest/concepts/static-files.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,31 @@ pipeline, optimizes it, and adds a content hash to the filename for cache
4747
busting. Keeping these files outside `static/` ensures they're only included
4848
once in your build output.
4949

50+
## Multiple static directories
51+
52+
You can serve files from more than one directory by passing an array to the
53+
`staticDir` option. When the same filename exists in multiple directories, the
54+
first directory in the array takes precedence.
55+
56+
```ts vite.config.ts
57+
import { defineConfig } from "vite";
58+
import { fresh } from "@fresh/plugin-vite";
59+
60+
export default defineConfig({
61+
plugins: [
62+
fresh({
63+
staticDir: ["static", "generated"],
64+
}),
65+
],
66+
});
67+
```
68+
69+
This is useful when you have a build step that generates assets into a separate
70+
directory and you want to keep them apart from hand-authored static files.
71+
72+
> [info]: If you're using the [Builder](/docs/advanced/builder) API instead of
73+
> Vite, the same `staticDir` option accepts a string or an array of strings.
74+
5075
## Caching headers
5176

5277
By default, Fresh adds caching headers for the `src` and `srcset` attributes on

docs/latest/examples/active-links.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,27 @@ current page within a set of pages.
1010

1111
- `aria-current="page"` - Added to links with an exact path match, enhancing
1212
accessibility by indicating the current page to assistive technologies.
13+
- `aria-current="true"` - Added to ancestor links (e.g. `/docs` when the current
14+
page is `/docs/intro`).
1315

1416
As we aim to improve accessibility, we encourage the use of aria-current for
1517
styling current links where applicable.
1618

19+
### Query parameters
20+
21+
When a link's `href` includes query parameters, Fresh considers them during
22+
matching. A link to `/products?sort=name` will only receive
23+
`aria-current="page"` when the current URL also has `?sort=name`. If the query
24+
parameters differ, the link is treated as an ancestor instead. Links without
25+
query parameters in their `href` match regardless of the current URL's query
26+
string.
27+
28+
### Preserving custom `aria-current`
29+
30+
If you set `aria-current` on an `<a>` element yourself, Fresh will leave it
31+
untouched. This is useful when integrating with component libraries (e.g.
32+
daisyUI tabs) that manage their own active state.
33+
1734
## Styling with CSS
1835

1936
The aria-current attribute is easily styled with CSS using attribute selectors,

0 commit comments

Comments
 (0)