Skip to content

emmpetersen/pioneer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pioneer Theme

A WordPress theme that gets out of your way.

The Point

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.

Quick Start

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 staging

That's it. You're working.

Commands You'll Actually Use

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

Where Things Live

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

Folder Structure

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

Blocks

Making a New Block

  1. Run npm run dev
  2. Create a folder: blocks/my-block/
  3. Files appear automatically. _my-block.php and _my-block.scss just show up.
  4. In WP Admin, make an ACF field group. Assign it to acf/my-block.
  5. Save the field group. The PHP template generates itself.
  6. Optional: add _config.php for settings or _my-block.js for scripts.

That's the whole process. Folder exists, files scaffold, ACF generates the template.

How the Scaffolder Works

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.json
  • blocks/ 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

ACF Template Generation

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.)

Block Example

<?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>

Block Config

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'],
        ],
    ],
];

Block Supports

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

Component Library

Pre-built components live in library/. They don't load until you copy them to partials/.

What's Available

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

Using a Component

cp -r library/partials/slider partials/

Then in your templates:

pioneer_component('slider', $args);

Templates

Templates work like blocks. Each gets its own folder. They can include scoped blocks.

Template Structure

templates/
└── homepage/
    ├── _homepage.php           # The template
    ├── _homepage.scss          # Styles
    ├── _config.json            # Config
    └── blocks/                 # Scoped blocks
        ├── home-hero/
        └── home-features/

Making a New Template

  1. Run npm run dev
  2. Create folder: templates/my-template/
  3. Files appear. Edit _config.json to set the name.
  4. Build your template using blocks with InnerBlocks.

Template Config

{
    "name": "Homepage",
    "description": "Custom homepage template",
    "allowedBlocks": ["acf/homepage-hero", "acf/homepage-features", "core/paragraph"]
}

Template-Scoped Blocks

Blocks in templates/{name}/blocks/ register globally as acf/{template}-{block}:

  • templates/homepage/blocks/hero/ becomes acf/homepage-hero
  • templates/homepage/blocks/features/ becomes acf/homepage-features

They get their own Gutenberg category. They can technically be used anywhere, but they're organized with their template.

Template Example

<?php
// templates/homepage/_homepage.php
?>
<div class="homepage-template">
    <?php the_content(); ?>
</div>

Archive Controller

This is the feature clients actually love. They can edit archive layouts themselves.

How It Works

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

Setup

  1. Create a page. Title: "Category Archive" (or whatever)
  2. Set slug to archive-category
  3. Add blocks to design the layout
  4. Drop in the Posts Grid block where posts should appear
  5. Posts Grid auto-detects the archive query

Posts Grid Block

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.

Archive Context

$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.

Caching

Designed so clients never call you about "my changes aren't showing."

  • Logged-in users never see cache. Always fresh.
  • Visitors get cached pages.

Local Dev

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.

Clear Cache

  • Admin bar: Click "Clear Pioneer Cache"
  • URL: /wp-admin/?pioneer_clear_cache=1

Development

Vite Dev Server

Just run it. The theme detects it automatically.

npm run dev

Assets load from Vite on port 5173 with hot reload. When Vite isn't running, assets load from assets/dist/.

Production Build

npm run build

Assets go to assets/dist/ with hashed filenames.

Image Optimizer

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.

Bulk Uploads

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
}

Hooks

Header

pioneer_before_header, pioneer_header_start, pioneer_after_branding, pioneer_before_nav, pioneer_after_nav, pioneer_header_end, pioneer_after_header

Content

pioneer_before_main, pioneer_main_start, pioneer_before_content, pioneer_post_before, pioneer_post_after, pioneer_after_content, pioneer_main_end, pioneer_after_main

Footer

pioneer_before_footer, pioneer_footer_start, pioneer_footer_end, pioneer_after_footer

CSS

Main Files

  • assets/scss/main.scss
  • assets/scss/abstracts/ (variables, mixins)

Auto-Injected Abstracts

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;
    }
}

Manual Import

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;
}

Troubleshooting

Site Shows Error After Changes

  1. Check for PHP syntax errors
  2. Clear cache: /wp-admin/?pioneer_clear_cache=1
  3. Check wp-content/debug.log

Blocks Not Appearing

  1. ACF Pro active?
  2. Folder structure correct? blocks/name/_name.php
  3. Clear cache

Styles Not Loading

  1. Run npm run build
  2. Check assets/dist/ has files
  3. Vite running? (npm run dev)

Vite Not Working

  1. Check port 5173: lsof -i :5173
  2. Port in use? Set a different one in .env:
    VITE_PORT=5174
  3. Run npm install first

Requirements

  • PHP 8.0+
  • WordPress 6.0+
  • ACF Pro (bundled or installed as plugin)
  • Node.js 18+

ACF Pro

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.

ACF JSON Sync

Field groups auto-save to acf-json/ and load from there. Version control keeps everything synced across environments.

Environment Config

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:10000

Copy .env.example to .env when starting a new site.

Testing

Running Tests

./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

Test Coverage

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

Pre-Push Hooks

Git hooks run lint and tests before push:

# Runs automatically:
# 1. ESLint
# 2. PHPCS
# 3. PHPUnit

# Bypass (not recommended):
git push --no-verify

If 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.)

GitHub Actions

Skip Tests (Emergency Only)

  1. Go to repo Settings → Secrets and variables → Actions → Variables
  2. Add: SKIP_TESTS = true
  3. Push
  4. Remove the variable after (seriously, don't forget)

Database & Media Sync

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

How It Works

  • Database: Export, SSH transfer, import, search-replace for URLs
  • Media: rsync on wp-content/uploads/

URLs convert automatically (localhost:10000 → staging.example.com).

Notes

  • db:push overwrites staging. Be careful.
  • Plugin activation syncs with database. Plugin files don't.
  • Pull first if staging has content changes you want.

Missing Build Notice

If assets/dist/ is empty:

  • Admin sees a warning
  • Frontend shows an alert banner

Run npm run build.


Theme Version: 1.0.0

About

A PX Pioneer Starter Theme

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages