WP-Easy Theme Development Guide
A comprehensive guide for building modern WordPress themes using the WP-Easy framework—without the complexity of modern build tools.
Table of Contents
- What is WP-Easy?
- Getting Started
- Theme Structure
- Router System
- Single File Components (SFCs)
- Layouts
- Templates
- Components
- Styles & SCSS
- Scripts & JavaScript
- SVG Usage
- Fonts
- Hot Reload Development
- Helper Functions
- Post Object Extensions
- Common Patterns
What is WP-Easy?
WP-Easy is a framework designed for people who understand HTML, CSS, and JavaScript but want to build modern WordPress themes without dealing with complex build tools. There's no npm, no terminal commands, no webpack—just code, save, and refresh.
Key Features:
- No build step required - Just code and refresh
- Component-based architecture - Build reusable UI components
- Single File Components - Combine PHP, HTML, SCSS, and JS in one file
- Automatic asset loading - Styles and scripts are auto-enqueued
- Live reload - See changes instantly during development
- SCSS support - Write modern, nested styles
- Flexible routing - Map URLs to templates and layouts
Getting Started
1. Install WP-Easy Plugin
Upload the WP-Easy plugin to your wp-content/plugins directory and activate it.
2. Create Your Theme
Create a new theme directory in wp-content/themes/your-theme-name/ with this basic structure:
Check out the WP-Easy Theme starter theme.
your-theme/
├── index.php
├── functions.php
├── router.php
├── template.php
├── style.css
├── layouts/
│ └── default.php
├── templates/
├── components/
├── styles/
├── scripts/
└── images/
3. Enable Pretty Permalinks
Go to WordPress Settings > Permalinks and select any option other than "Plain".
4. Configure Routes
Set up your routes in router.php to map URLs to templates and layouts.
Theme Structure
Core Files
router.php - Entry point that determines routing
<? $routes = [ 'home' => '/', 'work' => '/work/', 'work-detail' => [ 'path' => '/work/:slug/', 'layout' => 'default', 'template' => 'work-detail' ] ]; return $routes;
Core Directories
/layouts/ - Layout files that wrap your page content
- Purpose: Provide the overall page structure (header, footer, navigation)
- Usage: Wraps templates with common page elements
- Example:
default.phpcontains header, main content area, and footer - Key function: Usesd for different site wide layout (such as conditonal header or footer, logged in chrome etc.)
/templates/ - Page-level templates for different routes
- Purpose: Define the main content for each page/route
- Usage: Contains the specific content for home, blow, about pages, etc.
- Example:
home.phpfor homepage,work-detail.phpfor work pages - Key function: Uses
use_component()to build pages from reusable pieces
/components/ - Reusable UI components, loaded using use_component()
- Purpose: Build modular, reusable pieces of your site
- Usage: Header, footer, buttons, image blocks, etc.
- Example:
header.php,work-block.php,wp-image.php - Key function: Accept props via
$argsand render specific UI elements
functions/utils.php - Your theme functions would go in here.
/styles/- Global SCSS/CSS files (auto-loaded)/scripts/- JavaScript files (auto-loaded)/images/- Static images and SVGs. SVG's loaded usinguse_svg()
Router System
The router is the heart of WP-Easy's template system. It's the entry point that determines which layout and template to use for each URL. Define your routes in router.php:
<? $routes = [ 'home' => '/', 'work' => '/work/', 'work-detail' => [ 'path' => '/work/:slug/', 'layout' => 'default', 'template' => 'work-detail' ], 'about' => '/about/', ]; return $routes;
Router Syntax
Simple Routes:
'home' => '/', // Maps to /templates/home.php 'work' => '/work/', // Maps to /templates/work.php
Advanced Routes:
'work-detail' => [ 'path' => '/work/:slug/', // URL pattern with parameter 'layout' => 'alternate', // Use /layouts/alternate.php 'template' => 'work-detail' // Use /templates/work-detail.php ]
Route Parameters
Use :parameter syntax for dynamic routes:
:slug- Post slug:id- Post ID:page- Page number
These parameters aren't used for anything currently, it's simply for human readability.
How a request leads to a built page
- Router (
router.php) - Determines which layout and template to use - Layout (
/layouts/default.php) - Wraps the content with header, footer, etc. - Template (
/templates/home.php) - Contains the main page content - Components - Reusable pieces used within templates (and in layouts or inside other components too!)
Fallback Behavior
If no route matches, WP-Easy falls back to the theme's /index.php file.
Single File Components (SFCs)
SFCs allow you to combine PHP logic, HTML markup, SCSS styles, and JavaScript in a single .php file.
SFC Structure
<? // PHP logic at the top $args = set_defaults($args, ['title' => 'Default Title']); ?> <head> <!-- Optional tag: Third-party stylesheets or scripts that should go in the head--> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <template> <!-- Your HTML markup --> <div class="component"> <h2><?= $args['title']; ?></h2> </div> </template> <style> /* SCSS/CSS styles */ .component { background: red; .title { color: blue; } } </style> <script> // JavaScript for this component $('.component').click(function() { // jQuery is available as $() }); </script>
SFC Blocks Explained
- PHP Block - Logic and data preparation
<head>Block - Third-party stylesheets/scripts<template>Block - HTML markup<style>Block - Scoped SCSS/CSS<script>Block - Component-specific JavaScript
Layouts
Layouts wrap your page content and provide the overall page structure.
Default Layout (/layouts/default.php)
<? $header_size = 'normal'; $show_footer = true; switch (use_route_name()) { case 'work-detail': $header_size = 'small'; $show_footer = false; break; } ?> <template> <? use_component('header', ['size' => $header_size]); ?> <main id="content"> <? use_outlet(); ?> </main> <? if ($show_footer) : ?> <? use_component('footer'); ?> <? endif; ?> </template> <style> /* Layout-specific styles */ #content { min-height: 100vh; } </style>
Layout Features
use_outlet()- Renders the current templateuse_component()- Includes header, footer, or other components- Route-based logic - Different layouts for different pages
- Scoped styles - Layout-specific CSS
Templates
Templates represent the main content for each route.
Basic Template (/templates/home.php)
<template>
<main class="template-home">
<? use_component('hero-section'); ?>
<section class="featured-work">
<? foreach (use_children() as $post) : ?>
<? use_component('work-block', [
'title' => $post->title,
'url' => $post->url,
'image_id' => $post->thumbnail_id
]); ?>
<? endforeach; ?>
</section>
</main>
</template>
<style>
.template-home {
.featured-work {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
}
</style>Template Features
- Post data access - Use
$postobject with extensions - Component composition - Build pages from reusable components
- Scoped styles - Template-specific CSS
- Helper functions - Use
use_children(),use_posts(), etc.
Components
Components are reusable UI pieces that can be used across templates and layouts.
Basic Component (/components/work-block.php)
<? $args = set_defaults($args, [ 'title' => '', 'url' => '', 'image_id' => 0, 'class' => '' ]); ?> <template> <article class="work-block <?= $args['class']; ?>"> <a href="<?= esc_url($args['url']); ?>"> <? use_component('wp-image', [ 'image_id' => $args['image_id'], 'class' => 'work-image' ]); ?> <h3 class="work-title"><?= esc_html($args['title']); ?></h3> </a> </article> </template> <style> .work-block { position: relative; overflow: hidden; .work-image { transition: transform 0.3s ease; } &:hover .work-image { transform: scale(1.05); } } </style>
Component Best Practices
- Use
set_defaults()- Set default values for props - Scoped styles - Component-specific CSS
- Reusable props - Design for flexibility
Styles & SCSS
WP-Easy provides powerful SCSS support with automatic compilation and loading.
- SCSS compilation - Automatic SCSS to CSS conversion
- Global imports - Files in
/styles/global/are auto-imported - Component scoping - Styles are scoped to components
- Media queries - Use
$lt-phone,$lt-tabletvariables - CSS variables - Use
var(--color-primary)for theming
Global Styles (/styles/main.scss)
// Variables $primary-color: #ff6b6b; $font-primary: 'Helvetica', sans-serif; // Mixins @mixin button-style { padding: 1rem 2rem; border: none; border-radius: 4px; cursor: pointer; } // Global styles body { font-family: $font-primary; line-height: 1.6; } .button { @include button-style; background: $primary-color; color: white; }
Component Styles in SFCs
<style> .component { background: var(--color-primary); .title { font-size: 2rem; margin-bottom: 1rem; } // Responsive design @media #{$lt-phone} { .title { font-size: 1.5rem; } } } </style>
Special Theme Styles
WP-Easy automatically handles two special CSS files for WordPress-specific areas:
Login Page Styling (/styles/login.css)
Customize the WordPress login page appearance:
body.login { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } #login h1 a { background-image: url(../images/logo.svg); background-position: center center; background-size: auto; }
Admin Area Styling (/styles/admin.css)
Customize the WordPress admin dashboard:
#wpadminbar { background: #2c3e50; }
Special Features
- Automatic loading - Files are automatically enqueued when present
- Login page -
login.cssonly loads on/wp-login.php - Admin area -
admin.cssonly loads in the WordPress admin - Excluded from frontend - These files don't load on your public website
- Favicon integration - Admin CSS automatically includes your theme's favicon
Scripts & JavaScript
JavaScript files are automatically loaded from the /scripts folder and can be modular.
Main Script (/scripts/main.js)
// Site-wide JavaScript $(document).ready(function() { // Initialize components initNavigation(); initScrollEffects(); }); function initNavigation() { $('.hamburger').click(function() { $('.menu-tray').toggleClass('is-open'); }); } function initScrollEffects() { $(window).scroll(function() { if ($(window).scrollTop() > 100) { $('.header').addClass('is-scrolled'); } else { $('.header').removeClass('is-scrolled'); } }); }
Scripts in SFCs
- jQuery included -
$is available globally - ES modules - Modern JavaScript module system
- Auto-loading - All files in
/scripts/are loaded - Component scripts - Scoped to specific components
- No document.ready needed - Component
<script>blocks run when component is rendered.
<script> // Component-specific JavaScript $('.work-block').hover( function() { $(this).find('.work-image').addClass('is-hovered'); }, function() { $(this).find('.work-image').removeClass('is-hovered'); } ); </script>
Component Isolation Best Practices
✅ Good - Component stays isolated:
<script> // Only manipulate elements within this component $('.work-block').click(function() { $(this).toggleClass('is-expanded'); }); </script>
❌ Avoid - Don't manipulate other parts of the site:
<script> // Don't do this - affects other components $('.header').addClass('work-page'); $('.footer').hide(); </script>
✅ For site-wide functionality, use /scripts/main.js on use a Template or Layout <script> block
// /scripts/main.js - Site-wide functionality $(document).ready(function() { // Global navigation, scroll effects, etc. initNavigation(); initScrollEffects(); });
ES Modules Support
WP-Easy automatically converts component scripts to ES modules:
<script> // This becomes an ES module automatically export function initWorkBlock() { $('.work-block').click(function() { // Component logic here }); } </script>
Module Features:
- Automatic conversion - Component scripts become ES modules
- Dependency management - Modules can import/export functions
- Performance - Only loads when component is used
- Isolation - Component scripts don't conflict with each other
Module Names:
- Component modules:
{type}-{filename}(e.g.,components-work-block,templates-home) - Utility modules:
utils-{filename}(e.g.,utils-clamp,utils-delay) - Main module:
main(site-wide scripts)
Importing Between Modules:
// In a component script <script> import { clamp } from 'utils-clamp'; import { delay } from 'utils-delay'; export function initWorkBlock() { $('.work-block').click(function() { const value = clamp(0, 100, 50); // From utils-clamp delay(1000).then(() => { // From utils-delay }); }); } </script>
Exporting from Components:
<script> // Export functions for other modules to use export function showWorkDetails() { $('.work-details').slideDown(); } export function hideWorkDetails() { $('.work-details').slideUp(); } </script>
Loading Custom JavaScript Libraries
Use the <head> tag to load third-party libraries that need to be available globally:
<? // Component that uses Chart.js $args = set_defaults($args, ['chart_data' => []]); ?> <head> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <template> <div class="chart-container"> <canvas id="myChart"></canvas> </div> </template> <script> // Chart.js is now available globally const ctx = $('#myChart').get(0).getContext('2d'); const chart = new Chart(ctx, { type: 'bar', data: <?= json_encode($args['chart_data']); ?> }); </script>
Common Use Cases:
- Analytics scripts - Google Analytics, Facebook Pixel
- Maps - Google Maps, Mapbox
- Charts - Chart.js, D3.js
- UI libraries - Bootstrap, Foundation
SVG Usage
WP-Easy makes it easy to include and customize SVGs.
Basic SVG Usage
// Will render `/images/logo.svg` with supplied attributes. <? use_svg('logo', ['class' => 'site-logo', 'width' => 120]); ?>
SVG with Custom Attributes
<? use_svg('icon-arrow', [ 'class' => 'arrow-icon', 'width' => 24, 'height' => 24, 'fill' => 'currentColor' ]); ?>
SVG Features
- Automatic loading - SVGs from
/images/directory - Custom attributes - Add classes, dimensions, etc.
- Security - Strips XML declarations and unwanted tags
- Performance - Inline SVGs for better performance
Fonts
WP-Easy supports font loading using Webfontloader.
Font config (/scripts/fonts.js)
- Local fonts - Host fonts in your theme
- Google Fonts - Easy integration
- Adobe Fonts - Creative Cloud integration
- Performance - Optimized loading
See Webfontloader for explanation on how to load all the supported font types.
// Load local fonts WebFont.load({ custom: { families: ['CustomFont'], urls: ['/fonts/custom-font.css'] } });
Hot Reload Development
WP-Easy includes built-in hot reload for instant development feedback.
Setup
-
Enable WordPress Debug Mode
// In wp-config.php define('WP_DEBUG', true); define('WP_DEBUG_LOG', true);
-
That's it! Hot reload automatically activates when you're logged in as an administrator.
How It Works
- File monitoring - Watches for changes in theme files
- Instant refresh - Browser refreshes automatically
- Child theme support - Works with both parent and child themes
- All file types - PHP, SCSS, CSS, and JavaScript
What Gets Monitored
- All theme subdirectories
- Excludes:
/node_modules,/.git,/vendor,/images
Troubleshooting
Hot reload not working?
- Ensure
WP_DEBUGis set totrue - Check browser console for errors
- Verify you're logged in as administrator
Helper Functions
WP-Easy provides powerful helper functions for common tasks.
Routing & Layout
use_route_name() or get_route_name - Get current route name
if (use_route_name() === 'home') { // Home page logic }
Components
use_component($name, $props) - Render component with props
use_component('work-block', [ 'title' => 'Project Title', 'url' => '/work/project/', 'image_id' => 123 ]);
Data Helpers
use_children($args) - Get child pages of current page, ordered by menu_order
- Args: Standard
WP_Queryarguments with sensible defaults - Defaults:
post_type: 'any',post_parent: current_post_id,posts_per_page: -1,order: 'ASC',orderby: 'menu_order'
<? foreach (use_children() as $post) : ?> <h3><?= $post->title; ?></h3> <? endforeach; ?> <? foreach (use_children(['post_type' => 'work', 'posts_per_page' => 6]) as $post) : ?> <h3><?= $post->title; ?></h3> <? endforeach; ?>
use_posts($args) - Get posts for the current page, with pagination links also.
- Args: Standard
WP_Queryarguments with sensible defaults - Defaults:
post_type: 'post',posts_per_page: get_option('posts_per_page'),paged: current_page,orderby: 'date',order: 'DESC'
<? $posts_data = use_posts(); foreach ($posts_data->posts as $post) : ?> <article><?= $post->title; ?></article> <? endforeach; ?> <? $work_posts = use_posts(['post_type' => 'work', 'posts_per_page' => 12]); foreach ($work_posts->posts as $post) : ?> <article><?= $post->title; ?></article> <? endforeach; ?> <? if ($posts_data->next_posts_url) : ?> <a href="<?= $posts_data->next_posts_url; ?>">Next Page</a> <? endif; ?>
use_adjacent($post_id, $direction) - Get next/previous page/post
$next_post = use_adjacent($post->ID, 'next'); $prev_post = use_adjacent($post->ID, 'previous');
SVGs
use_svg($name, $attrs) - Render SVG with attributes. $name is the filename of SVG file inside /images/.
use_svg('logo', ['class' => 'header-logo', 'width' => 120]);
Utilities
set_defaults($args, $defaults) - Set default values for a component's $args.
$args = set_defaults($args, [ 'title' => 'Default Title', 'class' => 'default-class' ]);
set_attribute($name, $condition) - Conditional HTML attributes
<button <?= set_attribute('disabled', !$is_enabled); ?>> Submit </button>
<button <?= set_attribute('class="is-opened"', $is_opened); ?>> Close </button>
Post Object Extensions
WP-Easy extends the WordPress $post object with useful shortcuts.
Available Extensions
$post->id - Post ID (same as $post->ID)
$post->url - Post permalink
<a href="<?= $post->url; ?>">Read More</a>
$post->title - Filtered post title
<h1><?= $post->title; ?></h1>
$post->content - Filtered post content
<div class="content"><?= $post->content; ?></div>
$post->excerpt - Post excerpt, will return an auto excerpt if user generated one is empty.
<p><?= $post->excerpt; ?></p>
$post->thumbnail_id - Featured image ID
<? use_component('wp-image', [ 'image_id' => $post->thumbnail_id, 'class' => 'featured-image' ]); ?>
ACF Field Shortcuts
ACF (Advanced Custom Fields) fields are automatically available as shortcuts on the $post object:
// If you have ACF fields like 'video_url', 'director_credit', 'gallery_images' <? if ($post->video_url) : ?> <video src="<?= $post->video_url; ?>" controls></video> <? endif; ?> <? if ($post->director_credit) : ?> <p>Director: <?= $post->director_credit; ?></p> <? endif; ?> <? foreach ($post->gallery_images as $image) : ?> <? use_component('wp-image', [ 'image_id' => $image['id'], 'class' => 'gallery-image' ]); ?> <? endforeach; ?>
Usage Examples
// In a template <article class="post"> <h2><a href="<?= $post->url; ?>"><?= $post->title; ?></a></h2> <? if ($post->thumbnail_id) : ?> <? use_component('wp-image', [ 'image_id' => $post->thumbnail_id, 'class' => 'post-image' ]); ?> <? endif; ?> <div class="excerpt"><?= $post->excerpt; ?></div> </article>
Common Patterns
1. Work/Portfolio Grid
<template>
<section class="work-grid">
<? foreach (use_children() as $post) : ?>
<article class="work-item">
<a href="<?= $post->url; ?>">
<? use_component('wp-image', [
'image_id' => $post->thumbnail_id,
'class' => 'work-image'
]); ?>
<h3><?= $post->title; ?></h3>
</a>
</article>
<? endforeach; ?>
</section>
</template>2. Navigation Menu
<template>
<nav class="main-nav">
<? wp_nav_menu([
'menu_class' => 'nav-menu',
'container' => false,
'menu' => 'primary'
]); ?>
</nav>
</template>3. Blog Post List
<template>
<div class="blog-posts">
<?
$posts_data = use_posts();
foreach ($posts_data->posts as $post) : ?>
<article class="blog-post">
<h2><a href="<?= $post->url; ?>"><?= $post->title; ?></a></h2>
<div class="excerpt"><?= $post->excerpt; ?></div>
</article>
<? endforeach; ?>
<? if ($posts_data->next_posts_url) : ?>
<a href="<?= $posts_data->next_posts_url; ?>" class="load-more">Load More</a>
<? endif; ?>
</div>
</template>4. Image Gallery
<template>
<div class="gallery">
<? foreach ($post->gallery_images as $image) : ?>
<? use_component('wp-image', [
'image_id' => $image['id'],
'class' => 'gallery-image'
]); ?>
<? endforeach; ?>
</div>
</template>5. Conditional Content
<template>
<div class="content">
<? if ($post->video_url) : ?>
<video src="<?= $post->video_url; ?>" controls></video>
<? elseif ($post->thumbnail_id) : ?>
<? use_component('wp-image', [
'image_id' => $post->thumbnail_id,
'class' => 'featured-image'
]); ?>
<? endif; ?>
<div class="text-content"><?= $post->content; ?></div>
</div>
</template>For more examples and advanced usage, check out the WP-Easy Theme starter theme.