Focus:CMS

Migrate a PHP / Laravel / WordPress site

Focus:CMS lives on the JavaScript side — its editor primitives are React components that hydrate post-load. PHP-rendered sites can still adopt the editor, with one of three patterns.

Option A — Coexist (recommended for WordPress/Laravel content sites)

  1. Keep the PHP app rendering pages as it does today.
  2. Inject a single React bundle into the page (after the closing </body>'s last meaningful content) that mounts an <EditableRawHtml> overlay scoped to the page body.
  3. The PHP server fetches the body content however it does today; the React island re-mounts it through Focus:CMS in edit mode and persists patches back via the Supabase RPC.

Loose sketch:

<?php
// in your theme footer.php
?>
<div id="focus-cms-host" data-body-html="<?= htmlspecialchars($pageHtml) ?>">
  <?= $pageHtml ?>
</div>
<script src="/wp-content/themes/yours/focus-cms.js" defer></script>

The React bundle:

import { createRoot } from "react-dom/client";
import { EditableRawHtml } from "./editable-raw-html";

const host = document.getElementById("focus-cms-host");
if (host) {
  createRoot(host).render(
    <EditableRawHtml html={host.dataset.bodyHtml ?? ""} htmlPath="blocks/0/html" />
  );
}

Saves go to Supabase. The PHP app keeps reading from its own DB. When the editor hits Save, you'll need a small sync job (cron or webhook) that pulls the latest focus_cms_content row and writes it back to your PHP-side store, OR you switch the PHP renderer to read directly from Supabase for that page.

Option B — Migrate page content to Supabase

Move the editable page bodies out of the PHP DB and into focus_cms_content. PHP templates fetch the row at render time:

$page = supabase_select("focus_cms_content", ["key" => "pages/$slug"]);
echo $page["data"]["blocks"][0]["html"];

This gives PHP read-only access to the canonical content. All editing happens through Focus:CMS on a sister Next.js app or via the React island.

Option C — Replace the PHP renderer with a static Next.js shell

Cleanest long-term. The PHP backend becomes API-only; a Next.js static export renders the content pages. See the Next.js recipe. This is most worth it when content pages dominate the site and the PHP layer is light.

WordPress-specific notes

WordPress's editor (Gutenberg) is a competing editor. Pick one — running both gets confusing for editors and brittle for engineers. The migration path: export each post's post_content, seed it as a pages/{slug} row, and disable Gutenberg for those URLs.

Auth

PHP sessions don't map to Supabase Auth sessions. The editor login flow uses Supabase Auth directly (email + password). Editors sign in once via the admin link in the footer; the session is stored in localStorage by supabase-js.

Don't try to bridge PHP sessions into Supabase — let the editor own its own auth.