Skip to content

Code-first WordPress pages

Code-first pages are for designs that don’t fit a builder. You hand-write HTML, CSS, and (optionally) JS — usually with Claude Code’s help — and the worker drops them into WordPress as a single Gutenberg <!-- wp:html --> block. The catch: WordPress themes are messy. To stop your .btn from fighting their .btn, the worker auto-scopes every CSS rule to a class unique to your page.

When to use this vs Gutenberg vs Elementor

Section titled “When to use this vs Gutenberg vs Elementor”
You want…Use
Editable by a non-developer in wp-adminGutenberg or Elementor — Pages workflow
95% of the time same layout, just text swapsElementor template duplicate
Pixel-perfect design, custom interactions, “vibe-coded” from a briefCode-first pages (this page)
Strong isolation from the WP theme entirelyProbably an Astro route instead
ToolPurpose
create_page_from_codeNew page from { html, css, js }
update_page_from_codePatch a deployed code page
diagnose_siteConfirm the seo-navigator-code-page plugin is installed

Both are WordPress-only; they refuse Duda and Astro sites with E_NOT_SUPPORTED.

The CSS and JS bytes are stored as page meta. A tiny plugin in the code repo emits them in wp_head / wp_footer. Without the plugin the page will render the wrapper <div> only — your styles and scripts never load.

Install:

  1. Zip the folder wp-plugins/seo-navigator-code-page/ from the code repo.
  2. wp-admin → Plugins → Add New → Upload Plugin → upload + activate.
  3. Confirm with the MCP tool:

Diagnose site blog-acme.

The response includes code_page_plugin: { installed: true }.

The create_page_from_code call expects four pieces:

{
"site_id": "blog-acme",
"slug": "pricing",
"title": "Pricing — Acme Detailing",
"html": "<section>… inner content …</section>",
"css": "/* selectors as if your page is standalone */",
"js": "// optional"
}

The HTML must be inner content only — no <html>, <head>, or <body> tags. The worker wraps it in a single <div> and inserts that as a Gutenberg HTML block.

  1. Compute a deterministic scope class from SHA-256(site_id + " " + slug) and take the first 4 bytes as hex → cc-page-9f3a (8-character suffix). Stable across updates as long as slug doesn’t change.
  2. Auto-prefix every CSS top-level selector with that scope class. Recurses into @media, @supports, @container, @layer. Leaves @keyframes selectors alone. Rewrites :root, html, body rules to the scope class itself.
  3. Wrap JS in an IIFE that binds root to the scope element. If the element isn’t on the page (e.g. plugin missing), the IIFE bails out cleanly without throwing.
  4. Wrap HTML in <div class="cc-page-9f3a">…</div> inside a Gutenberg <!-- wp:html --> block.
  5. Write meta _cc_inline_css and _cc_inline_js on the page. The WP plugin emits them in wp_head / wp_footer.
  6. Create as status=draft. Always. There is no “publish from code” path.

Input:

:root { --primary: indigo; }
body { font-family: system-ui; }
.btn { background: var(--primary); padding: 8px 16px; }
.btn:hover { opacity: 0.9; }
@media (max-width: 600px) {
.btn { padding: 6px 12px; }
}
@keyframes spin {
from { transform: rotate(0) }
to { transform: rotate(360deg) }
}

After scoping with cc-page-9f3a:

.cc-page-9f3a { --primary: indigo; }
.cc-page-9f3a { font-family: system-ui; }
.cc-page-9f3a .btn { background: var(--primary); padding: 8px 16px; }
.cc-page-9f3a .btn:hover { opacity: 0.9; }
@media (max-width: 600px) {
.cc-page-9f3a .btn { padding: 6px 12px; }
}
@keyframes spin { from { transform: rotate(0) } to { transform: rotate(360deg) } }

Note:

  • :root and body collapse to the scope class — CSS custom properties + base styles apply only inside the page wrapper.
  • @media recurses; inner rules are scoped.
  • @keyframes is untouched — its inner selectors (from, to, 0%) are positional, not selectors.

The worker rejects the request before it ever hits WP REST when:

