create-pracht 0.6.1 → 0.6.2
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/package.json +1 -1
- package/skills/add-auth/SKILL.md +63 -143
- package/skills/add-capabilities/SKILL.md +409 -0
- package/skills/add-content/SKILL.md +242 -0
- package/skills/add-db/SKILL.md +93 -202
- package/skills/add-i18n/SKILL.md +178 -217
- package/skills/add-images/SKILL.md +203 -0
- package/skills/add-observability/SKILL.md +118 -15
- package/skills/add-openapi/SKILL.md +209 -0
- package/skills/audit-a11y/SKILL.md +8 -9
- package/skills/audit-agent-surface/SKILL.md +335 -0
- package/skills/audit-auth/SKILL.md +16 -11
- package/skills/audit-bundles/SKILL.md +48 -12
- package/skills/audit-csrf/SKILL.md +9 -10
- package/skills/audit-deps/SKILL.md +8 -8
- package/skills/audit-headers/SKILL.md +9 -10
- package/skills/audit-islands/SKILL.md +9 -10
- package/skills/audit-loaders/SKILL.md +23 -8
- package/skills/audit-redirects/SKILL.md +9 -10
- package/skills/audit-secrets/SKILL.md +6 -6
- package/skills/audit-seo/SKILL.md +8 -8
- package/skills/audit-shells/SKILL.md +8 -9
- package/skills/configure-isg/SKILL.md +9 -10
- package/skills/migrate-nextjs/SKILL.md +200 -424
- package/skills/pracht-debug/SKILL.md +165 -128
- package/skills/pracht-deploy/SKILL.md +246 -331
- package/skills/pracht-scaffold/SKILL.md +123 -146
- package/skills/pracht-test-api/SKILL.md +10 -10
- package/skills/pre-deploy/SKILL.md +165 -200
- package/skills/scaffold-e2e/SKILL.md +11 -12
- package/skills/scaffold-tests/SKILL.md +10 -12
- package/skills/tune-render-mode/SKILL.md +7 -8
- package/skills/typed-routes/SKILL.md +9 -9
- package/skills/upgrade-pracht/SKILL.md +7 -8
- package/src/index.js +38 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pracht-deploy
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.3.0
|
|
4
4
|
description: |
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Use
|
|
9
|
-
|
|
10
|
-
export", or
|
|
11
|
-
"production build".
|
|
5
|
+
Configure a pracht adapter and deploy to Node, Cloudflare Workers, Netlify,
|
|
6
|
+
Vercel, or a pure static host: platform config, build, Docker, production
|
|
7
|
+
checklist.
|
|
8
|
+
Use for "deploy", "set up deployment", "configure adapter", "deploy to
|
|
9
|
+
cloudflare/netlify/vercel", "static export", "production build".
|
|
12
10
|
allowed-tools:
|
|
13
11
|
- Bash
|
|
14
12
|
- Read
|
|
@@ -21,87 +19,64 @@ allowed-tools:
|
|
|
21
19
|
|
|
22
20
|
# Pracht Deploy
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
22
|
+
Read `vite.config.ts` and `package.json` before giving any advice — never
|
|
23
|
+
assume the current adapter — and ask the user for the target if their message
|
|
24
|
+
does not name one. Then read only that adapter's section below. Install the
|
|
25
|
+
adapter if it is missing (`pnpm add @pracht/adapter-*`), run `pracht build` to
|
|
26
|
+
confirm the build succeeds, smoke-test the production runtime, and never push
|
|
27
|
+
to production without explicit confirmation.
|
|
28
|
+
|
|
29
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer
|
|
30
|
+
`inspect_build`/`doctor`/`verify` over shelling out. `inspect_build` (like
|
|
31
|
+
`pracht inspect build`) needs a prior `pracht build`; `pracht inspect` needs
|
|
32
|
+
the pracht plugin in the vite config.
|
|
33
|
+
|
|
34
|
+
| Target | Adapter package | Local smoke test |
|
|
35
|
+
| ------------------ | ---------------------------- | -------------------------------------- |
|
|
36
|
+
| Node.js | `@pracht/adapter-node` | `pracht preview` |
|
|
37
|
+
| Cloudflare Workers | `@pracht/adapter-cloudflare` | `pracht preview` (delegates to `wrangler dev`) |
|
|
38
|
+
| Netlify | `@pracht/adapter-netlify` | `pracht build && netlify dev` |
|
|
39
|
+
| Vercel | `@pracht/adapter-vercel` | `vercel build` / `vercel dev` |
|
|
40
|
+
| Static export | `@pracht/adapter-static` | `pracht preview` |
|
|
41
|
+
|
|
42
|
+
Every adapter is wired the same way — `pracht({ adapter: <name>Adapter(…) })`
|
|
43
|
+
in the `plugins` array of `vite.config.ts` — and every target builds with
|
|
44
|
+
`pracht build`, which emits `dist/client/` (assets + prerendered HTML),
|
|
45
|
+
`dist/server/` (server entry and build tooling),
|
|
46
|
+
`dist/server/isg-manifest.json` when ISG routes exist, and
|
|
47
|
+
`dist/client/.vite/manifest.json`.
|
|
42
48
|
|
|
43
49
|
---
|
|
44
50
|
|
|
45
|
-
## Node.js
|
|
46
|
-
|
|
47
|
-
### Setup
|
|
48
|
-
|
|
49
|
-
1. Ensure `@pracht/adapter-node` is installed.
|
|
50
|
-
2. In `vite.config.ts`:
|
|
51
|
-
```ts
|
|
52
|
-
import { pracht } from "@pracht/vite-plugin";
|
|
53
|
-
import { nodeAdapter } from "@pracht/adapter-node";
|
|
54
|
-
export default {
|
|
55
|
-
plugins: [
|
|
56
|
-
pracht({
|
|
57
|
-
adapter: nodeAdapter({ canonicalOrigin: "https://app.example.com" }),
|
|
58
|
-
}),
|
|
59
|
-
],
|
|
60
|
-
};
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Pin `canonicalOrigin` in production so `request.url` does not depend on the
|
|
64
|
-
incoming `Host` header. `maxBodySize` is also available on `nodeAdapter()`.
|
|
65
|
-
Only custom entries behind a trusted proxy that overwrites forwarded headers
|
|
66
|
-
should use `createNodeRequestHandler({ trustProxy: true })`.
|
|
67
|
-
If that proxy strips Vite's deploy base from the forwarded path, set
|
|
68
|
-
`nodeAdapter({ basePathStripped: true })` (or the same option on a custom
|
|
69
|
-
`createNodeRequestHandler`). Do not infer this from the first path segment: a
|
|
70
|
-
route may legitimately begin with the same segment as the deploy base. The
|
|
71
|
-
adapter restores the public base before `createContext()`, loaders, and API
|
|
72
|
-
handlers receive the request.
|
|
73
|
-
The proxy must also own the public bare-base redirect (`/app` to `/app/`) in
|
|
74
|
-
this mode because the stripped origin cannot distinguish it from a legitimate
|
|
75
|
-
base-free `/app` route.
|
|
76
|
-
|
|
77
|
-
The Node adapter compresses responses by default (brotli/gzip negotiated via
|
|
78
|
-
`Accept-Encoding`, streaming for dynamic bodies, an in-memory LRU for static
|
|
79
|
-
assets). When the deployment sits behind a reverse proxy or CDN that already
|
|
80
|
-
compresses responses, set `nodeAdapter({ compression: false })` so bodies are
|
|
81
|
-
not compressed twice.
|
|
82
|
-
|
|
83
|
-
### Build
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
pracht build
|
|
87
|
-
```
|
|
51
|
+
## Node.js
|
|
88
52
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- `dist/client/` — static assets (JS, CSS, prerendered HTML)
|
|
92
|
-
- `dist/server/server.js` — Node server entry
|
|
93
|
-
- `dist/server/isg-manifest.json` — ISG revalidation config (if ISG routes exist)
|
|
94
|
-
- `dist/client/.vite/manifest.json` — asset manifest for script/style injection
|
|
95
|
-
|
|
96
|
-
### Run
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
node dist/server/server.js
|
|
53
|
+
```ts
|
|
54
|
+
pracht({ adapter: nodeAdapter({ canonicalOrigin: "https://app.example.com" }) });
|
|
100
55
|
```
|
|
101
56
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
57
|
+
Run with `node dist/server/server.js` (port 3000 by default). For production,
|
|
58
|
+
put it behind a reverse proxy (nginx, Caddy) and a process manager (PM2,
|
|
59
|
+
systemd) with `NODE_ENV=production`. `pracht preview` builds and runs it in one
|
|
60
|
+
step (`--port <n>`, `--skip-build` to reuse a build).
|
|
61
|
+
|
|
62
|
+
- Pin `canonicalOrigin` in production so `request.url` does not depend on the
|
|
63
|
+
incoming `Host` header. `maxBodySize` is also available on `nodeAdapter()`.
|
|
64
|
+
- `createNodeRequestHandler({ trustProxy: true })` is only for custom entries
|
|
65
|
+
behind a trusted proxy that overwrites forwarded headers.
|
|
66
|
+
- If that proxy strips Vite's deploy base from the forwarded path, set
|
|
67
|
+
`nodeAdapter({ basePathStripped: true })` (same option on a custom
|
|
68
|
+
`createNodeRequestHandler`). Do not infer this from the first path segment —
|
|
69
|
+
a route may legitimately begin with the same segment as the deploy base. The
|
|
70
|
+
adapter restores the public base before `createContext()`, loaders, and API
|
|
71
|
+
handlers see the request, and the proxy must then own the public bare-base
|
|
72
|
+
redirect (`/app` → `/app/`), since the stripped origin cannot tell it from a
|
|
73
|
+
legitimate base-free `/app` route.
|
|
74
|
+
- Responses are compressed by default (brotli/gzip negotiated via
|
|
75
|
+
`Accept-Encoding`, streaming for dynamic bodies, an in-memory LRU for static
|
|
76
|
+
assets). Behind a proxy or CDN that already compresses, set
|
|
77
|
+
`nodeAdapter({ compression: false })` to avoid doing it twice.
|
|
78
|
+
|
|
79
|
+
Docker:
|
|
105
80
|
|
|
106
81
|
```dockerfile
|
|
107
82
|
FROM node:22-alpine
|
|
@@ -114,52 +89,18 @@ CMD ["node", "dist/server/server.js"]
|
|
|
114
89
|
|
|
115
90
|
---
|
|
116
91
|
|
|
117
|
-
## Cloudflare Workers
|
|
118
|
-
|
|
119
|
-
### Setup
|
|
92
|
+
## Cloudflare Workers
|
|
120
93
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
import { pracht } from "@pracht/vite-plugin";
|
|
125
|
-
import { cloudflareAdapter } from "@pracht/adapter-cloudflare";
|
|
126
|
-
export default { plugins: [pracht({ adapter: cloudflareAdapter() })] };
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Build & Deploy
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
pracht build
|
|
133
|
-
npx wrangler deploy
|
|
94
|
+
```ts
|
|
95
|
+
pracht({ adapter: cloudflareAdapter() });
|
|
134
96
|
```
|
|
135
97
|
|
|
136
|
-
To smoke-test the built worker locally first, run `pracht preview` — it builds and then delegates to `wrangler dev`, which serves the wrangler config's `main` entry, `dist/server/worker.js`. Keep `no_bundle: true` and the JavaScript `ESModule` rule: Pracht's Vite output is already bundled and can contain lazy server chunks that Wrangler must upload separately.
|
|
137
|
-
|
|
138
|
-
Wrangler owns the Worker's binding environment. Put local-only secrets such as
|
|
139
|
-
`PRACHT_CONFIRMATION_SECRET` and `PRACHT_REVALIDATE_TOKEN` in a gitignored
|
|
140
|
-
`.dev.vars`; prefixing the host command with those variables does not
|
|
141
|
-
automatically expose them inside the Worker. Keep production values in
|
|
142
|
-
`wrangler secret`.
|
|
143
|
-
|
|
144
|
-
If the config contains a custom-domain route, preview can listen on localhost
|
|
145
|
-
while `request.url` inside the Worker uses the custom domain. Sign that
|
|
146
|
-
effective `@authority` for Web Bot Auth or temporarily disable the route. To use
|
|
147
|
-
a separate local config, build first, then run:
|
|
148
|
-
|
|
149
98
|
```bash
|
|
150
|
-
pracht build
|
|
151
|
-
npx wrangler dev --config wrangler.local.jsonc --port 3000
|
|
99
|
+
pracht build && npx wrangler deploy
|
|
152
100
|
```
|
|
153
101
|
|
|
154
|
-
The local config must keep `main: "dist/server/worker.js"`, keep
|
|
155
|
-
`no_bundle: true`, include the JavaScript `ESModule` rule, and omit the
|
|
156
|
-
production route. `pracht preview` does not forward Wrangler's `--config`
|
|
157
|
-
flag.
|
|
158
|
-
|
|
159
|
-
### Wrangler Configuration
|
|
160
|
-
|
|
161
102
|
```jsonc
|
|
162
|
-
// wrangler.jsonc
|
|
103
|
+
// wrangler.jsonc — canonical version at examples/cloudflare/wrangler.jsonc
|
|
163
104
|
{
|
|
164
105
|
"name": "my-pracht-app",
|
|
165
106
|
"main": "dist/server/worker.js",
|
|
@@ -174,36 +115,37 @@ flag.
|
|
|
174
115
|
}
|
|
175
116
|
```
|
|
176
117
|
|
|
177
|
-
`
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
`
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
118
|
+
`no_bundle: true`, the `ESModule` rule, `"binding": "ASSETS"`, and
|
|
119
|
+
`"run_worker_first": true` are all required. Without the first two, Wrangler
|
|
120
|
+
re-bundles Pracht's already-bundled Vite output — folding lazy server chunks
|
|
121
|
+
into the entry or dropping them from the upload. Without the binding,
|
|
122
|
+
`env.ASSETS` silently resolves to `null`: headers and ISG manifests load empty,
|
|
123
|
+
so SSG serving, ISG revalidation, and per-route headers all no-op. If you
|
|
124
|
+
rename the binding via `cloudflareAdapter({ assetsBinding: "STATIC" })`, the
|
|
125
|
+
wrangler `binding` value must match.
|
|
126
|
+
|
|
127
|
+
**Local preview.** `pracht preview` builds, then delegates to `wrangler dev`
|
|
128
|
+
against the config's `main`. Wrangler owns the binding environment: put
|
|
129
|
+
local-only secrets like `PRACHT_CONFIRMATION_SECRET` and
|
|
130
|
+
`PRACHT_REVALIDATE_TOKEN` in a gitignored `.dev.vars` (prefixing the host
|
|
131
|
+
command with them does not expose them inside the Worker) and keep production
|
|
132
|
+
values in `wrangler secret`. With a custom-domain route in the config, preview
|
|
133
|
+
listens on localhost while `request.url` inside the Worker uses the custom
|
|
134
|
+
domain — sign that effective `@authority` for Web Bot Auth, or temporarily
|
|
135
|
+
remove the route. `pracht preview` does not forward Wrangler's `--config`, so a
|
|
136
|
+
separate local config means `pracht build && npx wrangler dev --config
|
|
137
|
+
wrangler.local.jsonc --port 3000`, and that config must keep
|
|
138
|
+
`main: "dist/server/worker.js"`, `no_bundle: true`, and the `ESModule` rule
|
|
139
|
+
while omitting the production route.
|
|
140
|
+
|
|
141
|
+
**Bindings.** Read them inside a loader, API handler, capability `run()`, or
|
|
142
|
+
another request-time function — `context.env.MY_KV.get("key")`. Workers permits
|
|
143
|
+
top-level `env` reads, but graph inspection deliberately fails them rather than
|
|
144
|
+
report binding metadata it cannot authoritatively resolve. Durable Object and
|
|
145
|
+
Workflow classes are named Worker exports: re-export them from
|
|
146
|
+
`workerExportsFrom`. Queue consumers, Cron Triggers, and Email Routing are
|
|
147
|
+
methods on the default export: expose named `queue`, `scheduled`, or `email`
|
|
148
|
+
functions from `workerHandlersFrom`.
|
|
207
149
|
|
|
208
150
|
```ts
|
|
209
151
|
cloudflareAdapter({
|
|
@@ -212,220 +154,193 @@ cloudflareAdapter({
|
|
|
212
154
|
});
|
|
213
155
|
```
|
|
214
156
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
Before enabling it, audit ISG URLs for unbounded query strings. Workers Caching
|
|
157
|
+
**ISG.** Works with no cache option: the worker-managed path serves the
|
|
158
|
+
build-time snapshot, detects staleness, regenerates in the background per colo
|
|
159
|
+
via the Workers Cache API, and `POST /__pracht/revalidate` forces regeneration.
|
|
160
|
+
`cloudflareAdapter({ cache: true })` plus `{ "cache": { "enabled": true } }` in
|
|
161
|
+
wrangler.jsonc moves that to edge-tier Workers Caching: time-revalidated pages
|
|
162
|
+
then render on demand, are cached for their `revalidate` window (stale served
|
|
163
|
+
instantly while the Worker re-renders), and can be purged early with
|
|
164
|
+
`purgeCache()` from `@pracht/adapter-cloudflare/cache`. Webhook-only ISG routes
|
|
165
|
+
keep their build-time snapshots and the worker-managed path either way.
|
|
166
|
+
|
|
167
|
+
Audit ISG URLs for unbounded query strings before enabling it — Workers Caching
|
|
229
168
|
keys the exact path and query string, including parameter order and trailing
|
|
230
|
-
slashes
|
|
231
|
-
with a pathname-only `cf.cacheKey`, and normalize `Accept` there for
|
|
232
|
-
export markdown or declare `markdown: true
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
Time-revalidated ISG pages then render on demand, are cached at the edge for
|
|
236
|
-
their `revalidate` window (stale pages served instantly while the Worker
|
|
237
|
-
re-renders in the background), and can be purged early with `purgeCache()` from
|
|
238
|
-
`@pracht/adapter-cloudflare/cache`. Webhook-only ISG routes keep their
|
|
239
|
-
build-time snapshots and the worker-managed path either way.
|
|
169
|
+
slashes. Use a bounded query allowlist or canonical redirect, or an uncached
|
|
170
|
+
gateway with a pathname-only `cf.cacheKey`, and normalize `Accept` there for
|
|
171
|
+
routes that export markdown or declare `markdown: true`. See
|
|
172
|
+
`docs/ADAPTERS.md#cache-key-cardinality`.
|
|
240
173
|
|
|
241
174
|
---
|
|
242
175
|
|
|
243
|
-
## Netlify
|
|
176
|
+
## Netlify
|
|
244
177
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
2. In `vite.config.ts`:
|
|
249
|
-
```ts
|
|
250
|
-
import { pracht } from "@pracht/vite-plugin";
|
|
251
|
-
import { netlifyAdapter } from "@pracht/adapter-netlify";
|
|
252
|
-
export default { plugins: [pracht({ adapter: netlifyAdapter() })] };
|
|
253
|
-
```
|
|
254
|
-
3. Add `netlify.toml`:
|
|
255
|
-
```toml
|
|
256
|
-
[build]
|
|
257
|
-
command = "pnpm build"
|
|
258
|
-
publish = "dist/client"
|
|
178
|
+
```ts
|
|
179
|
+
pracht({ adapter: netlifyAdapter() });
|
|
180
|
+
```
|
|
259
181
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
182
|
+
```toml
|
|
183
|
+
# netlify.toml
|
|
184
|
+
[build]
|
|
185
|
+
command = "pnpm build"
|
|
186
|
+
publish = "dist/client"
|
|
263
187
|
|
|
264
|
-
|
|
188
|
+
[functions]
|
|
189
|
+
directory = "netlify/functions"
|
|
190
|
+
```
|
|
265
191
|
|
|
266
192
|
```bash
|
|
267
193
|
npx pracht build && npx netlify dev
|
|
268
194
|
npx netlify deploy --build --prod
|
|
269
195
|
```
|
|
270
196
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
`
|
|
297
|
-
|
|
197
|
+
`pracht preview` exits with guidance here — it cannot emulate Netlify's
|
|
198
|
+
Functions and CDN. Build the generated function first, then use `netlify dev`.
|
|
199
|
+
Set `PRACHT_REVALIDATE_TOKEN` in Netlify when webhook revalidation is enabled.
|
|
200
|
+
|
|
201
|
+
The build emits `netlify/functions/pracht.mjs`. Page requests go through it so
|
|
202
|
+
Markdown negotiation and route-state requests stay correct; hashed assets
|
|
203
|
+
bypass it and stay outside the function bundle at the origin root. With a Vite
|
|
204
|
+
deploy base, the function instead bundles and serves the base-free asset and
|
|
205
|
+
`/_pracht` trees so `/app/...` requests stay inside the mount; custom
|
|
206
|
+
`excludedPath` entries still bypass their literal origin-root URLs, but their
|
|
207
|
+
files remain bundled for base-prefixed requests. The generated config
|
|
208
|
+
enumerates only client files the function can serve and roots exclusions at the
|
|
209
|
+
function file, so Netlify's tracer cannot re-add bypassed trees.
|
|
210
|
+
When MCP OAuth is enabled, an exclusion matching either the protected MCP
|
|
211
|
+
resource path or its reserved metadata paths is a build error because it would
|
|
212
|
+
bypass the framework's authentication handler.
|
|
213
|
+
|
|
214
|
+
Caching: Netlify durable caching implements time-based ISG and per-path cache
|
|
215
|
+
tags implement authenticated webhook revalidation. A trailing-slash ISG
|
|
216
|
+
document request permanently redirects to the canonical slashless URL before
|
|
217
|
+
rendering, and webhook revalidation normalizes either spelling before purging
|
|
218
|
+
the tag. Only `Cache-Control`, `CDN-Cache-Control`, and
|
|
219
|
+
`Netlify-CDN-Cache-Control` override the adapter's cache defaults —
|
|
220
|
+
provider-specific headers for another CDN do not; a window of `0` disables
|
|
221
|
+
stale serving or freshness. `Netlify-Vary` owns route-state variants while
|
|
222
|
+
standard `Vary: Accept` owns Markdown negotiation, and cacheable negotiated SSG
|
|
223
|
+
representations reuse their prerendered HTML's `Netlify-Vary` instructions.
|
|
224
|
+
Shared ISG renders strip visitor-specific request data and Netlify context
|
|
225
|
+
metadata before loaders or context factories run.
|
|
298
226
|
|
|
299
227
|
---
|
|
300
228
|
|
|
301
|
-
## Vercel
|
|
229
|
+
## Vercel
|
|
302
230
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
2. In `vite.config.ts`:
|
|
307
|
-
```ts
|
|
308
|
-
import { pracht } from "@pracht/vite-plugin";
|
|
309
|
-
import { vercelAdapter } from "@pracht/adapter-vercel";
|
|
310
|
-
export default { plugins: [pracht({ adapter: vercelAdapter() })] };
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### Build & Deploy
|
|
231
|
+
```ts
|
|
232
|
+
pracht({ adapter: vercelAdapter() });
|
|
233
|
+
```
|
|
314
234
|
|
|
315
235
|
```bash
|
|
316
|
-
pracht build
|
|
317
|
-
npx vercel deploy --prebuilt
|
|
236
|
+
pracht build && npx vercel deploy --prebuilt
|
|
318
237
|
```
|
|
319
238
|
|
|
320
|
-
|
|
239
|
+
Emits `.vercel/output/config.json`, `.vercel/output/static/`, and
|
|
240
|
+
`.vercel/output/functions/render.func/server.js`.
|
|
321
241
|
|
|
322
|
-
There is no faithful local Vercel production runtime, so `pracht preview`
|
|
323
|
-
|
|
242
|
+
There is no faithful local Vercel production runtime, so `pracht preview` exits
|
|
243
|
+
with guidance — use `vercel build` or `vercel dev`. Set
|
|
324
244
|
`PRACHT_REVALIDATE_TOKEN` at build time when using webhook revalidation; its
|
|
325
245
|
Vercel bypass token is embedded in `.prerender-config.json`. Rename the main
|
|
326
|
-
Edge Function with `vercelAdapter({ functionName })` if
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
246
|
+
Edge Function with `vercelAdapter({ functionName })` if the default `render`
|
|
247
|
+
would collide with an ISG route. Custom entries must export the `nodeListener`
|
|
248
|
+
created by `createVercelNodeListener(handle)` for Node ISR functions.
|
|
249
|
+
When remote MCP is configured, the generated route table sends its endpoint
|
|
250
|
+
and OAuth metadata paths to the runtime before method-agnostic SSG rewrites;
|
|
251
|
+
keep page routes off the MCP pathname even though the runtime wins that collision.
|
|
330
252
|
|
|
331
253
|
---
|
|
332
254
|
|
|
333
|
-
## Static
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
manifest-registered capabilities participate; every registered capability
|
|
342
|
-
module must load successfully so exposure validation can fail closed. The
|
|
343
|
-
`notFound` page must use full hydration (the default), because the shared
|
|
344
|
-
`404.html` needs the client router to adopt the visitor's actual URL. Sub-path
|
|
345
|
-
deploys (GitHub Pages *project* sites, S3 key prefixes) set Vite `base` to that
|
|
346
|
-
path; CDN and document-relative bases (`""` / `"./"`) are build errors,
|
|
347
|
-
because they split assets from the deploy root or resolve them beneath nested
|
|
348
|
-
page directories. Under a base,
|
|
349
|
-
internal navigation must go through `<Link route>` / `href()` — a hand-written
|
|
350
|
-
`<a href="/about">` still means the origin root.
|
|
351
|
-
Pracht's preview and first-party serverful adapters redirect the bare base
|
|
352
|
-
(`/app`) to its trailing-slash form (`/app/`) before serving the root document;
|
|
353
|
-
custom adapters receive the same behavior through `handlePrachtRequest()`.
|
|
354
|
-
Framework-owned browser URLs from the default image loader and OpenAPI
|
|
355
|
-
companion artifacts pick up the same base automatically.
|
|
356
|
-
|
|
357
|
-
### Setup
|
|
358
|
-
|
|
359
|
-
1. Ensure `@pracht/adapter-static` is installed.
|
|
360
|
-
2. In `vite.config.ts`:
|
|
361
|
-
```ts
|
|
362
|
-
import { pracht } from "@pracht/vite-plugin";
|
|
363
|
-
import { staticAdapter } from "@pracht/adapter-static";
|
|
364
|
-
export default { plugins: [pracht({ adapter: staticAdapter() })] };
|
|
365
|
-
// With dynamic SPA routes, add { fallback: "200.html" } and configure the
|
|
366
|
-
// host to rewrite unmatched URLs to it. If the route or shell exports
|
|
367
|
-
// head(), also set generic fallbackHead metadata shared by every rewrite.
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
### Build & Deploy
|
|
255
|
+
## Static export
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
pracht({ adapter: staticAdapter() });
|
|
259
|
+
// Dynamic SPA routes: staticAdapter({ fallback: "200.html" }) plus a host
|
|
260
|
+
// rewrite for unmatched URLs. If the route or shell exports head(), also set
|
|
261
|
+
// generic fallbackHead metadata shared by every rewrite.
|
|
262
|
+
```
|
|
371
263
|
|
|
372
264
|
```bash
|
|
373
265
|
pracht build # dist/client/ is the whole deployment
|
|
374
266
|
pracht preview # local static file server over dist/client/
|
|
375
267
|
```
|
|
376
268
|
|
|
377
|
-
Upload `dist/client/` to any static host
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
the
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
269
|
+
Upload `dist/client/` to any static host. `dist/server/` is build tooling —
|
|
270
|
+
never deploy it. The host must serve `<dir>/index.html` for clean URLs and
|
|
271
|
+
should use `404.html` as its error document. See `docs/ADAPTERS.md` § Static
|
|
272
|
+
Adapter for host header configuration and the markdown-negotiation and
|
|
273
|
+
base-path limitations.
|
|
274
|
+
|
|
275
|
+
**Eligibility.** Every route must be `render: "ssg"` (or a loaderless,
|
|
276
|
+
full-hydration `"spa"`), with no request middleware, API routes, or
|
|
277
|
+
HTTP/MCP/WebMCP-exposed capabilities. Anything else fails the build with an
|
|
278
|
+
error naming the offenders — that is the signal to pick a serverful adapter.
|
|
279
|
+
SSG loaders run only at build time and must produce HTML plus valid JSON route
|
|
280
|
+
state; dynamic SSG routes must export `getStaticPaths()`. Only
|
|
281
|
+
manifest-registered capabilities participate, and every registered capability
|
|
282
|
+
module must load successfully so exposure validation fails closed. The
|
|
283
|
+
`notFound` page must use full hydration (the default) because the shared
|
|
284
|
+
`404.html` needs the client router to adopt the visitor's actual URL.
|
|
285
|
+
|
|
286
|
+
**Deploy base.** Sub-path deploys (GitHub Pages *project* sites, S3 key
|
|
287
|
+
prefixes) set Vite `base` to that path. CDN and document-relative bases (`""`,
|
|
288
|
+
`"./"`) are build errors — they split assets from the deploy root or resolve
|
|
289
|
+
them beneath nested page directories. Under a base, internal navigation must go
|
|
290
|
+
through `<Link route>` / `href()`; a hand-written `<a href="/about">` still
|
|
291
|
+
means the origin root. Preview and the first-party serverful adapters redirect
|
|
292
|
+
the bare base (`/app` → `/app/`) before serving the root document, and custom
|
|
293
|
+
adapters get the same behavior via `handlePrachtRequest()`. Framework-owned
|
|
294
|
+
browser URLs from the default image loader and the OpenAPI companion artifacts
|
|
295
|
+
pick up the base automatically.
|
|
296
|
+
|
|
297
|
+
**Route state.** Client navigation fetches collision-safe bounded opaque
|
|
298
|
+
`.json` files under `_pracht/state/`, for full-hydration SSG routes whose
|
|
299
|
+
loader or route/shell `head()` metadata participates in navigation. Equivalent
|
|
300
|
+
raw-Unicode and percent-encoded URL segment spellings resolve to the same state
|
|
301
|
+
file. Explicitly loaderless and headless routes fetch no state; loaderless
|
|
302
|
+
routes with head metadata fetch static state for font-head fragments but still
|
|
303
|
+
hit an external API from the browser for live data.
|
|
304
|
+
|
|
305
|
+
**Build-time collision guards** (all fail the build rather than overwrite):
|
|
306
|
+
|
|
307
|
+
- A file under `public/_pracht/state/` occupying a generated route-state path.
|
|
308
|
+
- A `public/` or Vite-emitted file occupying the generated `404.html` or the
|
|
309
|
+
configured fallback path, including case- or Unicode-normalization-equivalent
|
|
310
|
+
spellings.
|
|
311
|
+
- Prerendered pages that do not map to distinct portable filesystem paths:
|
|
312
|
+
duplicate, case-folded, or Unicode-normalization-equivalent outputs;
|
|
313
|
+
Windows-invalid or overlong filename components; file/directory conflicts
|
|
314
|
+
such as `/` against `/index.html`. Fallback names additionally reject Windows
|
|
315
|
+
reserved device names and the portable 255-byte/code-unit component limit.
|
|
316
|
+
|
|
317
|
+
Pages are written to the percent-decoded output path, matching how static hosts
|
|
318
|
+
resolve requests; `pracht preview` decodes request segments the same way.
|
|
319
|
+
|
|
320
|
+
**SPA fallback.** It only client-renders matched SPA routes. Dynamic SSG paths
|
|
321
|
+
omitted by `getStaticPaths()` render the app's not-found page with the
|
|
322
|
+
build-time loader data or handled error state carried over from `404.html`. The
|
|
323
|
+
host rewrite answers unknown URLs with status 200 (a soft 404), and an app with
|
|
324
|
+
no `notFound` page and no unshadowed client-routable SPA catch-all renders them
|
|
325
|
+
blank — the build warns about that shape. A dynamic SPA route, its shell, or
|
|
326
|
+
the not-found page exporting `head()` requires an explicit `fallbackHead`,
|
|
327
|
+
because the shared static document cannot evaluate URL-specific server
|
|
328
|
+
metadata; generic `fallbackHead` fonts stay registered while the fallback
|
|
329
|
+
commits a loaderless dynamic SPA route.
|
|
411
330
|
|
|
412
331
|
---
|
|
413
332
|
|
|
414
|
-
##
|
|
415
|
-
|
|
416
|
-
1.
|
|
417
|
-
2.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
2. Run `pracht build` to verify the build succeeds before deploying.
|
|
427
|
-
3. Smoke-test the production runtime before pushing to production. For Node.js and Cloudflare, run `pracht preview`; for Netlify, run `pracht build && netlify dev`.
|
|
428
|
-
4. If the user needs an adapter that isn't installed, help them add it (`pnpm add @pracht/adapter-*`).
|
|
429
|
-
5. Don't push to production without the user's explicit confirmation.
|
|
333
|
+
## Pre-flight checklist
|
|
334
|
+
|
|
335
|
+
1. `pracht build` succeeds and `dist/` looks right.
|
|
336
|
+
2. Every secret and config value the loaders need is present in the target
|
|
337
|
+
runtime.
|
|
338
|
+
3. `dist/client/` holds prerendered HTML for SSG routes — and for ISG routes,
|
|
339
|
+
except time-revalidated ones on Cloudflare with Workers Caching enabled,
|
|
340
|
+
which render on demand. Webhook-only ISG routes keep build-time snapshots.
|
|
341
|
+
4. The ISG manifest exists if ISG is in use: `dist/server/isg-manifest.json`,
|
|
342
|
+
plus `dist/client/_pracht/isg.json` on Cloudflare.
|
|
343
|
+
5. API endpoints answer correctly in the production runtime.
|
|
344
|
+
6. Auth and redirect middleware behave correctly in production.
|
|
430
345
|
|
|
431
346
|
$ARGUMENTS
|