{"slug":"configure-isg","title":"configure-isg","summary":"Wire ISG revalidation for the project's adapter: route-level `timeRevalidate`/`webhookRevalidate`, the authenticated `/__pracht/revalidate` webhook, Cloudflare Workers Cache, Vercel ISR, and cache-key pitfalls — then verify locally. Use for \"set up ISG\", \"configure revalidation\",","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-19T21:17:25.655749Z","repo":{"url":"https://github.com/JoviDeCroock/pracht","stars":99,"forks":5,"license":"MIT","updatedAt":"2026-10-19T16:43:31Z"},"bodyHtml":"<hr>\n<p>name: configure-isg\nversion: 1.0.1\ndescription: |\nWire ISG revalidation for the project's adapter: route-level\n<code>timeRevalidate</code>/<code>webhookRevalidate</code>, the authenticated <code>/__pracht/revalidate</code>\nwebhook, Cloudflare Workers Cache, Vercel ISR, and cache-key pitfalls — then\nverify locally.\nUse for \"set up ISG\", \"configure revalidation\", \"add a revalidation webhook\", \"my\nISG page never updates\", \"cache this page at the edge\".\nallowed-tools:</p>\n<ul>\n<li>Bash</li>\n<li>Read</li>\n<li>Edit</li>\n<li>Write</li>\n<li>Grep</li>\n<li>Glob</li>\n<li>AskUserQuestion</li>\n</ul>\n<hr>\n<h1>Pracht Configure ISG</h1>\n<p>ISG renders at build time and regenerates after a time window or an\nauthenticated webhook (<a href=\"https://pracht.resynapse.dev/docs/rendering#isg--incremental-static-generation\">https://pracht.resynapse.dev/docs/rendering#isg--incremental-static-generation</a>, <a href=\"https://pracht.resynapse.dev/docs/adapters\">https://pracht.resynapse.dev/docs/adapters</a>). The\nmechanics differ per adapter, and a route with <code>render: \"isg\"</code> but <strong>no\n<code>revalidate</code> policy never regenerates</strong> — the prerenderer only writes an ISG\nmanifest entry when a policy exists. This skill wires the policy, the\nwebhook, and the adapter-specific cache correctly.</p>\n<h2>Step 1: Identify adapter and candidate routes</h2>\n<p>MCP: when the pracht MCP server is registered (<code>pracht dev-mcp</code>), prefer its\n<code>inspect_routes</code>/<code>inspect_api</code>/<code>inspect_build</code>/<code>doctor</code>/<code>verify</code> tools over\nshelling out.</p>\n<pre><code>pracht inspect routes --json    # render + revalidate per route\npracht inspect build --json     # adapterTarget (requires a prior `pracht build`)\n</code></pre>\n<p>Prerequisite: <code>pracht inspect</code> needs a vite config with the pracht plugin\nwired up. If unsure <em>which</em> routes deserve ISG at all, run\n<code>/tune-render-mode</code> first — this skill assumes the mode choice is made.</p>\n<h2>Step 2: Wire the revalidate policy (manifest router)</h2>\n<pre><code>// src/routes.ts\nimport { timeRevalidate, webhookRevalidate } from \"@pracht/core\";\n\nroute(\"/pricing\", () =&gt; import(\"./routes/pricing.tsx\"), {\n  render: \"isg\",\n  revalidate: [timeRevalidate(3600), webhookRevalidate()],\n});\n</code></pre>\n<ul>\n<li><code>timeRevalidate(seconds)</code> — requires a positive <strong>integer</strong>; anything else\nthrows at manifest evaluation.</li>\n<li><code>webhookRevalidate()</code> — no arguments; opts the route into the webhook\nendpoint.</li>\n<li><code>revalidate</code> accepts one policy or an array (<code>RouteRevalidate</code>); the array\nabove means \"hourly, or sooner when a webhook names this path\".</li>\n</ul>\n<p><strong>Pages router:</strong> time-based ISG is expressed with two static page exports:</p>\n<pre><code>export const RENDER_MODE = \"isg\";\nexport const REVALIDATE = 3600;\n</code></pre>\n<p><code>REVALIDATE</code> must be a positive integer literal number of seconds. Build,\n<code>doctor</code>, and <code>verify</code> reject a missing or misplaced policy. Pages mode does\nnot accept policies on <code>_app</code> or <code>404</code>, and fenced Markdown/MDX examples are\nignored. It does not support webhook or combined policies; eject with <code>generateRoutesFile</code>\nfrom <code>@pracht/vite-plugin/pages-router</code> for those.</p>\n<p>For dynamic routes, <code>getStaticPaths()</code> enumerates the prerendered params.\nPaths it did not enumerate render per-request without a cached copy, and\nwebhooks naming them are <code>skipped</code> on Node/Cloudflare (nothing to refresh).</p>\n<h2>Step 3: The revalidation webhook</h2>\n<p>All adapters expose <code>POST &lt;base&gt;/__pracht/revalidate</code>\n(<code>PRACHT_REVALIDATE_ENDPOINT</code> from <code>@pracht/core</code>). For example, an app with\n<code>base: \"/app/\"</code> uses:</p>\n<pre><code>curl -X POST https://example.com/app/__pracht/revalidate \\\n  -H \"Authorization: Bearer $PRACHT_REVALIDATE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"paths\":[\"/pricing\"]}'\n</code></pre>\n<p>At the default <code>base: \"/\"</code>, omit <code>/app</code>. Keep request-body paths base-free;\nthey identify manifest routes rather than public deployment URLs.</p>\n<ul>\n<li>Auth: <code>PRACHT_REVALIDATE_TOKEN</code> env var; fails closed with <code>401</code> when unset\nor wrong. Providers that can't send bearer auth may use the\n<code>x-pracht-revalidate-token</code> header instead.</li>\n<li>Body: <code>paths</code> array, max 64 entries (else <code>400</code>). Response reports\n<code>revalidated</code> / <code>skipped</code> / <code>failed</code> arrays plus a <code>details</code> array naming why\neach path was skipped (<code>not_a_route</code>, <code>not_isg</code>, <code>not_prerendered</code>,\n<code>no_webhook_policy</code>) or failed — check <code>details</code> first when a webhook appears\nto do nothing. <code>scope</code> says how far a refresh reached: <code>instance</code> (Node:\nthat server only), <code>colo</code> (Cloudflare Cache API: that data center only), or\n<code>global</code> (Netlify, Vercel, Cloudflare routes purged via <code>cache: true</code>).\nFailed paths keep serving the previous copy. Regeneration is\nsingle-flighted per path and never replays the caller's cookies/auth headers.</li>\n</ul>\n<h2>Step 4: Adapter mechanics</h2>\n<table>\n<thead>\n<tr>\n<th>Adapter</th>\n<th>Time revalidation</th>\n<th>Webhook revalidation</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Node</td>\n<td>File mtime vs window, per server; serves stale, refreshes in background</td>\n<td>Regenerates the on-disk HTML synchronously on the <strong>receiving replica only</strong> — serve ISG routes from a single instance</td>\n</tr>\n<tr>\n<td>Cloudflare (default)</td>\n<td>Worker-managed Cache API timestamp, <code>env.ASSETS</code> fallback — <strong>per colo</strong></td>\n<td>Overwrites the Cache API entry in the receiving colo only (<code>scope: \"colo\"</code>); a colo with no cached copy serves the build snapshot as <code>stale</code> and re-renders, a colo holding a copy keeps it until eviction</td>\n</tr>\n<tr>\n<td>Cloudflare (<code>cache: true</code>)</td>\n<td>Edge-tier Workers Caching in front of the Worker for time-revalidated routes</td>\n<td>Webhook-only routes keep the worker-managed path; time+webhook routes also get their edge entry purged</td>\n</tr>\n<tr>\n<td>Vercel</td>\n<td>Build Output prerender functions: <code>.prerender-config.json</code> with <code>expiration</code> from the time policy and build HTML as fallback, next to a Node Serverless Function per ISG route (Vercel rejects ISR on an Edge Function)</td>\n<td>For webhook revalidation, <code>x-vercel-cache</code> verifies the bypass; <code>PRACHT_REVALIDATE_TOKEN</code> becomes the <code>bypassToken</code> and <strong>must be set at build time</strong> (runtime-only setting → webhook paths report <code>failed</code> until you rebuild). Time-only ISR does not require the token.</td>\n</tr>\n</tbody>\n</table>\n<p>Cloudflare specifics (<a href=\"https://pracht.resynapse.dev/docs/adapters#isg-and-workers-caching\">https://pracht.resynapse.dev/docs/adapters#isg-and-workers-caching</a>):</p>\n<ul>\n<li>The default per-colo path needs <strong>no extra config</strong>. The <code>cache: true</code>\nupgrade needs both <code>cloudflareAdapter({ cache: true })</code> in vite config\n(optionally <code>{ cache: { staleWhileRevalidate: &lt;seconds&gt; } }</code>) <strong>and</strong>\n<code>{ \"cache\": { \"enabled\": true } }</code> in wrangler config.</li>\n<li>With it on, time-revalidated pages are no longer emitted as build-time\nsnapshots (first request after deploy renders cold); webhook-only routes\nkeep their snapshots.</li>\n<li>Programmatic purge: <code>purgeCache</code> / <code>routeCacheTag</code> from\n<code>@pracht/adapter-cloudflare/cache</code> — protect any purge API route with a\nsecret.</li>\n<li>Cache safety: responses with <code>Set-Cookie</code>, <code>Cache-Control: private</code>/<code>no-store</code>, or <code>Vary: Cookie</code>/<code>Authorization</code>/<code>*</code> are never stored\nin the shared edge cache.</li>\n</ul>\n<h2>Step 5: Cache-key cardinality caveats</h2>\n<p>See <a href=\"https://pracht.resynapse.dev/docs/adapters#isg-and-workers-caching\">https://pracht.resynapse.dev/docs/adapters#isg-and-workers-caching</a>. Node and worker-managed\nCloudflare ISG key generated pages by <strong>pathname</strong>. Workers Caching keys by\nexact path <strong>plus query string</strong> (param order and trailing slash included), so\n<code>/pricing?ref=a</code> and <code>/pricing?ref=b</code> are independent entries with independent\nrevalidation — and attacker-chosen query values create unbounded cold entries.\nBefore enabling <code>cache: true</code>, canonicalize or reject stray query params (the\ndocs describe an uncached-gateway pattern), and note that routes exporting\n<code>markdown</code> or declaring middleware-owned <code>markdown: true</code> carry <code>Vary: Accept</code>,\nwhich multiplies variants per <code>Accept</code> string. Vercel prerender functions are\ngenerated with <code>allowQuery: []</code>, so\nquery strings do not fragment that cache. Middleware never runs for cached ISG\nhits on any adapter — keep per-visitor logic on SSR routes.</p>\n<h2>Step 6: Verify locally</h2>\n<ul>\n<li><strong>Node</strong>: <code>PRACHT_REVALIDATE_TOKEN=dev-secret pracht preview --port 3000</code>\n(builds, then runs <code>dist/server/server.js</code> with inherited env;\n<code>--skip-build</code> reuses a build). Then curl the Step 3 command against\n<code>localhost:3000</code> and check the JSON reports your path under <code>revalidated</code>;\nre-fetch the page and confirm the change. For time policies, use a short\nwindow (e.g. <code>timeRevalidate(5)</code>), request after expiry — first response is\nthe stale copy, the next one is fresh.</li>\n<li><strong>Cloudflare</strong>: <code>pracht preview</code> delegates to <code>wrangler dev</code>; put\n<code>PRACHT_REVALIDATE_TOKEN</code> in <code>.dev.vars</code> so the worker sees it.</li>\n<li><strong>Vercel</strong>: no faithful local production runtime — <code>pracht preview</code> points\nat <code>vercel build</code>/<code>vercel dev</code>; verify webhook behavior on a real\ndeployment built with <code>PRACHT_REVALIDATE_TOKEN</code> set.</li>\n</ul>\n<p>Finish with the standard gate — and run <code>/pre-deploy</code> before shipping:</p>\n<pre><code>pracht verify --json\npracht typegen   # if src/routes.ts changed\n</code></pre>\n<h2>Rules</h2>\n<ol>\n<li>Never overwrite <code>wrangler.jsonc</code>/<code>wrangler.toml</code> or <code>vercel.json</code> — diff\nand merge, confirming collisions with <code>AskUserQuestion</code>.</li>\n<li>Never propose ISG for personalized responses. ISG HTML renders always run\non a sanitized request (<code>GET</code>, <code>Accept: text/html</code>, path only — no cookies,\ncredentials, query, or body); Cloudflare Workers Caching uses the same\nisolation with <code>Accept: text/markdown</code> for its markdown cache variant. A\nloader that reads the session therefore sees an anonymous visitor. On top of that, <code>Set-Cookie</code> or\n<code>Cache-Control: private</code>/<code>no-store</code> output fails regeneration on Node and\nCloudflare (on Vercel the credential headers are stripped and the mismatch\nis logged), and <code>Vary: Cookie</code>/<code>Authorization</code>/<code>*</code> is kept out of shared\ncaches by design.</li>\n<li>Always pair <code>render: \"isg\"</code> with an explicit <code>revalidate</code> policy — without\none the route silently behaves like SSG.</li>\n<li>On Vercel, set <code>PRACHT_REVALIDATE_TOKEN</code> in the build environment, not\njust at runtime.</li>\n<li>Webhook invalidation is per-colo on Cloudflare's default path and\nper-instance on Node (the response's <code>scope</code> says which); <code>pracht build</code>\nwarns about Cloudflare webhook routes. A webhook-only route never goes stale\nby time, so pair it with <code>timeRevalidate()</code> to bound the lag, plus\n<code>cache: true</code> on Cloudflare so the webhook purges every colo.</li>\n</ol>\n<p>$ARGUMENTS</p>\n","files":[{"path":"SKILL.md","sizeBytes":9983,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-19T21:18:10.87624Z","sha256":"7A73B5C8690FC4BF44AD835D492B310282EB875782486EEAD94A8549B3EDA734","sizeBytes":4606},"review":null,"source":{"repositoryUrl":"https://github.com/JoviDeCroock/pracht","path":"skills/configure-isg","license":"MIT","commit":"8bcdd52989de3e97708e7f16221839d6313cfdac","subtreeSha":"06859DB830C3F778A111B5871AA094B51AE05CCE847BCBF666709FE0FBEC4AF1","lastSyncedAt":"2026-10-19T21:17:21.911347Z"},"reviewedAt":"2026-10-19T21:19:40.153711Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/configure-isg"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart"},{"target":"git","command":"git clone https://github.com/JoviDeCroock/pracht.git"}]}