Radicle WordPress Architecture
A comprehensive guide to the Radicle WordPress stack architecture, configuration, development workflow, and Hot Module Replacement (HMR).
Last Updated: 2025-10-11
📋 Table of Contents
- Overview & Technology Stack
- Directory Structure
- Core Configuration Files
- Request Lifecycle
- Service Providers
- Blade Templating System
- Asset Pipeline & Build Process
- Hot Module Replacement (HMR)
- Configuration System
- View Composers
- Blade Components
- Development Workflow
- Troubleshooting
- Practical Examples
- CLI Commands Reference
- Additional Resources
Overview & Technology Stack
Radicle is a modern WordPress stack from Roots that combines several powerful tools to create a Laravel-powered WordPress development environment.
Core Components
┌─────────────────────────────────────────────────────────┐
│ RADICLE STACK │
├─────────────────────────────────────────────────────────┤
│ Bedrock → WordPress boilerplate + Composer │
│ Acorn → Laravel framework for WordPress │
│ Sage → Modern theme framework using Blade │
│ Bud.js → Modern asset build tool (webpack) │
│ Tailwind → Utility-first CSS framework │
│ Alpine.js → Lightweight JavaScript framework │
└─────────────────────────────────────────────────────────┘
Technology Stack
- PHP 8.2+ with Composer dependency management
- Node.js with Yarn/NPM for frontend tooling
- Laravel components (Blade, Collections, Service Container)
- Bud.js for asset compilation and Hot Module Replacement
- TailwindCSS for utility-first styling
- Alpine.js for lightweight interactivity
- Blade templating engine
- TypeScript for type-safe JavaScript
What Makes This Different
This project uses a radically different approach to WordPress development:
Configuration over Code:
- Settings live in
config/directory, not procedural PHP - Service providers bootstrap features from configuration
- Separation of concerns: config, logic, templates, and assets
Modern Development:
- Laravel patterns in WordPress (Service Container, Collections, Blade)
- Component-based architecture with reusable Blade components
- Hot Module Replacement for instant feedback
- Modern build tooling with TypeScript and Tailwind
Better Organization:
- WordPress core as a Composer dependency in
public/wp/ - Source files in
resources/, compiled output inpublic/dist/ - Application logic in
app/, following Laravel conventions - Environment-based configuration with
.envfiles
Directory Structure
radicle/
├── app/ # Laravel-style application code
│ ├── Providers/ # Service providers (bootstrap features)
│ │ ├── AssetsServiceProvider.php
│ │ ├── BlocksServiceProvider.php
│ │ ├── PostTypesServiceProvider.php
│ │ └── ThemeServiceProvider.php
│ ├── View/ # View-related classes
│ │ └── Composers/ # Share data with views
│ │ ├── App.php # Global data (all views)
│ │ ├── Post.php # Post-specific data
│ │ └── Comments.php # Comments data
│ └── helpers.php # Global helper functions
│
├── config/ # Laravel-style configuration
│ ├── app.php # Application settings
│ ├── assets.php # Asset loading config
│ ├── blade-icons.php # Icon system config
│ ├── filesystems.php # File storage
│ ├── post-types.php # Custom post types
│ ├── prettify.php # URL rewriting
│ ├── theme.php # Theme features, menus, sidebars
│ └── view.php # Blade template settings
│
├── database/ # Database operations
│ ├── migrations/ # Database migrations
│ └── seeders/ # Database seeders
│
├── public/ # WEB ROOT (document root)
│ ├── content/ # wp-content replacement
│ │ ├── plugins/ # WordPress plugins
│ │ ├── themes/ # WordPress themes
│ │ │ └── radicle/ # Theme stub (minimal)
│ │ │ ├── index.php # Single entry point
│ │ │ └── style.css # Theme metadata only
│ │ ├── mu-plugins/ # Must-use plugins
│ │ └── uploads/ # Media uploads
│ ├── dist/ # COMPILED ASSETS (auto-generated)
│ │ ├── css/
│ │ │ ├── app.[hash].css
│ │ │ └── editor.[hash].css
│ │ ├── js/
│ │ │ ├── app.[hash].js
│ │ │ ├── editor.[hash].js
│ │ │ └── runtime.[hash].js
│ │ ├── images/ # Optimized images
│ │ ├── entrypoints.json # Asset manifest
│ │ ├── manifest.json # Full asset manifest
│ │ └── theme.json # Gutenberg config
│ ├── wp/ # WordPress core (Composer managed)
│ │ ├── wp-admin/
│ │ ├── wp-includes/
│ │ └── ...
│ ├── index.php # Application entry point
│ └── wp-config.php # WordPress config (Bedrock)
│
├── resources/ # SOURCE FILES (not served directly)
│ ├── fonts/ # Font files
│ ├── images/ # Source images
│ │ └── icons/ # SVG icons
│ ├── scripts/ # JavaScript/TypeScript source
│ │ ├── app.ts # Main frontend entry
│ │ ├── editor.ts # Block editor entry
│ │ └── editor/ # Block editor scripts
│ ├── styles/ # CSS source
│ │ ├── app.css # Main frontend styles
│ │ └── editor.css # Block editor styles
│ └── views/ # BLADE TEMPLATES
│ ├── blocks/ # Custom Gutenberg blocks
│ ├── components/ # Reusable UI components
│ ├── forms/ # Form templates
│ ├── layouts/ # Base layouts
│ │ └── app.blade.php # Main layout
│ ├── partials/ # Template partials
│ ├── sections/ # Page sections (header, footer)
│ ├── utils/ # Utility templates
│ ├── 404.blade.php # 404 error page
│ ├── index.blade.php # Blog index
│ ├── single.blade.php # Single post
│ ├── page.blade.php # Page template
│ └── ... # Other templates
│
├── routes/ # Application routes (experimental)
│
├── storage/ # Cache, logs, compiled views
│ ├── framework/ # Framework cache
│ │ ├── cache/ # Application cache
│ │ └── views/ # Compiled Blade templates
│ └── logs/ # Application logs
│
├── vendor/ # Composer dependencies (auto-generated)
├── node_modules/ # NPM dependencies (auto-generated)
│
├── .env # Environment configuration (NOT in git)
├── .env.example # Example environment file
├── bud.config.ts # Bud.js build configuration
├── composer.json # PHP dependencies
├── package.json # Node dependencies
├── tailwind.config.ts # Tailwind configuration
└── wp-cli.yml # WP-CLI configuration
Important Paths
| Path | URL | Description |
|---|---|---|
public/ |
/ |
Web root |
public/wp/ |
/wp/ |
WordPress core |
public/content/ |
/content/ |
Media & plugins |
public/dist/ |
/dist/ |
Compiled assets |
Core Configuration Files
1. composer.json - PHP Dependencies
Manages PHP dependencies and WordPress core installation.
{
"require": {
"php": ">=8.2",
"roots/wordpress": "6.6.1",
"roots/acorn": "^4.0",
"blade-ui-kit/blade-icons": "^1.5",
"roots/bedrock-autoloader": "^1.0"
},
"extra": {
"acorn": {
"providers": [
"App\\Providers\\AssetsServiceProvider",
"App\\Providers\\BlocksServiceProvider",
"App\\Providers\\ThemeServiceProvider",
"App\\Providers\\PostTypesServiceProvider"
]
},
"installer-paths": {
"public/content/plugins/{$name}/": ["type:wordpress-plugin"],
"public/content/themes/{$name}/": ["type:wordpress-theme"]
},
"wordpress-install-dir": "public/wp"
}
}
Key Features:
- WordPress core lives in
public/wp/ - Service Providers are auto-registered via
extra.acorn.providers - Plugins/themes installed via Composer go to
public/content/ - Autoloader follows PSR-4 standard
2. bud.config.ts - Build Configuration
Configures asset compilation, bundling, and WordPress integration.
export default async (bud: Bud) => {
bud
.proxy(`http://radicle.test`) // Development proxy URL
.serve(`http://localhost:4000`) // Dev server port
.watch([bud.path(`resources/views`), bud.path(`app`)])
// Entry points for compilation
.entry(`app`, [`@scripts/app`, `@styles/app`])
.entry(`editor`, [`@scripts/editor`, `@styles/editor`])
.copyDir(`images`) // Copy static assets
.setPublicPath(`/dist/`) // Output directory
// WordPress Block Editor (Gutenberg) integration
.wpjson.setSettings({
/* theme.json settings */
});
};
Entry Points:
- app: Main frontend bundle (JavaScript + CSS)
- editor: WordPress block editor styles and scripts
Build Output:
public/dist/
├── js/
│ ├── app.[hash].js
│ ├── editor.[hash].js
│ └── runtime.[hash].js
├── css/
│ ├── app.[hash].css
│ └── editor.[hash].css
└── images/
Aliases:
@scripts→resources/scripts/@styles→resources/styles/@images→resources/images/
3. .env - Environment Configuration
Stores environment-specific configuration and sensitive data.
# Database Configuration
DB_NAME='radicle'
DB_USER='radicle'
DB_PASSWORD='radicle'
DB_HOST='localhost'
DB_PREFIX='wp_'
# WordPress URLs
WP_ENV='development' # development, staging, production
WP_HOME='http://localhost:8000' # Your site URL
WP_SITEURL="${WP_HOME}/wp" # WordPress core URL
# Security Keys (generate at: https://roots.io/salts.html)
AUTH_KEY='generateme'
SECURE_AUTH_KEY='generateme'
LOGGED_IN_KEY='generateme'
NONCE_KEY='generateme'
AUTH_SALT='generateme'
SECURE_AUTH_SALT='generateme'
LOGGED_IN_SALT='generateme'
NONCE_SALT='generateme'
# Acorn Features
ACORN_ENABLE_EXPIRIMENTAL_ROUTER='True'
Environment Modes:
development: Debug enabled, error reporting onstaging: Similar to production, but with some debug featuresproduction: Debug off, error reporting off, caching enabled
⚠️ Security: Never commit .env to version control!
Request Lifecycle
Complete Request Flow
┌──────────────────────────────────────────────────────────────────┐
│ 1. HTTP REQUEST │
│ User visits: http://localhost:8000/sample-page │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 2. WEB SERVER │
│ • Routes request to public/index.php │
│ • Document root is public/ │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 3. BEDROCK BOOTSTRAP (public/index.php) │
│ • Load .env variables via vlucas/phpdotenv │
│ • Set WordPress constants (DB_NAME, WP_HOME, etc.) │
│ • Require vendor/autoload.php (Composer autoloader) │
│ • Set ABSPATH to public/wp/ │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 4. WORDPRESS BOOTSTRAP (public/wp/wp-blog-header.php) │
│ • Load WordPress core files │
│ • Connect to database │
│ • Load activated plugins │
│ • Load active theme (Radicle) │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 5. ACORN BOOTSTRAP (Laravel) │
│ • Initialize Acorn application │
│ • Load service container │
│ • Register service providers: │
│ ├─ ThemeServiceProvider │
│ ├─ AssetsServiceProvider │
│ ├─ BlocksServiceProvider │
│ └─ PostTypesServiceProvider │
│ • Boot all providers │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 6. WORDPRESS ROUTING │
│ • Determine post type/page based on URL │
│ • Apply template hierarchy: │
│ page-{slug}.blade.php │
│ page-{id}.blade.php │
│ page.blade.php │
│ singular.blade.php │
│ index.blade.php │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 7. SAGE TEMPLATE LOADER │
│ • WordPress tries to load: page.php │
│ • Sage intercepts and looks for: page.blade.php │
│ • Found at: resources/views/page.blade.php │
│ • Theme stub delegates to Acorn: │
│ echo view(app('sage.view'), app('sage.data'))->render(); │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 8. BLADE TEMPLATE ENGINE │
│ • Find appropriate Blade template │
│ • Compile Blade → PHP (if not cached) │
│ • Cache compiled template in storage/framework/views/ │
│ • Execute compiled PHP template │
│ • Process @extends, @include, @yield directives │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 9. VIEW COMPOSERS RUN │
│ • App\View\Composers\App runs (applies to all views) │
│ • Adds data to view: siteName, containerClasses, etc. │
│ • Other composers run based on view name │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 10. ASSET INJECTION │
│ • AssetsServiceProvider reads entrypoints.json │
│ • Enqueues compiled CSS/JS with correct paths │
│ • wp_head() outputs <link> and <script> tags │
│ • wp_footer() outputs footer scripts │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 11. HTML RESPONSE │
│ • Complete HTML page generated │
│ • Sent to browser │
│ • Assets loaded from /dist/ │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 12. CLIENT-SIDE (Browser) │
│ • Parse HTML │
│ • Load CSS from /dist/css/ │
│ • Execute JavaScript from /dist/js/ │
│ • Initialize Alpine.js │
│ • Page interactive │
└──────────────────────────────────────────────────────────────────┘
Cache Locations
Blade Templates:
storage/framework/views/
└── [hash].php (compiled Blade templates)
Acorn Cache:
storage/framework/cache/
├── config.php (compiled configuration)
└── [other caches]
WordPress Object Cache:
public/content/cache/ (if plugin installed)
Service Providers
Service providers are the central place to bootstrap application features. They run early in the WordPress lifecycle and organize your application’s initialization logic.
How Service Providers Work
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class MyServiceProvider extends ServiceProvider
{
/**
* Register services
* Runs FIRST, before all providers are booted
*/
public function register()
{
// Register bindings in the service container
// Set up configuration
// Register WordPress hooks
}
/**
* Bootstrap services
* Runs SECOND, after all providers are registered
*/
public function boot()
{
// Code that depends on other providers
// Additional WordPress hooks
}
}
Service providers are auto-loaded from composer.json → extra.acorn.providers:
register()method runs early in WordPress lifecycleboot()method runs after all providers are registered- Uses Laravel Collections for elegant array manipulation
- Reads configuration from
config/files
ThemeServiceProvider
Location: app/Providers/ThemeServiceProvider.php
Handles theme features, menus, sidebars, and image sizes.
<?php
namespace App\Providers;
use Illuminate\Support\Collection;
use Roots\Acorn\Sage\SageServiceProvider;
class ThemeServiceProvider extends SageServiceProvider
{
public function register()
{
parent::register();
// Register theme features
add_action('after_setup_theme', function (): void {
// Add theme support from config
Collection::make(config('theme.support'))
->map(fn ($params, $feature) =>
is_array($params) ? [$feature, $params] : [$params])
->each(fn ($params) => add_theme_support(...$params));
// Remove theme support
Collection::make(config('theme.remove'))
->each(fn ($params) => remove_theme_support(...$params));
// Register navigation menus
register_nav_menus(config('theme.menus'));
// Register custom image sizes
Collection::make(config('theme.image_sizes'))
->each(fn ($params, $name) => add_image_size($name, ...$params));
}, 20);
// Register sidebars/widget areas
add_action('widgets_init', function (): void {
Collection::make(config('theme.sidebar.register'))
->each(fn ($instance) => register_sidebar(
array_merge(config('theme.sidebar.config'), $instance)
));
});
}
}
What it does:
- ✅ Reads configuration from
config/theme.php - ✅ Registers theme support (post-thumbnails, html5, etc.)
- ✅ Registers navigation menus
- ✅ Registers sidebars/widget areas
- ✅ Registers custom image sizes
- ✅ Uses Laravel Collections for elegant code
AssetsServiceProvider
Location: app/Providers/AssetsServiceProvider.php
Automatically enqueues compiled assets from the build process.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use function Roots\bundle;
class AssetsServiceProvider extends ServiceProvider
{
public function register()
{
// Enqueue frontend assets
add_action('wp_enqueue_scripts', function (): void {
// Automatically reads from public/dist/entrypoints.json
bundle('app')->enqueue();
// Optional: Remove WordPress default styles
remove_action('wp_body_open', 'wp_global_styles_render_svg_filters');
}, 100);
// Enqueue block editor assets
add_action('enqueue_block_editor_assets', function (): void {
bundle('editor')->enqueue();
}, 100);
// Add type="module" to .mjs scripts
add_filter('script_loader_tag', function (string $tag): string {
if (str_contains($tag, '.mjs"') && !str_contains($tag, 'type="module"')) {
return str_replace(' src=', ' type=module src=', $tag);
}
return $tag;
}, 10, 2);
// Use theme.json from dist/ directory
add_filter('theme_file_path', function (string $path, string $file): string {
if ($file === 'theme.json') {
return public_path() . '/dist/theme.json';
}
return $path;
}, 10, 2);
}
}
What it does:
- ✅ Automatically enqueues compiled assets from
public/dist/ - ✅ Reads asset manifest (
entrypoints.json) - ✅ Handles cache-busting hashes
- ✅ Manages dependencies automatically
- ✅ Integrates with Gutenberg block editor
The Magic:
bundle('app')->enqueue();
This one line:
- Reads
public/dist/entrypoints.json - Finds all assets for the ‘app’ entry
- Enqueues CSS files with
wp_enqueue_style() - Enqueues JS files with
wp_enqueue_script() - Handles dependencies automatically
- Adds integrity hashes
PostTypesServiceProvider
Location: app/Providers/PostTypesServiceProvider.php
Registers custom post types and taxonomies from configuration.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class PostTypesServiceProvider extends ServiceProvider
{
public function register()
{
add_action('init', function (): void {
// Register custom post types from config
collect(config('post-types.register'))
->each(fn ($args, $name) => register_post_type($name, $args));
// Register taxonomies from config
collect(config('post-types.taxonomies'))
->each(fn ($args, $name) => register_taxonomy($name, ...$args));
});
}
}
What it does:
- ✅ Reads configuration from
config/post-types.php - ✅ Registers custom post types
- ✅ Registers custom taxonomies
BlocksServiceProvider
Location: app/Providers/BlocksServiceProvider.php
Registers custom Gutenberg blocks.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class BlocksServiceProvider extends ServiceProvider
{
public function boot()
{
add_action('acorn/blocks.register', function () {
// Register custom Gutenberg blocks
// Blocks are defined in resources/views/blocks/
});
}
}
What it does:
- ✅ Registers custom Gutenberg blocks
- ✅ Blocks use Blade templates (not React JSX)
Blade Templating System
Blade is Laravel’s powerful templating engine that replaces traditional PHP templates with cleaner, more expressive syntax.
Template Hierarchy
WordPress template hierarchy works with .blade.php extension:
WordPress Template Hierarchy → Blade Templates
Single Post:
single-{post-type}-{slug}.php → single-{post-type}-{slug}.blade.php
single-{post-type}.php → single-{post-type}.blade.php
single.php → single.blade.php
singular.php → singular.blade.php
index.php → index.blade.php
Page:
page-{slug}.php → page-{slug}.blade.php
page-{id}.php → page-{id}.blade.php
page.php → page.blade.php
singular.php → singular.blade.php
index.php → index.blade.php
Archive:
archive-{post-type}.php → archive-{post-type}.blade.php
archive.php → archive.blade.php
index.php → index.blade.php
Category:
category-{slug}.php → category-{slug}.blade.php
category-{id}.php → category-{id}.blade.php
category.php → category.blade.php
archive.php → archive.blade.php
index.php → index.blade.php
Blade Syntax Reference
{{-- Comments (not rendered in HTML) --}}
{{-- Echo escaped output (safe) --}}
{{ $variable }}
{{ get_the_title() }}
{{-- Echo raw HTML (be careful!) --}}
{!! $html !!}
{{-- Conditionals --}}
@if ($show)
<p>Visible</p>
@elseif ($maybe)
<p>Maybe visible</p>
@else
<p>Not visible</p>
@endif
@unless ($hide)
<p>Shown unless hide is true</p>
@endunless
@isset($variable)
<p>Variable is set</p>
@endisset
@empty($variable)
<p>Variable is empty</p>
@endempty
{{-- Loops --}}
@foreach ($items as $item)
<p>{{ $item }}</p>
@endforeach
@forelse ($items as $item)
<p>{{ $item }}</p>
@empty
<p>No items</p>
@endforelse
@while (have_posts())
@php(the_post())
<p>{{ get_the_title() }}</p>
@endwhile
@for ($i = 0; $i < 10; $i++)
<p>Iteration {{ $i }}</p>
@endfor
{{-- Include other templates --}}
@include('partials.sidebar')
@include('partials.content', ['post' => $post])
{{-- Conditional includes --}}
@includeIf('partials.optional')
@includeWhen($condition, 'partials.conditional')
@includeUnless($condition, 'partials.unless')
{{-- Layout inheritance --}}
@extends('layouts.app')
@section('content')
<p>Page content</p>
@endsection
@section('sidebar')
<p>Sidebar content</p>
@endsection
{{-- In parent layout --}}
@yield('content')
@yield('sidebar', '<p>Default sidebar</p>')
{{-- PHP execution --}}
@php
$items = get_posts();
$count = count($items);
@endphp
{{-- Or single line --}}
@php(the_post())
@php(wp_head())
{{-- Blade components --}}
<x-alert type="success">
Operation successful!
</x-alert>
<x-article-card :post="$post" class="featured" />
Layout Example
Base Layout: resources/views/layouts/app.blade.php
<!doctype html>
<html @php(language_attributes())>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@php(do_action('get_header'))
@php(wp_head())
</head>
<body @php(body_class())>
@php(wp_body_open())
<div id="app">
{{-- Skip to content link --}}
<a class="sr-only focus:not-sr-only" href="#main">
{{ __('Skip to content') }}
</a>
{{-- Header --}}
@include('sections.header')
{{-- Main content area --}}
<main id="main" class="min-h-screen">
@yield('content')
</main>
{{-- Footer --}}
@include('sections.footer')
</div>
@php(do_action('get_footer'))
@php(wp_footer())
</body>
</html>
Page Template: resources/views/page.blade.php
@extends('layouts.app')
@section('content')
@while (have_posts())
@php(the_post())
<article @php(post_class())>
<header>
<h1>{{ get_the_title() }}</h1>
</header>
<div class="prose prose-lg max-w-none">
@php(the_content())
</div>
</article>
@endwhile
@endsection
Asset Pipeline & Build Process
How Assets Flow
┌─────────────────────────────────────────────────────────────┐
│ 1. SOURCE FILES (resources/) │
├─────────────────────────────────────────────────────────────┤
│ scripts/ │
│ ├── app.ts → Main frontend JavaScript │
│ ├── editor.ts → Block editor JavaScript │
│ └── modules/ → JavaScript modules │
│ │
│ styles/ │
│ ├── app.css → Main frontend CSS (Tailwind) │
│ └── editor.css → Block editor CSS │
│ │
│ images/ │
│ └── *.svg, *.png → Images and icons │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. BUD.JS BUILD TOOL │
├─────────────────────────────────────────────────────────────┤
│ Processes: │
│ • TypeScript → JavaScript (via SWC compiler) │
│ • PostCSS → CSS (Tailwind, Autoprefixer, Minify) │
│ • Image optimization │
│ • Code splitting and tree-shaking │
│ • Cache-busting hashes │
│ • Source maps (development) │
│ • Minification (production) │
│ │
│ Generates: │
│ • entrypoints.json (asset manifest) │
│ • manifest.json (full manifest) │
│ • theme.json (Gutenberg config) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. COMPILED OUTPUT (public/dist/) │
├─────────────────────────────────────────────────────────────┤
│ css/ │
│ ├── app.e1b9e6.css (hashed for cache-busting) │
│ └── editor.c0c299.css │
│ │
│ js/ │
│ ├── runtime.215685.js (webpack runtime) │
│ ├── app.a02500.js (your app code) │
│ └── editor.56f8b5.js (block editor code) │
│ │
│ images/ (optimized) │
│ │
│ entrypoints.json (tells WP what to load) │
│ manifest.json (full asset list) │
│ theme.json (Gutenberg configuration) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. ASSETS SERVICE PROVIDER │
├─────────────────────────────────────────────────────────────┤
│ add_action('wp_enqueue_scripts', function() { │
│ bundle('app')->enqueue(); │
│ }); │
│ │
│ Reads entrypoints.json and automatically enqueues assets │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. WORDPRESS RENDERS HTML │
├─────────────────────────────────────────────────────────────┤
│ <head> │
│ <link rel="stylesheet" href="/dist/css/app.e1b9e6.css">│
│ </head> │
│ <body> │
│ ...content... │
│ <script src="/dist/js/runtime.215685.js"></script> │
│ <script src="/dist/js/app.a02500.js"></script> │
│ </body> │
└─────────────────────────────────────────────────────────────┘
Build Commands
# Development build (watch mode with HMR)
yarn dev
# Production build (optimized and minified)
yarn build
# Clean build cache
npx bud clean
# Check for build tool updates
npx bud upgrade
entrypoints.json Example
{
"app": {
"js": ["js/runtime.215685.js", "js/app.a02500.js"],
"css": ["css/app.e1b9e6.css"],
"dependencies": []
},
"editor": {
"js": ["js/runtime.215685.js", "js/editor.56f8b5.js"],
"css": ["css/editor.c0c299.css"],
"dependencies": ["react", "wp-blocks", "wp-i18n"]
}
}
Hot Module Replacement (HMR)
Hot Module Replacement allows you to see code changes in the browser without a full page reload. This dramatically speeds up development by preserving application state and providing instant feedback.
What is HMR?
Hot Module Replacement updates your code in the browser without requiring a full page reload:
✅ Instant feedback - See changes in milliseconds
✅ Preserved state - Forms, scroll position, app state stay intact
✅ Better DX - Stay in flow without waiting for reloads
✅ Faster iteration - Test changes 10-20x faster
HMR Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Terminal 1: WordPress Server │
├─────────────────────────────────────────────────────────────────┤
│ $ php -S localhost:8080 -t public/ │
│ │
│ Serves: WordPress, PHP, Database │
│ URL: http://localhost:8080 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Terminal 2: Bud.js Development Server (HMR) │
├─────────────────────────────────────────────────────────────────┤
│ $ yarn dev │
│ │
│ Serves: Asset compilation + WebSocket server │
│ Dev Server: http://localhost:4000 │
│ Proxy URL: http://localhost:8080 (WordPress) │
│ │
│ Watches: │
│ - resources/scripts/**/*.ts │
│ - resources/styles/**/*.css │
│ - resources/views/**/*.blade.php │
│ - app/**/*.php │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Browser: http://localhost:4000 │
├─────────────────────────────────────────────────────────────────┤
│ 1. Opens http://localhost:4000 (Bud.js proxy) │
│ 2. Bud.js proxies requests to http://localhost:8080 (WordPress) │
│ 3. WordPress returns HTML with <script> tags │
│ 4. Scripts include HMR client (@roots/wordpress-hmr) │
│ 5. HMR client opens WebSocket to localhost:4000 │
│ 6. Listens for update notifications │
└─────────────────────────────────────────────────────────────────┘
How to Use HMR
Step 1: Start WordPress Server
cd ~/Projects/radicle
php -S localhost:8080 -t public/
Step 2: Start Bud.js HMR Server
cd ~/Projects/radicle
yarn dev
Step 3: Visit the Bud.js Dev Server URL
✅ CORRECT: http://localhost:4000
❌ WRONG: http://localhost:8080
Why? The Bud.js server at :4000 proxies requests to WordPress at :8080 AND injects the HMR client script.
What Gets Hot-Reloaded?
✅ Full HMR Support (No Page Reload)
CSS/Styles:
- Changes to
resources/styles/app.csshot-reload instantly - Tailwind class changes update without refresh
- CSS swaps out in < 100ms
JavaScript Modules:
- TypeScript/JavaScript module changes can hot-swap
- State preserved (if module accepts HMR)
- Updates in 1-2 seconds
⚠️ Partial HMR Support (Browser Refresh Required)
Blade Templates:
- Bud.js detects changes → triggers browser refresh
- Fast but not “hot” (still requires page reload)
- Why? PHP templates run server-side
Service Providers (PHP):
- Bud.js detects changes → triggers browser refresh
- Requires WordPress to reinitialize
❌ No HMR Support (Manual Action Required)
Configuration Files:
- Changes to
config/*.phprequire:wp acorn optimize:clear - Why? Config files are cached by Laravel/Acorn
Composer Dependencies:
- Requires restart of PHP server and clear caches
HMR Configuration
The HMR configuration is in bud.config.ts:
export default async (bud: Bud) => {
const wpHome = process.env.WP_HOME ?? `http://localhost:8080`;
bud
// PROXY: WordPress server URL
.proxy(wpHome)
// SERVE: HMR development server URL
.serve(`http://localhost:4000`)
// WATCH: File patterns to monitor for changes
.watch([
bud.path(`resources/views`), // Blade templates
bud.path(`app`), // PHP service providers
])
// ENTRY POINTS
.entry(`app`, [`@scripts/app`, `@styles/app`])
.entry(`editor`, [`@scripts/editor`, `@styles/editor`]);
};
HMR Client Implementation
The HMR client is automatically included in your bundles. In your entry file:
// resources/scripts/app.ts
// At the end of your entry file:
if (import.meta.webpackHot) {
import.meta.webpackHot.accept(console.error);
}
This tells the HMR client to:
- Accept hot updates for this module
- Log any errors to console
- Reconnect if WebSocket drops
Ports & URLs
| Service | URL | Purpose |
|---|---|---|
| WordPress | http://localhost:8080 |
PHP server (backend) |
| Bud.js HMR | http://localhost:4000 |
Dev server (visit this!) |
| WebSocket | ws://localhost:4000 |
HMR communication |
Configuration System
Configuration files live in the config/ directory and are loaded by Acorn. All configuration files return PHP arrays.
config/theme.php
Theme features, menus, sidebars, and image sizes.
<?php
return [
/**
* Navigation menus
*/
'menus' => [
'primary_navigation' => __('Primary Navigation', 'radicle'),
'footer_navigation' => __('Footer Navigation', 'radicle'),
],
/**
* Custom image sizes
*/
'image_sizes' => [
'article-thumb' => [400, 250, true],
'article-large' => [1200, 675, true],
'article-featured' => [1600, 900, true],
],
/**
* Sidebars/widget areas
*/
'sidebar' => [
'register' => [
['name' => __('Primary Sidebar', 'radicle'), 'id' => 'sidebar-primary'],
['name' => __('Footer', 'radicle'), 'id' => 'sidebar-footer']
],
'config' => [
'before_widget' => '<section class="widget %1$s %2$s">',
'after_widget' => '</section>',
'before_title' => '<h3>',
'after_title' => '</h3>'
],
],
/**
* Theme support
*/
'support' => [
'html5' => [
'caption',
'comment-form',
'comment-list',
'gallery',
'search-form',
'script',
'style',
],
'align-wide',
'title-tag',
'post-thumbnails',
'responsive-embeds',
'editor-styles',
'wp-block-styles',
],
/**
* Remove theme support
*/
'remove' => [
'block-templates',
'core-block-patterns',
],
];
Access via:
config('theme.menus'); // Returns menus array
config('theme.support'); // Returns support array
config('theme.sidebar.register'); // Returns sidebar array
config/post-types.php
Custom post types and taxonomies.
<?php
return [
/**
* Register custom post types
*/
'register' => [
'portfolio' => [
'public' => true,
'labels' => [
'name' => __('Portfolio'),
'singular_name' => __('Portfolio Item'),
],
'menu_icon' => 'dashicons-portfolio',
'supports' => ['title', 'editor', 'thumbnail'],
'has_archive' => true,
'show_in_rest' => true,
],
],
/**
* Register taxonomies
*/
'taxonomies' => [
// Define custom taxonomies here
],
];
config/assets.php
Asset loading configuration.
<?php
return [
'defer' => true, // Defer JavaScript loading
'async' => false, // Async JavaScript loading
];
View Composers
View composers allow you to share data with views automatically without manually passing it each time.
Example: App Composer
Location: app/View/Composers/App.php
<?php
namespace App\View\Composers;
use Roots\Acorn\View\Composer;
class App extends Composer
{
/**
* Views this composer applies to
* '*' = all views
*/
protected static $views = ['*'];
/**
* Data to pass to views
*/
public function with()
{
return [
'siteName' => $this->siteName(),
'siteDescription' => $this->siteDescription(),
'currentYear' => date('Y'),
];
}
public function siteName()
{
return get_bloginfo('name', 'display');
}
public function siteDescription()
{
return get_bloginfo('description', 'display');
}
}
Result: ALL views now have access to:
{{ $siteName }}
{{ $siteDescription }}
{{ $currentYear }}
Example: Post Composer
<?php
namespace App\View\Composers;
use Roots\Acorn\View\Composer;
class Post extends Composer
{
/**
* Only applies to these views
*/
protected static $views = [
'single',
'partials.content-single',
];
public function with()
{
return [
'author' => $this->author(),
'readTime' => $this->readTime(),
];
}
public function author()
{
return get_the_author_meta('display_name');
}
public function readTime()
{
$content = get_the_content();
$word_count = str_word_count(strip_tags($content));
$reading_time = ceil($word_count / 200);
return $reading_time . ' min read';
}
}
Blade Components
Blade components are reusable UI elements that can accept props and slots.
Creating a Component
File: resources/views/components/alert.blade.php
@props([
'type' => 'info'
])
@php
$classes = [
'alert',
'p-4',
'rounded-lg',
'border',
match($type) {
'success' => 'bg-green-100 border-green-500 text-green-800',
'error' => 'bg-red-100 border-red-500 text-red-800',
'warning' => 'bg-yellow-100 border-yellow-500 text-yellow-800',
default => 'bg-blue-100 border-blue-500 text-blue-800',
}
];
@endphp
<div {{ $attributes->class($classes) }}>
{{ $slot }}
</div>
Usage:
<x-alert type="success">
Post saved successfully!
</x-alert>
<x-alert type="error" class="mb-4">
An error occurred.
</x-alert>
Component with Props
File: resources/views/components/article-card.blade.php
@props([
'post',
'showExcerpt' => true,
'showAuthor' => false,
])
<article {{ $attributes->class(['article-card', 'bg-white', 'rounded-lg', 'shadow']) }}>
@if (has_post_thumbnail($post))
<a href="{{ get_permalink($post) }}">
{{ get_the_post_thumbnail($post, 'article-thumb', ['class' => 'rounded-t-lg']) }}
</a>
@endif
<div class="p-4">
<h3 class="text-xl font-bold mb-2">
<a href="{{ get_permalink($post) }}">
{{ get_the_title($post) }}
</a>
</h3>
@if ($showExcerpt)
<p class="text-gray-600 mb-4">
{{ get_the_excerpt($post) }}
</p>
@endif
@if ($showAuthor)
<p class="text-sm text-gray-500">
By {{ get_the_author_meta('display_name', $post->post_author) }}
</p>
@endif
</div>
</article>
Usage:
{{-- Basic usage --}}
<x-article-card :post="$post" />
{{-- With props --}}
<x-article-card
:post="$post"
:showAuthor="true"
:showExcerpt="false"
class="featured"
/>
{{-- In a loop --}}
@foreach ($posts as $post)
<x-article-card :post="$post" />
@endforeach
Development Workflow
Local Development Setup
# 1. Clone repository (if needed)
git clone <repository-url>
cd radicle
# 2. Install dependencies
composer install
yarn install
# 3. Configure environment
cp .env.example .env
# Edit .env with your database credentials and URLs
# 4. Create database
mysql -u root -p
CREATE DATABASE radicle;
CREATE USER 'radicle'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON radicle.* TO 'radicle'@'localhost';
FLUSH PRIVILEGES;
# 5. Build assets
yarn build
# 6. Start development servers
# Terminal 1:
php -S localhost:8080 -t public/
# Terminal 2:
yarn dev
Daily Development
# Terminal 1: PHP Server
php -S localhost:8080 -t public/
# Terminal 2: Asset compilation with HMR
yarn dev
# Visit: http://localhost:4000
Making Changes
Template Changes:
# Edit Blade templates
resources/views/page.blade.php
# Changes are reflected immediately (no compilation needed)
# Browser refreshes automatically
Style Changes:
# Edit CSS files
resources/styles/app.css
# Bud.js auto-compiles → public/dist/css/app.[hash].css
# HMR injects changes (no page reload)
JavaScript Changes:
# Edit TypeScript files
resources/scripts/app.ts
# Bud.js auto-compiles → public/dist/js/app.[hash].js
# HMR updates module (preserves state)
Configuration Changes:
# Edit config files
config/theme.php
# Requires cache clear:
wp acorn optimize:clear
PHP Code Changes:
# Edit service providers or app code
app/Providers/ThemeServiceProvider.php
# Requires cache clear:
wp acorn optimize:clear
Production Deployment
# 1. Build optimized assets
yarn build
# 2. Install production dependencies only
composer install --no-dev --optimize-autoloader
# 3. Optimize Acorn cache
wp acorn optimize
# 4. Cache icons (if using Blade Icons)
wp acorn icons:cache
# 5. Deploy to server
# (Copy files, excluding node_modules, .git, tests, etc.)
Troubleshooting
HMR Not Working
Symptoms: Changes don’t appear, or page always reloads
Checklist:
-
Are you visiting the correct URL?
✅ CORRECT: http://localhost:4000 (Bud.js proxy) ❌ WRONG: http://localhost:8080 (WordPress direct) -
Is the HMR server running?
# Check Terminal 2 - should show: ➜ App http://localhost:4000 ➜ Proxy http://localhost:8080 -
Check browser console for errors:
// Look for WebSocket connection: [HMR] Waiting for update signal from WDS... [WDS] Hot Module Replacement enabled. -
Is the WordPress server running?
# Check Terminal 1 - should be running: php -S localhost:8080 -t public/ -
Check .env configuration:
# .env should have: WP_HOME='http://localhost:8080' WP_ENV='development'
WebSocket Connection Failed
Symptoms: Console shows WebSocket connection to 'ws://localhost:4000/' failed
Solutions:
-
Firewall blocking port 4000:
# Temporarily allow port (Nobara/Fedora): sudo firewall-cmd --add-port=4000/tcp -
Port already in use:
# Find what's using port 4000: lsof -i :4000 # Kill the process or change port in bud.config.ts: .serve(`http://localhost:5000`) // Different port -
Restart Bud.js:
# Ctrl+C to stop, then: yarn dev
Changes Not Detected
Symptoms: File changes don’t trigger rebuild
Solutions:
-
File not in watch list:
// bud.config.ts bud.watch([ bud.path(`resources/views`), bud.path(`app`), bud.path(`config`), // ADD THIS if editing configs ]); -
Too many files (inotify limit on Linux):
# Increase file watch limit: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf sudo sysctl -p -
Clear Bud.js cache:
npx bud clean yarn dev
Styles Not Applying
Symptoms: CSS changes compile but don’t appear in browser
Solutions:
-
Browser cache:
- Hard refresh:
Ctrl + Shift + R(Linux/Windows) - Or open DevTools → Network tab → Disable cache
- Hard refresh:
-
Check CSS specificity:
/* Your CSS might be overridden by more specific rules */ /* Check in browser DevTools → Elements → Styles */ -
Verify compilation:
# Check if CSS file exists: ls -la public/dist/css/ # Should show: app.[hash].css
PHP Changes Require Restart
Symptoms: PHP code changes don’t apply even after browser refresh
Solutions:
-
OPcache is enabled:
# Check if OPcache is on: php -i | grep opcache.enable # Disable for development: php -S localhost:8080 -t public/ -d opcache.enable=0 -
Clear Acorn cache:
wp acorn optimize:clear wp acorn view:clear -
Restart PHP server:
# In Terminal 1: Ctrl+C php -S localhost:8080 -t public/
Build Errors
Symptoms: yarn dev or yarn build fails
Solutions:
-
Clear caches and reinstall:
npx bud clean rm -rf node_modules yarn install -
Check Node.js version:
node --version # Should be 16+ -
Check for syntax errors in source files:
- Review error messages for file paths
- Fix TypeScript/JavaScript syntax errors
- Fix CSS syntax errors
Practical Examples
Example 1: Add a Custom Post Type
Step 1: Edit config/post-types.php
<?php
return [
'register' => [
'portfolio' => [
'public' => true,
'labels' => [
'name' => __('Portfolio', 'radicle'),
'singular_name' => __('Portfolio Item', 'radicle'),
'add_new_item' => __('Add New Portfolio Item', 'radicle'),
],
'menu_icon' => 'dashicons-portfolio',
'supports' => ['title', 'editor', 'thumbnail', 'excerpt'],
'has_archive' => true,
'rewrite' => ['slug' => 'portfolio'],
'show_in_rest' => true,
],
],
];
Step 2: Clear cache
wp acorn optimize:clear
Step 3: Create template
{{-- resources/views/single-portfolio.blade.php --}}
@extends('layouts.app')
@section('content')
@while (have_posts())
@php(the_post())
<article class="portfolio-item">
<h1>{{ get_the_title() }}</h1>
@if (has_post_thumbnail())
{{ the_post_thumbnail('large') }}
@endif
<div class="content">
@php(the_content())
</div>
</article>
@endwhile
@endsection
Done! The PostTypesServiceProvider automatically registers it.
Example 2: Create a Reusable Button Component
Step 1: Create component file
File: resources/views/components/button.blade.php
@props([
'variant' => 'primary',
'size' => 'md',
'href' => null,
])
@php
$tag = $href ? 'a' : 'button';
$classes = [
'btn',
'inline-flex',
'items-center',
'justify-center',
'font-medium',
'rounded-lg',
'transition-colors',
match($variant) {
'primary' => 'bg-blue-600 hover:bg-blue-700 text-white',
'secondary' => 'bg-gray-600 hover:bg-gray-700 text-white',
'outline' => 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50',
default => 'bg-blue-600 hover:bg-blue-700 text-white',
},
match($size) {
'sm' => 'px-3 py-1.5 text-sm',
'md' => 'px-4 py-2 text-base',
'lg' => 'px-6 py-3 text-lg',
default => 'px-4 py-2 text-base',
}
];
@endphp
<{{ $tag }}
@if($href) href="{{ $href }}" @endif
{{ $attributes->class($classes) }}
>
{{ $slot }}
</{{ $tag }}>
Step 2: Use it anywhere
{{-- In any template --}}
<x-button>
Click Me
</x-button>
<x-button variant="secondary" size="lg">
Large Button
</x-button>
<x-button variant="outline" href="/contact">
Contact Us
</x-button>
<x-button variant="primary" class="w-full">
Full Width Button
</x-button>
Example 3: Add Navigation Menu
Step 1: Edit config/theme.php
return [
'menus' => [
'primary_navigation' => __('Primary Navigation', 'radicle'),
'footer_navigation' => __('Footer Navigation', 'radicle'),
'mobile_navigation' => __('Mobile Navigation', 'radicle'), // Add this
],
// ... rest of config
];
Step 2: Clear cache
wp acorn optimize:clear
Step 3: Use in template
{{-- resources/views/sections/header.blade.php --}}
<nav class="mobile-menu">
@if (has_nav_menu('mobile_navigation'))
{!! wp_nav_menu([
'theme_location' => 'mobile_navigation',
'menu_class' => 'mobile-nav-items',
'container' => false,
]) !!}
@endif
</nav>
No need to touch PHP code! Service provider automatically reads config.
CLI Commands Reference
WP-CLI Commands
# Acorn (Laravel Artisan for WordPress)
wp acorn optimize # Compile and cache config/routes
wp acorn optimize:clear # Clear all caches
wp acorn view:clear # Clear Blade template cache
wp acorn icons:cache # Cache Blade Icons
# Standard WP-CLI
wp db create # Create database
wp core install # Install WordPress
wp plugin list # List plugins
wp theme list # List themes
wp user create # Create user
wp rewrite flush # Flush rewrite rules
Composer Commands
composer install # Install dependencies
composer update # Update all dependencies
composer require vendor/package # Add dependency
composer require --dev vendor/package # Add dev dependency
composer remove vendor/package # Remove dependency
composer dump-autoload # Regenerate autoloader
Yarn/NPM Commands
yarn install # Install dependencies
yarn dev # Development build with HMR
yarn build # Production build
yarn add package # Add dependency
yarn remove package # Remove dependency
Bud.js Commands
npx bud dev # Start HMR development server
npx bud build # Build (development mode)
npx bud build production # Build (production mode)
npx bud clean # Clean build cache
npx bud upgrade # Check for updates
npx bud --help # View all commands
Additional Resources
Official Documentation
- Roots Documentation: https://roots.io/docs/
- Bedrock Documentation: https://roots.io/bedrock/docs/
- Sage (Acorn) Documentation: https://roots.io/sage/docs/
- Acorn Documentation: https://roots.io/acorn/
- Bud.js Documentation: https://bud.js.org/
- Laravel Blade Documentation: https://laravel.com/docs/blade
- Laravel Collections: https://laravel.com/docs/collections
- WordPress Codex: https://codex.wordpress.org/
- Tailwind CSS: https://tailwindcss.com/docs
- Alpine.js: https://alpinejs.dev/
Community & Support
- Roots Discourse: https://discourse.roots.io/
- GitHub Issues: https://github.com/roots/
- Roots Slack: Community Slack channel
Related Documentation
SETUP_GUIDE.md- Initial setup and installationREADME.md- Project overviewroadmap/- Project roadmap and planning documents
Security Considerations
Environment Variables
- ✅ Keep
.envout of version control (use.gitignore) - ✅ Use different keys for each environment
- ✅ Generate secure keys: https://roots.io/salts.html
- ✅ Never commit database credentials
File Permissions
# Set correct permissions
chmod 644 .env
chmod 755 public/
chmod 775 storage/
WordPress Core
- ✅ WordPress core managed by Composer
- ✅ Easy to update:
composer update roots/wordpress - ✅ Core files isolated in
public/wp/ - ✅ No accidental core file edits
Dependencies
# Check for security vulnerabilities
composer audit
npm audit
# Fix vulnerabilities
composer update
npm audit fix
Key Takeaways
The Magic Single Line
The entire theme is bootstrapped by ONE line in the WordPress theme stub:
// public/content/themes/radicle/index.php
<?php
echo view(app('sage.view'), app('sage.data'))->render();
This delegates everything to Acorn/Sage, which handles:
- Template resolution
- Data passing
- Blade compilation
- Asset enqueuing
- Everything else!
Core Principles
-
Separation of Concerns
- Configuration →
config/ - Logic →
app/Providers/ - Templates →
resources/views/ - Assets →
resources/scripts/&resources/styles/
- Configuration →
-
Laravel Patterns in WordPress
- Service Providers for bootstrapping
- Blade templates for views
- Configuration files for settings
- Collections for data manipulation
- Service container for dependency injection
-
Modern Tooling
- Composer for PHP dependencies
- NPM/Yarn for JavaScript dependencies
- Bud.js for asset compilation
- TypeScript for type safety
- Tailwind for utility-first CSS
-
Better Developer Experience
- Hot Module Replacement (HMR)
- Automatic asset management
- Component-based architecture
- Cleaner, more expressive templates
- Better code organization
Last Updated: 2025-10-11
Project: Radicle WordPress Stack (Bedrock + Sage + Acorn + Bud.js)