A WordPress theme that gets out of your way.
New site in 60 seconds. Blocks that auto-register. Styles that auto-inject. Clients who can actually edit their own archives.
The whole system is designed around one idea: you shouldn't have to think about boilerplate.
git clone <repo> && cd pioneer
cp .env.example .env # Add your staging creds
composer install && npm install
npm run dev # Go
npm run build && git push # Deploys to stagingThat's it. You're working.
npm run dev # Dev server with hot reload
npm run build # Production build
npm run db:pull # Pull staging database
npm run db:push # Push to staging (this overwrites everything, be careful)
./vendor/bin/phpunit # Run tests| Location | What It Does | Who Touches It |
|---|---|---|
blocks/ |
Page sections (hero, features, CTA) | Client via Gutenberg |
partials/ |
UI atoms (button, card, icon) | You |
templates/ |
Page wrappers + block rules | You |
library/ |
Pre-built stuff (copy to activate) | Nobody until you copy it |
assets/ |
Global styles and scripts | You |
pioneer/
├── assets/
│ ├── scss/ # Source styles
│ ├── js/ # Source JS
│ └── dist/ # Built assets (gitignored)
├── blocks/ # ACF blocks (auto-registered)
├── templates/ # Page templates (with scoped blocks)
├── partials/ # Active components (auto-loaded)
├── library/ # Pre-built components (copy to use)
├── inc/
│ ├── class-pioneer-theme.php # Main theme class
│ ├── class-archive-controller.php # Client-editable archives
│ ├── class-accessible-walker.php # Nav walker
│ ├── acf-pro/ # Bundled ACF Pro
│ └── image-optimizer/ # Auto image optimization
├── acf-json/ # ACF field definitions (auto-synced)
├── vite-plugins/ # Custom Vite plugins
├── tests/ # PHPUnit tests
└── .husky/ # Git hooks
- Run
npm run dev - Create a folder:
blocks/my-block/ - Files appear automatically.
_my-block.phpand_my-block.scssjust show up. - In WP Admin, make an ACF field group. Assign it to
acf/my-block. - Save the field group. The PHP template generates itself.
- Optional: add
_config.phpfor settings or_my-block.jsfor scripts.
That's the whole process. Folder exists, files scaffold, ACF generates the template.
When Vite is running, new folders trigger file generation:
Blocks (blocks/my-block/):
_my-block.php(empty, ready for ACF)_my-block.scss(abstracts already injected)
Partials (partials/my-component/):
_my-component.php(with docblock)_my-component.scss
Templates (templates/my-template/):
_my-template.php(with InnerBlocks placeholder)_my-template.scss_config.jsonblocks/directory for scoped blocks
You'll see this in your terminal:
🔧 Theme scaffolder watching for new folders...
✓ Created templates/homepage/_homepage.php
✓ Created templates/homepage/_homepage.scss
✓ Created templates/homepage/_config.json
✓ Created templates/homepage/blocks/
→ Template "Homepage" ready to use
When you save an ACF field group assigned to a block, the theme writes the PHP for you. It handles:
pioneer_block_open()/close()wrappers- Field assignments with
get_field() - Proper escaping per field type
- Component partials for cloned groups
- Repeater and flexible content loops
img_acf()for images
To regenerate a template: empty the file and save the ACF field group again.
(Warning: this overwrites your manual edits. Back up first if you've customized anything.)
<?php
// blocks/hero/_hero.php
$heading = get_field('heading');
$background = get_field('background_image');
?>
<section class="hero-block">
<h1><?php echo esc_html($heading); ?></h1>
</section>Add a _config.php to customize settings:
<?php
// blocks/hero/_config.php
return [
'title' => 'Hero',
'description' => 'A hero section with background image.',
'icon' => 'cover-image',
'keywords' => ['hero', 'banner', 'header'],
'supports' => [
'align' => ['wide', 'full'],
'color' => [
'background' => true,
'text' => true,
'gradients' => true,
],
'spacing' => [
'padding' => true,
'margin' => ['top', 'bottom'],
],
],
];These enable native Gutenberg controls:
| Support | What It Does |
|---|---|
align |
Alignment toolbar |
anchor |
HTML anchor field (on by default) |
color.background |
Background color picker |
color.text |
Text color picker |
color.gradients |
Gradient picker |
spacing.padding |
Padding controls |
spacing.margin |
Margin controls |
typography.fontSize |
Font size picker |
typography.lineHeight |
Line height control |
Full list: Block Supports docs
Pre-built components live in library/. They don't load until you copy them to partials/.
library/partials/
├── accordion/ # Expandable sections
├── archive/ # Archive layouts
├── contact/ # Contact info display
├── cta/ # Call-to-action sections
├── drawer/ # Slide-out panels
├── heading/ # Styled headings
├── modal/ # Dialog overlays
├── search/ # Search form
├── slider/ # Carousels (Swiper)
├── tabs/ # Tabbed content
├── tooltip/ # Hover tooltips
└── video/ # Video embeds with lazy loading
cp -r library/partials/slider partials/Then in your templates:
pioneer_component('slider', $args);Templates work like blocks. Each gets its own folder. They can include scoped blocks.
templates/
└── homepage/
├── _homepage.php # The template
├── _homepage.scss # Styles
├── _config.json # Config
└── blocks/ # Scoped blocks
├── home-hero/
└── home-features/
- Run
npm run dev - Create folder:
templates/my-template/ - Files appear. Edit
_config.jsonto set the name. - Build your template using blocks with InnerBlocks.
{
"name": "Homepage",
"description": "Custom homepage template",
"allowedBlocks": ["acf/homepage-hero", "acf/homepage-features", "core/paragraph"]
}Blocks in templates/{name}/blocks/ register globally as acf/{template}-{block}:
templates/homepage/blocks/hero/becomesacf/homepage-herotemplates/homepage/blocks/features/becomesacf/homepage-features
They get their own Gutenberg category. They can technically be used anywhere, but they're organized with their template.
<?php
// templates/homepage/_homepage.php
?>
<div class="homepage-template">
<?php the_content(); ?>
</div>This is the feature clients actually love. They can edit archive layouts themselves.
Create a page with a magic slug. That page controls the archive layout.
| Page Slug | Controls |
|---|---|
archive-category |
Category archives (/category/news/) |
archive-tag |
Tag archives (/tag/featured/) |
archive-search |
Search results (/?s=keyword) |
archive-author |
Author archives (/author/john/) |
archive-date |
Date archives (/2024/01/) |
archive-blog |
Blog home |
- Create a page. Title: "Category Archive" (or whatever)
- Set slug to
archive-category - Add blocks to design the layout
- Drop in the Posts Grid block where posts should appear
- Posts Grid auto-detects the archive query
Queries and displays posts. On archive pages, it uses the current archive query automatically.
Fields: Post type, category filter, posts per page, columns, toggle for featured image/excerpt/date/author, "Use Archive Query" toggle.
$context = pioneer_get_archive_context();
// Returns: type, title, description, term
echo $context['title']; // "News" for /category/news/If no controller page exists, the theme falls back to a sensible default.
Designed so clients never call you about "my changes aren't showing."
- Logged-in users never see cache. Always fresh.
- Visitors get cached pages.
Enable WP_DEBUG in wp-config.php:
define('WP_DEBUG', true);This disables discovery caching so new blocks appear immediately.
In Local by Flywheel: Right-click site, Site Setup, Advanced, toggle WP_DEBUG on.
- Admin bar: Click "Clear Pioneer Cache"
- URL:
/wp-admin/?pioneer_clear_cache=1
Just run it. The theme detects it automatically.
npm run devAssets load from Vite on port 5173 with hot reload. When Vite isn't running, assets load from assets/dist/.
npm run buildAssets go to assets/dist/ with hashed filenames.
Runs silently. Every upload gets:
- WebP conversion (when supported)
- Resize for oversized images
- Compression without visible quality loss
No config needed. It just prevents clients from uploading 10MB photos.
Large imports get queued and processed in the background. 5 images per minute via WP-Cron.
- View queue: Media → Optimization Queue
- Admin bar shows pending count
// Queue all unoptimized images (for existing sites)
pioneer_queue_all_unoptimized();
// Check if still processing
if (pioneer_is_optimization_pending($id)) {
// show loading state
}pioneer_before_header, pioneer_header_start, pioneer_after_branding, pioneer_before_nav, pioneer_after_nav, pioneer_header_end, pioneer_after_header
pioneer_before_main, pioneer_main_start, pioneer_before_content, pioneer_post_before, pioneer_post_after, pioneer_after_content, pioneer_main_end, pioneer_after_main
pioneer_before_footer, pioneer_footer_start, pioneer_footer_end, pioneer_after_footer
assets/scss/main.scssassets/scss/abstracts/(variables, mixins)
Vite injects abstracts into all SCSS entry points. No imports needed in blocks, partials, or templates:
// blocks/hero/_hero.scss
.hero-block {
padding: $spacing-lg;
@include media(md) {
padding: $spacing-xl;
}
}Files in assets/scss/ that are imported via @use still need the import:
// assets/scss/components/_buttons.scss
@use '../abstracts' as *;
.button {
@include button-base;
}- Check for PHP syntax errors
- Clear cache:
/wp-admin/?pioneer_clear_cache=1 - Check
wp-content/debug.log
- ACF Pro active?
- Folder structure correct?
blocks/name/_name.php - Clear cache
- Run
npm run build - Check
assets/dist/has files - Vite running? (
npm run dev)
- Check port 5173:
lsof -i :5173 - Port in use? Set a different one in
.env:VITE_PORT=5174
- Run
npm installfirst
- PHP 8.0+
- WordPress 6.0+
- ACF Pro (bundled or installed as plugin)
- Node.js 18+
Bundled in inc/acf-pro/ and committed to the repo. Auto-loads. No plugin activation needed.
If ACF Pro is already installed as a plugin, the bundled version skips itself.
License key goes in ACF settings in wp-admin or via ACF_PRO_LICENSE in wp-config.php.
Field groups auto-save to acf-json/ and load from there. Version control keeps everything synced across environments.
Create .env in the theme root:
VITE_PORT=5173
STAGING_SSH_USER=your-spinup-ssh-user
STAGING_SSH_HOST=your.server.ip.address
STAGING_SITE_PATH=/sites/your-domain.com/files
STAGING_URL=your-domain.com
SSH_KEY_PATH=~/.ssh/your-key-name
LOCAL_URL=localhost:10000Copy .env.example to .env when starting a new site.
./vendor/bin/phpunit # All tests
./vendor/bin/phpunit tests/SecurityTest.php # Specific file
npm run lint # ESLint
./vendor/bin/phpcs # PHP CodeSniffer
./vendor/bin/phpcbf # Auto-fix PHP| Suite | What It Tests |
|---|---|
| ThemeSetupTest | Core functionality |
| BlockDiscoveryTest | ACF block registration |
| ComponentsTest | Partials |
| SecurityTest | CSS/SVG sanitization, XSS |
| AccessibilityTest | ARIA, semantic HTML, focus |
| SchemaTest | JSON-LD structured data |
| ImageOptimizerTest | Optimization, WebP, uploads |
Git hooks run lint and tests before push:
# Runs automatically:
# 1. ESLint
# 2. PHPCS
# 3. PHPUnit
# Bypass (not recommended):
git push --no-verifyIf pre-push fails, fix the issues. Your CI will fail anyway.
(Some GUI clients and CI environments skip hooks. GitHub Actions is the source of truth.)
- Go to repo Settings → Secrets and variables → Actions → Variables
- Add:
SKIP_TESTS=true - Push
- Remove the variable after (seriously, don't forget)
npm run db:push # Local → staging (overwrites staging)
npm run db:pull # Staging → local
npm run media:push # Local uploads → staging
npm run media:pull # Staging uploads → local- Database: Export, SSH transfer, import, search-replace for URLs
- Media: rsync on
wp-content/uploads/
URLs convert automatically (localhost:10000 → staging.example.com).
db:pushoverwrites staging. Be careful.- Plugin activation syncs with database. Plugin files don't.
- Pull first if staging has content changes you want.
If assets/dist/ is empty:
- Admin sees a warning
- Frontend shows an alert banner
Run npm run build.
Theme Version: 1.0.0