Focus:CMS

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:

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:

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:

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.