Focus:CMS

Migrate a Node / Express site

Server-rendered Express apps can host Focus:CMS in two ways. The right choice depends on how much template logic lives in JS vs HTML.

Option A — React island per template

Keep your Express routes and EJS/Handlebars/Pug templates. Inject a single React island into each editable page that hosts <EditableRawHtml> and the editor toolbar.

  1. Add React + Focus:CMS to your project:

    npm install react react-dom focus-cms @supabase/supabase-js zustand fast-json-patch
    
  2. Build a small client bundle (editor-island.tsx) that mounts on a target <div> in each rendered page:

    import { createRoot } from "react-dom/client";
    import { EditableRawHtml } from "./components/editable-raw-html";
    
    const host = document.getElementById("focus-cms-host");
    if (host) {
      const bodyHtml = host.dataset.bodyHtml ?? "";
      createRoot(host).render(
        <EditableRawHtml html={bodyHtml} htmlPath="blocks/0/html" />
      );
    }
    
  3. In your Express template, expose the page body as a data attribute and let the island take over in edit mode:

    <div id="focus-cms-host" data-body-html="{{ pageHtml | escapeAttribute }}">
      {{{ pageHtml }}}
    </div>
    <script src="/editor-island.js" defer></script>
    

The island stays inert until a Supabase Auth session is present.

Option B — Migrate page bodies to a static Next.js shell

Cleaner long-term. Move page content from Express templates into focus_cms_content rows. Serve the editable surface from a Next.js static export pointed at the same Supabase project. Keep the Express app for non-page logic (API endpoints, webhooks, dynamic forms).

The two apps can share a domain via Cloudflare:

See the Next.js recipe for the rest of the setup.

Cross-key edits

Either option uses the cross-key store for navigation, footer, and site meta. See the theme guide for the full pattern.

When to pick which