5 min read

Modern WordPress Tooling: The Complete Roots.io Workflow

End-to-end WordPress development workflow using the complete Roots.io ecosystem from local development through production deployment

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:

ToolPurpose
BedrockWordPress boilerplate with modern project structure
SageStarter theme with Laravel Blade, Tailwind CSS, and a build pipeline
TrellisAnsible-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 .env files. No more hardcoded database credentials in wp-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:

  1. Start a new project with Bedrock and Sage.
  2. Manage all dependencies (WordPress core, plugins, PHP packages) through Composer.
  3. Build your theme using Blade templates, Composers, and Tailwind CSS with hot reloading.
  4. Version control everything — your entire application is in Git, with .env files excluded.
  5. Provision your server with Trellis — SSL, security hardening, and optimized Nginx configs out of the box.
  6. 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:

  1. Start with Bedrock on your next project. Even without Sage or Trellis, the improved project structure and Composer-managed dependencies are immediately valuable.
  2. Add Sage once you’re comfortable with Bedrock. The Blade templating and Composers will fundamentally change how you think about WordPress theming.
  3. 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. 🌱