When I realized that WordPress was fully embracing Full Site Editing, I got curious — could I build a clean theme with no PHP templates at all, powered only by blocks, JSON, and JavaScript?
This post breaks down the process, the challenges I faced, and what I learned while creating my own FSE theme.
🧩 Idea & Goal
- Modernize an old base theme and embrace editor-first workflows.
- Give non-technical editors visual control via the Site Editor, patterns, and native blocks.
- Keep front-end fidelity to an existing design system without overloading
theme.json.
⚙️ Technology Stack
- Gutenberg / FSE — Site Editor, block themes, patterns, template parts.
- theme.json — global styles, palettes, typography tokens, spacing.
- Build tooling:
@wordpress/scripts(or Webpack/Rollup) for block assets. - Styling: SCSS (optional Tailwind if standardized in your team).
- ACF Blocks: only when a data-heavy, form-like UI is needed; otherwise rely on native or custom JS/PHP blocks.
- Environment: PHP 8.4, WordPress 6.7+, Gutenberg 18+.
🧱 Theme Structure
Templates and Parts
/templates/
├── index.html
├── front-page.html
├── single.html
├── archive.html
└── and others html
/parts/
├── header.html
└── footer.html
Blocks
/blocks/
├── button/
├── buttons/
├── reading-time/
└── portfolio-links/
Each block contains:
block.json
editor.js
view.js
(optional) style.css
(optional) render.php
Patterns
/patterns/
└── hero.php
Patterns use native blocks and your theme’s CSS classes for reusable editor layouts.
Central Config
theme.json holds color, spacing, and typography tokens, plus block supports.
Avoid adding component-specific CSS there — it’s a global config, not a stylesheet.
🧩 Enqueue via PHP (example)
<?php
/**
* Enqueue theme assets (frontend).
*/
function base_enqueue_assets(): void
{
$theme_uri = get_template_directory_uri();
$theme_dir = get_template_directory();
$style_path = $theme_dir . '/build/index.css';
$script_path = $theme_dir . '/build/index.js';
if (file_exists($style_path)) {
wp_enqueue_style('base-fse-theme', $theme_uri . '/build/index.css', [], filemtime($style_path));
}
if (file_exists($script_path)) {
wp_enqueue_script('base-fse-script', $theme_uri . '/build/index.js', [], filemtime($script_path));
}
}
add_action('wp_enqueue_scripts', 'base_enqueue_assets');
/**
* Enqueue editor assets (block editor only).
*/
function base_enqueue_block_editor_assets(): void
{
$theme_dir = get_template_directory();
$editor_style_path = $theme_dir . '/build/editor.css';
if (file_exists($editor_style_path)) {
wp_enqueue_style('base-editor-style', get_template_directory_uri() . '/build/editor.css', ['wp-edit-blocks'], filemtime($editor_style_path));
}
}
add_action('enqueue_block_editor_assets', 'base_enqueue_block_editor_assets');
🧩 Block Registration (example)
/**
* Auto-register every block located under /blocks.
*/
function base_register_blocks(): void
{
$blocks_dir = get_template_directory() . '/blocks';
if (! is_dir($blocks_dir)) {
return;
}
foreach (glob($blocks_dir . '/*/block.json') as $block_json_path) {
register_block_type_from_metadata(dirname($block_json_path));
}
}
add_action('init', 'base_register_blocks', 5); // Priority 5 — after core blocks.
🧠 Gutenberg / FSE Workflow
- Prefer native blocks and patterns for content sections (Hero, Features, Testimonials).
- Use custom blocks when core markup or behavior can’t meet design requirements (mega-menu, specialized buttons).
- Template parts (
header.html,footer.html) keep global sections editable. - Patterns speed up page building while keeping consistent CSS classnames.
🧩 Problems & Solutions
Loading assets from /build
Use filemtime() for versioning and enqueue from theme URI.
For blocks, declare editorScript, script, and style in block.json or register manually in PHP.
PHPCS formatting and return vs echo
Dynamic blocks should return strings from render_callback — not echo.
Template parts rely purely on block HTML; no PHP output needed.
Webpack CleanPlugin removing assets
Configure it to preserve /assets/ and only clean /build/.
With @wordpress/scripts, keep entry points minimal and avoid cleaning non-build folders.
Gutenberg 18+ compatibility
Always use register_block_type_from_metadata().
Ensure block.json fields (apiVersion, supports) are valid.
If a block doesn’t appear in the editor, enqueue assets explicitly in enqueue_block_editor_assets.
🧰 What I Built
Site Editor Setup
Editable parts/header.html and parts/footer.html; core templates in /templates/.
Custom Blocks
- Buttons:
base-fse/button→ saves<a class="wp-block-base-fse-button">…</a>base-fse/buttons→ wraps<div class="wp-block-base-fse-buttons"><InnerBlocks/></div>
Editors get a native block experience, front-end renders exact HTML expected by CSS.
- Hero Section:
- Dynamic block (
blocks/hero-section/) for prototyping. - Pattern (
patterns/hero.php) for production.
- Dynamic block (
- Mega Header:
Dynamic block combining menu, CTA, badge, and meta area when core Navigation wasn’t flexible enough.
Full control over HTML and mobile toggle behavior.
Example Single Template
templates/single.html combines Header, Post Title, Post Content, Comments, and Footer blocks — no PHP required.
Editor Preview
The Site Editor displays the real header/footer.
Patterns are available in the inserter.
Minimal editor-only CSS ensures layout parity (e.g., horizontal .stx-buttons).
🧭 Why FSE over ACF-first
Pros
- Editor-first authoring and fewer meta-forms.
- Faster layout iteration.
- Patterns reusable by non-devs.
- Cleaner separation between content and presentation.
- Custom blocks fill functional gaps without abandoning the visual workflow.
Cons
- Core Navigation block is still too rigid for complex menu UX.
- Some editor canvases require minor editor-only CSS adjustments.
- “Invalid content” warnings can occur if dynamic wrappers mix with saved markup (solved by clear save/render split).
🪄 Takeaways
What I liked
- Patterns + native blocks cover ~80 % of typical marketing/front-page layouts.
- Custom blocks output exact HTML aligned with existing CSS.
- Site Editor empowers content teams — fewer dev roundtrips.
What I didn’t
- Advanced navigation UX in core Navigation still lags behind complex design systems.
- Occasional editor sync issues when mixing dynamic HTML and saved content.
What I’d change next time
- Start with patterns; introduce custom blocks only when truly needed.
- Define a minimal editor-only CSS layer early for parity.
🚀 Next Steps
- Document how to add new sections as patterns and when to prefer custom vs native blocks.
Leave a Reply