vite-plugin-html-pages 2.0.7 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +342 -334
- package/dist/index.js +505 -369
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -5,133 +5,98 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://vitejs.dev)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**Static site generation for Vite — no framework, no components, no magic.**
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Write JavaScript (or TypeScript, or JSX) functions that return HTML.
|
|
11
|
+
Get a complete static site with file-based routing, dynamic pages, data
|
|
12
|
+
loading, an asset pipeline, sitemap, RSS, and a live dev server.
|
|
12
13
|
|
|
13
14
|
⭐ If this project helps you, please consider starring it.
|
|
14
15
|
|
|
15
|
-
# Built for the Vite ecosystem
|
|
16
|
-
|
|
17
|
-
Works seamlessly with:
|
|
18
|
-
|
|
19
|
-
- ⚡ **Vite**
|
|
20
|
-
- 📦 **Rollup / Rolldown**
|
|
21
|
-
- 🧩 **javascript-to-html**
|
|
22
|
-
|
|
23
|
-
A minimal **static site generator for Vite** that keeps pages as simple
|
|
24
|
-
JavaScript functions returning HTML.
|
|
25
|
-
|
|
26
16
|
---
|
|
27
17
|
|
|
28
|
-
|
|
18
|
+
## TL;DR
|
|
29
19
|
|
|
30
|
-
Write:
|
|
20
|
+
Write a function that returns HTML:
|
|
31
21
|
|
|
32
22
|
```js
|
|
33
23
|
// src/index.ht.js
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
head
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
body
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
47
|
-
)
|
|
25
|
+
export default () => `
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<title>My website</title>
|
|
29
|
+
<link rel="stylesheet" href="/styles.css">
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<h1>Hello world</h1>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
`
|
|
48
36
|
```
|
|
49
37
|
|
|
50
|
-
Run:
|
|
38
|
+
Run:
|
|
51
39
|
|
|
52
|
-
```
|
|
40
|
+
```bash
|
|
53
41
|
vite build
|
|
54
42
|
```
|
|
55
43
|
|
|
56
44
|
Get:
|
|
57
45
|
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<head>
|
|
64
|
-
<title>My website</title>
|
|
65
|
-
</head>
|
|
66
|
-
<body>
|
|
67
|
-
<h1>Hello world</h1>
|
|
68
|
-
</body>
|
|
69
|
-
</html>
|
|
46
|
+
```
|
|
47
|
+
dist/
|
|
48
|
+
index.html ← rendered, with <!DOCTYPE html> added for you
|
|
49
|
+
styles.css ← bundled + minified, because your page referenced it
|
|
50
|
+
404.html ← generated automatically
|
|
70
51
|
```
|
|
71
52
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# Features
|
|
75
|
-
|
|
76
|
-
- File-based routing
|
|
77
|
-
- Dynamic routes `[slug]`
|
|
78
|
-
- Multiple parameters `[year]/[slug]`
|
|
79
|
-
- Catch-all routes `[...slug]`
|
|
80
|
-
- Optional catch-all routes `[...slug]?`
|
|
81
|
-
- Route groups `(admin)/users.ht.js`
|
|
82
|
-
- Index routes `blog/[slug]/index.ht.js`
|
|
83
|
-
- Static params generation
|
|
84
|
-
- Fetch caching for data loading
|
|
85
|
-
- Dev server SSR rendering
|
|
86
|
-
- Parallel static generation
|
|
87
|
-
- Automatic `404.html`
|
|
88
|
-
- Automatic `sitemap.xml`
|
|
89
|
-
- Optional RSS feed generation
|
|
90
|
-
- Debug logging
|
|
53
|
+
That's the whole mental model. Everything else is convenience on top.
|
|
91
54
|
|
|
92
55
|
---
|
|
93
56
|
|
|
94
|
-
|
|
57
|
+
## Features
|
|
95
58
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
59
|
+
- **File-based routing** — `src/about.ht.js` → `/about`
|
|
60
|
+
- **Dynamic routes** — `[slug]`, `[year]/[slug]`, catch-all `[...path]`, optional catch-all `[...path]?`
|
|
61
|
+
- **Route groups** — `(admin)/users.ht.js` → `/users`
|
|
62
|
+
- **Bring your own HTML** — template literals, [javascript-to-html](https://www.npmjs.com/package/javascript-to-html), or JSX/TSX
|
|
63
|
+
- **Data loading** — `data()` runs at build time, with built-in fetch caching
|
|
64
|
+
- **Typed pages** — per-route param types inferred from the filename
|
|
65
|
+
- **Smart asset pipeline** — JS/TS/CSS referenced by your HTML is bundled and minified; server-only code never leaks into `dist`
|
|
66
|
+
- **Asset validation** — broken `<script src>` / stylesheet links fail the build
|
|
67
|
+
- **Real dev server** — pages render on request with Vite HMR-style full reload and readable error frames
|
|
68
|
+
- **Parallel static generation** — renders large sites concurrently
|
|
69
|
+
- **`404.html`, `sitemap.xml`, RSS** — generated for you
|
|
99
70
|
|
|
100
71
|
---
|
|
101
72
|
|
|
102
|
-
|
|
73
|
+
## Why this exists
|
|
103
74
|
|
|
104
|
-
Modern static site tools are powerful, but they
|
|
105
|
-
|
|
106
|
-
- frameworks
|
|
107
|
-
- component systems
|
|
108
|
-
- complex build pipelines
|
|
109
|
-
- opinionated conventions
|
|
75
|
+
Modern static site tools are powerful, but they bring frameworks,
|
|
76
|
+
component systems, hydration strategies, and opinionated conventions.
|
|
110
77
|
|
|
111
78
|
Sometimes you just want to:
|
|
112
79
|
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
`vite-plugin-html-pages` exists for that use case.
|
|
80
|
+
- write HTML
|
|
81
|
+
- organize pages in folders
|
|
82
|
+
- run `vite build`
|
|
118
83
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- dynamic pages
|
|
123
|
-
- static generation
|
|
124
|
-
- dev server rendering
|
|
125
|
-
|
|
126
|
-
while keeping pages as **simple JavaScript functions that return HTML**.
|
|
84
|
+
`vite-plugin-html-pages` exists for exactly that. Pages are plain
|
|
85
|
+
functions that return a string of HTML. No runtime ships to the
|
|
86
|
+
browser unless *you* add a script.
|
|
127
87
|
|
|
128
88
|
---
|
|
129
89
|
|
|
130
|
-
|
|
90
|
+
## Installation
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install -D vite-plugin-html-pages
|
|
94
|
+
```
|
|
131
95
|
|
|
132
|
-
|
|
96
|
+
Requires **Node 18+** and **Vite 8+**.
|
|
133
97
|
|
|
134
98
|
```js
|
|
99
|
+
// vite.config.js
|
|
135
100
|
import { defineConfig } from "vite"
|
|
136
101
|
import htmlPages from "vite-plugin-html-pages"
|
|
137
102
|
|
|
@@ -140,230 +105,205 @@ export default defineConfig({
|
|
|
140
105
|
})
|
|
141
106
|
```
|
|
142
107
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
|
|
108
|
+
```bash
|
|
109
|
+
vite # dev server with live rendering
|
|
110
|
+
vite build # static site in dist/
|
|
147
111
|
```
|
|
148
|
-
src/
|
|
149
|
-
|
|
150
|
-
index.ht.js
|
|
151
|
-
about.ht.js
|
|
152
112
|
|
|
153
|
-
|
|
154
|
-
index.ht.js
|
|
155
|
-
[slug].ht.js
|
|
156
|
-
[year]/[slug].ht.js
|
|
113
|
+
Add the generated helper types to your `.gitignore`:
|
|
157
114
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
(admin)/
|
|
162
|
-
users.ht.js
|
|
115
|
+
```
|
|
116
|
+
.vite-plugin-html-pages/
|
|
163
117
|
```
|
|
164
118
|
|
|
165
119
|
---
|
|
166
120
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
Routes are generated directly from the filesystem.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
| Feature | File | URL |
|
|
173
|
-
|-----|-----|-----|
|
|
174
|
-
| static routes | `index.ht.js` | `/` |
|
|
175
|
-
| dynamic routes | `blog/[slug].ht.js` | `/blog/my-post` |
|
|
176
|
-
| multiple params | `blog/[year]/[slug].ht.js` | `/blog/2026/my-post` |
|
|
177
|
-
| catch-all | `docs/[...slug].ht.js` | `/docs/api/auth/login` |
|
|
178
|
-
| optional catch-all | `docs/[...slug]?.ht.js` | `/docs` or `/docs/getting-started` |
|
|
179
|
-
| index routes | `products/[product]/index.ht.js` | `/products/iphone-18` |
|
|
180
|
-
| route groups | `(admin)/users.ht.js` | `/users` |
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
# Dynamic Routes
|
|
121
|
+
## Project structure
|
|
185
122
|
|
|
186
123
|
```
|
|
187
|
-
src/
|
|
188
|
-
|
|
124
|
+
src/
|
|
125
|
+
index.ht.js → /
|
|
126
|
+
about.ht.js → /about
|
|
127
|
+
styles.css → bundled if referenced
|
|
128
|
+
main.js → bundled if referenced
|
|
129
|
+
lib/
|
|
130
|
+
api.js → build-time only (never emitted unless referenced)
|
|
189
131
|
|
|
190
|
-
|
|
132
|
+
blog/
|
|
133
|
+
index.ht.js → /blog
|
|
134
|
+
[slug].ht.js → /blog/:slug
|
|
191
135
|
|
|
192
|
-
|
|
193
|
-
/
|
|
194
|
-
/blog/my-first-post
|
|
195
|
-
```
|
|
136
|
+
docs/
|
|
137
|
+
[...path]?.ht.js → /docs, /docs/a, /docs/a/b, ...
|
|
196
138
|
|
|
197
|
-
|
|
139
|
+
(admin)/
|
|
140
|
+
users.ht.js → /users
|
|
198
141
|
|
|
199
|
-
|
|
200
|
-
|
|
142
|
+
404.ht.js → dist/404.html
|
|
143
|
+
```
|
|
201
144
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{ slug: 'hello-world' },
|
|
205
|
-
{ slug: 'my-first-post' }
|
|
206
|
-
]
|
|
207
|
-
}
|
|
145
|
+
Any file ending in a page extension is a page. Everything else in
|
|
146
|
+
`src/` is treated as an asset (see [Assets](#assets--styling)).
|
|
208
147
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
html(
|
|
212
|
-
body(
|
|
213
|
-
h1(params.slug)
|
|
214
|
-
)
|
|
215
|
-
)
|
|
216
|
-
)
|
|
217
|
-
```
|
|
148
|
+
Default page extensions: `.ht.js`, `.html.js`, `.ht.ts`, `.html.ts`,
|
|
149
|
+
`.ht.jsx`, `.html.jsx`, `.ht.tsx`, `.html.tsx`.
|
|
218
150
|
|
|
219
151
|
---
|
|
220
152
|
|
|
221
|
-
|
|
153
|
+
## Writing pages
|
|
222
154
|
|
|
223
|
-
|
|
224
|
-
src/blog/[year]/[slug].ht.js
|
|
225
|
-
```
|
|
155
|
+
A page module's **default export** can be any of the following.
|
|
226
156
|
|
|
227
|
-
|
|
157
|
+
### 1. A function returning an HTML string
|
|
228
158
|
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
|
|
159
|
+
```js
|
|
160
|
+
export default ({ params, data, dev }) => `
|
|
161
|
+
<html>
|
|
162
|
+
<body><h1>Hello</h1></body>
|
|
163
|
+
</html>
|
|
164
|
+
`
|
|
232
165
|
```
|
|
233
166
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
``` js
|
|
237
|
-
import { fragment, html, body, h1 } from 'javascript-to-html'
|
|
238
|
-
|
|
239
|
-
export function generateStaticParams() {
|
|
240
|
-
return [
|
|
241
|
-
{
|
|
242
|
-
year: 2026,
|
|
243
|
-
slug: "vite-routing"
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
year: 2025,
|
|
247
|
-
slug: 'my-first-post'
|
|
248
|
-
}
|
|
249
|
-
]
|
|
250
|
-
}
|
|
167
|
+
### 2. A plain string
|
|
251
168
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
html(
|
|
255
|
-
body(
|
|
256
|
-
h1(params.slug)
|
|
257
|
-
)
|
|
258
|
-
)
|
|
259
|
-
)
|
|
169
|
+
```js
|
|
170
|
+
export default `<html><body><h1>Static as it gets</h1></body></html>`
|
|
260
171
|
```
|
|
261
172
|
|
|
262
|
-
|
|
173
|
+
### 3. A structured module
|
|
263
174
|
|
|
264
|
-
|
|
175
|
+
Keeps `render`, `data`, and `generateStaticParams` together in one object:
|
|
265
176
|
|
|
266
|
-
```
|
|
267
|
-
|
|
177
|
+
```js
|
|
178
|
+
export default {
|
|
179
|
+
generateStaticParams: () => [{ slug: "hello" }],
|
|
180
|
+
data: ({ params }) => ({ title: params.slug }),
|
|
181
|
+
render: ({ data }) => `<html><body><h1>${data.title}</h1></body></html>`,
|
|
182
|
+
}
|
|
268
183
|
```
|
|
269
184
|
|
|
270
|
-
|
|
185
|
+
### 4. JSX / TSX
|
|
271
186
|
|
|
272
|
-
|
|
273
|
-
/
|
|
274
|
-
/docs/guides/rendering/static
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Params:
|
|
187
|
+
Name the file `*.ht.jsx` or `*.ht.tsx` and return JSX — it is rendered
|
|
188
|
+
to **static HTML** at build time with `react-dom/server`:
|
|
278
189
|
|
|
279
|
-
```
|
|
280
|
-
|
|
190
|
+
```tsx
|
|
191
|
+
// src/index.ht.tsx
|
|
192
|
+
export default function Home() {
|
|
193
|
+
return (
|
|
194
|
+
<html lang="en">
|
|
195
|
+
<head><title>My site</title></head>
|
|
196
|
+
<body><h1>Hello from TSX</h1></body>
|
|
197
|
+
</html>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
281
200
|
```
|
|
282
201
|
|
|
283
|
-
|
|
202
|
+
JSX pages require `react` and `react-dom` in your project (they are
|
|
203
|
+
optional peer dependencies — string-based pages don't need them).
|
|
204
|
+
Since output is static, event-handler props like `onClick` won't do
|
|
205
|
+
anything in the browser; the dev server warns you if it finds any.
|
|
284
206
|
|
|
285
|
-
|
|
207
|
+
### 5. javascript-to-html
|
|
286
208
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
209
|
+
Prefer composable functions over template strings? The companion
|
|
210
|
+
library [javascript-to-html](https://www.npmjs.com/package/javascript-to-html)
|
|
211
|
+
works great:
|
|
290
212
|
|
|
291
|
-
|
|
213
|
+
```js
|
|
214
|
+
import { html, head, title, body, h1 } from 'javascript-to-html'
|
|
292
215
|
|
|
216
|
+
export default () =>
|
|
217
|
+
html({ lang: 'en' },
|
|
218
|
+
head(title('My website')),
|
|
219
|
+
body(h1('Hello world'))
|
|
220
|
+
)
|
|
293
221
|
```
|
|
294
|
-
/docs
|
|
295
|
-
/docs/getting-started
|
|
296
|
-
/docs/api/auth/login
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
Params:
|
|
300
|
-
|
|
301
|
-
| URL | params.slug |
|
|
302
|
-
|-----|-------------|
|
|
303
|
-
| `/docs` | "" |
|
|
304
|
-
| `/docs/api` | "api" |
|
|
305
|
-
| `/docs/api/auth` | "api/auth" |
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
|
-
# Route Groups
|
|
310
222
|
|
|
311
|
-
|
|
223
|
+
> If a page's output starts with `<html>`, `<!DOCTYPE html>` is
|
|
224
|
+
> prepended automatically.
|
|
312
225
|
|
|
313
|
-
|
|
314
|
-
src/(admin)/users.ht.js
|
|
315
|
-
```
|
|
226
|
+
### Render context
|
|
316
227
|
|
|
317
|
-
|
|
228
|
+
Every page function receives one argument:
|
|
318
229
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
230
|
+
| Property | Type | Description |
|
|
231
|
+
|----------|------|-------------|
|
|
232
|
+
| `params` | `Record<string, string \| string[]>` | Route params for this page |
|
|
233
|
+
| `data` | `unknown` | Whatever your `data()` returned |
|
|
234
|
+
| `page` | `object` | Route metadata (`routePath`, `relativePath`, ...) |
|
|
235
|
+
| `dev` | `boolean` | `true` in the dev server, `false` at build |
|
|
322
236
|
|
|
323
237
|
---
|
|
324
238
|
|
|
325
|
-
|
|
239
|
+
## Routing
|
|
326
240
|
|
|
327
|
-
|
|
241
|
+
Routes come straight from the filesystem:
|
|
328
242
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
243
|
+
| Feature | File | URL |
|
|
244
|
+
|---------|------|-----|
|
|
245
|
+
| Static routes | `index.ht.js` | `/` |
|
|
246
|
+
| Nested routes | `blog/index.ht.js` | `/blog` |
|
|
247
|
+
| Dynamic routes | `blog/[slug].ht.js` | `/blog/my-post` |
|
|
248
|
+
| Multiple params | `blog/[year]/[slug].ht.js` | `/blog/2026/my-post` |
|
|
249
|
+
| Catch-all | `docs/[...path].ht.js` | `/docs/api/auth/login` |
|
|
250
|
+
| Optional catch-all | `docs/[...path]?.ht.js` | `/docs` and `/docs/anything/below` |
|
|
251
|
+
| Index routes | `products/[id]/index.ht.js` | `/products/iphone-18` |
|
|
252
|
+
| Route groups | `(admin)/users.ht.js` | `/users` |
|
|
333
253
|
|
|
334
|
-
|
|
254
|
+
More specific routes always win: static segments beat dynamic ones,
|
|
255
|
+
dynamic beat catch-alls. Two files generating the same URL is a build
|
|
256
|
+
error, not a silent overwrite.
|
|
335
257
|
|
|
336
|
-
|
|
258
|
+
### Static params
|
|
337
259
|
|
|
338
|
-
Dynamic routes
|
|
260
|
+
Dynamic routes declare their pages by exporting `generateStaticParams`:
|
|
339
261
|
|
|
340
262
|
```js
|
|
263
|
+
// src/blog/[slug].ht.js
|
|
341
264
|
export function generateStaticParams() {
|
|
342
265
|
return [
|
|
343
|
-
{ slug:
|
|
344
|
-
{ slug:
|
|
266
|
+
{ slug: 'hello-world' },
|
|
267
|
+
{ slug: 'my-first-post' },
|
|
345
268
|
]
|
|
346
269
|
}
|
|
270
|
+
|
|
271
|
+
export default ({ params }) => `
|
|
272
|
+
<html><body><h1>${params.slug}</h1></body></html>
|
|
273
|
+
`
|
|
347
274
|
```
|
|
348
275
|
|
|
276
|
+
Values can be strings, numbers, or booleans — they are stringified and
|
|
277
|
+
URL-encoded for you. Catch-all params accept arrays (`{ path: ['a', 'b'] }`)
|
|
278
|
+
or slash-separated strings (`{ path: 'a/b' }`).
|
|
279
|
+
|
|
280
|
+
A dynamic page that generates zero routes prints a warning so it can't
|
|
281
|
+
silently vanish from your site.
|
|
282
|
+
|
|
349
283
|
---
|
|
350
284
|
|
|
351
|
-
|
|
285
|
+
## Data loading
|
|
352
286
|
|
|
353
|
-
|
|
287
|
+
Export a `data()` function and its result appears as `ctx.data` in your
|
|
288
|
+
render function. It runs at build time (and per-request in dev):
|
|
354
289
|
|
|
355
290
|
```js
|
|
356
|
-
export async function data({ params }) {
|
|
357
|
-
|
|
291
|
+
export async function data({ params, dev }) {
|
|
292
|
+
const res = await fetch(`https://api.example.com/posts/${params.slug}`)
|
|
293
|
+
return await res.json()
|
|
358
294
|
}
|
|
359
|
-
```
|
|
360
295
|
|
|
361
|
-
|
|
296
|
+
export default ({ data }) => `
|
|
297
|
+
<html><body>
|
|
298
|
+
<h1>${data.title}</h1>
|
|
299
|
+
${data.body}
|
|
300
|
+
</body></html>
|
|
301
|
+
`
|
|
302
|
+
```
|
|
362
303
|
|
|
363
|
-
|
|
304
|
+
### fetchWithCache
|
|
364
305
|
|
|
365
|
-
|
|
366
|
-
cached to avoid repeated network calls across page builds.
|
|
306
|
+
Building 500 pages against the same API? Cache the responses:
|
|
367
307
|
|
|
368
308
|
```js
|
|
369
309
|
import { fetchWithCache } from 'vite-plugin-html-pages'
|
|
@@ -371,162 +311,230 @@ import { fetchWithCache } from 'vite-plugin-html-pages'
|
|
|
371
311
|
export async function data({ params }) {
|
|
372
312
|
const res = await fetchWithCache(
|
|
373
313
|
`https://api.example.com/posts/${params.slug}`,
|
|
374
|
-
{
|
|
375
|
-
// fetch API options
|
|
376
|
-
},
|
|
314
|
+
{ /* standard fetch options */ },
|
|
377
315
|
{ maxAge: 3600 }
|
|
378
316
|
)
|
|
379
|
-
|
|
380
|
-
return { post }
|
|
317
|
+
return { post: await res.json() }
|
|
381
318
|
}
|
|
382
319
|
```
|
|
383
320
|
|
|
384
|
-
## Options
|
|
385
|
-
|
|
386
321
|
| Option | Description |
|
|
387
322
|
|--------|-------------|
|
|
388
|
-
| `maxAge` | Cache TTL in seconds (default: 3600) |
|
|
389
|
-
| `cacheKey` | Custom cache key (default: hash of URL + method + headers) |
|
|
390
|
-
| `forceRefresh` | Bypass cache and fetch fresh |
|
|
323
|
+
| `maxAge` | Cache TTL in seconds (default: `3600`) |
|
|
324
|
+
| `cacheKey` | Custom cache key (default: hash of URL + method + headers + body) |
|
|
325
|
+
| `forceRefresh` | Bypass the cache and fetch fresh |
|
|
391
326
|
| `cache` | `'auto'` \| `'memory'` \| `'fs'` \| `'none'` |
|
|
392
327
|
|
|
393
|
-
|
|
328
|
+
Cache modes:
|
|
394
329
|
|
|
395
|
-
- **`auto`** (default)
|
|
396
|
-
- **`memory
|
|
397
|
-
- **`fs
|
|
398
|
-
- **`none
|
|
330
|
+
- **`auto`** (default) — memory in dev, filesystem in production builds
|
|
331
|
+
- **`memory`** — in-process, cleared when the process exits
|
|
332
|
+
- **`fs`** — persisted in `node_modules/.cache/vite-plugin-html-pages/fetch/`
|
|
333
|
+
- **`none`** — always fetch
|
|
399
334
|
|
|
400
|
-
Only `GET` requests are cached by default
|
|
401
|
-
|
|
335
|
+
Only `GET` requests are cached by default (pass a `cacheKey` to cache
|
|
336
|
+
other methods), and error responses are never cached — a flaky API
|
|
337
|
+
during one build won't poison the next one.
|
|
402
338
|
|
|
403
339
|
---
|
|
404
340
|
|
|
405
|
-
|
|
341
|
+
## TypeScript & typed params
|
|
406
342
|
|
|
407
|
-
|
|
343
|
+
Pages can be written in TypeScript (`.ht.ts` / `.ht.tsx`) with zero
|
|
344
|
+
configuration.
|
|
408
345
|
|
|
409
|
-
|
|
410
|
-
import { fragment, html, head, body } from 'javascript-to-html'
|
|
346
|
+
Helper functions give your page modules full type inference:
|
|
411
347
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
)
|
|
420
|
-
)
|
|
348
|
+
```ts
|
|
349
|
+
// src/blog/[slug].ht.ts
|
|
350
|
+
import { definePageModule } from 'vite-plugin-html-pages/page'
|
|
351
|
+
|
|
352
|
+
export default definePageModule({
|
|
353
|
+
generateStaticParams: () => [{ slug: 'hello' }],
|
|
354
|
+
data: ({ params }) => ({ title: params.slug }),
|
|
355
|
+
render: ({ data }) => `<html><body><h1>${data.title}</h1></body></html>`,
|
|
356
|
+
})
|
|
421
357
|
```
|
|
422
358
|
|
|
423
|
-
|
|
359
|
+
Individual helpers (`definePage`, `defineData`, `defineStaticParams`)
|
|
360
|
+
are also exported. At build time this import is transparently swapped
|
|
361
|
+
for a **per-route generated module** whose `PageParams` are inferred
|
|
362
|
+
from the filename: `[slug]` → `{ slug: string }`, `[...path]` →
|
|
363
|
+
`{ path: string[] }`, `[...path]?` → `{ path?: string[] }`.
|
|
424
364
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|------|------|
|
|
429
|
-
| `pagesDir` | root directory for pages |
|
|
430
|
-
| `include` | page glob |
|
|
431
|
-
| `exclude` | excluded files |
|
|
432
|
-
| `cleanUrls` | `/page/index.html` instead of `/page.html` |
|
|
433
|
-
| `renderConcurrency` | parallel rendering |
|
|
434
|
-
| `renderBatchSize` | batch size |
|
|
435
|
-
| `debug` | enable debug logging |
|
|
436
|
-
| `site` | base URL for sitemap |
|
|
437
|
-
| `rss` | RSS configuration |
|
|
365
|
+
Matching type declarations are generated into
|
|
366
|
+
`.vite-plugin-html-pages/types/` whenever the dev server or a build
|
|
367
|
+
runs — add that folder to `.gitignore`.
|
|
438
368
|
|
|
439
369
|
---
|
|
440
370
|
|
|
441
|
-
|
|
371
|
+
## Assets & styling
|
|
442
372
|
|
|
443
|
-
|
|
373
|
+
Reference assets from your HTML with root-relative URLs and the plugin
|
|
374
|
+
handles the rest:
|
|
444
375
|
|
|
445
376
|
```js
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
377
|
+
export default () => `
|
|
378
|
+
<html>
|
|
379
|
+
<head>
|
|
380
|
+
<link rel="stylesheet" href="/styles.css">
|
|
381
|
+
<script type="module" src="/main.js"></script>
|
|
382
|
+
</head>
|
|
383
|
+
<body>...</body>
|
|
384
|
+
</html>
|
|
385
|
+
`
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
At build time:
|
|
389
|
+
|
|
390
|
+
- **Referenced JS / TS / CSS is bundled** with esbuild — imports are
|
|
391
|
+
inlined, output is minified, and `.ts` files compile to `.js`.
|
|
392
|
+
- **Unreferenced code files are not emitted.** A helper like
|
|
393
|
+
`src/lib/api.ts` that you only import from `data()` stays out of
|
|
394
|
+
`dist/` — server-only code (and its secrets) never ships by accident.
|
|
395
|
+
- **Everything else is copied** (images, fonts, videos, ...), so CSS
|
|
396
|
+
`url()` references keep working.
|
|
397
|
+
- **`public/` behaves like normal Vite** — copied verbatim.
|
|
398
|
+
|
|
399
|
+
In dev, the same URLs are served through Vite's transform pipeline, so
|
|
400
|
+
TypeScript and CSS work identically without a build.
|
|
401
|
+
|
|
402
|
+
### Missing-asset validation
|
|
403
|
+
|
|
404
|
+
Every generated page is checked: a `<script src="/x.js">` or stylesheet
|
|
405
|
+
`href` pointing at a file that exists in neither `src/` nor `public/`
|
|
406
|
+
**fails the build** with the exact paths that were checked. Prefer a
|
|
407
|
+
warning instead?
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
htmlPages({ missingAssets: 'warn' })
|
|
449
411
|
```
|
|
450
412
|
|
|
451
|
-
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Dev server
|
|
452
416
|
|
|
453
|
-
|
|
454
|
-
[vite-plugin-html-pages] dev pages [...]
|
|
455
|
-
[vite-plugin-html-pages] render bundle ...
|
|
417
|
+
`vite dev` gives you the real site, not an approximation:
|
|
456
418
|
|
|
457
|
-
|
|
458
|
-
|
|
419
|
+
- Pages render **on request** through Vite's SSR module runner — edit a
|
|
420
|
+
page, its `data()`, or any imported module and reload.
|
|
421
|
+
- File changes inside your pages directory trigger an automatic
|
|
422
|
+
**full-reload** in the browser.
|
|
423
|
+
- Errors show a **source-mapped code frame** in the terminal pointing
|
|
424
|
+
at the exact line in your page — the server stays alive while you fix it.
|
|
459
425
|
|
|
460
|
-
|
|
426
|
+
```
|
|
427
|
+
── PAGE RELOAD ERROR ───────────────────── src/index.ht.js:6:20
|
|
461
428
|
|
|
462
|
-
|
|
429
|
+
ReferenceError: title is not defined
|
|
463
430
|
|
|
464
|
-
|
|
431
|
+
> 6 │ head(title('My website')),
|
|
432
|
+
│ ^
|
|
465
433
|
|
|
466
|
-
|
|
467
|
-
|
|
434
|
+
Fix the error and save again.
|
|
435
|
+
Watching for file changes...
|
|
468
436
|
```
|
|
469
437
|
|
|
470
438
|
---
|
|
471
439
|
|
|
472
|
-
|
|
440
|
+
## Generated extras
|
|
441
|
+
|
|
442
|
+
### 404 page
|
|
443
|
+
|
|
444
|
+
Create `src/404.ht.js` and it's emitted as `dist/404.html` (the
|
|
445
|
+
convention GitHub Pages, Netlify, and Cloudflare Pages all understand).
|
|
446
|
+
No 404 page? A clean default is generated.
|
|
447
|
+
|
|
448
|
+
### Sitemap
|
|
449
|
+
|
|
450
|
+
Set your site URL and `dist/sitemap.xml` is generated from all static
|
|
451
|
+
routes, correctly escaped:
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
htmlPages({ site: 'https://example.com' })
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### RSS feed
|
|
473
458
|
|
|
474
459
|
```js
|
|
475
460
|
htmlPages({
|
|
476
461
|
rss: {
|
|
477
|
-
site:
|
|
478
|
-
title:
|
|
479
|
-
description:
|
|
480
|
-
routePrefix:
|
|
462
|
+
site: 'https://example.com',
|
|
463
|
+
title: 'My Blog',
|
|
464
|
+
description: 'Latest posts',
|
|
465
|
+
routePrefix: '/blog', // which routes become feed items
|
|
481
466
|
}
|
|
482
467
|
})
|
|
483
468
|
```
|
|
484
469
|
|
|
485
|
-
Produces
|
|
470
|
+
Produces `dist/rss.xml` with an item for every page under `routePrefix`.
|
|
486
471
|
|
|
487
|
-
|
|
488
|
-
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Plugin options
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
htmlPages({
|
|
478
|
+
pagesDir: 'src',
|
|
479
|
+
cleanUrls: true,
|
|
480
|
+
site: 'https://example.com',
|
|
481
|
+
missingAssets: 'error',
|
|
482
|
+
debug: false,
|
|
483
|
+
})
|
|
489
484
|
```
|
|
490
485
|
|
|
491
|
-
|
|
486
|
+
| Option | Default | Description |
|
|
487
|
+
|--------|---------|-------------|
|
|
488
|
+
| `pagesDir` | `'src'` | Directory containing pages and assets |
|
|
489
|
+
| `pageExtensions` | `['.ht.js', '.html.js', ...]` | Which file suffixes are pages |
|
|
490
|
+
| `include` | derived from `pagesDir` | Custom glob(s) for page discovery |
|
|
491
|
+
| `exclude` | `[]` | Glob(s) to exclude from discovery |
|
|
492
|
+
| `root` | Vite root | Override the project root |
|
|
493
|
+
| `cleanUrls` | `true` | `/about/index.html` (`/about`) instead of `/about.html` |
|
|
494
|
+
| `site` | — | Base URL; enables `sitemap.xml` |
|
|
495
|
+
| `rss` | — | RSS config (`site`, `title`, `description`, `routePrefix`) |
|
|
496
|
+
| `missingAssets` | `'error'` | `'error'` or `'warn'` for broken asset references |
|
|
497
|
+
| `mapOutputPath` | — | `(page) => string` to customize output filenames |
|
|
498
|
+
| `renderConcurrency` | `8` | Pages rendered in parallel |
|
|
499
|
+
| `renderBatchSize` | `max(concurrency, 32)` | Pages per render batch |
|
|
500
|
+
| `debug` | `false` | Verbose logging of discovery, routing, and emission |
|
|
492
501
|
|
|
493
|
-
|
|
502
|
+
### Performance
|
|
494
503
|
|
|
495
|
-
Large sites can
|
|
504
|
+
Large sites can raise the parallelism:
|
|
496
505
|
|
|
497
506
|
```js
|
|
498
507
|
htmlPages({
|
|
499
508
|
renderConcurrency: 16,
|
|
500
|
-
renderBatchSize: 128
|
|
509
|
+
renderBatchSize: 128,
|
|
501
510
|
})
|
|
502
511
|
```
|
|
503
512
|
|
|
504
513
|
---
|
|
505
514
|
|
|
506
|
-
|
|
515
|
+
## Comparison
|
|
507
516
|
|
|
508
|
-
| Tool |
|
|
509
|
-
|
|
510
|
-
| Astro |
|
|
511
|
-
| Next.js | React SSR
|
|
512
|
-
|
|
|
513
|
-
|
|
514
|
-
---
|
|
517
|
+
| Tool | What it is |
|
|
518
|
+
|------|------------|
|
|
519
|
+
| Astro | Component-based SSG with its own compiler and islands |
|
|
520
|
+
| Next.js | Full React framework with SSR/ISR |
|
|
521
|
+
| Eleventy | Template-language SSG (Nunjucks, Liquid, ...) |
|
|
522
|
+
| **vite-plugin-html-pages** | **Functions returning HTML, powered by plain Vite** |
|
|
515
523
|
|
|
516
|
-
|
|
524
|
+
If you want components, hydration, and a framework — use a framework.
|
|
525
|
+
If you want HTML files out of JavaScript functions with the Vite dev
|
|
526
|
+
experience, this is the smallest tool that does the whole job.
|
|
517
527
|
|
|
518
|
-
|
|
528
|
+
## Good fits
|
|
519
529
|
|
|
520
|
-
-
|
|
521
|
-
-
|
|
522
|
-
-
|
|
523
|
-
-
|
|
524
|
-
-
|
|
525
|
-
- **Minimal static site generators**
|
|
526
|
-
- **HTML‑first Vite projects**
|
|
530
|
+
- Marketing and landing pages
|
|
531
|
+
- Blogs and documentation sites
|
|
532
|
+
- HTML-first projects with a sprinkle of JS
|
|
533
|
+
- API-driven static sites (with `fetchWithCache`)
|
|
534
|
+
- Any site where "view source" should show exactly what you wrote
|
|
527
535
|
|
|
528
536
|
---
|
|
529
537
|
|
|
530
|
-
|
|
538
|
+
## License
|
|
531
539
|
|
|
532
540
|
MIT
|