← Back to posts

≈ 5 min

How I Built Full Site Editing (FSE) Theme for WordPress

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.
  • 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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *