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)
- Keep the PHP app rendering pages as it does today.
- 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. - 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.