This file provides guidance to Claude Code when working with the Nynaeve theme.
- Development Commands
- Block Development Philosophy
- Creating Blocks
- SVG Icons in Block Templates
- Block Standards
- Acorn Commands (Trellis VM)
- Architecture
- Code Standards
- Common Tasks
- Avoid reading entire files when only a specific section is needed
- Use
Grepto locate relevant code before reading - Prefer targeted reads with
offsetandlimitparameters over full file reads
cd site/web/app/themes/nynaeve
npm run dev # Start dev server with HMR
npm run build # Build for production
composer install && npm install
composer pint # PHP code quality (Laravel Pint)HMR: Use http://imagewize.test/ (not HTTPS) — HTTPS breaks WebSocket to Vite.
WP-CLI: Run all wp / wp acorn commands inside Trellis VM (local DB conflicts with VM).
PREFERRED APPROACH: Build blocks using InnerBlocks with native WordPress blocks whenever possible.
Key Principles:
- Maximum User Control: Users select styles, fonts, colors via block toolbar/inspector
- Avoid Hardcoded Classes: Never hardcode styling classes (e.g.,
is-style-*,has-*-font-size) - Native WordPress Blocks: Use core blocks (Button, Heading, Paragraph, Image) within custom containers
- Minimal Inspector Controls: Only add custom controls when absolutely necessary
1. InnerBlocks (MOST PREFERRED) — content blocks, full typography control, user-selectable styles 2. Sage Native Blocks with Custom Controls — dynamic JS interactivity, complex data structures 3. ACF Composer Blocks — repeater fields, server-side rendering, rigid brand control. See docs/ACF-BLOCKS.md
Always use real, publishable content in block templates — never placeholder: 'Text goes here...' (placeholder text only shows in the editor, not on the frontend).
Example:
const TEMPLATE = [
['core/heading', { level: 3, content: 'Professional WordPress Development' }],
['core/paragraph', { content: 'Transform your website with modern development practices.' }],
['core/buttons', { className: 'card__buttons', layout: { type: 'flex' } }, [
['core/button', { text: 'Get Started' }],
['core/button', { text: 'Learn More' }],
]],
];Never apply display: flex/inline-flex with ::before/::after pseudo-elements to elements WordPress uses as contenteditable RichText targets.
style.css loads in both the frontend and the editor. Pseudo-elements on flex containers break cursor placement and make blocks impossible to click-to-edit.
contenteditable targets (do NOT apply flex + pseudo-elements via style.css):
core/paragraph→<p>core/heading→<h1>–<h6>core/button→.wp-block-button__linkcore/list→<li>
Fix: override in editor.css:
.my-block .my-styled-paragraph { display: block !important; }
.my-block .my-styled-paragraph::before { display: none !important; }
.my-block .wp-block-button__link { display: block !important; }
.my-block .wp-block-button__link::after { display: none !important; }Affected blocks (fixed): related-links, service-hero, trust-bar.
WordPress does not reliably apply className to button links in InnerBlocks templates. Apply className to the parent core/buttons container.
// ❌ WRONG
['core/button', { text: 'Click Me', className: 'my-button' }]
// ✅ CORRECT
['core/buttons', { className: 'my-buttons-container', layout: { type: 'flex' } }, [
['core/button', { text: 'Click Me' }],
]]Blocks must NOT add horizontal padding. The WordPress layout system handles it automatically via theme.json root padding + app.css rules.
/* ✅ CORRECT */
.wp-block-imagewize-my-block { padding: 5rem 0; }
/* ❌ WRONG — creates double padding */
.wp-block-imagewize-my-block { padding: 5rem 1.25rem; }See docs/CONTENT-WIDTH-AND-LAYOUT.md for full details.
WordPress does not add the .wp-block-paragraph class to <p> elements rendered by InnerBlocks on the frontend. The class only exists in the editor. On the frontend, paragraphs render as plain <p> with no class (unless a custom className was set in the template).
Always target p in block style.css, never .wp-block-paragraph:
/* ✅ CORRECT — works on both frontend and editor */
.wp-block-imagewize-my-block p {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
/* ❌ WRONG — matches in editor only, invisible on frontend */
.wp-block-imagewize-my-block .wp-block-paragraph {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}Alternative: assign a custom className in the InnerBlocks template and target that (e.g. .service-hero__lead). Existing blocks like service-hero and trust-bar use this approach.
Affected blocks with dead .wp-block-paragraph selectors in style.css: content-image-text-card, review-profiles, service-hero, trust-bar. These blocks work because they also have custom className selectors, but the .wp-block-paragraph rules are dead code on the frontend.
All WP-CLI commands must be run from Trellis VM:
cd trellis
trellis vm shell --workdir /srv/www/imagewize.com/current/web/app/themes/nynaeve -- wp acorn sage-native-block:createTemplate categories: Basic Block, Generic (innerblocks, two-column, statistics, cta), Nynaeve Templates, Custom (from block-templates/)
Files created in resources/js/blocks/my-block-name/:
block.json, index.js, editor.jsx, save.jsx, style.css, editor.css, view.js
Blocks auto-register via ThemeServiceProvider — immediately available in the editor.
trellis vm shell --workdir /srv/www/imagewize.com/current/web/app/themes/nynaeve -- wp acorn acf:block MyBlock
# Creates: app/Blocks/MyBlock.php + resources/views/blocks/my-block.blade.phpSee docs/ACF-BLOCKS.md.
NEVER import SVGs via Vite for use as url in core/image InnerBlocks templates. Vite content-hashes filenames — the stored URL becomes a 404 on next build.
Solution: imagewize/theme-icon block binding + window.imagewizeIcons.
app/setup.phpregistersimagewize/theme-iconbinding source — PHP callback callsVite::asset()at render time (always returns the current URL).app/setup.phpinjectswindow.imagewizeIcons(key → current Vite URL) for editor display.editor.jsxuseswindow.imagewizeIcons[path]forurl+ addsmetadata.bindings.urlfor frontend.
REQUIRED — vite.config.js must build the icons (CRITICAL):
For Vite::asset('resources/images/icons/*.svg') to resolve, the icons must be
in the build manifest. They are not imported from any JS/CSS, so the laravel()
plugin needs the assets option:
laravel({
input: [ /* ... */ ],
assets: ['resources/images/**'], // emits icons into the manifest
refresh: true,
}),This is the supported mechanism for laravel-vite-plugin v3+ / Vite 8 (it replaced
the old import.meta.glob approach). Without it, Vite::asset() throws, the callback
returns null, window.imagewizeIcons values are empty strings, and every icon renders
broken in both the editor and the frontend. See docs/nynaeve/THEME-ICON-BINDING-BROKEN.md.
1. app/setup.php — add to both icon_maps:
'iconMyNew' => 'my-new-icon.svg', // relative to resources/images/icons/2. editor.jsx:
const icons = window.imagewizeIcons ?? {};
['core/image', {
url: icons['my-new-icon.svg'] ?? '',
alt: 'My icon',
sizeSlug: 'full',
linkDestination: 'none',
metadata: {
bindings: { url: { source: 'imagewize/theme-icon', args: { path: 'my-new-icon.svg' } } },
},
}]Do NOT:
- ❌
import myIcon from '...svg'— goes through Vite hashing - ❌ Add
assetFileNamesoverride tovite.config.js - ❌ Set
widthorheightattributes oncore/imageblocks — causes block validation failures (see below)
Block validation failure: width/height on core/image (CRITICAL):
Older WordPress emitted style="width:Xpx;height:Xpx" from width/height attributes on core/image. Newer WP does not. If a block was inserted with those attributes, the stored HTML has the inline style but the save function now outputs clean HTML — mismatch → validation error.
Fix: Remove width/height from core/image attributes in editor.jsx. Size via CSS (.my-block .wp-block-image img { width: 16px; height: 16px; }). Then delete and re-insert the affected block to clear stale stored HTML.
After re-inserting existing blocks: blocks saved before the binding pattern was introduced hold old hashed URLs — delete and re-insert once. Future rebuilds won't break them.
Migrated blocks: imagewize/icon-grid (8 icons), imagewize/feature-cards (6 icons), imagewize/trust-bar (4 icons).
block.json checklist:
"category": "imagewize/*"(semantic subcategories: hero, features, cta, testimonials, pricing, content, media, portfolio)"textdomain": "nynaeve"(the theme's Text Domain — NOT "sage" or "imagewize")"example": {}— enables inserter hover preview"align": "wide"default — centers at contentSize (880px), user can change"color": { "background": true, "text": true }for section-level blocks
Margin Reset for Full-Width Blocks (CRITICAL):
WP injects margin-block-start: 24px on all constrained layout children. For alignfull blocks this creates a visible gap. Fix via block.json default — NOT a CSS override:
"attributes": {
"align": { "type": "string", "default": "full" },
"style": {
"type": "object",
"default": { "spacing": { "margin": { "top": "0", "bottom": "0" } } }
}
}This renders as style="margin-top:0;margin-bottom:0" inline — users can still override via WP spacing controls. Applies only to newly inserted blocks; existing blocks must be updated manually.
Example block.json:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "imagewize/my-block",
"title": "My Block",
"category": "imagewize/content",
"icon": "grid-view",
"description": "Block description",
"keywords": ["keyword1"],
"example": {},
"textdomain": "nynaeve",
"editorStyle": "file:./editor.css",
"style": "file:./style.css",
"supports": {
"align": ["wide", "full"],
"anchor": true,
"spacing": { "margin": true, "padding": true },
"color": { "background": true, "text": true }
},
"attributes": {
"align": { "type": "string", "default": "wide" }
}
}All wp acorn commands require database access — must run from Trellis VM.
cd trellis && trellis vm shell
# Starts at /srv/www/demo.imagewize.com/current
cd /srv/www/imagewize.com/current/web/app/themes/nynaeve
wp acorn sage-native-block:create # New Sage Native block
wp acorn acf:block MyBlock # New ACF block
wp acorn acf:field MyFieldGroup # New ACF field group
wp acorn acf:clear # Clear ACF Composer cache
wp acorn acf:cache # Cache ACF Composer fieldsnynaeve/
├── app/
│ ├── Blocks/ # ACF Composer blocks (PHP/Blade)
│ ├── Providers/ # Service providers
│ └── View/Composers/ # Blade view composers
├── resources/
│ ├── css/ # Tailwind CSS styles
│ ├── js/blocks/ # Sage Native blocks (React/JavaScript)
│ └── views/ # Blade templates
└── public/build/ # Built assets (auto-generated)
Entry Points: resources/css/app.css, resources/js/app.js, resources/css/editor.css, resources/js/editor.js
Block Registration: Auto via app/Providers/ThemeServiceProvider.php
Static Assets: Reference via Vite::asset('resources/images/example.svg')
CSS colors (tailwind.config.js):
primary#017cb6 ·primary-accent#e6f4fb ·primary-dark#026492main#171b23 ·main-accent#465166base#ffffff ·secondary#98999a ·tertiary#f5f5f6border-light#ebeced ·border-dark#cbcbcb
Typography: Open Sans, Menlo, Montserrat
Button styles (filter): Default, Outline, Secondary, Light, Dark — in resources/js/filters/
imagewize/about— About section with image and contentimagewize/carousel— Image carouselimagewize/case-studies— Portfolio grid with featured highlightimagewize/contact-section— Dark contact section with Contact Form 7 form cardimagewize/content-image-text-card— Card with image, heading, text, buttonsimagewize/cta-block-blue— Blue CTA section with centered content and buttonimagewize/cta-columns— CTA columns layoutimagewize/elayne-hero— Hero section with gradient background and metrics rowimagewize/expect-list— Dark vertical list for "What to Expect" sectionsimagewize/faq— FAQ sectionimagewize/feature-cards— Six feature cards with SVG icons and hover effectsimagewize/feature-list-grid— Feature grid with checkmark listsimagewize/icon-grid— Auto-fit icon grid with eyebrow, title, and icon+text cardsimagewize/multi-column-content— Statistics and CTAimagewize/page-heading-blue— Full-width gradient bannerimagewize/pricing— Two-column pricing (white vs dark)imagewize/pricing-tiers— Three-column pricing with featured tierimagewize/related-articles— Related articles with tag filteringimagewize/related-links— Linked pill grid for related services sectionsimagewize/review-profiles— Customer review profiles gridimagewize/service-blocks— Stacked service cards with numbered heading and checklistimagewize/service-hero— Dark hero for service pages with dual CTA buttonsimagewize/service-intro— Introductory text section for service pagesimagewize/slide— Individual carousel slideimagewize/testimonial-grid— Testimonials in 3-column gridimagewize/trust-bar— Trust signal bar with 4 icon+text itemsimagewize/two-column-card— Professional card grid
See docs/BLOCKS.md.
- Run
wp acorn sage-native-block:createin Trellis VM - Develop in
resources/js/blocks/my-block/— InnerBlocks, real content, no hardcoded style classes - Block auto-registers; test in editor
See docs/PATTERN-TO-NATIVE-BLOCK.md.
{{-- content-page.blade.php --}}
<div class="wp-block-post-content alignfull is-layout-constrained">
@php(the_content())
</div>theme.json handles all layout: regular blocks center at 55rem, .alignwide at 64rem, .alignfull full-width. Never wrap the_content() in Tailwind containers.
Quote-based (no cart/checkout). Custom templates in resources/views/woocommerce/.
Product content — do NOT add color classes (CRITICAL):
// ❌ WRONG
{ textColor: "primary-accent" } // → has-primary-accent-color class
// ✅ CORRECT — let CSS control colors, use only typography/spacing attributes
{ fontFamily: "montserrat", fontSize: "xl" }app.css controls all product page colors. Hardcoded color classes override CSS and break the design system.