Generate recommended static markup for later import into CanAI: one complete HTML document per page (no React/Vue/build tools). Styling is Tailwind CSS utilities only; behavior is vanilla JS or Alpine.js; icons are Lucide (data-lucide).
Assume the agent runs in the user’s own folder (not necessarily the WordPress root). This skill focuses on CanAI-compatible, integration-ready single-HTML output only — no requirement for WP-CLI, .env.wplocal, or writing under wp-content/.
See references/BOILERPLATE.md for the canonical skeleton, Alpine/Lucide patterns, and the pages.json / menus.json formats. See references/STRUCTURE-NAVIGATION.md for the shared Atomic Design navigation contract used by both canai-prepare and canai-mcp.
Purpose
- Output goes into the current working directory (or a user-specified output path). Files use relative asset paths so the project folder is self-contained and portable — it can later be copied into WordPress uploads, served locally, or handed off to
**canai-mcp**for import. - Files are CanAI-shaped: Atomic Design navigation comments, semantic regions, and class names that map cleanly to Twig +
_canai_css/_canai_jslater.
Tech stack (strict)
| Allowed | Notes |
|---|---|
| Semantic HTML5 | <header>, <nav>, <main>, <section>, <article>, <aside>, <footer> |
| Tailwind CSS | Utility classes only; no <style> blocks in prepared HTML (CanAI stores CSS in _canai_css) |
| Vanilla JS | Small IIFE or DOMContentLoaded handlers at end of <body> |
| Alpine.js | Use when stateful UI is simpler than hand-written JS (dropdowns, tabs, accordions) |
| Lucide | <i data-lucide="icon-name" class="h-5 w-5"></i> |
| Disallowed |
|---|
React, Vue, Svelte, Angular, bundlers, JSX, .vue SFCs |
| Tailwind CLI/build step in this skill (Play CDN classes only) |
| Heavy CSS frameworks besides Tailwind utilities |
Library injection — CanAI handles it on the site
CanAI outputs the same libraries through WordPress hooks when enabled in WP Admin → CanAI → Settings (TailwindManager, LucideManager, AlpineManager, AssetManager):
| What the static preview file simulates | Where CanAI loads it |
|---|---|
<title>…</title> |
**wp_head()** — WordPress outputs the document title (page/post SEO title); do not duplicate a literal <title> inside _canai_html if the theme/layout already prints it via wp_head() |
<script> tags for Tailwind Play CDN, Alpine, Lucide UMD (inside WPCanAI-PREVIEW-LIBS in <head>) |
**wp_head()** — enqueue/inject head scripts the theme/CanAI pipeline would print before </head> |
<script>lucide.createIcons();</script> (inside WPCanAI-PREVIEW-LIBS before </body>) |
**wp_footer()** — footer scripts (icon init and similar) |
Do not assume every site has the same toggles: tell the user to enable Load Tailwind, Lucide, and Alpine (if templates use x-*) as needed.
Prepared .html files still include preview-only copies of those scripts so the file can be opened in a browser outside WordPress. Wrap them in:
<!-- WPCanAI-PREVIEW-LIBS:START -->
...
<!-- WPCanAI-PREVIEW-LIBS:END -->
When importing into CanAI, strip the preview blocks from markup you paste into templates — CanAI will supply Tailwind/Lucide/Alpine via **wp_head()** / **wp_footer()** instead. Page-specific JS stays outside the preview markers (maps to _canai_js).
Layout mapping — header / footer vs main content
In a full static HTML file, you may include <header> and <footer> for preview and for SPA splits. After import into CanAI, the usual split is:
**<header>...</header>** → a header-type template component (site navigation shell), conventionally the site header component used by the main layout template.**<footer>...</footer>** → a footer-type template component, conventionally the site footer component used by the main layout.**<main>...</main>** (page body) → the page or type-specific template’s**_canai_html**(inside{{ page_content }}in the layout).
When generating linked single-HTML files (Workflow B), duplicating header/footer across files is fine for static preview; on import, extract shared regions once into **wpcanai_template('site-header')** / **wpcanai_template('site-footer')** (or the project’s actual header/footer slugs) and keep only <main> (and layout-specific sections) in each page template.
Navigation — two menus, marked
A site has at most two menus:
primary(the header nav) andfooter(the footer nav). Mark both, keep their real links, and never invent a third menu name (topnav,mobile,main,social). On import they become the site's two WordPress menus, and CanAI never renders a hardcoded nav.
- Header nav:
<!-- Navigation / Primary -->immediately before<nav data-canai-menu="primary" aria-label="Primary">. - Footer nav:
<!-- Navigation / Footer -->immediately before<nav data-canai-menu="footer" aria-label="Footer">. When the footer links sit in several columns, wrap all the columns in that one<nav>. - Keep real
<a href>links inside both (href="about.html"), so the file previews in a browser. Do not write Twig or menu loops here:canai-mcpswaps the links for aget_menu()loop on import. - A mobile drawer repeats the primary links, so it is marked
data-canai-menu="primary"too. Breadcrumbs and pagination areNavigationlandmarks, not menus, and carry nodata-canai-menu. - Write
menus.jsonnext topages.json, listing each marked nav's links in order:{ "primary": [ … ], "footer": [ … ] }. An item is{ "title", "page" }for a link to a generated page (pageis that page'sslugfrompages.json) or{ "title", "url" }for any other link, with optional"target": "_blank"and"children": [ … ](at most 3 levels). That is exactly theitemsshape ofwpcanai-write-menu. Example in references/BOILERPLATE.md. - Mega menus are menu data too. A column heading or dropdown trigger that is not a link is an item with
childrenand neitherpagenorurl; a link's one-line blurb goes in"description"and its Lucide icon name in"icon"(the same name as itsdata-lucide). Keep the panel's real markup in the HTML (headings, icons, blurbs, links), and list the same structure inmenus.json. A thumbnail stays a relative<img>in the HTML and"image"is left out:canai-mcpsideloads it and adds the attachment id on import.
Output paths
Default: write under **./<project-slug>/** in the current working directory (or a path the user specifies). No WordPress root or uploads folder is required.
<project-slug>/
index.html
about.html
...
assets/
hero.webp
...
pages.json # optional; multi-page / SPA decomposition
menus.json # the primary / footer nav links (see Navigation)
**project-slug:** kebab-case, short (e.g.landing-q2,shop-redesign).**assets/:** binary media copied from designs or exported from SPAs; reference with relative paths (src="assets/photo.jpg").- Cross-page links: relative (
href="about.html"), not absolute domain.
Workflow A — Image / mockup → HTML
- Accept screenshot, Figma export, or other reference image(s).
- Infer layout: header, hero, sections, footer; typography scale; spacing; color roles (map to Tailwind palette + optional
tailwind.configextend in a small inline script only if needed — prefer standard utilities). - Run the Image framing preflight below before choosing image-container classes.
- Emit one
.htmlfile per distinct full-page design the user asked for. - Label navigable elements with the exact HTML form
<!-- Type / Short Label -->, such as<!-- Section / Hero -->,<!-- Card / Feature -->, and<!-- Button / Get Started -->. Follow the controlled vocabulary in references/STRUCTURE-NAVIGATION.md. - Save extracted raster assets into
assets/; reference them relatively. Use stable filenames; optionally list them in a shortREADME.txtin the project folder.
Image framing preflight
Run this for every meaningful raster image before writing its container markup. Logos, icons, decorative textures, and images rendered with object-contain do not need subject-crop analysis.
- Inspect the asset, not only the reference page. Read its intrinsic width and height with an available local image-info tool and inspect the pixels with the host's image viewer. Record the aspect (
width / height) and the important region: faces, full bodies, co-subjects, hands, products, awards, or text baked into the image. If the asset cannot be inspected, preserve it withobject-containor a close-to-source aspect ratio rather than guessing a destructive crop. - Choose the container from the source shape and design role. Prefer responsive
aspect-*utilities over shallow fixedh-*crops: portrait originals →aspect-[4/5],aspect-[3/4], oraspect-square; square →aspect-squareoraspect-[4/3]; landscape →aspect-[16/9],aspect-[3/2], oraspect-[4/3]. A portrait may use a landscape crop only when the inspected safe region demonstrably fits at every target breakpoint. - Set an optical focal point when using
object-cover. Start from these baselines, then adjust from the actual image: headshot/single portraitobject-[center_30%](roughly 25–35% Y); seated pairobject-[center_58%](55–60% Y); standing groupobject-[center_20%]; subject holding an itemobject-[center_80%](75–85% Y) with at leastaspect-[3/4]or comparable height. Off-centre subjects need both coordinates, for exampleobject-[80%_25%]. These are starting points, never a substitute for inspection. - Verify responsive crops. Preview at narrow mobile and wide desktop sizes. Faces, heads, co-subjects, and task-relevant objects must remain visible without pressing against an edge. Use breakpoint-specific aspect/focal classes when the composition genuinely needs them; if no crop works robustly, use a taller/source-matched container or
object-contain.
Example:
<div class="aspect-[4/5] overflow-hidden rounded-2xl sm:aspect-[3/4]">
<img src="assets/founder.webp" alt="Founder holding the product" class="h-full w-full object-cover object-[center_80%]" width="1200" height="1600" loading="lazy">
</div>
Workflow B — SPA / PWA → linked static HTML pages
- Discover routes: router config (
react-router, Vue Router, Next.jsapp/, etc.),sitemap, nav links, or user-provided list. - For each route, generate one self-contained HTML file (duplicate header/footer/nav per file — no partials in this skill).
- Replace client-side navigation with normal
**<a href="...">** links between generated files. - Downgrade framework components to semantic HTML + Tailwind + Alpine/vanilla behavior.
- Write
**pages.json**— array of{ "slug", "title", "file" }for traceability. - Write
**menus.json**from the marked header and footer navs (see Navigation — two menus, marked).
CanAI compatibility checklist
- Navigation comments use the exact
<!-- Type / Short Label -->grammar and controlled vocabulary. - Every
<main>,<section>,<header>,<footer>,<nav>, and<aside>has its required matching type immediately before the opening tag. - Header nav is
<!-- Navigation / Primary -->+<nav data-canai-menu="primary">, footer nav is<!-- Navigation / Footer -->+<nav data-canai-menu="footer">; no otherdata-canai-menuvalue;menus.jsonlists their links. - No
<style>in body content; no inlinestyle=""unless unavoidable (prefer utilities). - Scripts: preview libs inside
WPCanAI-PREVIEW-LIBS; page logic below, outside those markers. - Images: framing preflight completed; descriptive
alt, intrinsicwidth/height, responsive aspect container, andloading="lazy"below the fold. - Lucide: decorative icons
aria-hidden="true"; controls havearia-label. - Links: readable link text; avoid
href="#"for real navigation.
Handoff to CanAI
- Settings: Enable Tailwind, Lucide, and Alpine (if used) in CanAI → Settings so scripts load via
**wp_head()** /**wp_footer()**as above. - Document title: Set the WordPress page/post title (and SEO plugin fields if used);
**wp_head()** outputs<title>on the live site — omit a duplicate<title>from imported template fragments when the layout already includes{{ wp_head() }}. - Layout: Move header markup into the site header component template; footer into the site footer component; wire them from the main layout template (
{{ wpcanai_template('site-header') }},{{ wpcanai_template('site-footer') }}— use the project’s actual slugs). - Import: Use
**canai-mcp**(convert HTML → Twig, write_canai_html/_canai_css/_canai_js) — follow that skill for storage rules (e.g. nopost_contentfor CanAI bodies). - Strip
WPCanAI-PREVIEW-LIBSblocks when pasting into templates (avoid duplicating what CanAI already injects). - Images become ID-based helpers at import, not here. Prepared
.htmlkeeps relativesrc="assets/…"so the folder previews in a plain browser. On import,canai-mcpsideloads each asset and rewrites<img>→{{ image_attrs(id, 'src,alt') }}(and other surfaces →{{ media_url(id, size) }}) by media id — so keep the prepared markup clean and swappable: one<img>per asset, a descriptivealt,width/heightwhen known, and no inlinestylethat would fight the helper output. - Navigation comments map 1:1 on import:
<!-- Type / Short Label -->becomes{# Type / Short Label #}. Preserve the type and label exactly; only replace the delimiters. Follow references/STRUCTURE-NAVIGATION.md, including the required landmark mappings, sowpcanai-scanpasses and the editor's Structure metabox stays useful. - Keep copy translation-ready. On native-i18n target sites, downstream import turns each user-facing sentence into one
{{ t('…') }}translation source. Write each sentence as one unit inside its block element: inline tags inside the sentence are fine (<p>Get <strong>free shipping</strong> today.</p>becomes one source), and no block element ever sits inside a sentence. This mirrors the image →image_attrs()and section-comment handoffs. - WooCommerce pages — for shop / cart / checkout pages, emit the cart/checkout region as one navigable placeholder such as
<!-- Section / Cart -->, with any implementation instruction in a separate<!-- @dev Replace with wc_cart_block() on import -->comment. Downstream (canai-mcp) swaps in thewc_*Twig helpers; do not wire helper markup here. - Navigation becomes WordPress menus on import.
canai-mcpwritesmenus.jsonwithwpcanai-write-menu(one call per key, after the pages exist) and replaces each marked<nav data-canai-menu>'s links with theget_menu('primary')/get_menu('footer')loop. The prepared file keeps its real links; the site owner edits the menus in WordPress afterwards.
Related skills
**canai-mcp**— MCP tools for_canai_*on the configured server.