create-pracht 0.2.6 → 0.4.0
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 +38 -0
- package/package.json +3 -2
- package/skills/add-auth/SKILL.md +346 -0
- package/skills/add-db/SKILL.md +316 -0
- package/skills/add-i18n/SKILL.md +239 -0
- package/skills/add-observability/SKILL.md +270 -0
- package/skills/audit-a11y/SKILL.md +170 -0
- package/skills/audit-auth/SKILL.md +144 -0
- package/skills/audit-bundles/SKILL.md +167 -0
- package/skills/audit-csrf/SKILL.md +179 -0
- package/skills/audit-deps/SKILL.md +138 -0
- package/skills/audit-headers/SKILL.md +186 -0
- package/skills/audit-islands/SKILL.md +144 -0
- package/skills/audit-loaders/SKILL.md +135 -0
- package/skills/audit-redirects/SKILL.md +146 -0
- package/skills/audit-secrets/SKILL.md +150 -0
- package/skills/audit-seo/SKILL.md +164 -0
- package/skills/audit-shells/SKILL.md +142 -0
- package/skills/configure-isg/SKILL.md +172 -0
- package/skills/migrate-nextjs/SKILL.md +519 -0
- package/skills/pracht-debug/SKILL.md +146 -0
- package/skills/pracht-deploy/SKILL.md +208 -0
- package/skills/pracht-scaffold/SKILL.md +191 -0
- package/skills/pracht-test-api/SKILL.md +165 -0
- package/skills/pre-deploy/SKILL.md +164 -0
- package/skills/scaffold-e2e/SKILL.md +189 -0
- package/skills/scaffold-tests/SKILL.md +283 -0
- package/skills/tune-render-mode/SKILL.md +168 -0
- package/skills/typed-routes/SKILL.md +203 -0
- package/skills/upgrade-pracht/SKILL.md +151 -0
- package/src/index.js +472 -31
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-seo
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Per-route SEO audit for a pracht app: `head()` coverage, title/description
|
|
6
|
+
presence, Open Graph and Twitter card completeness, canonical URLs, robots
|
|
7
|
+
rules, and a generated `sitemap.xml` derived from the route manifest.
|
|
8
|
+
Use when asked to "audit SEO", "check meta tags", "generate a sitemap",
|
|
9
|
+
"are my OG cards set", or "review robots.txt".
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash
|
|
12
|
+
- Read
|
|
13
|
+
- Write
|
|
14
|
+
- Grep
|
|
15
|
+
- Glob
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Pracht Audit SEO
|
|
19
|
+
|
|
20
|
+
Pracht owns the document. Per-route SEO lives in the `head()` export
|
|
21
|
+
returning `{ title?, lang?, meta?, link?, script? }`. This skill audits
|
|
22
|
+
coverage and generates the static SEO artifacts.
|
|
23
|
+
|
|
24
|
+
## Step 1: Inventory
|
|
25
|
+
|
|
26
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
27
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
28
|
+
shelling out.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pracht inspect routes --json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
|
|
35
|
+
wired up; run from the app root.
|
|
36
|
+
|
|
37
|
+
For every route file, read the `head()` export (and the shell's `head()` for
|
|
38
|
+
inherited values).
|
|
39
|
+
|
|
40
|
+
## Step 2: Per-route checklist
|
|
41
|
+
|
|
42
|
+
For each route, capture presence and quality:
|
|
43
|
+
|
|
44
|
+
| Field | Expected |
|
|
45
|
+
| ---------------------------------------- | --------------------------------- |
|
|
46
|
+
| `title` | 30-60 chars, unique per route |
|
|
47
|
+
| `meta` `description` | 70-160 chars, unique per route |
|
|
48
|
+
| `meta` `og:title` | Present (often = `title`) |
|
|
49
|
+
| `meta` `og:description` | Present |
|
|
50
|
+
| `meta` `og:image` | Absolute URL, ≥ 1200×630 |
|
|
51
|
+
| `meta` `og:url` | Absolute, canonical |
|
|
52
|
+
| `meta` `twitter:card` | `summary_large_image` for content |
|
|
53
|
+
| `link` `canonical` | Absolute URL |
|
|
54
|
+
| `script` (JSON-LD) | Optional; see Step 6 |
|
|
55
|
+
| `lang` | Set on root or via shell |
|
|
56
|
+
|
|
57
|
+
Skip SPA-only / non-indexable routes (admin, dashboard) — flag as "noindex
|
|
58
|
+
candidate" if they don't already declare it.
|
|
59
|
+
|
|
60
|
+
## Step 3: Cross-route checks
|
|
61
|
+
|
|
62
|
+
- **Duplicate titles** across routes — list collisions.
|
|
63
|
+
- **Duplicate descriptions** — same.
|
|
64
|
+
- **Missing canonical** on any route that has any `?query` variants.
|
|
65
|
+
- **Missing OG image** — most common issue; recommend a default image at
|
|
66
|
+
shell level so every route inherits one.
|
|
67
|
+
|
|
68
|
+
## Step 4: `robots.txt`
|
|
69
|
+
|
|
70
|
+
`robots.txt` MUST be served at the origin root (`/robots.txt`) — crawlers
|
|
71
|
+
never look anywhere else. Pracht API routes are always mounted under `/api/`
|
|
72
|
+
(a `src/api/robots.ts` handler serves `/api/robots`, never `/robots.txt`), so
|
|
73
|
+
an API handler cannot provide it. The workable option is the static asset:
|
|
74
|
+
|
|
75
|
+
- `public/robots.txt` (Vite static asset, served at `/robots.txt`).
|
|
76
|
+
|
|
77
|
+
If absent, recommend creating `public/robots.txt`:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
User-agent: *
|
|
81
|
+
Allow: /
|
|
82
|
+
Disallow: /api/
|
|
83
|
+
Disallow: /admin/
|
|
84
|
+
Sitemap: https://<domain>/sitemap.xml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If present, validate:
|
|
88
|
+
- A `Sitemap:` line referencing an existing endpoint (see Step 5 — if the
|
|
89
|
+
sitemap is an API route, this must point at `/api/sitemap`).
|
|
90
|
+
- No accidental `Disallow: /` (full-site block).
|
|
91
|
+
- Path patterns match real routes (cross-reference with the manifest).
|
|
92
|
+
- Flag any `src/api/robots.ts` found in the repo as `warn`: it is dead
|
|
93
|
+
weight at `/api/robots` and does not serve `/robots.txt`.
|
|
94
|
+
|
|
95
|
+
## Step 5: sitemap
|
|
96
|
+
|
|
97
|
+
Generate (or recommend generating) a sitemap from the route manifest:
|
|
98
|
+
|
|
99
|
+
- Include routes by **indexability**, not prefetch strategy: public
|
|
100
|
+
`render: "ssg"`/`"isg"` routes (and public SSR routes worth indexing),
|
|
101
|
+
minus routes under auth middleware, minus routes declaring a
|
|
102
|
+
`robots`/`noindex` meta. (`prefetch` is a client navigation hint —
|
|
103
|
+
orthogonal to indexability; do not use it as a criterion.)
|
|
104
|
+
- Skip dynamic-segment routes unless `getStaticPaths` resolved them at build
|
|
105
|
+
time — pull resolved paths from the prerender output, which is laid out as
|
|
106
|
+
`dist/client/<route>/index.html` (clean URLs; e.g. `/about` →
|
|
107
|
+
`dist/client/about/index.html`).
|
|
108
|
+
- Default `<changefreq>` from the route's revalidate policy: `timeRevalidate`
|
|
109
|
+
→ `weekly` if `> 86400s`, `daily` if `> 3600s`, `hourly` otherwise.
|
|
110
|
+
|
|
111
|
+
Offer two output forms:
|
|
112
|
+
|
|
113
|
+
1. Static `public/sitemap.xml` regenerated at build time via a small script
|
|
114
|
+
(served at `/sitemap.xml`).
|
|
115
|
+
2. A pracht API route at `src/api/sitemap.ts` that emits XML on each request
|
|
116
|
+
from the inspected manifest. It is served at `/api/sitemap` (API routes
|
|
117
|
+
always mount under `/api/`), so the robots `Sitemap:` line must point at
|
|
118
|
+
`https://<domain>/api/sitemap`.
|
|
119
|
+
|
|
120
|
+
## Step 6: Structured data (optional)
|
|
121
|
+
|
|
122
|
+
If the user asks, scaffold JSON-LD via the `head()` export's native `script`
|
|
123
|
+
field — `HeadMetadata` supports `script?: HeadScriptDescriptor[]`, where each
|
|
124
|
+
descriptor takes attributes plus `children` for the inline body:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
export function head() {
|
|
128
|
+
return {
|
|
129
|
+
script: [
|
|
130
|
+
{
|
|
131
|
+
type: "application/ld+json",
|
|
132
|
+
children: JSON.stringify({ "@context": "https://schema.org", ... }),
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
No shell-level custom `<script>` JSX is needed. This is opt-in — do not push
|
|
140
|
+
it on every audit.
|
|
141
|
+
|
|
142
|
+
## Step 7: Report
|
|
143
|
+
|
|
144
|
+
| Route | Severity | Title | Description | OG image | Canonical | Verdict |
|
|
145
|
+
| ----- | -------- | ----- | ----------- | -------- | --------- | ------- |
|
|
146
|
+
|
|
147
|
+
Primary severity per finding: `error` (site blocked by robots, auth-gated
|
|
148
|
+
route in sitemap), `warn` (missing title/description/OG image/canonical on an
|
|
149
|
+
indexable route), `info` (nice-to-haves like JSON-LD). Keep the
|
|
150
|
+
`complete`/`partial`/`missing` verdict as a secondary per-route rollup,
|
|
151
|
+
grouped by verdict.
|
|
152
|
+
|
|
153
|
+
## Rules
|
|
154
|
+
|
|
155
|
+
1. Use the resolved manifest — shell `head()` inheritance matters.
|
|
156
|
+
2. Never auto-write `sitemap.xml` to the deployed site without user
|
|
157
|
+
confirmation; offer the file as a draft.
|
|
158
|
+
3. Recommend a default OG image at the shell level — single highest-leverage
|
|
159
|
+
fix.
|
|
160
|
+
4. Auth-gated routes should NOT appear in sitemaps.
|
|
161
|
+
5. Cross-reference with `tune-render-mode` — SSG routes are the sitemap
|
|
162
|
+
candidates; SSR routes need decision per case.
|
|
163
|
+
|
|
164
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-shells
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Audit pracht shells for composition bugs: missing `Loading()` on SPA-using
|
|
6
|
+
shells, accidental `<html>`/`<head>`/`<body>` rendering, shells that swallow
|
|
7
|
+
children, unused shells, and redundant `ErrorBoundary` exports (shell-level
|
|
8
|
+
boundaries are valid fallbacks; routes win when both declare one).
|
|
9
|
+
Use when asked to "audit shells", "check shell composition", "find unused
|
|
10
|
+
shells", or "is my layout structured correctly".
|
|
11
|
+
allowed-tools:
|
|
12
|
+
- Bash
|
|
13
|
+
- Read
|
|
14
|
+
- Grep
|
|
15
|
+
- Glob
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Pracht Audit Shells
|
|
19
|
+
|
|
20
|
+
Shells in pracht are named layout components composed around routes. The
|
|
21
|
+
framework owns the document — shells must not render `<html>`, `<head>`, or
|
|
22
|
+
`<body>`. They sit between the framework's HTML scaffold and the route
|
|
23
|
+
component.
|
|
24
|
+
|
|
25
|
+
## Step 1: Enumerate
|
|
26
|
+
|
|
27
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
28
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
29
|
+
shelling out.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pracht inspect routes --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
|
|
36
|
+
wired up; run it from the app root.
|
|
37
|
+
|
|
38
|
+
For every shell used by the app, read its source file. Also list which routes
|
|
39
|
+
use which shell (the JSON output includes resolved `shell` and `shellFile` per
|
|
40
|
+
route). The JSON's top-level `mode` field tells you which router the app uses
|
|
41
|
+
— see Step 3 for how that changes shell discovery.
|
|
42
|
+
|
|
43
|
+
## Step 2: Per-shell checks
|
|
44
|
+
|
|
45
|
+
For each shell file:
|
|
46
|
+
|
|
47
|
+
### 2a. Document-level tag misuse
|
|
48
|
+
|
|
49
|
+
Grep for `<html`, `<head>`, `<body>`, `<meta`, `<title>`, `<link rel`. Any of
|
|
50
|
+
these inside a shell is a bug — the framework injects them based on `head()`
|
|
51
|
+
exports. Recommend moving meta/title to the shell's `head()` export and
|
|
52
|
+
removing the JSX tags.
|
|
53
|
+
|
|
54
|
+
### 2b. `Shell` export shape
|
|
55
|
+
|
|
56
|
+
- Must be a function component named `Shell`.
|
|
57
|
+
- Must accept `{ children }: ShellProps`.
|
|
58
|
+
- Must render `{children}` somewhere — flag shells that never render
|
|
59
|
+
`children` (hard to spot, blank page everywhere).
|
|
60
|
+
|
|
61
|
+
### 2c. `Loading()` for SPA routes
|
|
62
|
+
|
|
63
|
+
If any route assigned to this shell has `render: "spa"`, the shell SHOULD
|
|
64
|
+
export a `Loading()` function that renders a placeholder during the
|
|
65
|
+
client-only data fetch. Without it, users see blank content during navigation.
|
|
66
|
+
|
|
67
|
+
### 2d. `head()` export
|
|
68
|
+
|
|
69
|
+
- Optional, but recommended if the shell sets shared meta tags.
|
|
70
|
+
- Verify return shape matches `{ title?, lang?, meta?, link?, script? }`.
|
|
71
|
+
- Flag shells whose `head()` returns `undefined` unconditionally — delete the
|
|
72
|
+
export.
|
|
73
|
+
|
|
74
|
+
### 2e. `ErrorBoundary` export
|
|
75
|
+
|
|
76
|
+
`ErrorBoundary` is a valid export on **both** shells and routes (see
|
|
77
|
+
`ShellModule` and `RouteModule` in `packages/framework/src/types.ts`). The
|
|
78
|
+
runtime resolves `routeMod.ErrorBoundary ?? shellModule.ErrorBoundary` — a
|
|
79
|
+
shell-level boundary is the fallback for every route under that shell when
|
|
80
|
+
the route doesn't declare its own.
|
|
81
|
+
|
|
82
|
+
- A shell exporting `ErrorBoundary` is fine — often the right place for a
|
|
83
|
+
shared error fallback.
|
|
84
|
+
- If **both** the shell and a route declare one, the route wins. Flag as
|
|
85
|
+
`info` at most, and only if the shell boundary is thereby unreachable for
|
|
86
|
+
every route under it (dead code).
|
|
87
|
+
|
|
88
|
+
### 2f. `headers()` export
|
|
89
|
+
|
|
90
|
+
Shells may export `headers()` to contribute response headers (merged with
|
|
91
|
+
route `headers()`). Optional, but if present:
|
|
92
|
+
- Verify the return shape is a plain `HeadersInit`.
|
|
93
|
+
- Flag shells whose `headers()` returns `undefined` unconditionally — delete
|
|
94
|
+
the export.
|
|
95
|
+
|
|
96
|
+
## Step 3: Coverage and waste
|
|
97
|
+
|
|
98
|
+
Shell registration is mode-aware — check the `mode` field from the Step 1
|
|
99
|
+
JSON first:
|
|
100
|
+
|
|
101
|
+
- **Manifest apps** (`mode: "manifest"`): shells are
|
|
102
|
+
registered in `defineApp({ shells })`. Diff that registry against the
|
|
103
|
+
per-route resolved `shell`/`shellFile` values from the inspect JSON — the
|
|
104
|
+
JSON has no shell registry of its own, so "unused" means "registered in
|
|
105
|
+
`defineApp` but referenced by no route or group".
|
|
106
|
+
- **Pages apps** (`mode: "pages"`): there is no `defineApp` shell registry.
|
|
107
|
+
The shell is `src/pages/_app.tsx`, auto-registered under the name `"pages"`
|
|
108
|
+
and applied to every route (see docs/ROUTING.md). "Unused shells" analysis
|
|
109
|
+
does not apply; instead verify `_app.tsx` (if present) shows up as the
|
|
110
|
+
resolved shell on every route.
|
|
111
|
+
|
|
112
|
+
Then report:
|
|
113
|
+
|
|
114
|
+
- **Unused shells** (manifest apps only): registered but referenced by no
|
|
115
|
+
route or group. Recommend removal.
|
|
116
|
+
- **Single-use shells**: shells used by exactly one route — sometimes a
|
|
117
|
+
signal the layout should be inlined. Flag as `info`.
|
|
118
|
+
- **Routes without shells**: routes resolved to no shell. Usually intentional
|
|
119
|
+
for raw HTML responses, but worth listing.
|
|
120
|
+
|
|
121
|
+
## Step 4: Report
|
|
122
|
+
|
|
123
|
+
| Shell | File | Used by | Issue | Severity |
|
|
124
|
+
| ----- | ---- | ------- | ----- | -------- |
|
|
125
|
+
|
|
126
|
+
Severities: `error` (document tags, missing children), `warn` (no `Loading`
|
|
127
|
+
on SPA routes, empty `headers()`), `info` (unused, single-use, shell
|
|
128
|
+
`ErrorBoundary` shadowed by route-level boundaries on every route).
|
|
129
|
+
|
|
130
|
+
## Rules
|
|
131
|
+
|
|
132
|
+
1. Source of truth is `pracht inspect routes --json` — it shows resolved
|
|
133
|
+
shell-per-route after group inheritance.
|
|
134
|
+
2. Read the shell source — do not infer from names.
|
|
135
|
+
3. `Loading()` is a shell export (SPA-only fallback). `ErrorBoundary` is
|
|
136
|
+
valid on both shells and routes: the shell's boundary is the fallback,
|
|
137
|
+
the route's wins when both exist. Do not flag shell boundaries as bugs.
|
|
138
|
+
4. Recommend deletions for unused shells; do not delete automatically.
|
|
139
|
+
5. When in doubt about render mode interaction, cross-reference with
|
|
140
|
+
`tune-render-mode`.
|
|
141
|
+
|
|
142
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: configure-isg
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: |
|
|
5
|
+
Wire ISG (incremental static generation) revalidation correctly for the
|
|
6
|
+
project's adapter: route-level timeRevalidate/webhookRevalidate policies,
|
|
7
|
+
the authenticated /__pracht/revalidate webhook, Cloudflare Workers Caching,
|
|
8
|
+
Vercel native ISR, and cache-key pitfalls — then verify it locally.
|
|
9
|
+
Use when asked to "set up ISG", "configure revalidation", "add a
|
|
10
|
+
revalidation webhook", "my ISG page never updates", or "cache this page at
|
|
11
|
+
the edge".
|
|
12
|
+
allowed-tools:
|
|
13
|
+
- Bash
|
|
14
|
+
- Read
|
|
15
|
+
- Edit
|
|
16
|
+
- Write
|
|
17
|
+
- Grep
|
|
18
|
+
- Glob
|
|
19
|
+
- AskUserQuestion
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Pracht Configure ISG
|
|
23
|
+
|
|
24
|
+
ISG renders at build time and regenerates after a time window or an
|
|
25
|
+
authenticated webhook (`docs/RENDERING_MODES.md`, `docs/ADAPTERS.md`). The
|
|
26
|
+
mechanics differ per adapter, and a route with `render: "isg"` but **no
|
|
27
|
+
`revalidate` policy never regenerates** — the prerenderer only writes an ISG
|
|
28
|
+
manifest entry when a policy exists. This skill wires the policy, the
|
|
29
|
+
webhook, and the adapter-specific cache correctly.
|
|
30
|
+
|
|
31
|
+
## Step 1: Identify adapter and candidate routes
|
|
32
|
+
|
|
33
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
34
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
35
|
+
shelling out.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pracht inspect routes --json # render + revalidate per route
|
|
39
|
+
pracht inspect build --json # adapterTarget (requires a prior `pracht build`)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
|
|
43
|
+
wired up. If unsure *which* routes deserve ISG at all, run
|
|
44
|
+
`/tune-render-mode` first — this skill assumes the mode choice is made.
|
|
45
|
+
|
|
46
|
+
## Step 2: Wire the revalidate policy (manifest router)
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// src/routes.ts
|
|
50
|
+
import { timeRevalidate, webhookRevalidate } from "@pracht/core";
|
|
51
|
+
|
|
52
|
+
route("/pricing", () => import("./routes/pricing.tsx"), {
|
|
53
|
+
render: "isg",
|
|
54
|
+
revalidate: [timeRevalidate(3600), webhookRevalidate()],
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `timeRevalidate(seconds)` — requires a positive **integer**; anything else
|
|
59
|
+
throws at manifest evaluation.
|
|
60
|
+
- `webhookRevalidate()` — no arguments; opts the route into the webhook
|
|
61
|
+
endpoint.
|
|
62
|
+
- `revalidate` accepts one policy or an array (`RouteRevalidate`); the array
|
|
63
|
+
above means "hourly, or sooner when a webhook names this path".
|
|
64
|
+
|
|
65
|
+
**Pages router caveat:** `export const RENDER_MODE = "isg"` exists, but there
|
|
66
|
+
is no `REVALIDATE` page constant — the pages scanner only extracts
|
|
67
|
+
`RENDER_MODE` and `HYDRATION`, so pages-router ISG routes are frozen
|
|
68
|
+
build-time snapshots. To attach a policy, eject to an explicit manifest with
|
|
69
|
+
`generateRoutesFile` from `@pracht/vite-plugin/pages-router` (see
|
|
70
|
+
docs/ROUTING.md "Ejecting to Explicit Manifest") and edit the generated route.
|
|
71
|
+
|
|
72
|
+
For dynamic routes, `getStaticPaths()` enumerates the prerendered params.
|
|
73
|
+
Paths it did not enumerate render per-request without a cached copy, and
|
|
74
|
+
webhooks naming them are `skipped` on Node/Cloudflare (nothing to refresh).
|
|
75
|
+
|
|
76
|
+
## Step 3: The revalidation webhook
|
|
77
|
+
|
|
78
|
+
All adapters expose `POST /__pracht/revalidate` (`PRACHT_REVALIDATE_ENDPOINT`
|
|
79
|
+
from `@pracht/core`):
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
curl -X POST https://example.com/__pracht/revalidate \
|
|
83
|
+
-H "Authorization: Bearer $PRACHT_REVALIDATE_TOKEN" \
|
|
84
|
+
-H "Content-Type: application/json" \
|
|
85
|
+
-d '{"paths":["/pricing"]}'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- Auth: `PRACHT_REVALIDATE_TOKEN` env var; fails closed with `401` when unset
|
|
89
|
+
or wrong. Providers that can't send bearer auth may use the
|
|
90
|
+
`x-pracht-revalidate-token` header instead.
|
|
91
|
+
- Body: `paths` array, max 64 entries (else `400`). Response reports
|
|
92
|
+
`revalidated` / `skipped` / `failed` arrays; failed paths keep serving the
|
|
93
|
+
previous copy. Regeneration is single-flighted per path and never replays
|
|
94
|
+
the caller's cookies/auth headers.
|
|
95
|
+
|
|
96
|
+
## Step 4: Adapter mechanics
|
|
97
|
+
|
|
98
|
+
| Adapter | Time revalidation | Webhook revalidation |
|
|
99
|
+
| ------- | ----------------- | -------------------- |
|
|
100
|
+
| Node | File mtime vs window; serves stale, refreshes in background | Regenerates the on-disk HTML synchronously |
|
|
101
|
+
| Cloudflare (default) | Worker-managed Cache API timestamp, `env.ASSETS` fallback — **per colo** | Overwrites the Cache API entry in the receiving colo only |
|
|
102
|
+
| Cloudflare (`cache: true`) | Edge-tier Workers Caching in front of the Worker for time-revalidated routes | Webhook-only routes keep the worker-managed path; time+webhook routes also get their edge entry purged |
|
|
103
|
+
| Vercel | Build Output prerender functions: `.prerender-config.json` with `expiration` from the time policy and build HTML as fallback | `x-vercel-cache`-verified bypass; `PRACHT_REVALIDATE_TOKEN` becomes the `bypassToken` and **must be set at build time** (runtime-only setting → webhook paths report `failed` until you rebuild) |
|
|
104
|
+
|
|
105
|
+
Cloudflare specifics (`docs/ADAPTERS.md#isg-via-workers-caching-cache`):
|
|
106
|
+
|
|
107
|
+
- The default per-colo path needs **no extra config**. The `cache: true`
|
|
108
|
+
upgrade needs both `cloudflareAdapter({ cache: true })` in vite config
|
|
109
|
+
(optionally `{ cache: { staleWhileRevalidate: <seconds> } }`) **and**
|
|
110
|
+
`{ "cache": { "enabled": true } }` in wrangler config.
|
|
111
|
+
- With it on, time-revalidated pages are no longer emitted as build-time
|
|
112
|
+
snapshots (first request after deploy renders cold); webhook-only routes
|
|
113
|
+
keep their snapshots.
|
|
114
|
+
- Programmatic purge: `purgeCache` / `routeCacheTag` from
|
|
115
|
+
`@pracht/adapter-cloudflare/cache` — protect any purge API route with a
|
|
116
|
+
secret.
|
|
117
|
+
- Cache safety: responses with `Set-Cookie`, `Cache-Control:
|
|
118
|
+
private`/`no-store`, or `Vary: Cookie`/`Authorization`/`*` are never stored
|
|
119
|
+
in the shared edge cache.
|
|
120
|
+
|
|
121
|
+
## Step 5: Cache-key cardinality caveats
|
|
122
|
+
|
|
123
|
+
See `docs/ADAPTERS.md#cache-key-cardinality`. Node and worker-managed
|
|
124
|
+
Cloudflare ISG key generated pages by **pathname**. Workers Caching keys by
|
|
125
|
+
exact path **plus query string** (param order and trailing slash included), so
|
|
126
|
+
`/pricing?ref=a` and `/pricing?ref=b` are independent entries with independent
|
|
127
|
+
revalidation — and attacker-chosen query values create unbounded cold entries.
|
|
128
|
+
Before enabling `cache: true`, canonicalize or reject stray query params (the
|
|
129
|
+
docs describe an uncached-gateway pattern), and note that routes exporting
|
|
130
|
+
`markdown` carry `Vary: Accept`, which multiplies variants per `Accept`
|
|
131
|
+
string. Vercel prerender functions are generated with `allowQuery: []`, so
|
|
132
|
+
query strings do not fragment that cache. Middleware never runs for cached ISG
|
|
133
|
+
hits on any adapter — keep per-visitor logic on SSR routes.
|
|
134
|
+
|
|
135
|
+
## Step 6: Verify locally
|
|
136
|
+
|
|
137
|
+
- **Node**: `PRACHT_REVALIDATE_TOKEN=dev-secret pracht preview --port 3000`
|
|
138
|
+
(builds, then runs `dist/server/server.js` with inherited env;
|
|
139
|
+
`--skip-build` reuses a build). Then curl the Step 3 command against
|
|
140
|
+
`localhost:3000` and check the JSON reports your path under `revalidated`;
|
|
141
|
+
re-fetch the page and confirm the change. For time policies, use a short
|
|
142
|
+
window (e.g. `timeRevalidate(5)`), request after expiry — first response is
|
|
143
|
+
the stale copy, the next one is fresh.
|
|
144
|
+
- **Cloudflare**: `pracht preview` delegates to `wrangler dev`; put
|
|
145
|
+
`PRACHT_REVALIDATE_TOKEN` in `.dev.vars` so the worker sees it.
|
|
146
|
+
- **Vercel**: no faithful local production runtime — `pracht preview` points
|
|
147
|
+
at `vercel build`/`vercel dev`; verify webhook behavior on a real
|
|
148
|
+
deployment built with `PRACHT_REVALIDATE_TOKEN` set.
|
|
149
|
+
|
|
150
|
+
Finish with the standard gate — and run `/pre-deploy` before shipping:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pracht verify --json
|
|
154
|
+
pracht typegen # if src/routes.ts changed
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Rules
|
|
158
|
+
|
|
159
|
+
1. Never overwrite `wrangler.jsonc`/`wrangler.toml` or `vercel.json` — diff
|
|
160
|
+
and merge, confirming collisions with `AskUserQuestion`.
|
|
161
|
+
2. Never propose ISG for personalized responses: `Set-Cookie` or
|
|
162
|
+
`Cache-Control: private`/`no-store` output fails regeneration, and
|
|
163
|
+
`Vary: Cookie`/`Authorization`/`*` is kept out of shared caches by design.
|
|
164
|
+
3. Always pair `render: "isg"` with an explicit `revalidate` policy — without
|
|
165
|
+
one the route silently behaves like SSG.
|
|
166
|
+
4. On Vercel, set `PRACHT_REVALIDATE_TOKEN` in the build environment, not
|
|
167
|
+
just at runtime.
|
|
168
|
+
5. Cloudflare webhook invalidation on the default path is per-colo, not a
|
|
169
|
+
global purge — use shorter time windows (or `cache: true` + purge) when
|
|
170
|
+
global freshness matters.
|
|
171
|
+
|
|
172
|
+
$ARGUMENTS
|