> wp-sage
Conventions for WordPress Bedrock + Sage + Acorn + Blade + Tailwind CSS 4 + Vite projects. Always-active rules.
curl "https://skillshub.wtf/alessioarzenton/claude-code-wp-toolkit/wp-sage?format=md"WP-Sage Stack — Bedrock + Sage + Acorn
Stack
- WordPress 6.x + Bedrock
- PHP 8.2+ (recommended 8.4)
- Theme: Sage + Acorn ^5.0 — Laravel Blade templating
- CSS: Tailwind CSS 4 (CSS-first config via
@theme, prefix{{PREFIX}}) - Bundler: Vite (NOT Bud) —
laravel-vite-plugin+@roots/vite-plugin - Custom Fields: ACF Pro + ACF Composer (
log1x/acf-composer) - Post Types/Taxonomies: Acorn Post Types (
roots/acorn-post-types) + Extended CPTs (johnbillion/extended-cpts)
Typical project structure
{{THEME_DIR}}/
├── app/
│ ├── Blocks/ # ACF Composer blocks
│ ├── Fields/ # ACF Composer field groups
│ ├── Options/ # ACF Composer option pages
│ ├── Providers/ThemeServiceProvider.php
│ ├── View/Composers/ # View composers (App, Header, Footer, etc.)
│ ├── setup.php # Theme setup (features, nav menus)
│ ├── filters.php
│ └── import.php # Helper functions
├── config/
│ ├── acf.php # ACF Composer configuration
│ └── post-types.php # Post types and taxonomies registration
├── resources/
│ ├── views/
│ │ ├── layouts/app.blade.php
│ │ ├── components/ # Composite Blade components
│ │ ├── blocks/ # ACF/Gutenberg block templates
│ │ ├── sections/ # Header, footer
│ │ ├── partials/ # Content partials, cards
│ │ ├── common/ # Reusable utilities
│ │ └── forms/ # Form templates
│ ├── css/
│ │ ├── app.css # CSS entry point
│ │ ├── common/ # theme.css, custom-properties.css, semantic-color.css, base.css
│ │ └── components/ # Atomic design: atoms/ molecules/ organisms/ design-system/
│ ├── js/
│ ├── images/ and fonts/
└── vite.config.js
Prefix {{PREFIX}} — MANDATORY
| Type | Format | Example |
|---|---|---|
| Tailwind utilities | {{PREFIX}}:{utility} | {{PREFIX}}:flex, {{PREFIX}}:p-4, {{PREFIX}}:md:grid |
| CSS components | {{PREFIX}}-{name} | .{{PREFIX}}-button, .{{PREFIX}}-card, .{{PREFIX}}-container |
| Semantic utilities | {{PREFIX}}-{cat}-{var} | {{PREFIX}}-content-01, {{PREFIX}}-background-primary |
CSS import: @import 'tailwindcss' prefix({{PREFIX}});
Approach
- Utility-first: don't create custom CSS classes when TW4 utilities suffice
- Use
@themevariables — never hardcode colors, spacing, or fonts - Mobile-first: base styles = mobile, then
{{PREFIX}}:md:,{{PREFIX}}:lg:,{{PREFIX}}:xl:
Naming
| Type | Convention |
|---|---|
| Blade files | kebab-case (page-header-simple.blade.php) |
| PHP classes | PascalCase |
| Helper functions | snake_case |
| Blade variables | camelCase |
Code Style
- PHP: PSR-12 + Laravel Pint (
pint.json) - JS/CSS/HTML: Prettier (120 chars, single quotes, trailing comma ES5)
- Blade: blade-formatter (
.bladeformatterrc) - Tailwind:
prettier-plugin-tailwindcssfor class sorting
Blade & View Composers
- Blade components are used with
@include(), not<x-component> - View Composers extend
Roots\Acorn\View\Composer - Composer method
with()returns an array of data for the view - Automatic registration via ThemeServiceProvider
Post Types and Taxonomies (roots/acorn-post-types)
Post types and taxonomies are defined in config/post-types.php and automatically registered via roots/acorn-post-types using John Billion's Extended CPTs library.
config/post-types.php structure:
return [
'post_types' => [
'cpt_name' => [
'names' => [
'singular' => 'Singular',
'plural' => 'Plural',
'slug' => 'cpt-name',
],
'labels' => [...], // Full WP labels
'menu_icon' => 'dashicons-admin-post',
'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
'hierarchical' => false,
'has_archive' => true,
'show_in_rest' => true,
'public' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
],
],
'taxonomies' => [
'tax_name' => [
'post_types' => ['post', 'page', 'cpt_name'],
'names' => [
'singular' => 'Singular',
'plural' => 'Plural',
],
'labels' => [...],
'hierarchical' => true,
'show_in_rest' => true,
],
],
];
Loading config in ThemeServiceProvider:
public function register()
{
parent::register();
$configPath = get_theme_file_path('config/post-types.php');
if (is_file($configPath)) {
$this->app->make('config')->set('post-types', require $configPath);
}
}
Best practices:
- Always use Extended CPTs features (admin cols, filters, etc.)
hierarchical: truefor page-like post typesshow_in_rest: truefor Gutenbergexclude_from_search: trueonly for internal CPTs- Slugs always in English, labels localized
ACF Composer — Field Groups
ACF field groups are defined as PHP classes in app/Fields/ extending Log1x\AcfComposer\Field.
Example app/Fields/ExampleFields.php:
<?php
namespace App\Fields;
use Log1x\AcfComposer\Builder;
use Log1x\AcfComposer\Field;
class ExampleFields extends Field
{
public function fields(): array
{
$fields = Builder::make('example_fields');
$fields
->setLocation()
->where('post_type', 'post');
$fields
->addTab(__('General', '{{TEXT_DOMAIN}}'), [
'placement' => 'top',
])
->addText('title', [
'label' => __('Title', '{{TEXT_DOMAIN}}'),
'instructions' => __('Enter a title', '{{TEXT_DOMAIN}}'),
'required' => 1,
])
->addTextarea('description', [
'label' => __('Description', '{{TEXT_DOMAIN}}'),
'maxlength' => 300,
])
->addImage('image', [
'label' => __('Image', '{{TEXT_DOMAIN}}'),
'return_format' => 'array',
'preview_size' => 'medium',
])
->addRepeater('items', [
'label' => __('Items', '{{TEXT_DOMAIN}}'),
'layout' => 'table',
'button_label' => __('Add Item', '{{TEXT_DOMAIN}}'),
])
->addText('name')
->addTextarea('description')
->endRepeater();
return $fields->build();
}
}
Location rules:
$fields
->setLocation()
->where('post_type', 'post')
->or('post_type', 'page')
->or('page_template', 'template-custom.blade.php');
Useful commands:
wp acorn acf:make field FieldName # Generate field group
wp acorn acf:cache # Cache fields (prod)
wp acorn acorn ide:helpers # PHPDoc autocomplete
ACF Composer — Gutenberg Blocks
Blocks are defined in app/Blocks/ extending Log1x\AcfComposer\Block.
Example app/Blocks/ExampleBlock.php:
<?php
namespace App\Blocks;
use Log1x\AcfComposer\Block;
use Log1x\AcfComposer\Builder;
class ExampleBlock extends Block
{
public $name = 'Example Block';
public $description = 'Block description';
public $category = 'theme'; // or 'common', 'formatting', etc.
public $icon = 'admin-post'; // dashicon
public $keywords = ['example', 'test'];
public $post_types = ['page', 'post']; // Restrict to specific CPTs
// Supported alignments
public $supports = [
'align' => ['wide', 'full'],
'mode' => false, // Disable edit/preview toggle
'jsx' => true,
];
public function fields(): array
{
$fields = Builder::make('example_block');
$fields
->addText('title', [
'label' => __('Title', '{{TEXT_DOMAIN}}'),
])
->addWysiwyg('content', [
'label' => __('Content', '{{TEXT_DOMAIN}}'),
]);
return $fields->build();
}
// Data passed to the Blade template
public function with(): array
{
return [
'title' => $this->title,
'content' => $this->content,
'classes' => $this->classes, // Automatic CSS classes
];
}
}
Template resources/views/blocks/example-block/example-block.blade.php:
<div {!! $attributes !!}>
<h2>{{ $title }}</h2>
<div>{!! $content !!}</div>
</div>
$attributes helper: automatically generates class, id, data- attributes.
Commands:
wp acorn acf:make block BlockName # Generate block + template
ACF Composer — Option Pages
Option pages for theme/site settings in app/Options/.
Example app/Options/ThemeSettings.php:
<?php
namespace App\Options;
use Log1x\AcfComposer\Builder;
use Log1x\AcfComposer\Options as Field;
class ThemeSettings extends Field
{
public $name = 'Theme Settings';
public $title = 'Settings | Theme';
public $menu_slug = 'theme-settings';
public $parent = 'options-general.php'; // Under "Settings"
public $capability = 'manage_options';
public $position = 30;
public $redirect = false;
public function fields(): array
{
$fields = Builder::make('theme_settings');
$fields
->addTab(__('General', '{{TEXT_DOMAIN}}'))
->addText('site_phone', [
'label' => __('Phone', '{{TEXT_DOMAIN}}'),
])
->addText('site_email', [
'label' => __('Email', '{{TEXT_DOMAIN}}'),
]);
return $fields->build();
}
}
Retrieving options:
$phone = get_field('site_phone', 'option');
Commands:
wp acorn acf:make options OptionsName # Generate option page
Vite Aliases
@scripts → js, @styles → css, @fonts → fonts, @images → images
What NOT to do
- Don't forget the
{{PREFIX}}:prefix on Tailwind utilities — it's mandatory - Don't use
@applyin CSS — prefer utilities in markup - Don't hardcode colors, spacing, fonts — use
@themevariables - Don't regenerate existing components — check
{{COMPONENTS_CATALOG}} - Don't use
bud.config.js— the bundler is Vite - Don't modify
vite.config.jswithout confirmation - Don't generate JavaScript unless requested
- Don't use
<a role="button">or clickable<div>— use native elements - Don't remove
outlineon:focuswithout a visible alternative - Don't omit
aria-hidden="true" focusable="false"on decorative SVGs - Don't add alt text to decorative images — use
alt="" - Don't use fixed heading levels in reusable components — make them parametric
- Don't register post types/taxonomies with
register_post_type()— useconfig/post-types.php - Don't create ACF field groups via JSON — use ACF Composer (
app/Fields/) - Don't register blocks with
register_block_type()— use ACF Composer (app/Blocks/) - Don't use
get_field()directly in templates — pass data viawith()in the Block/Composer
Nunjucks → Blade Migration (if applicable)
If the project migrates from a Nunjucks design system:
.njk→.blade.phpinresources/views/components/{atoms|molecules|organisms}/.js→resources/js/{atoms|molecules|organisms}/.css→ stay inresources/css/components/(unchanged).config.yml→ View Composer or@include()parameters
> related_skills --same-repo
> test-driven-development
Use when implementing any feature or bugfix, before writing implementation code
> systematic-debugging
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
> seo
Optimize for search engine visibility and ranking. Use when asked to "improve SEO", "optimize for search", "fix meta tags", "add structured data", "sitemap optimization", or "search engine optimization".
> performance
Optimize web performance for faster loading and better user experience. Use when asked to "speed up my site", "optimize performance", "reduce load time", "fix slow loading", "improve page speed", or "performance audit".