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:
components/editable-raw-html.tsxcomponents/block-grid-editor.tsxcomponents/focus-editor-enhancer.tsxcomponents/focus-cms-shell.tsxcomponents/editor-toolbar.tsxcomponents/section-editor-panel.tsxlib/focus-editor-ui.tslib/cross-key-store.tslib/focus-cms-config.tslib/focus-cms-auth.tslib/supabase.tslib/site-data.server.tslib/page-html.ts
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.