Focus:CMS

Migrate a plain HTML / CSS site

This is the most common migration path — you have a static site (hand-written HTML, Squarespace export, Webflow export, etc.) and you want each page editable without rebuilding everything in React.

The trick: wrap each page's body in a single Next.js shell route that uses <EditableRawHtml>. The original markup ships untouched; the editor only kicks in when an editor session is active.

1. Stand up a Next.js shell

npx create-next-app@latest your-site --typescript --app --no-tailwind --no-src --import-alias '@/*'
cd your-site
npm install focus-cms @supabase/supabase-js zustand fast-json-patch

In next.config.mjs:

export default {
  output: "export",
  images: { unoptimized: true },
  trailingSlash: true,
};

2. Move HTML into Supabase

For each page (about.html, contact.html, etc.) extract the inner content (everything between <body> open/close) and seed it as a pages/{slug} row in focus_cms_content. Use the focus_cms_apply_patches RPC with the service-role key:

await client.rpc("focus_cms_apply_patches", {
  p_site_slug: "your-site-slug",
  p_key: "pages/about",
  p_patches: [{ op: "add", path: "", value: pageDoc }],
  p_editor: null,
  p_new_data: pageDoc,
});

Where pageDoc is:

{
  pathname: "/about",
  title: "About",
  description: "…",
  bodyClass: "your-original-body-class",
  blocks: [{ _template: "rawHtml", html: bodyInnerHtml }]
}

3. Render via EditableRawHtml

app/[slug]/page.tsx:

import { RawPage } from "@/components/raw-page";
import { getPageContent, listPageSlugs } from "@/lib/site-data.server";

export async function generateStaticParams() {
  const slugs = await listPageSlugs();
  return slugs.filter((s) => s !== "home").map((slug) => ({ slug }));
}

export default async function Page({ params }) {
  const { slug } = await params;
  const stored = await getPageContent(slug);
  return <RawPage page={stored} />;
}

components/raw-page.tsx:

import { EditableRawHtml } from "@/components/editable-raw-html";

export function RawPage({ page }) {
  return (
    <EditableRawHtml
      html={page.blocks[0].html}
      htmlPath="blocks/0/html"
      className="your-page-shell"
    />
  );
}

4. Copy the editor components

From the reference theme (style-aesthetics), copy:

These are theme-agnostic — no style-aesthetics-specific imports.

5. 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>
  );
}

6. Done

Every text node, image, link, and section in the original HTML is now editable in ?edit=1 mode. Squarespace fluid-engine grids (if your HTML came from Squarespace) get the per-block grid editor — drag/resize each .fe-block in a 24-column grid.

The page renders byte-for-byte identical when no editor session is present, so SEO, Lighthouse, and the public visitor experience stay unchanged.