The Four-Pass Pattern Validation System: Why All Steps Matter in Elayne
Your WordPress block pattern looks perfect in the editor. You commit it. You deploy it. And then — the browser throws a block validation error, the CSS doesn’t apply, or the layout breaks on mobile. What went wrong?
In Elayne, we use a four-pass validation system to catch issues before they reach production. Each pass serves a distinct purpose, and skipping any of them leaves gaps that will come back to bite you. This post explains why all four passes are necessary — especially Pass 1 — and what each one actually checks.
The Core Problem: Patterns Can Fail in Multiple Ways
A WordPress block pattern is a complex artifact. It contains:
- Block JSON — the structure and attributes of every block
- Block HTML — the rendered markup that WordPress generates
- Semantic variables — references to
theme.jsonpresets likevar:preset|color|primary - Translation strings — all user-facing text wrapped in
esc_html_e()or similar - CSS dependencies — block styles, theme styles, and global styles
Each of these layers can break independently. A pattern might have:
- Syntactically invalid JSON (unbalanced braces, malformed attributes)
- Semantically incorrect values (hardcoded colors, wrong spacing presets)
- Runtime serialization mismatches (JS
save()produces different HTML than the PHP pattern) - Template integration issues (missing attributes, wrong block types in HTML templates)
No single validator can catch all of these. That’s why we need four.
Quick summary: The Four Passes
- Pass 1 (Gutenberg structural): Validates block markup syntax via PHP parser — catches unbalanced braces, malformed comments, bad nesting
- Pass 2 (Elayne compliance): Validates semantic variables, translations, and Elayne-specific rules via pt-cli
- Pass 3 (Playwright runtime): Tests actual browser behavior — catches JS serialization mismatches PHP validator misses
- Pass 4 (HTML templates): Validates
templates/andparts/.html files for block template-specific issues
The key insight: Pass 1 is not redundant. It catches structural issues that Pass 3 cannot see, and it auto-fixes many of them. They validate orthogonal concerns.
Pass 1: Gutenberg Structural Validator
Tool: wp pattern validate (WP-CLI, requires Trellis VM)
What it does: Uses PHP’s parse_blocks() and serialize_blocks() to verify the pattern’s block structure is valid WordPress block markup.
Why This Pass Exists
This is the foundation. If your pattern has structural issues — unbalanced JSON braces, malformed block delimiters, improperly nested blocks — nothing else will work. Pass 1 catches:
| Issue | Example | Consequence if Missed |
| Unbalanced JSON braces | "style":{"color":"red" (missing }) | Pattern fails to parse entirely |
| Malformed block comments | <!-- wp:heading { (missing closing -->) | Blocks don’t render |
| Bad nesting | <!-- wp:paragraph --><div><!-- wp:heading --> | Invalid block tree |
| Missing required attributes | <!-- wp:image --> without id or url | Block validation errors |
The Critical Distinction: PHP vs JavaScript Parsing
WordPress has two different block parsers:
- PHP parser — Used by
wp pattern validate, runs on the server - JavaScript parser — Used by the block editor, runs in the browser
These parsers handle edge cases differently. Pass 1 uses the PHP parser to catch structural issues that would prevent the pattern from even loading.
Without Pass 1: You might spend hours debugging JS serialization issues that are actually caused by syntax errors that Pass 1 would have caught in seconds.
How to Run It
# Dry run (check only)
cd ~/code/imagewize.com/trellis
trellis vm shell --workdir /srv/www/demo.imagewize.com/current -- \
wp pattern validate web/app/themes/elayne/patterns/my-pattern.php
# Auto-fix structural issues
cd ~/code/imagewize.com/trellis
trellis vm shell --workdir /srv/www/demo.imagewize.com/current -- \
wp pattern validate web/app/themes/elayne/patterns/my-pattern.php --fix
# All patterns in directory
cd ~/code/imagewize.com/trellis
trellis vm shell --workdir /srv/www/demo.imagewize.com/current -- \
wp pattern validate web/app/themes/elayne/patterns/ --fix
Note: Files sync automatically via Lima, so patterns edited on the host are immediately available in the VM.
Pass 2: Elayne Compliance Checker
Tool: composer check (pt-cli, runs on host machine)
What it does: Validates that patterns follow Elayne’s specific rules — semantic variables, translation functions, naming conventions, and block attribute ordering.
Why This Pass Exists
Pass 1 ensures the pattern is structurally valid WordPress markup. Pass 2 ensures it’s Elayne-compliant markup. This catches:
| Issue | Example | Why It Matters |
| Hardcoded colors | color: #0073aa | Must use var:preset|color|primary for theme consistency |
| Unwrapped text | Hello World | Must use esc_html_e('Hello World', 'elayne') for WP.org compliance |
| Wrong attribute order | {"style":{...},"className":"foo"} | Root-level attributes must come first |
| Non-semantic spacing | padding: 20px | Must use var:preset|spacing|medium |
| Invalid preset slugs | var:preset|spacing|xx-small | xx-small doesn’t exist; use 2-x-small |
How to Run It
# From theme directory
cd ~/code/imagewize.com/demo/web/app/themes/elayne
composer check
# Check specific file
composer check patterns/main-hero.php
# With autofix
composer check patterns/ --autofix
Note: This runs automatically in GitHub Actions on every PR, but you should run it locally first.
Pass 3: Playwright Runtime Validator
Tool: Custom Playwright script (runs on host machine)
What it does: Launches a real browser, logs into WordPress admin, inserts the pattern into a draft page, saves it, and checks for JavaScript block validation errors and content mismatches.
Why This Pass Exists (And Why It Doesn’t Replace Pass 1)
This is where the “but doesn’t Pass 3 catch everything?” question comes in. No, it doesn’t. Here’s why:
Pass 3 catches JS-specific serialization issues that Pass 1 cannot see.
WordPress’s JavaScript save() function sometimes produces different output than PHP’s serialize_blocks(). Examples:
| Issue | PHP Behavior | JS Behavior | Caught By |
|---|---|---|---|
| Border style auto-injection | Keeps only color and width | Adds style: "solid" automatically | Pass 3 |
| Class ordering | Preserves order | Reorders has-text-color before has-{preset}-font-size | Pass 3 |
| Button link classes | Preserves order | Reorders wp-block-button__link has-custom-font-size wp-element-button | Pass 3 |
backgroundColor: "base" | Preserves attribute | Drops attribute entirely | Pass 3 |
But Pass 3 depends on Pass 1. If your pattern has structural errors, Playwright might:
- Fail to insert the pattern at all
- Insert it with missing blocks
- Produce cryptic errors that mask the real problem
How to Run It
# From repo root
cd ~/code/imagewize.com
# Single file
.playwright/scripts/validate-pattern-demo.sh demo/web/app/themes/elayne/patterns/my-pattern.php
# With verbose diff output
.playwright/scripts/validate-pattern-demo.sh demo/web/app/themes/elayne/patterns/my-pattern.php --verbose
# All patterns in a directory
.playwright/scripts/validate-pattern-demo.sh demo/web/app/themes/elayne/patterns/ --all
# WooCommerce store subsite
.playwright/scripts/validate-pattern-demo.sh --subsite=store demo/web/app/themes/elayne/patterns/my-pattern.php
Pass 4: HTML Template Compliance Checker
Tool: pt-cli check:templates (runs on host machine)
What it does: Validates HTML templates (templates/*.html) and template parts (parts/*.html) for block template-specific issues.
Why This Pass Exists
Block patterns (.php files) and HTML templates (.html files) have different requirements. Pass 4 catches:
| Issue | Example | Why It Matters |
| WooCommerce filter blocks missing wrapper | Missing <div> around product-filters | WC 9.x+ save() change breaks layout |
| Missing CSS custom properties | product-filters without required vars | Styles don’t apply |
taxQuery as object | taxQuery: {} | Must be array [] |
Missing theme attribute | <!-- wp:template-part --> | Required for template parts |
| Unbalanced HTML tags | Missing </div> | Breaks document structure |
How to Run It
# Check templates directory
pt-cli check:templates demo/web/app/themes/elayne/templates/ --theme=elayne
# Check parts directory
pt-cli check:templates demo/web/app/themes/elayne/parts/ --theme=elayne
# With autofix
pt-cli check:templates demo/web/app/themes/elayne/templates/ --theme=elayne --autofix
# Check single file
pt-cli check:templates demo/web/app/themes/elayne/templates/archive-product.html --theme=elayne
The Workflow: Putting It All Together
The four passes form a deliberate progression. Order matters:
- Pass 1 first — fixes structural issues that would break parsing
- Pass 2 next — ensures Elayne-specific compliance (translations, semantic vars)
- Pass 3 then — verifies runtime behavior in actual browser
- Pass 4 last — checks HTML templates if applicable
Why You Can’t Skip Pass 1
This is the question that prompted this post: “Pass 3 does all Pass 1 does, doesn’t it?”
No. They are fundamentally different.
| Pass 1 | Pass 3 | |
|---|---|---|
| Runs | PHP parser on the server | JavaScript save() in a real browser |
| Checks | Is this valid WordPress block markup? | Does this pattern survive the editor’s serialization? |
| Fixes | Structural syntax errors (auto-fix) | Nothing (reports only) |
| Time | Seconds | 30-60 seconds per pattern |
| Dependencies | WordPress database (VM required) | Live WordPress site, browser, admin access |
What Happens If You Skip Pass 1
Imagine your pattern has a missing closing brace:
<!-- wp:group {"style":{"spacing":{"margin":{"top":"0"} -->
<div class="wp-block-group" style="margin-top:0">
With this syntax error:
- Pass 1: Catches this immediately, can auto-fix it
- Pass 3: Might fail to insert the pattern, or insert it with corrupted blocks, producing errors like:
Block validation failed for `core/group`.
The `style` attribute is invalid.
You’d then spend time debugging a “style attribute” error when the real problem is a syntax error that Pass 1 would have fixed automatically.
Real-World Example: main-hero.php
Here’s what each pass caught for the main-hero.php pattern:
| Pass | Issue Found | Severity |
|---|---|---|
| Pass 1 | None (structure was valid) | N/A |
| Pass 2 | Missing translation wrapper on stat text | Medium |
| Pass 3 | border-top-style:solid auto-injected by JS but missing from pattern | High |
| Pass 4 | N/A (not a template file) | N/A |
If we had skipped Pass 1: We would have missed structural issues in other patterns, but more importantly, we wouldn’t have had confidence that the pattern was even parseable before moving to the more expensive Pass 3.
If we had skipped Pass 3: The pattern would have passed structural and compliance checks, but would have thrown block validation errors in the actual editor.
When to Run Which Pass
| Scenario | Pass 1 | Pass 2 | Pass 3 | Pass 4 |
|---|---|---|---|---|
| New pattern created | ✅ | ✅ | ✅ | ❌ |
| Pattern content updated | ✅ | ✅ | ✅ | ❌ |
| CSS changes only | ❌ | ✅ | ✅ | ❌ |
| Translation fixes | ❌ | ✅ | ❌ | ❌ |
| Template changes | ❌ | ❌ | ❌ | ✅ |
| Pre-commit check | ✅ | ✅ | ⚠️ | ⚠️ |
| Pre-deploy check | ✅ | ✅ | ✅ | ✅ |
⚠️ Notes: Pass 3 and Pass 4 are slower. Run them selectively for final verification, not on every minor change.
Common Objections Addressed
“Pass 1 seems redundant if Pass 3 catches everything in the browser”
It doesn’t. Pass 3 catches JS serialization issues. Pass 1 catches PHP parsing issues. They’re different error classes. Moreover, Pass 1 can auto-fix many issues, while Pass 3 can only report them.
“But I only changed text/CSS — do I really need all four passes?”
- Text changes: Pass 2 (translation validation) + Pass 3 (ensures text renders correctly)
- CSS changes: Pass 2 (semantic variable validation) + Pass 3 (visual regression check)
- Skip Pass 1 only if: You’re 100% certain you didn’t touch block structure
In practice: Run Pass 1 anyway. It takes 5 seconds and gives you confidence.
“Can’t we combine these into one tool?”
Theoretically, yes. But:
- Pass 1 requires the VM (WordPress database)
- Pass 2 requires the host (composer/pt-cli)
- Pass 3 requires a browser
- Pass 4 requires different logic for HTML templates
Combining them would mean either:
- A very complex tool with many dependencies, or
- Running everything in the VM (slow for browser-based tests)
The current separation is intentional: each tool does one thing well, and you can run them in parallel for different files.
The Bottom Line
The four-pass system exists because patterns can fail in four different ways, and each pass is optimized to catch one class of failure:
| Pass | Failure Type | Tool | Location |
|---|---|---|---|
| 1 | Structural / Syntax | wp pattern validate | VM |
| 2 | Semantic / Compliance | composer check | Host |
| 3 | Runtime / Serialization | Playwright | Host |
| 4 | Template / Integration | pt-cli check:templates | Host |
Pass 1 is not useless. It’s the foundation that makes the other passes reliable. Skipping it is like building a house without checking the foundation — you might get away with it for a while, but eventually something will crack.
The total time for all four passes on a single pattern: ~2 minutes. The time saved by catching issues early: hours of debugging.
Run all four. Every time.
Need Expert FSE Block Theme Development?
Imagewize specializes in custom WordPress block themes and pattern libraries. Our four-pass validation system ensures your patterns are production-ready before deployment. We build FSE themes using Moiraine and maintain the Elayne pattern library for our own projects.
From €80-120/hour | No minimum commitment | Pay as you go | Fixed-price project quotes available