RuleError
html > 200 KBE_PAYLOAD_TOO_LARGE
css > 100 KBE_PAYLOAD_TOO_LARGE
js > 100 KBE_PAYLOAD_TOO_LARGE
<script src="…"> in HTMLE_DISALLOWED_CONTENT
<iframe> in HTMLE_DISALLOWED_CONTENT
@import url(https://…) in CSSE_DISALLOWED_CONTENT

Why these rules:

  • The plugin only inlines JS — external <script src> would be silently dropped by some caching plugins.
  • <iframe> is the #1 vector for accidental embeds that drag in third-party styles.
  • @import blocks rendering and bypasses the scope rewriter entirely.

If you need a font, use a regular <link> injected by your theme header, or self-host the file and @font-face it from local URL.

{
"tool": "update_page_from_code",
"args": {
"site_id": "blog-acme",
"page_id": "142",
"slug": "pricing",
"css": "/* fresh CSS */"
}
}

Pass only the parts you want to change. As long as slug matches the original, the scope class is identical — external caches stay valid (the generated <style> block has a stable ID derived from the page ID, but the selectors inside don’t change).

If you change slug, the scope class changes too. The CSS still works because it’s inlined with the page meta — but any third-party cache keyed on the old slug will be stale until it refreshes.

The intended workflow is:

You (in Claude Desktop, talking to a Claude Code subagent):
"Build a pricing page with 3 tiers (Starter $9, Pro $29, Enterprise),
gradient purple background, sticky mobile CTA."
Claude Code:
1. Creates pricing/index.html, pricing/style.css, pricing/script.js
2. Optionally compiles Tailwind to style.css (recommended — see below)
3. Calls create_page_from_code(site_id, slug, title, html, css, js)
4. Returns: { id: "142", status: "draft", scope_class: "cc-page-9f3a", ... }
You:
"Drop Pro to $19 and change the gradient to blue."
Claude Code:
1. Edits style.css
2. Re-compiles if Tailwind
3. Calls update_page_from_code(site_id, page_id="142", slug="pricing", css=<new>)
4. Page draft updated in-place. Same scope class. CSS cache stays valid.

Throughout, the page stays at status=draft. You click Publish in wp-admin when you’re ready — the MCP server has no path to do it for you.

The cleanest Tailwind setup for code pages compiles to a static file at build time (avoid the CDN runtime — it conflicts with most WP cache plugins):

tailwind.config.js
module.exports = {
prefix: 'tw-',
important: '.cc-page-pricing', // your scope class
corePlugins: { preflight: false },
content: ['./*.html'],
};

Compile:

Terminal window
npx tailwindcss -i src.css -o style.css --minify

Then pass the compiled style.css to create_page_from_code.

  • prefix: 'tw-' — WordPress themes ship classes like .flex, .block, .hidden. Prefixing puts your utilities at .tw-flex instead — no clash.

  • preflight: false — Preflight resets body, *, headings globally. Inside someone else’s theme, that breaks everything. Write your own minimal reset inside the scope:

    .cc-page-pricing *,
    .cc-page-pricing *::before,
    .cc-page-pricing *::after { box-sizing: border-box; }
    .cc-page-pricing { line-height: 1.5; }

The scope class is cc-page-<hash> where <hash> is the first 4 bytes of SHA-256(site_id + " " + slug). Compute it in Node so you can use the same literal as Tailwind’s important:

Terminal window
node -e "crypto.subtle.digest('SHA-256', new TextEncoder().encode('blog-acme pricing')).then(b => console.log('cc-page-' + Array.from(new Uint8Array(b)).slice(0,4).map(x => x.toString(16).padStart(2,'0')).join('')))"

Save the output into tailwind.config.js once. As long as slug doesn’t change, it stays valid.

The worker wraps your JS in:

(function () {
var root = document.querySelector(".cc-page-9f3a");
if (!root) return;
/* your code here */
})();

Use root for queries to stay inside the page:

const cta = root.querySelector(".tw-bg-indigo-600");
cta.addEventListener("click", () => { /* … */ });

If you write window.foo = …, that still leaks globally — the IIFE is about cleanup, not sandboxing. Be tidy.

SymptomCauseFix
Page renders empty in front-endseo-navigator-code-page plugin not installedInstall it; rerun diagnose_site.
Page styles bleed into the headerThe scope class wasn’t applied to your custom resetMake sure all rules in style.css use selectors (no naked body { … } after compilation — the scope rewriter handles input but Tailwind preflight skips through if preflight: true).
E_DISALLOWED_CONTENT on first pushYou have <script src> or <iframe> in the HTMLInline it or move to an Astro route.
E_PAYLOAD_TOO_LARGEOne of html / css / js > limitSplit the page; consider hosting heavy assets externally and linking from the theme <head>.
E_NOT_SUPPORTED on Duda siteCode pages are WordPress-onlyUse duplicate_page + Duda Section Library instead.