create-pracht 0.6.0 → 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 +56 -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 -415
- package/skills/pracht-debug/SKILL.md +165 -120
- package/skills/pracht-deploy/SKILL.md +248 -329
- package/skills/pracht-scaffold/SKILL.md +123 -146
- package/skills/pracht-test-api/SKILL.md +10 -10
- package/skills/pre-deploy/SKILL.md +166 -195
- 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 +15 -11
- package/skills/upgrade-pracht/SKILL.md +12 -10
- package/src/index.js +43 -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,53 +89,23 @@ 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`.
|
|
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"` and omit the
|
|
155
|
-
production route. `pracht preview` does not forward Wrangler's `--config` flag.
|
|
156
|
-
|
|
157
|
-
### Wrangler Configuration
|
|
158
|
-
|
|
159
102
|
```jsonc
|
|
160
|
-
// wrangler.jsonc
|
|
103
|
+
// wrangler.jsonc — canonical version at examples/cloudflare/wrangler.jsonc
|
|
161
104
|
{
|
|
162
105
|
"name": "my-pracht-app",
|
|
163
106
|
"main": "dist/server/worker.js",
|
|
107
|
+
"no_bundle": true,
|
|
108
|
+
"rules": [{ "type": "ESModule", "globs": ["**/*.js", "**/*.mjs"] }],
|
|
164
109
|
"compatibility_date": "2026-04-06",
|
|
165
110
|
"assets": {
|
|
166
111
|
"binding": "ASSETS",
|
|
@@ -170,36 +115,37 @@ production route. `pracht preview` does not forward Wrangler's `--config` flag.
|
|
|
170
115
|
}
|
|
171
116
|
```
|
|
172
117
|
|
|
173
|
-
`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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`.
|
|
203
149
|
|
|
204
150
|
```ts
|
|
205
151
|
cloudflareAdapter({
|
|
@@ -208,220 +154,193 @@ cloudflareAdapter({
|
|
|
208
154
|
});
|
|
209
155
|
```
|
|
210
156
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
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
|
|
225
168
|
keys the exact path and query string, including parameter order and trailing
|
|
226
|
-
slashes
|
|
227
|
-
with a pathname-only `cf.cacheKey`, and normalize `Accept` there for
|
|
228
|
-
export markdown or declare `markdown: true
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Time-revalidated ISG pages then render on demand, are cached at the edge for
|
|
232
|
-
their `revalidate` window (stale pages served instantly while the Worker
|
|
233
|
-
re-renders in the background), and can be purged early with `purgeCache()` from
|
|
234
|
-
`@pracht/adapter-cloudflare/cache`. Webhook-only ISG routes keep their
|
|
235
|
-
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`.
|
|
236
173
|
|
|
237
174
|
---
|
|
238
175
|
|
|
239
|
-
## Netlify
|
|
176
|
+
## Netlify
|
|
240
177
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
2. In `vite.config.ts`:
|
|
245
|
-
```ts
|
|
246
|
-
import { pracht } from "@pracht/vite-plugin";
|
|
247
|
-
import { netlifyAdapter } from "@pracht/adapter-netlify";
|
|
248
|
-
export default { plugins: [pracht({ adapter: netlifyAdapter() })] };
|
|
249
|
-
```
|
|
250
|
-
3. Add `netlify.toml`:
|
|
251
|
-
```toml
|
|
252
|
-
[build]
|
|
253
|
-
command = "pnpm build"
|
|
254
|
-
publish = "dist/client"
|
|
178
|
+
```ts
|
|
179
|
+
pracht({ adapter: netlifyAdapter() });
|
|
180
|
+
```
|
|
255
181
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
182
|
+
```toml
|
|
183
|
+
# netlify.toml
|
|
184
|
+
[build]
|
|
185
|
+
command = "pnpm build"
|
|
186
|
+
publish = "dist/client"
|
|
259
187
|
|
|
260
|
-
|
|
188
|
+
[functions]
|
|
189
|
+
directory = "netlify/functions"
|
|
190
|
+
```
|
|
261
191
|
|
|
262
192
|
```bash
|
|
263
193
|
npx pracht build && npx netlify dev
|
|
264
194
|
npx netlify deploy --build --prod
|
|
265
195
|
```
|
|
266
196
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`
|
|
293
|
-
|
|
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.
|
|
294
226
|
|
|
295
227
|
---
|
|
296
228
|
|
|
297
|
-
## Vercel
|
|
229
|
+
## Vercel
|
|
298
230
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
2. In `vite.config.ts`:
|
|
303
|
-
```ts
|
|
304
|
-
import { pracht } from "@pracht/vite-plugin";
|
|
305
|
-
import { vercelAdapter } from "@pracht/adapter-vercel";
|
|
306
|
-
export default { plugins: [pracht({ adapter: vercelAdapter() })] };
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Build & Deploy
|
|
231
|
+
```ts
|
|
232
|
+
pracht({ adapter: vercelAdapter() });
|
|
233
|
+
```
|
|
310
234
|
|
|
311
235
|
```bash
|
|
312
|
-
pracht build
|
|
313
|
-
npx vercel deploy --prebuilt
|
|
236
|
+
pracht build && npx vercel deploy --prebuilt
|
|
314
237
|
```
|
|
315
238
|
|
|
316
|
-
|
|
239
|
+
Emits `.vercel/output/config.json`, `.vercel/output/static/`, and
|
|
240
|
+
`.vercel/output/functions/render.func/server.js`.
|
|
317
241
|
|
|
318
|
-
There is no faithful local Vercel production runtime, so `pracht preview`
|
|
319
|
-
|
|
242
|
+
There is no faithful local Vercel production runtime, so `pracht preview` exits
|
|
243
|
+
with guidance — use `vercel build` or `vercel dev`. Set
|
|
320
244
|
`PRACHT_REVALIDATE_TOKEN` at build time when using webhook revalidation; its
|
|
321
245
|
Vercel bypass token is embedded in `.prerender-config.json`. Rename the main
|
|
322
|
-
Edge Function with `vercelAdapter({ functionName })` if
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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.
|
|
326
252
|
|
|
327
253
|
---
|
|
328
254
|
|
|
329
|
-
## Static
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
manifest-registered capabilities participate; every registered capability
|
|
338
|
-
module must load successfully so exposure validation can fail closed. The
|
|
339
|
-
`notFound` page must use full hydration (the default), because the shared
|
|
340
|
-
`404.html` needs the client router to adopt the visitor's actual URL. Sub-path
|
|
341
|
-
deploys (GitHub Pages *project* sites, S3 key prefixes) set Vite `base` to that
|
|
342
|
-
path; CDN and document-relative bases (`""` / `"./"`) are build errors,
|
|
343
|
-
because they split assets from the deploy root or resolve them beneath nested
|
|
344
|
-
page directories. Under a base,
|
|
345
|
-
internal navigation must go through `<Link route>` / `href()` — a hand-written
|
|
346
|
-
`<a href="/about">` still means the origin root.
|
|
347
|
-
Pracht's preview and first-party serverful adapters redirect the bare base
|
|
348
|
-
(`/app`) to its trailing-slash form (`/app/`) before serving the root document;
|
|
349
|
-
custom adapters receive the same behavior through `handlePrachtRequest()`.
|
|
350
|
-
Framework-owned browser URLs from the default image loader and OpenAPI
|
|
351
|
-
companion artifacts pick up the same base automatically.
|
|
352
|
-
|
|
353
|
-
### Setup
|
|
354
|
-
|
|
355
|
-
1. Ensure `@pracht/adapter-static` is installed.
|
|
356
|
-
2. In `vite.config.ts`:
|
|
357
|
-
```ts
|
|
358
|
-
import { pracht } from "@pracht/vite-plugin";
|
|
359
|
-
import { staticAdapter } from "@pracht/adapter-static";
|
|
360
|
-
export default { plugins: [pracht({ adapter: staticAdapter() })] };
|
|
361
|
-
// With dynamic SPA routes, add { fallback: "200.html" } and configure the
|
|
362
|
-
// host to rewrite unmatched URLs to it. If the route or shell exports
|
|
363
|
-
// head(), also set generic fallbackHead metadata shared by every rewrite.
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### 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
|
+
```
|
|
367
263
|
|
|
368
264
|
```bash
|
|
369
265
|
pracht build # dist/client/ is the whole deployment
|
|
370
266
|
pracht preview # local static file server over dist/client/
|
|
371
267
|
```
|
|
372
268
|
|
|
373
|
-
Upload `dist/client/` to any static host
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
the
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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.
|
|
407
330
|
|
|
408
331
|
---
|
|
409
332
|
|
|
410
|
-
##
|
|
411
|
-
|
|
412
|
-
1.
|
|
413
|
-
2.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
2. Run `pracht build` to verify the build succeeds before deploying.
|
|
423
|
-
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`.
|
|
424
|
-
4. If the user needs an adapter that isn't installed, help them add it (`pnpm add @pracht/adapter-*`).
|
|
425
|
-
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.
|
|
426
345
|
|
|
427
346
|
$ARGUMENTS
|