Modern WordPress Tooling: The Complete Roots.io Workflow
If you’ve ever inherited a WordPress project and found a tangled mess of manually edited theme files, plugins installed via the admin panel, and a production server that someone FTPs into at 2 AM — you’re not alone. For years, WordPress development felt disconnected from the modern development workflow that frameworks like Laravel, Rails, and Next.js enjoy.
That’s exactly the problem the Roots.io ecosystem was built to solve.
Roots.io offers a complete, opinionated stack that brings modern WordPress development in line with contemporary software engineering practices. We’re talking Composer-managed dependencies, a proper build pipeline, Blade templating, and automated provisioning and deployment — the full devops lifecycle, from your local machine to production.
In this post, I’ll walk you through the entire Roots.io ecosystem and show you how each piece fits together to create a professional, repeatable development workflow for WordPress.
The Three Pillars of the Roots.io Stack
The Roots ecosystem is built on three core projects, each handling a different layer of the stack:
| Tool | Purpose |
|---|---|
| Bedrock | WordPress boilerplate with modern project structure |
| Sage | Starter theme with Laravel Blade, Tailwind CSS, and a build pipeline |
| Trellis | Ansible-based provisioning and deployment for servers |
Think of it this way: Bedrock is your foundation, Sage is your theme layer, and Trellis is your infrastructure. Together, they cover the full journey from git init to a live, SSL-secured production site.
Let’s break each one down.
Bedrock: WordPress as a Proper Application
Traditional WordPress dumps everything — core files, plugins, themes, uploads, config — into one messy directory. Bedrock restructures this into something that actually makes sense for version control and team collaboration.
Here’s what a Bedrock project looks like:
# Create a new Bedrock project
composer create-project roots/bedrock my-wordpress-site
cd my-wordpress-site
The resulting directory structure is immediately refreshing:
my-wordpress-site/
├── config/
│ ├── application.php # Main WordPress config
│ └── environments/
│ ├── development.php
│ ├── staging.php
│ └── production.php
├── web/
│ ├── app/ # wp-content equivalent
│ │ ├── mu-plugins/
│ │ ├── plugins/
│ │ ├── themes/
│ │ └── uploads/
│ ├── wp/ # WordPress core (Composer-managed)
│ └── index.php
├── vendor/
├── composer.json
├── .env # Environment variables
└── .env.example
The key principles here are transformative:
- WordPress core is a Composer dependency. You never touch it. Updating WordPress is just
composer update. - Environment variables via
.envfiles. No more hardcoded database credentials inwp-config.php. - Plugins are managed via Composer, using WordPress Packagist as a repository.
Here’s what your composer.json looks like when managing plugins properly:
{
"name": "your-project/my-wordpress-site",
"type": "project",
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org",
"only": ["wpackagist-plugin/*", "wpackagist-theme/*"]
}
],
"require": {
"php": ">=8.1",
"roots/bedrock-autoloader": "^1.0",
"roots/bedrock-disallow-indexing": "^2.0",
"roots/wordpress": "^6.5",
"roots/wp-config": "^1.0",
"roots/wp-password-bcrypt": "^1.1",
"wpackagist-plugin/advanced-custom-fields": "^6.2",
"wpackagist-plugin/wordpress-seo": "^22.0",
"wpackagist-plugin/wp-migrate-db": "^2.6"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.9"
}
}
Now every developer on your team runs composer install and gets the exact same WordPress installation. No more “works on my machine.” No more mystery plugins that someone installed through the admin panel six months ago.
Your .env file handles per-environment configuration cleanly:
DB_NAME='my_wordpress_site'
DB_USER='db_user'
DB_PASSWORD='secure_password'
DB_HOST='localhost'
WP_ENV='development'
WP_HOME='https://my-wordpress-site.test'
WP_SITEURL="${WP_HOME}/wp"
# Generate these at https://roots.io/salts.html
AUTH_KEY='generate-me'
SECURE_AUTH_KEY='generate-me'
# ... other salt keys
Sage: A Theme That Doesn’t Feel Like 2008
Sage is where the front-end magic happens. It’s a WordPress starter theme that gives you Laravel Blade templating, a modern asset pipeline powered by Bud.js, and a component-driven architecture.
# Inside your Bedrock themes directory
cd web/app/themes
composer create-project roots/sage my-theme
cd my-theme
yarn install
Blade Templating
If you’ve used Laravel, you’ll feel right at home. Instead of the classic WordPress template hierarchy with raw PHP spaghetti, you get clean, composable Blade templates:
{{-- resources/views/partials/content-single.blade.php --}}
<article @php(post_class())>
<header>
<h1 class="entry-title">
{!! $title !!}
</h1>
@include('partials.entry-meta')
</header>
<div class="entry-content">
@php(the_content())
</div>
<footer>
@if($categories)
<div class="category-list">
<span class="font-semibold">Filed under:</span>
@foreach($categories as $category)
<a href="{{ get_category_link($category) }}" class="text-blue-600 hover:underline">
{{ $category->name }}
</a>
@endforeach
</div>
@endif
</footer>
</article>
Composers: Passing Data to Views
Sage uses Composers (inspired by Laravel View Composers) to keep your business logic out of your templates. This is a game-changer for maintainability:
<?php
// app/View/Composers/Post.php
namespace App\View\Composers;
use Roots\Acorn\View\Composer;
class Post extends Composer
{
protected static $views = [
'partials.content',
'partials.content-single',
];
public function with(): array
{
return [
'title' => $this->title(),
'categories' => $this->categories(),
'readTime' => $this->estimatedReadTime(),
];
}
public function title(): string
{
if ($this->view->name() === 'partials.content-single') {
return get_the_title();
}
return sprintf(
'<a href="%s">%s</a>',
get_permalink(),
get_the_title()
);
}
public function categories(): array
{
return get_the_category() ?: [];
}
public function estimatedReadTime(): int
{
$wordCount = str_word_count(strip_tags(get_the_content()));
return max(1, (int) ceil($wordCount / 200));
}
}
The development experience is excellent. Run yarn dev and you get hot module replacement, Tailwind CSS compilation, and all the niceties of a modern front-end workflow.
# Development with HMR
yarn dev
# Production build with minification and cache-busting
yarn build
Trellis: From Zero to Deployed with Ansible
This is where the devops story really shines. Trellis is an Ansible-based tool that handles server provisioning and zero-downtime deployments. It’s designed to work seamlessly with Bedrock.
Your project structure typically looks like this:
your-project/
├── trellis/ # Server provisioning & deployment
└── site/ # Your Bedrock application
Trellis configures everything your WordPress site needs on an Ubuntu server: Nginx, PHP, MariaDB, SSL (via Let’s Encrypt), fail2ban, and more. You define your sites in a simple YAML file:
# trellis/group_vars/production/wordpress_sites.yml
wordpress_sites:
my-wordpress-site.com:
site_hosts:
- canonical: my-wordpress-site.com
redirects:
- www.my-wordpress-site.com
local_path: ../site
repo: git@github.com:your-org/my-wordpress-site.git
repo_subtree_path: site
branch: main
multisite:
enabled: false
ssl:
enabled: true
provider: letsencrypt
cache:
enabled: true
duration: 30s
Provisioning a fresh server is a single command:
cd trellis
# Provision the production server
ansible-playbook server.yml -e env=production
# Deploy your application
ansible-playbook deploy.yml -e env=production -e site=my-wordpress-site.com
Trellis deployments are atomic — they use a symlinked releases directory (similar to Capistrano), so a failed deployment never takes down your live site. Rollbacks are instant.
Putting It All Together
Here’s what the complete workflow looks like in practice:
- Start a new project with Bedrock and Sage.
- Manage all dependencies (WordPress core, plugins, PHP packages) through Composer.
- Build your theme using Blade templates, Composers, and Tailwind CSS with hot reloading.
- Version control everything — your entire application is in Git, with
.envfiles excluded. - Provision your server with Trellis — SSL, security hardening, and optimized Nginx configs out of the box.
- Deploy with a single command — Trellis pulls from your repo, runs Composer install, builds assets, and swaps the symlink. Zero downtime.
This modern WordPress approach means every environment is reproducible, every change is tracked, and deployments are boring (in the best possible way).
Conclusion & Next Steps
The Roots.io ecosystem isn’t just a collection of tools — it’s a philosophy: WordPress deserves the same engineering rigor as any other web application. Once you experience this development workflow, going back to manual FTP deployments and admin-panel plugin management feels physically painful.
Here’s how I’d recommend getting started:
- Start with Bedrock on your next project. Even without Sage or Trellis, the improved project structure and Composer-managed dependencies are immediately valuable.
- Add Sage once you’re comfortable with Bedrock. The Blade templating and Composers will fundamentally change how you think about WordPress theming.
- Introduce Trellis when you’re ready to own your deployment pipeline. Start with a staging server to build confidence.
Check out the excellent Roots.io documentation and their active Discourse community. The learning curve is real, but the payoff — in productivity, reliability, and developer happiness — is enormous.
Happy building. 🌱