Migrate a Next.js site
This is the reference stack. The full integration lives in the style-aesthetics repo. If you have a Next.js 14 App Router project, follow these steps verbatim.
1. Install the package
npm install focus-cms @supabase/supabase-js zustand fast-json-patch
For now, vendor the package as a tarball if you need patches:
{
"dependencies": {
"focus-cms": "file:./vendor/focus-cms.tgz"
}
}
2. Provision Supabase
Create a project, run supabase/migrations/0001_initial.sql from the reference theme. It creates focus_cms_sites, focus_cms_content, focus_cms_drafts, the focus_cms_apply_patches RPC, and role-based RLS.
Add an editor row:
insert into focus_cms_site_editors (site_id, user_id, role)
select s.id, u.id, 'admin'
from focus_cms_sites s
cross join auth.users u
where s.slug = 'your-site-slug'
and u.email = 'editor@example.com';
3. Wire the shell
app/layout.tsx:
import { FocusCmsShell } from "@/components/focus-cms-shell";
import { getFocusCmsConfig } from "@/lib/focus-cms-config";
export default async function Layout({ children }) {
return (
<html>
<body>
<FocusCmsShell {...getFocusCmsConfig()}>{children}</FocusCmsShell>
</body>
</html>
);
}
Copy components/focus-cms-shell.tsx and lib/focus-cms-config.ts from the reference theme. They handle:
- Pathname-keyed adapter binding
- Supabase Auth integration
- Session-based activation (no
?edit=1slug required) - Login modal
4. Make pages editable
For raw-HTML routes (Squarespace clones, ported HTML, etc.):
// app/[slug]/page.tsx
import { RawPage } from "@/components/raw-page";
import { getPageContent } from "@/lib/site-data.server";
export default async function Page({ params }) {
const { slug } = await params;
const page = await getPageContent(slug);
return <RawPage page={page} />;
}
For structured routes (home page with hand-coded React blocks):
import { RichEditableText, EditableImage } from "focus-cms";
<RichEditableText path={`${path}.heading`} as="h1">{block.heading}</RichEditableText>
<EditableImage path={`${path}.imageUrl`} src={block.imageUrl} alt="" />
5. Add the toolbar to non-page surfaces
Navigation, footer, and site meta need their own adapters since they live in non-page Supabase keys (nav/primary, nav/mobile, footer/main, site/contact). Use the cross-key store:
import { useCrossKeyStore } from "@/lib/cross-key-store";
const pushPatch = useCrossKeyStore((s) => s.pushPatch);
pushPatch("nav/primary", { op: "add", path: "/items", value: nextItems });
The bottom-left toolbar will pick up the pending count automatically and fan out the save.
6. Visual + editability tests
Copy these scripts:
scripts/probe-editability.mjs— Playwright; asserts every route surfaces editable atoms when an editor session is present.scripts/visual-diff.mjs— pixel-diff vs committed baselines; enforces 0% display-mode regression.
Wire both into CI alongside npm run typecheck and npm run lint.
7. Deploy
Static export → Cloudflare Pages:
next build
wrangler pages deploy out --project-name your-site --branch staging
Environment variables your build needs:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYSUPABASE_SECRET_KEY(build-time only — never exposed to the client)NEXT_PUBLIC_FOCUS_CMS_SITE_SLUG
That's it. Once the deploy lands, hit /? in a browser, click the Admin link in the footer, log in with a seeded editor, and start editing.