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.
@@ -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