Skip to content

Commit 057dac3

Browse files
authored
Merge pull request #22 from instructa/codex/docs-free-plan-prefetch
Stabilize docs on Cloudflare Workers
2 parents c9895b5 + fcfb2ab commit 057dac3

9 files changed

Lines changed: 32 additions & 13 deletions

File tree

apps/docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ The canonical deployment is a Next.js application on Cloudflare Workers through
7575
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`;
7676
2. use `.env.example` only when overriding the public canonical origin for a local build;
7777
3. validate locally with `pnpm docs:build` and optionally `pnpm docs:alchemy:dev`;
78-
4. deploy production with `pnpm docs:deploy` from the repository root; the package script pins `--stage prod` explicitly;
78+
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;
7979
5. verify the emitted URL and `https://planr.so` with the release-live and browser gates.
8080

8181
`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.

apps/docs/app/docs/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { source } from '@/lib/source';
55

66
export default function DocumentationLayout({ children }: { children: ReactNode }) {
77
return (
8-
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
8+
<DocsLayout {...baseOptions()} tree={source.getPageTree()} sidebar={{ prefetch: false }}>
99
{children}
1010
</DocsLayout>
1111
);

apps/docs/app/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from 'next';
22
import type { ReactNode } from 'react';
33
import { RootProvider } from 'fumadocs-ui/provider/next';
4+
import { NoPrefetchLink } from '@/components/no-prefetch-link';
45
import './global.css';
56

67
export const metadata: Metadata = {
@@ -32,7 +33,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
3233
return (
3334
<html lang="en" suppressHydrationWarning>
3435
<body className="flex min-h-screen flex-col">
35-
<RootProvider>{children}</RootProvider>
36+
<RootProvider components={{ Link: NoPrefetchLink }}>{children}</RootProvider>
3637
</body>
3738
</html>
3839
);

apps/docs/app/page.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ export default function HomePage() {
3434
MCP clients, and humans can share without losing ownership or evidence.
3535
</p>
3636
<div className="hero-actions">
37-
<Link className="button-primary" href="/docs/getting-started/installation">
37+
<Link prefetch={false} className="button-primary" href="/docs/getting-started/installation">
3838
Install Planr
3939
<ArrowRight aria-hidden="true" />
4040
</Link>
41-
<Link className="button-secondary" href="/docs/getting-started/why-planr">
41+
<Link prefetch={false} className="button-secondary" href="/docs/getting-started/why-planr">
4242
See how it works
4343
</Link>
4444
</div>
@@ -71,21 +71,21 @@ export default function HomePage() {
7171
<span>Planr gives every client the same durable plan, task graph, and evidence trail.</span>
7272
</div>
7373
<div className="agent-grid">
74-
<Link className="agent-card" href="/docs/integrations/codex">
74+
<Link prefetch={false} className="agent-card" href="/docs/integrations/codex">
7575
<span className="agent-card__mark">
7676
<Image src="/agents/codex.svg" width={80} height={80} alt="Codex logo" />
7777
</span>
7878
<span><strong>Codex</strong><small>Plugin, MCP, hooks, and roles</small></span>
7979
<ArrowRight aria-hidden="true" />
8080
</Link>
81-
<Link className="agent-card" href="/docs/integrations/claude-code">
81+
<Link prefetch={false} className="agent-card" href="/docs/integrations/claude-code">
8282
<span className="agent-card__mark">
8383
<Image src="/agents/claude.svg" width={80} height={80} alt="Claude logo" />
8484
</span>
8585
<span><strong>Claude Code</strong><small>Plugin and project-scoped MCP</small></span>
8686
<ArrowRight aria-hidden="true" />
8787
</Link>
88-
<Link className="agent-card" href="/docs/integrations/cursor">
88+
<Link prefetch={false} className="agent-card" href="/docs/integrations/cursor">
8989
<span className="agent-card__mark">
9090
<Image src="/agents/cursor.svg" width={80} height={80} alt="Cursor logo" />
9191
</span>
@@ -140,9 +140,9 @@ export default function HomePage() {
140140
<footer className="home-footer">
141141
<p>Planr is open source and local-first.</p>
142142
<nav aria-label="Footer navigation">
143-
<Link href="/docs">Documentation</Link>
143+
<Link prefetch={false} href="/docs">Documentation</Link>
144144
<Link href="https://github.com/instructa/planr">GitHub</Link>
145-
<Link href="/docs/contributing">Contributing</Link>
145+
<Link prefetch={false} href="/docs/contributing">Contributing</Link>
146146
</nav>
147147
</footer>
148148
</HomeLayout>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use client';
2+
3+
import NextLink from 'next/link';
4+
import type { Framework } from 'fumadocs-core/framework';
5+
6+
export const NoPrefetchLink: NonNullable<Framework['Link']> = ({ href, ...props }) => {
7+
if (!href) return <a {...props} />;
8+
return <NextLink {...props} href={href} prefetch={false} />;
9+
};

apps/docs/content/docs/operations/docs-deployment.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Local deployment credentials belong in the Alchemy profile created by OAuth; do
2121

2222
<CommandBlock command="pnpm docs:deploy" label="Deploy planr.so" />
2323

24+
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.
25+
2426
`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.
2527

2628
For Cloudflare-local development, use `pnpm docs:alchemy:dev`. Normal content work can continue to use `pnpm docs:dev` without cloud credentials.

apps/docs/open-next.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
2+
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
23

3-
export default defineCloudflareConfig({});
4+
export default defineCloudflareConfig({
5+
incrementalCache: staticAssetsIncrementalCache,
6+
enableCacheInterception: true,
7+
});

apps/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"bundle:worker": "wrangler deploy --dry-run --outdir .alchemy-worker",
1212
"build:deploy": "pnpm run build:worker && pnpm run bundle:worker",
1313
"start": "next start",
14-
"deploy": "alchemy deploy --stage prod",
14+
"deploy": "alchemy deploy --stage prod --yes && opennextjs-cloudflare populateCache remote --config wrangler.jsonc",
1515
"destroy": "alchemy destroy --stage prod",
1616
"content": "fumadocs-mdx",
1717
"typecheck": "fumadocs-mdx && tsc --noEmit",

apps/docs/scripts/verify-maintenance.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ for (const [name, version] of Object.entries({ ...packageJson.dependencies, ...p
124124
assert(!version.startsWith('^') && !version.startsWith('~'), `apps/docs dependency ${name} must use an exact version`);
125125
}
126126
assert(packageJson.engines.node === '>=22', 'apps/docs must require Node.js 22 or newer');
127-
assert(packageJson.scripts.deploy === 'alchemy deploy --stage prod', 'apps/docs deploy must target the Alchemy prod stage explicitly');
127+
assert(
128+
packageJson.scripts.deploy === 'alchemy deploy --stage prod --yes && opennextjs-cloudflare populateCache remote --config wrangler.jsonc',
129+
'apps/docs deploy must target the Alchemy prod stage and populate the OpenNext SSG cache',
130+
);
128131
assert(packageJson.scripts.destroy === 'alchemy destroy --stage prod', 'apps/docs destroy must target the Alchemy prod stage explicitly');
129132
assert(packageJson.scripts['build:worker'] === 'opennextjs-cloudflare build --skipWranglerConfigCheck', 'worker build must use OpenNext');
130133
assert(packageJson.scripts['bundle:worker'] === 'wrangler deploy --dry-run --outdir .alchemy-worker', 'worker bundle must use Wrangler without deploying');

0 commit comments

Comments
 (0)