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.md
source 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
    @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

TypeFormatExample
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
    @theme
    variables — never hardcode colors, spacing, or fonts
  • Mobile-first: base styles = mobile, then
    {{PREFIX}}:md:
    ,
    {{PREFIX}}:lg:
    ,
    {{PREFIX}}:xl:

Naming

TypeConvention
Blade fileskebab-case (
page-header-simple.blade.php
)
PHP classesPascalCase
Helper functionssnake_case
Blade variablescamelCase

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-tailwindcss
    for 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: true
    for page-like post types
  • show_in_rest: true
    for Gutenberg
  • exclude_from_search: true
    only 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
    @apply
    in CSS — prefer utilities in markup
  • Don't hardcode colors, spacing, fonts — use
    @theme
    variables
  • Don't regenerate existing components — check
    {{COMPONENTS_CATALOG}}
  • Don't use
    bud.config.js
    — the bundler is Vite
  • Don't modify
    vite.config.js
    without confirmation
  • Don't generate JavaScript unless requested
  • Don't use
    <a role="button">
    or clickable
    <div>
    — use native elements
  • Don't remove
    outline
    on
    :focus
    without 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()
    — use
    config/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 via
    with()
    in the Block/Composer

Nunjucks → Blade Migration (if applicable)

If the project migrates from a Nunjucks design system:

  • .njk
    .blade.php
    in
    resources/views/components/{atoms|molecules|organisms}/
  • .js
    resources/js/{atoms|molecules|organisms}/
  • .css
    → stay in
    resources/css/components/
    (unchanged)
  • .config.yml
    → View Composer or
    @include()
    parameters