diff --git a/apps/docs/README.md b/apps/docs/README.md
index e17f253..ffb76ef 100644
--- a/apps/docs/README.md
+++ b/apps/docs/README.md
@@ -75,7 +75,7 @@ The canonical deployment is a Next.js application on Cloudflare Workers through
1. authenticate the Cloudflare account with `pnpm --filter @planr/docs exec alchemy login --configure`; Alchemy stores local OAuth credentials in its profile, not in `.env`;
2. use `.env.example` only when overriding the public canonical origin for a local build;
3. validate locally with `pnpm docs:build` and optionally `pnpm docs:alchemy:dev`;
-4. deploy production with `pnpm docs:deploy` from the repository root; the package script pins `--stage prod` explicitly;
+4. deploy production with `pnpm docs:deploy` from the repository root; the package script pins `--stage prod`, accepts the non-interactive Alchemy plan, and populates the read-only OpenNext SSG cache;
5. verify the emitted URL and `https://planr.so` with the release-live and browser gates.
`apps/docs/alchemy.run.ts` is the infrastructure source of truth. It uses the Alchemy v2 Effect stack and Cloudflare remote state. `Cloudflare.Website.StaticSite` runs OpenNext, passes the result through Wrangler's local dry-run bundler, and deploys `.alchemy-worker/worker.js` with `bundle: false`. It adopts the named Worker when it already exists and binds `planr.so` only for `prod`. `NEXT_PUBLIC_SITE_URL` is set to the canonical production origin during that build.
diff --git a/apps/docs/app/docs/layout.tsx b/apps/docs/app/docs/layout.tsx
index 2d10ba6..49f8472 100644
--- a/apps/docs/app/docs/layout.tsx
+++ b/apps/docs/app/docs/layout.tsx
@@ -5,7 +5,7 @@ import { source } from '@/lib/source';
export default function DocumentationLayout({ children }: { children: ReactNode }) {
return (
-
+
{children}
);
diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx
index 1334728..9650d16 100644
--- a/apps/docs/app/layout.tsx
+++ b/apps/docs/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import { RootProvider } from 'fumadocs-ui/provider/next';
+import { NoPrefetchLink } from '@/components/no-prefetch-link';
import './global.css';
export const metadata: Metadata = {
@@ -32,7 +33,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return (
- {children}
+ {children}
);
diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx
index 6ae4210..672c22c 100644
--- a/apps/docs/app/page.tsx
+++ b/apps/docs/app/page.tsx
@@ -34,11 +34,11 @@ export default function HomePage() {
MCP clients, and humans can share without losing ownership or evidence.
-
+
Install Planr
-
+
See how it works
@@ -71,21 +71,21 @@ export default function HomePage() {
Planr gives every client the same durable plan, task graph, and evidence trail.
-
+
CodexPlugin, MCP, hooks, and roles
-
+
Claude CodePlugin and project-scoped MCP
-
+
@@ -140,9 +140,9 @@ export default function HomePage() {
diff --git a/apps/docs/components/no-prefetch-link.tsx b/apps/docs/components/no-prefetch-link.tsx
new file mode 100644
index 0000000..590cc3c
--- /dev/null
+++ b/apps/docs/components/no-prefetch-link.tsx
@@ -0,0 +1,9 @@
+'use client';
+
+import NextLink from 'next/link';
+import type { Framework } from 'fumadocs-core/framework';
+
+export const NoPrefetchLink: NonNullable = ({ href, ...props }) => {
+ if (!href) return ;
+ return ;
+};
diff --git a/apps/docs/content/docs/operations/docs-deployment.mdx b/apps/docs/content/docs/operations/docs-deployment.mdx
index b5dc8fa..ae65129 100644
--- a/apps/docs/content/docs/operations/docs-deployment.mdx
+++ b/apps/docs/content/docs/operations/docs-deployment.mdx
@@ -21,6 +21,8 @@ Local deployment credentials belong in the Alchemy profile created by OAuth; do
+The command deploys the Alchemy production stack and then populates OpenNext's read-only static-assets incremental cache with the prerendered documentation routes. Keep both steps together: without cache population, Cloudflare must execute the OpenNext server path for every SSG request.
+
`apps/docs/alchemy.run.ts` is authoritative. `Cloudflare.Website.StaticSite` runs OpenNext and Wrangler's local dry-run bundler, uploads the resulting `.alchemy-worker/worker.js` with `bundle: false`, and serves `.open-next/assets`. The extra local bundle step retains OpenNext's Wrangler-compatible runtime behavior while keeping the compressed Worker below Cloudflare's 3 MiB Free-plan limit. It adopts the named `planr-docs-prod` Worker when present and passes `domain: 'planr.so'` only for the `prod` stage. The `planr.so` zone must already be available in the authenticated Cloudflare account; Cloudflare provisions the Worker custom-domain record and certificate. `Cloudflare.state()` keeps stack state remotely available to local and CI deploys.
For Cloudflare-local development, use `pnpm docs:alchemy:dev`. Normal content work can continue to use `pnpm docs:dev` without cloud credentials.
diff --git a/apps/docs/open-next.config.ts b/apps/docs/open-next.config.ts
index 1a57e31..3fd9d41 100644
--- a/apps/docs/open-next.config.ts
+++ b/apps/docs/open-next.config.ts
@@ -1,3 +1,7 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
+import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
-export default defineCloudflareConfig({});
+export default defineCloudflareConfig({
+ incrementalCache: staticAssetsIncrementalCache,
+ enableCacheInterception: true,
+});
diff --git a/apps/docs/package.json b/apps/docs/package.json
index bfdb695..860df6d 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -11,7 +11,7 @@
"bundle:worker": "wrangler deploy --dry-run --outdir .alchemy-worker",
"build:deploy": "pnpm run build:worker && pnpm run bundle:worker",
"start": "next start",
- "deploy": "alchemy deploy --stage prod",
+ "deploy": "alchemy deploy --stage prod --yes && opennextjs-cloudflare populateCache remote --config wrangler.jsonc",
"destroy": "alchemy destroy --stage prod",
"content": "fumadocs-mdx",
"typecheck": "fumadocs-mdx && tsc --noEmit",
diff --git a/apps/docs/scripts/verify-maintenance.mjs b/apps/docs/scripts/verify-maintenance.mjs
index be5622e..b55478f 100644
--- a/apps/docs/scripts/verify-maintenance.mjs
+++ b/apps/docs/scripts/verify-maintenance.mjs
@@ -124,7 +124,10 @@ for (const [name, version] of Object.entries({ ...packageJson.dependencies, ...p
assert(!version.startsWith('^') && !version.startsWith('~'), `apps/docs dependency ${name} must use an exact version`);
}
assert(packageJson.engines.node === '>=22', 'apps/docs must require Node.js 22 or newer');
-assert(packageJson.scripts.deploy === 'alchemy deploy --stage prod', 'apps/docs deploy must target the Alchemy prod stage explicitly');
+assert(
+ packageJson.scripts.deploy === 'alchemy deploy --stage prod --yes && opennextjs-cloudflare populateCache remote --config wrangler.jsonc',
+ 'apps/docs deploy must target the Alchemy prod stage and populate the OpenNext SSG cache',
+);
assert(packageJson.scripts.destroy === 'alchemy destroy --stage prod', 'apps/docs destroy must target the Alchemy prod stage explicitly');
assert(packageJson.scripts['build:worker'] === 'opennextjs-cloudflare build --skipWranglerConfigCheck', 'worker build must use OpenNext');
assert(packageJson.scripts['bundle:worker'] === 'wrangler deploy --dry-run --outdir .alchemy-worker', 'worker bundle must use Wrangler without deploying');