A WordPress theme framework for custom development. Build portable components and editor blocks with co-located CSS, JS and templating. Auto-loading modules keep features self-contained. A cohesive CSS system of design tokens, layout utilities and contextual color, built on top of Tailwind v4. A routing system links editable block content to dynamic pages (archives, search, 404) and also supports custom static routes.
Personal, opinionated framework – not a polished product.
- Components - Co-located template, logic, styles, and scripts in portable directories
- Modules - Self-contained features that auto-load (e.g., ACF integration, post types)
- Router - Static routes and editable content for dynamic pages (archives, search, 404) via auto-generated WordPress pages with slots
- CSS Framework - BEM-style components, custom utilities and patterns (
content-grid,stack-*,grid-auto). - Color System - A contextual color system with automatic foreground calculation and CSS variables.
- Tailwind v4 - Tailwind used for custom utilities and available globally, but used sparingly in templates.
- ACF Blocks - Components become Gutenberg blocks with co-located field definitions
- Vite Build - HMR, glob imports, manifest-based cache busting
- Dev Kit -
/_devbrowser-based environment for component testing, style guide, and QA - WordPress Optimizations - Asset handling, color system, image helpers, SVG support, menu utilities
- JS Utilities - Accessible UI helpers including
Disclosure(expandable sections, accordions, nav menus) andDialog(modal dialogs), plus viewport, mutation/resize observer, and dynamic element utilities - AI Development - Claude Code skills for AI-assisted development workflows (component scaffolding, testing, website spec generation)
Typed PHP component classes with IDE autocomplete, validation, and transformation hooks.
use Gust\Components\Accordion;
use Gust\Components\Card;
// Named arguments with type support
echo Accordion::make(
heading: 'FAQ',
accordion_items: $items,
);
// From WP_Post
echo Card::make(object: $post, show_read_more: false);Each component lives in components/ with co-located files:
components/accordion/
├── Accordion.php # Typed class with ::make() factory
├── template.php # Template markup
├── styles.pcss # Component styles (bundled)
├── scripts.js # Component JS (bundled)
├── block.json # ACF block registration (optional)
└── group_*.json # ACF field group (optional)
Components support:
validate()- return false to skip renderingtransform()- modify args before rendergetDefaults()- default values- WordPress filters via
gust/component/{name}
Add editable block content to archives, search results, and 404 pages. Router creates WordPress pages linked to these routes—editors customize them in the block editor, and slots render dynamic content like post loops.
Integrates with WordPress' template hierarchy, query handling, and admin. No custom tables or separate admin screens.
use Gust\Router;
// Cross-cutting routes live in Theme/Routes/routes.php
Router::decorateSearch(SearchController::class)
->withPage('search')
->withSlot('template-content', fn() => SearchController::renderResults());
// Post type modules self-register their own routes in module.php
// Theme/Modules/Events/module.php
Router::decoratePostType('event', static::class)
->withPage('events')
->withSlot('template-content', [static::class, 'renderArchive']);
// Custom URLs when you need them
Router::route('/tools/demo', fn() => DemoController::index())
->template('tools/demo');- Router Pages - Auto-created WordPress pages, protected from deletion
- Slots - Named injection points for dynamic content (e.g.
template-content) - Controllers - Class-based or closure handlers
- Template Resolution - Follows WordPress hierarchy (
archive-{type}.php, etc.)
Route Types:
post_type:{name}- CPT archive (also matches blog home)taxonomy:{name}- Taxonomy archivesearch- Search results404- Not found
Tailwind v4 with theme-specific custom utilities for layout, spacing, and animation.
Layout:
content-grid- 12-column grid with full/wide/prose alignmentgrid-simple/grid-auto- Flexible CSS grid utilitiesflex-grid- Gap-based flex layoutsstack-*- Vertical rhythm spacing (e.g.,stack-24)
Spacing:
--gap,--col-gap,--row-gapCSS properties--colsfor grid column control- Fluid spacing utilities
Color:
color-context-{name}- Sets background, foreground, link, and focus colors based on theme confighas-{name}-background-color- WordPress block editor alias for color contextsforeground-from-{name}- Sets just the foreground/text color
Browse all utilities with live examples at /_dev/utilities (development only).
Components become Gutenberg blocks by adding block.json:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "theme/accordion",
"title": "Accordion",
"description": "An accordion block",
"category": "theme-blocks",
"icon": "arrow-down-alt2",
"acf": {
"mode": "auto",
"renderCallback": "Gust\\Components\\Accordion::renderBlock"
},
"supports": {
"anchor": true,
"align": ["wide", "full"],
"color": {
"background": true,
"text": false,
"gradients": false
}
}
}renderBlockis inherited fromComponentBase— no additional PHP needed- Block field groups (
group_component_*.json) auto-load from component directories - ACF fields are passed to the component via
get_fields()inrenderBlock() - Supports anchor, alignment, and color controls via the
supportskey
- PHP >=8.2
- Node 24 (see
.nvmrc)
cp .env.example .env # Update with your APP_URL
cp auth.json.example auth.json # Add your ACF Pro key and site URL
pnpm run setup # Install deps + build
pnpm run site-setup # Configure WP site for dev (optional, see dev-scripts/site-setup.sh)
pnpm run dev # Start Vite dev serverauth.json gives Composer access to the ACF Pro package. Set username to your ACF Pro licence key and password to the site URL the licence is activated for (e.g. https://example.test). It's gitignored.
Access WordPress at your normal URL (.env APP_URL). Don't use localhost:5173 directly.
| Command | Description |
|---|---|
pnpm run dev |
Vite dev server with HMR |
pnpm run build |
Production build |
pnpm run deploy:production |
Build + deploy to production |
pnpm run deploy:staging |
Build + deploy to staging |
pnpm run scaffold <name> |
Scaffold new component |
pnpm run pot |
Generate translation files |
pnpm run lint |
Check code (Biome) |
pnpm run fix |
Fix all code (Biome + PHP Pint) |
dev-scripts/site-setup.sh configures a fresh WordPress install for development. Requires WP-CLI.
Enables debugging, adds the ACF Pro licence key from auth.json to wp-config.php (ACF_PRO_LICENSE), sets UK locale, disables comments/pings, creates homepage, removes default content/plugins/themes, sets pretty permalinks, and activates all composer-installed plugins.
Deploy via rsync to staging or production. Environment config (SSH host, path) is defined in wp-sync.yml.
pnpm run deploy:staging # build + deploy to staging
pnpm run deploy:staging:dry # dry-run (no files transferred)
pnpm run deploy:production # build + deploy to production
pnpm run deploy:production:dry # dry-runThe production build runs pnpm install && composer install --no-dev && vite build before rsync.
Gust/ # Core framework
WordPress/ # WP integrations (Admin, Cleanup, Enqueue, Gutenberg, etc.)
Router/ # Routing system classes
Component.php # Component discovery & ACF blocks
ComponentBase.php # Base class for typed components
Config.php # Framework config loader
Helpers.php # Global helper functions
Module.php # Module loader
Vite.php # Dev server integration
Theme/ # Custom theme functionality
Controllers/ # Route controllers (Archive, NotFound, Search)
Modules/ # Feature modules (auto-loaded)
ACF/ # ACF Pro integration
Blog/ # Blog post type & category taxonomy
...
Routes/ # Route definitions (routes.php)
Utils/ # Utilities (YearShortcode, ObjectMeta)
components/ # UI components (PHP + assets)
.docs/ # Project documentation
_WEBSITE-SPEC.md # Website spec (components, data, pages)
dev-scripts/ # Build & dev tooling
assets/
main.pcss # Main stylesheet entry
main.js # Main script entry
styles/ # CSS architecture (ITCSS)
1-theme/ # Design tokens
2-base/ # Base elements
3-patterns/ # Reusable patterns
4-utilities/ # Utility classes
public/build/ # Compiled assets
Modules in Theme/Modules/*/module.php auto-load via Gust\Module::init(). Each has a Module class with init() method. Disable via gust/modules/disabled filter.
theme-config.json defines design tokens used across:
- CSS variables (
--color-accent) - Tailwind classes (
text-accent) - Block editor color palette
The theme includes a robust color system with color contexts, foreground calculation, and CSS variables/utilities.
Color configuration is in theme-config.json.
Generated CSS Variables (per color):
--color-{name} /* hex value */
--color-{name}--hsl /* HSL: "210 50% 40%" */
--color-{name}--h/s/l /* individual components */
--color-{name}--foreground /* contrasting text color */Generated Utilities:
color-context-{name}- Sets background, foreground, focus, and link colors (and other custom properties)has-{name}-background-color- WordPress block editor aliasforeground-from-{name}- Sets only foreground/text color
Usage:
<!-- Full color context (bg + text + links) -->
<section class="color-context-accent">
<p>White text on accent color, links inherit</p>
</section>
<!-- Just foreground color -->
<span class="foreground-from-accent">Colored text</span>Config structure (theme-config.json):
{
"colors": {
"base": {
"blue": {
"color": "#0707a3",
"name": "Blue",
"block_editor": true,
"foreground": "var(--color-white)",
"properties": {
"--link--color": "var(--color-white)"
}
},
"accent": { "namedColor": "blue" }
}
}
}config.json- Framework settings (comments, images, jQuery, etc.)theme.json- WordPress FSE/block settingsvite.config.js- Vite build configurationpostcss.config.js- PostCSS pluginsbiome.json- Biome linting/formattingpint.json- Laravel Pint PHP style
- Vite 7 for bundling with HMR
- Tailwind CSS v4 via PostCSS
- Biome for JS/CSS linting
- Glob imports for component scripts
- Manifest-based cache busting
pnpm run scaffold my-componentnamespace Gust\Components;
use Gust\ComponentBase;
class MyComponent extends ComponentBase
{
protected static string $name = 'my-component';
public static function make(
string $title = '',
string $type = 'default',
array $classes = [],
...$others
): ?static {
return static::createFromArgs(static::mergeArgs(get_defined_vars()));
}
protected static function validate(array $args): bool
{
return !empty($args['title']);
}
protected static function transform(array $args): array
{
if (!empty($args['type'])) {
$args['classes'][] = 'my-component--' . $args['type'];
}
return $args;
}
}// my-component/template.php
<div class="<?= classes('my-component', $this->classes) ?>">
<h2><?= esc_html($this->title) ?></h2>
</div>Two global functions are available in all PHP templates.
Builds an escaped class attribute value from variadic strings or arrays. Flattens nested arrays, deduplicates, and escapes with esc_attr().
classes('banner', 'wp-block', 'alignfull')
// → "banner wp-block alignfull"
// Mix of strings and arrays — append $this->classes last so callers can extend
classes('card', 'animate__item', $this->classes)
// → "card animate__item caller-class"
// Conditional — empty strings are filtered out
classes('menu-item', $isActive ? 'is-active' : '', $this->classes)
// → "menu-item is-active"Builds a space-separated string of escaped HTML attributes from a key-value array.
attributes(['disabled' => true])
// → 'disabled'
attributes(['id' => 'my-nav', 'aria-label' => 'Main navigation'])
// → 'id="my-nav" aria-label="Main navigation"'
attributes(['style' => ['color' => 'red', 'font-size' => '1rem']])
// → 'style="color: red; font-size: 1rem;"'Browser-based reference environment for component testing and QA. Only available when WP_ENVIRONMENT_TYPE=development (set in .env).
| URL | Description |
|---|---|
/_dev |
Dev Kit index |
/_dev/components |
List all components with examples |
/_dev/components/{name} |
View specific component examples |
/_dev/globals |
Theme colours, design tokens |
/_dev/utilities |
CSS utility and pattern class reference |
/_dev/content-flow |
WP block spacing and alignment behaviour |
Add example.php to any component to make it testable:
// components/my-component/example.php
<?php
use Gust\Components\MyComponent;
?>
<section class="component-example-section">
<h2 class="component-example-section__title">Default</h2>
<p class="component-example-section__description">Basic usage.</p>
<div class="component-example-section__preview">
<?= MyComponent::make(
title: 'Example Title',
content: '<p>Example content</p>',
); ?>
</div>
</section>
<section class="component-example-section">
<h2 class="component-example-section__title">Minimal</h2>
<div class="component-example-section__preview">
<?= MyComponent::make(title: 'Title Only'); ?>
</div>
</section>Use the standard component-example-section classes for consistent styling.
The theme includes Claude Code skills in .claude/skills/ for AI-assisted development.
| Skill | Description |
|---|---|
gust-dev |
Component development workflows (scaffold, test, debug, setup) |
website-spec |
Fill in the website specification |
Skills can be invoked naturally in conversation (e.g. "scaffold a card component from the spec") or explicitly with /gust-dev.
Scaffold - Create components from spec:
scaffold component from spec → reads .docs/_WEBSITE-SPEC.md → generates files
Testing - Verify changes work:
# Quick test
: > ../../debug.log && curl -sL $APP_URL -o /dev/null && cat ../../debug.log
# Full test with Chrome DevTools MCP
navigate → snapshot DOM → check console → check debug.logComponents - Patterns and usage reference for component development.
Setup - Create post types, taxonomies, routes from website spec.
.docs/_WEBSITE-SPEC.md is the single source of truth for a project's content model, routes, and components. It is used by AI-assisted workflows to scaffold code and keep implementation aligned with the design intent.
| Section | Description |
|---|---|
| Overview | Project title, URLs, PHP version |
| Required Plugins | Composer-managed plugins for the project |
| Content Types | Custom post types with URL, fields, archive routing |
| Taxonomies | Taxonomy definitions with archive routing |
| Standalone Routes | Pages and routes not tied to a content type |
| Site Settings | Global ACF options page fields |
| Menus | Registered nav menu locations |
| Components | Block/partial definitions with ACF field groups |
| Integrations | Third-party services and API keys |
| Other Functionality | Cron jobs, CLI commands, custom behaviors |
Fill in the spec before starting development. The website-spec skill helps write or expand it:
"Add an Events post type to the spec with start date, end date, and venue fields"
"Define a Page Header component as an ACF block with heading, subheading, and CTA"
Once the spec is populated, the gust-dev scaffold workflow reads it to generate components, post types, taxonomies, and routes.
This theme draws a lot from Granola, a theme framework I worked on at Wholegrain Digital. A lot of that theme, in particular the component system, owes much to Joshua Stopper's foundational work.