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-admin | Gutenberg or Elementor — Pages workflow |
| 95% of the time same layout, just text swaps | Elementor template duplicate |
| Pixel-perfect design, custom interactions, “vibe-coded” from a brief | Code-first pages (this page) |
| Strong isolation from the WP theme entirely | Probably an Astro route instead |
| Tool | Purpose |
|---|---|
create_page_from_code | New page from { html, css, js } |
update_page_from_code | Patch a deployed code page |
diagnose_site | Confirm the seo-navigator-code-page plugin is installed |
Both are WordPress-only; they refuse Duda and Astro sites with
E_NOT_SUPPORTED.
Prerequisite — install the WP plugin
Section titled “Prerequisite — install the WP plugin”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:
- Zip the folder
wp-plugins/seo-navigator-code-page/from the code repo. - wp-admin → Plugins → Add New → Upload Plugin → upload + activate.
- Confirm with the MCP tool:
Diagnose site
blog-acme.
The response includes code_page_plugin: { installed: true }.
Anatomy of a code page
Section titled “Anatomy of a code page”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.
What the worker does
Section titled “What the worker does”- 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 asslugdoesn’t change. - Auto-prefix every CSS top-level selector with that scope class.
Recurses into
@media,@supports,@container,@layer. Leaves@keyframesselectors alone. Rewrites:root,html,bodyrules to the scope class itself. - Wrap JS in an IIFE that binds
rootto the scope element. If the element isn’t on the page (e.g. plugin missing), the IIFE bails out cleanly without throwing. - Wrap HTML in
<div class="cc-page-9f3a">…</div>inside a Gutenberg<!-- wp:html -->block. - Write meta
_cc_inline_cssand_cc_inline_json the page. The WP plugin emits them inwp_head/wp_footer. - Create as
status=draft. Always. There is no “publish from code” path.
CSS scoping — concrete example
Section titled “CSS scoping — concrete example”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:
:rootandbodycollapse to the scope class — CSS custom properties + base styles apply only inside the page wrapper.@mediarecurses; inner rules are scoped.@keyframesis untouched — its inner selectors (from,to,0%) are positional, not selectors.
Validation rules
Section titled “Validation rules”The worker rejects the request before it ever hits WP REST when:
| Rule | Error |
|---|---|
html > 200 KB | E_PAYLOAD_TOO_LARGE |
css > 100 KB | E_PAYLOAD_TOO_LARGE |
js > 100 KB | E_PAYLOAD_TOO_LARGE |
<script src="…"> in HTML | E_DISALLOWED_CONTENT |
<iframe> in HTML | E_DISALLOWED_CONTENT |
@import url(https://…) in CSS | E_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.@importblocks 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.
Updating a deployed page
Section titled “Updating a deployed page”{ "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.
Working with Claude Code
Section titled “Working with Claude Code”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.
Tailwind recipe (recommended)
Section titled “Tailwind recipe (recommended)”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):
module.exports = { prefix: 'tw-', important: '.cc-page-pricing', // your scope class corePlugins: { preflight: false }, content: ['./*.html'],};Compile:
npx tailwindcss -i src.css -o style.css --minifyThen 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-flexinstead — no clash. -
preflight: false— Preflight resetsbody,*, 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; }
Deriving the scope class at build time
Section titled “Deriving the scope class at build time”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:
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.
JavaScript scope
Section titled “JavaScript scope”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.
Common errors
Section titled “Common errors”| Symptom | Cause | Fix |
|---|---|---|
| Page renders empty in front-end | seo-navigator-code-page plugin not installed | Install it; rerun diagnose_site. |
| Page styles bleed into the header | The scope class wasn’t applied to your custom reset | Make 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 push | You have <script src> or <iframe> in the HTML | Inline it or move to an Astro route. |
E_PAYLOAD_TOO_LARGE | One of html / css / js > limit | Split the page; consider hosting heavy assets externally and linking from the theme <head>. |
E_NOT_SUPPORTED on Duda site | Code pages are WordPress-only | Use duplicate_page + Duda Section Library instead. |
What’s next
Section titled “What’s next”- WordPress pages — Gutenberg & Elementor — for builder-editable pages.
- Astro routes — for pages hosted outside WordPress entirely.
- Architecture — how the scope rewriter
lives in
src/adapters/page-code.ts.