Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { source } from '@/lib/source';

export default function DocumentationLayout({ children }: { children: ReactNode }) {
return (
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
<DocsLayout {...baseOptions()} tree={source.getPageTree()} sidebar={{ prefetch: false }}>
{children}
</DocsLayout>
);
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -32,7 +33,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="flex min-h-screen flex-col">
<RootProvider>{children}</RootProvider>
<RootProvider components={{ Link: NoPrefetchLink }}>{children}</RootProvider>
</body>
</html>
);
Expand Down
14 changes: 7 additions & 7 deletions apps/docs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export default function HomePage() {
MCP clients, and humans can share without losing ownership or evidence.
</p>
<div className="hero-actions">
<Link className="button-primary" href="/docs/getting-started/installation">
<Link prefetch={false} className="button-primary" href="/docs/getting-started/installation">
Install Planr
<ArrowRight aria-hidden="true" />
</Link>
<Link className="button-secondary" href="/docs/getting-started/why-planr">
<Link prefetch={false} className="button-secondary" href="/docs/getting-started/why-planr">
See how it works
</Link>
</div>
Expand Down Expand Up @@ -71,21 +71,21 @@ export default function HomePage() {
<span>Planr gives every client the same durable plan, task graph, and evidence trail.</span>
</div>
<div className="agent-grid">
<Link className="agent-card" href="/docs/integrations/codex">
<Link prefetch={false} className="agent-card" href="/docs/integrations/codex">
<span className="agent-card__mark">
<Image src="/agents/codex.svg" width={80} height={80} alt="Codex logo" />
</span>
<span><strong>Codex</strong><small>Plugin, MCP, hooks, and roles</small></span>
<ArrowRight aria-hidden="true" />
</Link>
<Link className="agent-card" href="/docs/integrations/claude-code">
<Link prefetch={false} className="agent-card" href="/docs/integrations/claude-code">
<span className="agent-card__mark">
<Image src="/agents/claude.svg" width={80} height={80} alt="Claude logo" />
</span>
<span><strong>Claude Code</strong><small>Plugin and project-scoped MCP</small></span>
<ArrowRight aria-hidden="true" />
</Link>
<Link className="agent-card" href="/docs/integrations/cursor">
<Link prefetch={false} className="agent-card" href="/docs/integrations/cursor">
<span className="agent-card__mark">
<Image src="/agents/cursor.svg" width={80} height={80} alt="Cursor logo" />
</span>
Expand Down Expand Up @@ -140,9 +140,9 @@ export default function HomePage() {
<footer className="home-footer">
<p>Planr is open source and local-first.</p>
<nav aria-label="Footer navigation">
<Link href="/docs">Documentation</Link>
<Link prefetch={false} href="/docs">Documentation</Link>
<Link href="https://github.com/instructa/planr">GitHub</Link>
<Link href="/docs/contributing">Contributing</Link>
<Link prefetch={false} href="/docs/contributing">Contributing</Link>
</nav>
</footer>
</HomeLayout>
Expand Down
9 changes: 9 additions & 0 deletions apps/docs/components/no-prefetch-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use client';

import NextLink from 'next/link';
import type { Framework } from 'fumadocs-core/framework';

export const NoPrefetchLink: NonNullable<Framework['Link']> = ({ href, ...props }) => {
if (!href) return <a {...props} />;
return <NextLink {...props} href={href} prefetch={false} />;
};
2 changes: 2 additions & 0 deletions apps/docs/content/docs/operations/docs-deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Local deployment credentials belong in the Alchemy profile created by OAuth; do

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

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.
Expand Down
6 changes: 5 additions & 1 deletion apps/docs/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -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,
});
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/scripts/verify-maintenance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading