Skillshub wp-sage
Conventions for WordPress Bedrock + Sage + Acorn + Blade + Tailwind CSS 4 + Vite projects. Always-active rules.
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/alessioarzenton/claude-code-wp-toolkit/wp-sage" ~/.claude/skills/comeonoliver-skillshub-wp-sage && rm -rf "$T"
manifest:
skills/alessioarzenton/claude-code-wp-toolkit/wp-sage/SKILL.mdsource content
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
, prefix@theme
){{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 (
) + Extended CPTs (roots/acorn-post-types
)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
{{PREFIX}}| Type | Format | Example |
|---|---|---|
| Tailwind utilities | | , , |
| CSS components | | , , |
| Semantic utilities | | , |
CSS import:
@import 'tailwindcss' prefix({{PREFIX}});
Approach
- Utility-first: don't create custom CSS classes when TW4 utilities suffice
- Use
variables — never hardcode colors, spacing, or fonts@theme - Mobile-first: base styles = mobile, then
,{{PREFIX}}:md:
,{{PREFIX}}:lg:{{PREFIX}}:xl:
Naming
| Type | Convention |
|---|---|
| Blade files | kebab-case () |
| 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:
for class sortingprettier-plugin-tailwindcss
Blade & View Composers
- Blade components are used with
, not@include()<x-component> - View Composers extend
Roots\Acorn\View\Composer - Composer method
returns an array of data for the viewwith() - 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.)
for page-like post typeshierarchical: true
for Gutenbergshow_in_rest: true
only for internal CPTsexclude_from_search: true- 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>
helper: automatically generates $attributes
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 on Tailwind utilities — it's mandatory{{PREFIX}}: - Don't use
in CSS — prefer utilities in markup@apply - Don't hardcode colors, spacing, fonts — use
variables@theme - Don't regenerate existing components — check
{{COMPONENTS_CATALOG}} - Don't use
— the bundler is Vitebud.config.js - Don't modify
without confirmationvite.config.js - Don't generate JavaScript unless requested
- Don't use
or clickable<a role="button">
— use native elements<div> - Don't remove
onoutline
without a visible alternative:focus - Don't omit
on decorative SVGsaria-hidden="true" focusable="false" - 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
— useregister_post_type()config/post-types.php - Don't create ACF field groups via JSON — use ACF Composer (
)app/Fields/ - Don't register blocks with
— use ACF Composer (register_block_type()
)app/Blocks/ - Don't use
directly in templates — pass data viaget_field()
in the Block/Composerwith()
Nunjucks → Blade Migration (if applicable)
If the project migrates from a Nunjucks design system:
→.njk
in.blade.phpresources/views/components/{atoms|molecules|organisms}/
→.jsresources/js/{atoms|molecules|organisms}/
→ stay in.css
(unchanged)resources/css/components/
→ View Composer or.config.yml
parameters@include()