Repository: roots/docs Branch: docs Commit: b9b2674e1a18 Files: 101 Total size: 352.4 KB Directory structure: gitextract_yjm39ipx/ ├── .github/ │ └── workflows/ │ ├── lint-bash-blocks.yml │ └── trigger-docs-sync.yml ├── .gitignore ├── CLAUDE.md ├── README.md ├── acorn/ │ ├── available-packages.md │ ├── compatibility.md │ ├── controllers-middleware-kernel.md │ ├── creating-and-processing-laravel-queues.md │ ├── creating-and-running-laravel-migrations.md │ ├── creating-wp-cli-commands-with-artisan-console.md │ ├── directory-structure.md │ ├── eloquent-models.md │ ├── error-handling.md │ ├── installation.md │ ├── laravel-cache-alternative-to-wordpress-transients.md │ ├── laravel-redis-configuration.md │ ├── logging.md │ ├── package-development.md │ ├── rendering-blade-views.md │ ├── routing.md │ ├── upgrading-acorn.md │ ├── using-livewire-with-wordpress.md │ └── wp-cli.md ├── bedrock/ │ ├── auditing-wordpress-vulnerabilities-with-composer.md │ ├── bedrock-with-ddev.md │ ├── bedrock-with-devkinsta.md │ ├── bedrock-with-lando.md │ ├── bedrock-with-local.md │ ├── bedrock-with-valet.md │ ├── compatibility.md │ ├── composer.md │ ├── configuration.md │ ├── converting-wordpress-sites-to-bedrock.md │ ├── deployment.md │ ├── disable-plugins-based-on-environment.md │ ├── environment-variables.md │ ├── folder-structure.md │ ├── installation.md │ ├── local-development.md │ ├── mu-plugin-autoloader.md │ ├── patching-wordpress-plugins-with-composer.md │ ├── patching-wordpress-with-composer.md │ ├── private-or-commercial-wordpress-plugins-as-composer-dependencies.md │ ├── server-configuration.md │ ├── testing.md │ └── wp-cron.md ├── netlify.toml ├── sage/ │ ├── adding-linting.md │ ├── blade-templates.md │ ├── bootstrap.md │ ├── compatibility.md │ ├── compiling-assets.md │ ├── components.md │ ├── composers.md │ ├── configuration.md │ ├── deployment.md │ ├── fonts-setup.md │ ├── functionality.md │ ├── gutenberg.md │ ├── installation.md │ ├── localization.md │ ├── sass.md │ ├── structure.md │ ├── tailwind-css.md │ ├── theme-templates.md │ ├── use-blade-icons.md │ └── woocommerce.md └── trellis/ ├── ansible.md ├── cli.md ├── composer-authentication.md ├── configuring-php.md ├── cron-jobs.md ├── database-access.md ├── debugging-php.md ├── deploy-to-digitalocean.md ├── deploy-to-hetzner-cloud.md ├── deploy-with-github-actions.md ├── deployments.md ├── existing-projects.md ├── fastcgi-caching.md ├── install-wordpress-language-files.md ├── installation.md ├── local-development.md ├── mail.md ├── multiple-sites.md ├── multisite.md ├── nginx-includes.md ├── passwords.md ├── python.md ├── redis.md ├── remote-server-setup.md ├── sage-integration.md ├── security.md ├── server-logs.md ├── ssh-keys.md ├── ssl.md ├── troubleshooting.md ├── user-contributed-extensions.md ├── vault.md └── wordpress-sites.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/lint-bash-blocks.yml ================================================ name: Lint bash code blocks on: pull_request: paths: - '**/*.md' jobs: check-bash-blocks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check for multi-line bash code blocks run: | fail=0 while IFS= read -r file; do # Strip leading ./ so GitHub annotations link to the correct file clean="${file#./}" awk -v file="$clean" ' /^```bash/ { in_block=1; lines=0; start=NR; next } /^```/ && in_block { if (lines > 1) { printf "::error file=%s,line=%d::Bash code block has multiple commands. Each block must contain exactly one command.\n", file, start found=1 } in_block=0; next } in_block && /^[^#]/ && !/^[[:space:]]*$/ { lines++ } END { if (found) exit 1 } ' "$file" || fail=1 done < <(find . -name '*.md' -not -path './.git/*' -not -name 'CLAUDE.md') if [ "$fail" -eq 1 ]; then echo "" echo "Error: Found bash code blocks with multiple commands." echo "Each bash code block must contain exactly one command." exit 1 fi echo "All bash code blocks contain a single command." ================================================ FILE: .github/workflows/trigger-docs-sync.yml ================================================ name: Trigger docs sync on: push: branches: [docs] paths: - 'acorn/**' - 'bedrock/**' - 'sage/**' - 'trellis/**' jobs: trigger: runs-on: ubuntu-latest steps: - name: Trigger docs sync on roots.dev run: gh workflow run docs-sync.yml --repo roots/roots.dev env: GH_TOKEN: ${{ secrets.ROOTS_DEV_TOKEN }} ================================================ FILE: .gitignore ================================================ ================================================ FILE: CLAUDE.md ================================================ # Docs Conventions ## Markdown bash code blocks Each bash code block must contain exactly one command. Never combine multiple commands in a single block. On the roots.io front-end, bash code blocks have a clipboard copy button that only supports single lines. Good: ````markdown ```bash composer require example/package ``` ```bash wp plugin activate example ``` ```` Bad: ````markdown ```bash composer require example/package wp plugin activate example ``` ```` ================================================ FILE: README.md ================================================ # Roots Documentation This repository is synced with the Roots docs for our primary projects: - [Acorn Docs](https://roots.io/acorn/docs/) - [Bedrock Docs](https://roots.io/bedrock/docs/) - [Sage Docs](https://roots.io/sage/docs/) - [Trellis Docs](https://roots.io/trellis/docs/) ## Contributing Please use [Roots Discourse](https://discourse.roots.io/) to open a discussion about bigger changes before sending a pull request. If you have an account on Roots Discourse, add your username in the authors list (in alphabetical order) at the top of any docs that you contribute to. ================================================ FILE: acorn/available-packages.md ================================================ --- date_modified: 2026-03-03 12:00 date_published: 2021-11-19 11:58 description: Explore community-developed packages for Acorn and Sage. WooCommerce integration, additional Laravel features, and third-party extensions. title: Community Packages for Acorn authors: - alwaysblank - ben - QWp6t --- # Community Packages for Acorn | Package | Description | | ----------- | ----------- | | [`roots/acorn-ai`](https://github.com/roots/acorn-ai) | WordPress Abilities API integration and AI support for Acorn | | [`roots/acorn-fse-helper`](https://github.com/roots/acorn-fse-helper) | Bootstrap FSE support in Acorn-based WordPress themes | | [`roots/acorn-mail`](https://github.com/roots/acorn-mail) | A simple package handling WordPress SMTP using Acorn's mail configuration | | [`roots/acorn-post-types`](https://github.com/roots/acorn-post-types) | Simple post types and taxonomies using Extended CPTs | | [`roots/acorn-prettify`](https://github.com/roots/acorn-prettify) | A collection of modules to apply theme-agnostic front-end modifications to your Acorn-powered WordPress sites | | [`roots/acorn-user-roles`](https://github.com/roots/acorn-user-roles) | Simple user role management for Acorn | ## Community packages | Package | Description | | ----------- | ----------- | | [`40q/40q-seo-assistant`](https://github.com/40Q/40q-seo-assistant) | Editor-side SEO metadata suggestions for WordPress powered by Acorn | | [`blavetstudio/sage-woocommerce-subscriptions`](https://github.com/blavetstudio/sage-woocommerce-subscriptions) | Add WooCommerce Subscriptions support to Sage 10 | | [`digitalnodecom/substrate`](https://github.com/digitalnodecom/substrate) | AI MCP for Development with Bedrock, Acorn, Sage | | [`generoi/sage-cachetags`](https://github.com/generoi/sage-cachetags) | A sage package for tracking what data rendered pages rely on using Cache Tags | | [`generoi/sage-woocommerce`](https://github.com/generoi/sage-woocommerce) | Add WooCommerce support to Sage 10 | | [`istogram/wp-api-content-migration`](https://github.com/istogram/wp-api-content-migration) | Migrate WordPress content using the WP REST API | | [`leocolomb/wp-acorn-cache`](https://github.com/LeoColomb/wp-acorn-cache) | A WordPress cache manager powered by Laravel through Acorn | | [`millipress/acorn-millicache`](https://github.com/MilliPress/Acorn-MilliCache) | MilliCache integration for Roots Acorn and Bedrock projects | | [`log1x/acf-composer`](https://github.com/log1x/acf-composer) | ACF Composer is the ultimate tool for creating fields, blocks, widgets, and option pages using ACF Builder alongside Sage 10 | | [`log1x/acorn-disable-media-pages`](https://github.com/log1x/acorn-disable-media-pages) | Disable media attachment pages on WordPress sites using Acorn | | [`log1x/pagi`](https://github.com/log1x/pagi) | A better WordPress pagination utilizing Laravel's Pagination | | [`log1x/poet`](https://github.com/log1x/poet) | Poet provides simple configuration-based post type, taxonomy, editor color palette, block category, block pattern and block registration/modification | | [`log1x/sage-directives`](https://github.com/log1x/sage-directives) | A variety of useful Blade directives for use with Sage 10 including directives for WordPress, ACF, and various miscellaneous helpers | | [`log1x/sage-html-forms`](https://github.com/log1x/sage-html-forms) | This is a simple package for the HTML Forms plugin that allows you to easily render forms using a corresponding Blade view (if one is present) with Sage 10 | | [`log1x/sage-svg`](https://github.com/log1x/sage-svg) | Sage SVG is a simple package for using inline SVGs in your Sage 10 projects | | [`pixelcollective/acorn-db`](https://github.com/pixelcollective/acorn-db) | Provides Sage 10 and other Acorn projects with an eloquent Model layer straight from the heart of the Laravel framework | | [`supermundano/sage-the-events-calendar`](https://github.com/supermundano/sage-the-events-calendar) | Add The Events Calendar support to Sage 10 | | [`tombroucke/sage-html-forms-export-submissions`](https://github.com/tombroucke/sage-html-forms-export-submissions) | Export HTML Forms submissions to Excel and CSV | ================================================ FILE: acorn/compatibility.md ================================================ --- date_modified: 2026-05-05 16:35 date_published: 2024-04-26 10:35 description: Known compatibility issues between WordPress plugins and Acorn, including solutions and workarounds for common integration conflicts. title: WordPress Plugin Compatibility with Acorn authors: - ben - dalepgrant - joshf --- # WordPress Plugin Compatibility with Acorn Acorn is installed via Composer and includes many dependencies, that also include their own dependencies. WordPress plugin authors often include their own dependencies in a way that can conflict with Acorn. Compatibility issues that arise in Acorn with other WordPress plugins are most often related to a WordPress plugin that is including an older version of a dependency that exists in the Acorn dependency tree. **Plugin developers need to wrap their dependencies with their own namespace** in order to prevent conflicts with other plugins and with Acorn. The following tools can be used to handle this: * [PHP-Scoper](https://github.com/humbug/php-scoper) * [Imposter Plugin](https://github.com/TypistTech/imposter-plugin) * [Mozart](https://github.com/coenjacobs/mozart) **Acorn has no responsibility to fix compatibility issues that are the result of plugins that don't wrap their dependencies with their own namespace.** ## Known issues with plugins Composer patches can sometimes be used to work around issues with plugins. * **Google for WooCommerce** includes older versions of `psr/log` and `monolog/monolog`. [Patch available](https://gist.github.com/retlehs/3dfd033e196c25e376acbeb89fa41dbd). * **Gravity Forms** merge tags JS causes an error on the admin notifications page. [@tombroucke provided a workaround in roots/acorn#198](https://github.com/roots/acorn/issues/198#issuecomment-1365942893). * **Gravity Forms: Entry Automation FTP Extension** includes `league/flysystem` v1.1.4 which is incompatible with Acorn. * **WooCommerce PayPal Payments** includes an older version of `psr/log`. * **WooCommerce UPS Shipping** includes an older version of `psr/log`. [Patch available](https://gist.github.com/retlehs/4e76aee9a30cc0d3228cf6146eec64e0). * **WooCommerce USPS Shipping** includes an older version of `psr/log`. [Patch available](https://gist.github.com/retlehs/4e76aee9a30cc0d3228cf6146eec64e0). For more information on how to use Composer patches to resolve plugin conflicts, see [Patching WordPress Plugins with Composer](/bedrock/docs/patching-wordpress-plugins-with-composer/). ================================================ FILE: acorn/controllers-middleware-kernel.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2025-10-01 00:00 description: Build robust APIs and handle requests with Laravel controllers, middleware, and custom HTTP kernels in WordPress using Acorn. Clean separation of concerns with validation, authentication, and response formatting. title: Controllers, Middleware, and HTTP Kernel in WordPress authors: - ben --- # Controllers, Middleware, and HTTP Kernel in WordPress Acorn brings Laravel's controller and middleware system to WordPress, enabling you to build robust APIs, handle complex request logic, and implement clean separation of concerns. Controllers organize your route logic, while middleware provides a convenient mechanism for filtering HTTP requests. We recommend referencing the [Laravel docs on Controllers](https://laravel.com/docs/13.x/controllers) and [Middleware](https://laravel.com/docs/13.x/middleware) for a complete understanding. ## Creating controllers ### Generate a controller To create a new controller, use the `make:controller` Artisan command: #### Create a basic controller ```bash $ wp acorn make:controller PostController ``` #### Create an API resource controller ```bash $ wp acorn make:controller PostController --api ``` #### Create a controller with all CRUD methods ```bash $ wp acorn make:controller PostController --resource ``` This creates a new controller file in `app/Http/Controllers/`. ### Basic controller example ```php latest('ID') ->take(10) ->get(); return response()->json($posts); } public function show(int $id): JsonResponse { $post = Post::findOrFail($id); return response()->json($post); } public function store(Request $request): JsonResponse { $validated = $request->validate([ 'title' => 'required|max:255', 'content' => 'required', 'status' => 'in:draft,publish' ]); $post = Post::create([ 'post_title' => $validated['title'], 'post_content' => $validated['content'], 'post_status' => $validated['status'] ?? 'draft', 'post_type' => 'post', 'post_author' => get_current_user_id() ?: 1, ]); return response()->json($post, 201); } } ``` ### Using controllers in routes Define your routes in `routes/web.php`: ```php validate([ 'title' => 'required|max:255', 'content' => 'required', ]); $post_id = wp_insert_post([ 'post_title' => $validated['title'], 'post_content' => $validated['content'], 'post_status' => 'publish', 'post_type' => 'post', ]); return response()->json(['id' => $post_id], 201); } } ``` ## Creating middleware ### Generate middleware To create new middleware, use the `make:middleware` Artisan command: ```bash $ wp acorn make:middleware AuthenticateAdmin ``` This creates a new middleware file in `app/Http/Middleware/`. ### Authentication middleware example ```php json([ 'message' => 'Authentication required' ], 401); } if (!current_user_can('manage_options')) { return response()->json([ 'message' => 'Admin access required' ], 403); } return $next($request); } } ``` ## Applying middleware Apply middleware to routes: ```php middleware(AuthenticateAdmin::class); ``` ## Customizing the HTTP kernel For most middleware needs, use the `withMiddleware()` method when [booting Acorn](/acorn/docs/installation/#advanced-booting). If you need more control, you can override the HTTP kernel entirely. ### Creating a custom kernel Create a custom kernel class that extends Acorn's HTTP kernel. When overriding properties like `$middleware`, make sure to include any defaults you still need — setting the property replaces the parent's values entirely: ```php middleware[] = \Illuminate\Foundation\Http\Middleware\TrimStrings::class; parent::__construct($app, $router); } } ``` ### Registering the custom kernel Override the kernel singleton by rebinding it before `boot()`. The kernel is resolved during boot, so the binding must be in place before that happens: ```php use Roots\Acorn\Application; add_action('after_setup_theme', function () { $builder = Application::configure() ->withProviders() ->withRouting( web: base_path('routes/web.php'), wordpress: true, ); app()->singleton( \Illuminate\Contracts\Http\Kernel::class, \App\Http\Kernel::class ); $builder->boot(); }, 0); ``` ================================================ FILE: acorn/creating-and-processing-laravel-queues.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2025-09-29 00:00 description: Use Laravel's queue system in WordPress through Acorn. Process background jobs, handle async tasks, and schedule recurring operations efficiently. title: Creating and Processing Laravel Queues authors: - ben --- # Creating and Processing Laravel Queues Acorn brings Laravel's robust queue system to WordPress, enabling you to defer time-consuming tasks like image processing, email sending, or API calls to background jobs. This improves your application's response time and user experience by handling heavy operations asynchronously. We recommend referencing the [Laravel docs on Queues](https://laravel.com/docs/13.x/queues) for a complete understanding of the queue system. ## Setting up the queue system Before you can start using queues, you need to create the necessary database tables to store jobs and track their status. ### 1. Generate queue tables Create the migration files for queue functionality: #### Generate the jobs table migration ```bash $ wp acorn queue:table ``` #### Generate the job batches table (optional, for batch processing) ```bash $ wp acorn queue:batches-table ``` ### 2. Run migrations Apply the migrations to create the required tables: ```bash $ wp acorn migrate ``` This will create: - A `jobs` table to store queued jobs - A `job_batches` table for batch job processing (if generated) - A `failed_jobs` table to track failed job attempts ## Creating your first job To create a new job class, use the `make:job` command: ```bash $ wp acorn make:job ProcessImageOptimization ``` This creates a new job file in `app/Jobs/` with the basic structure needed for a queue job. ### Job file structure A typical job class contains several key components: ```php attachmentId = $attachmentId; } /** * Execute the job */ public function handle(): void { Log::info("Processing image optimization for attachment: {$this->attachmentId}"); $attachment = get_post($this->attachmentId); if (!$attachment || $attachment->post_type !== 'attachment') { Log::error("Invalid attachment ID: {$this->attachmentId}"); return; } $file_path = get_attached_file($this->attachmentId); // Your image optimization logic here // For example, using an image optimization library // Mark as processed using post meta update_post_meta($this->attachmentId, '_processed', true); update_post_meta($this->attachmentId, '_processed_at', current_time('timestamp')); Log::info("Successfully optimized image: {$this->attachmentId}"); } /** * Handle a job failure */ public function failed(\Throwable $exception): void { Log::error("Failed to optimize image {$this->attachmentId}: {$exception->getMessage()}"); // Notify administrators or take other actions } } ``` ## Dispatching jobs Once you've created a job, you can dispatch it from anywhere in your application: ### Basic dispatching ```php use App\Jobs\ProcessImageOptimization; // Dispatch a job to the default queue ProcessImageOptimization::dispatch($attachmentId); // Dispatch with a delay ProcessImageOptimization::dispatch($attachmentId) ->delay(now()->addMinutes(5)); // Dispatch to a specific queue ProcessImageOptimization::dispatch($attachmentId) ->onQueue('images'); ``` ### WordPress hook integration Integrate queue jobs with WordPress hooks for automatic processing: ```php // In your theme's functions.php or a service provider add_action('add_attachment', function ($attachmentId) { \App\Jobs\ProcessImageOptimization::dispatch($attachmentId); }); // Process form submissions asynchronously add_action('gform_after_submission', function ($entry, $form) { \App\Jobs\ProcessFormSubmission::dispatch($entry['id']); }, 10, 2); ``` ## Processing queued jobs To process jobs in the queue, you need to run a queue worker. ### Running a queue worker #### Process jobs continuously ```bash $ wp acorn queue:work ``` #### Process jobs from a specific queue ```bash $ wp acorn queue:work --queue=high,default ``` #### Process a single job and exit ```bash $ wp acorn queue:work --once ``` #### Process jobs for a specific duration ```bash $ wp acorn queue:work --stop-when-empty ``` ## Managing failed jobs When jobs fail after all retry attempts, they're moved to the `failed_jobs` table. ### View failed jobs ```bash $ wp acorn queue:failed ``` ### Retry all failed jobs ```bash $ wp acorn queue:retry all ``` ### Retry specific job ```bash $ wp acorn queue:retry 5 ``` ### Retry multiple jobs ```bash $ wp acorn queue:retry 5 6 7 ``` ### Remove all failed jobs ```bash $ wp acorn queue:flush ``` ### Remove a specific failed job ```bash $ wp acorn queue:forget 5 ``` ## Dispatching jobs ```php // In a controller or WordPress hook use App\Jobs\ProcessImageOptimization; // Dispatch immediately ProcessImageOptimization::dispatch($attachmentId); // Dispatch with delay ProcessImageOptimization::dispatch($attachmentId)->delay(now()->addMinutes(5)); ``` ================================================ FILE: acorn/creating-and-running-laravel-migrations.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2025-08-06 14:00 description: Use Laravel's migration system in WordPress through Acorn. Create, modify, and manage custom database tables with Artisan migration commands. title: Creating and Running Laravel Migrations authors: - ben --- # Creating and Running Laravel Migrations Acorn brings Laravel's powerful migration system to WordPress, allowing you to version control your database schema and manage custom tables with ease. Each migration contains instructions for creating or modifying database tables. We recommend referencing the [Laravel docs on Database Migrations](https://laravel.com/docs/13.x/migrations) for a complete understanding of the migration system. ## Creating your first migration To create a new migration, use the `make:migration` command: ```bash $ wp acorn make:migration create_app_settings_table ``` This will create a new migration file in `database/migrations/` with a timestamp prefix, like `2025_08_06_140000_create_app_settings_table.php`. ### Migration file structure A typical migration contains two methods: - `up()` - Defines changes to apply - `down()` - Defines how to reverse those changes Here's an example migration for an app settings table: ```php id(); $table->string('key')->unique(); $table->json('value')->nullable(); $table->string('group')->default('general'); $table->boolean('is_public')->default(false); $table->text('description')->nullable(); $table->timestamps(); $table->index('group'); $table->index('is_public'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('app_settings'); } }; ``` ## Running migrations To run all pending migrations: ```bash $ wp acorn migrate ``` To see the status of your migrations: ```bash $ wp acorn migrate:status ``` To rollback the last batch of migrations: ```bash $ wp acorn migrate:rollback ``` ## Adding columns to existing tables To add columns to an existing table, create a new migration: ```bash $ wp acorn make:migration add_encrypted_to_app_settings_table ``` ```php public function up(): void { Schema::table('app_settings', function (Blueprint $table) { $table->boolean('is_encrypted')->default(false)->after('is_public'); }); } public function down(): void { Schema::table('app_settings', function (Blueprint $table) { $table->dropColumn('is_encrypted'); }); } ``` ## Deployment You should run migrations as part of your deployment process. Add this to your deployment script after `wp acorn optimize`: ```bash $ wp acorn optimize ``` ```bash $ wp acorn migrate --force ``` The `--force` flag runs migrations without confirmation prompts in production environments. ## Troubleshooting If you get an error about the migrations table not existing, run: ```bash $ wp acorn migrate:install ``` ================================================ FILE: acorn/creating-wp-cli-commands-with-artisan-console.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2025-09-28 00:00 description: Create custom WP-CLI commands using Laravel's Artisan Console system with Acorn. Extend WordPress CLI with powerful Laravel functionality. title: Creating WP-CLI Commands with Artisan Console authors: - ben --- # Creating WP-CLI Commands with Artisan Console Acorn brings Laravel's powerful Artisan Console system to WordPress, allowing you to create custom WP-CLI commands with the same elegance and functionality you'd expect from Laravel. This enables you to build sophisticated command-line tools that integrate seamlessly with both WordPress and Laravel features. We recommend referencing the [Laravel docs on Artisan Console](https://laravel.com/docs/13.x/artisan) for a complete understanding of the console system. ## Creating your first command To create a new WP-CLI command, use the `make:command` Artisan command: ```bash $ wp acorn make:command SeoAuditCommand ``` This will create a new command file in `app/Console/Commands/` with the basic structure needed for a custom command. ### Command file structure A typical Artisan command contains several key properties and methods: - `$signature` - Defines the command name, arguments, and options - `$description` - Provides a description for the command - `handle()` - Contains the command logic Here's a basic example for auditing SEO: ```php option('post-type'); $limit = (int) $this->option('limit'); $this->components->info("Auditing {$postType} posts for SEO issues..."); $posts = get_posts([ 'post_type' => $postType, 'post_status' => 'publish', 'numberposts' => $limit, ]); if (empty($posts)) { $this->components->warn('No posts found to audit.'); return 0; } $issues = []; foreach ($posts as $post) { $postIssues = $this->auditPost($post); if (!empty($postIssues)) { $issues[$post->ID] = [ 'title' => $post->post_title, 'issues' => $postIssues, ]; } } if (empty($issues)) { $this->components->info('No SEO issues found! 🎉'); return 0; } $this->displayIssues($issues); return 0; } protected function auditPost($post) { $issues = []; $seoTitle = get_post_meta($post->ID, '_genesis_title', true) ?: $post->post_title; if (strlen($seoTitle) < 30) { $issues[] = 'SEO title too short (< 30 chars)'; } if (strlen($seoTitle) > 60) { $issues[] = 'SEO title too long (> 60 chars)'; } $description = get_post_meta($post->ID, '_genesis_description', true); if (empty($description)) { $issues[] = 'Missing SEO meta description'; } elseif (strlen($description) < 120) { $issues[] = 'Meta description too short (< 120 chars)'; } elseif (strlen($description) > 160) { $issues[] = 'Meta description too long (> 160 chars)'; } return $issues; } protected function displayIssues($issues) { $this->components->error('Found ' . count($issues) . ' posts with SEO issues:'); $this->newLine(); foreach ($issues as $postId => $data) { $this->components->twoColumnDetail( "Post #{$postId}", $data['title'] ); foreach ($data['issues'] as $issue) { $this->line(" → {$issue}"); } $this->newLine(); } } } ``` ## Command signature syntax ```php // Basic command protected $signature = 'newsletter:send'; // With arguments protected $signature = 'user:create {name} {email}'; // With options protected $signature = 'seo:audit {--post-type=post}'; ``` ## Running your commands Once created, your commands are automatically available through WP-CLI: #### Run your SEO audit command ```bash $ wp acorn seo:audit ``` #### Run with options ```bash $ wp acorn seo:audit --post-type=page --limit=50 ``` #### Get help for a command ```bash $ wp acorn help seo:audit ``` ## Console output ```php public function handle() { $this->info('Success message'); $this->error('Error message'); // Ask for input $name = $this->components->ask('What is your name?'); // Use WordPress functions $posts = get_posts(['numberposts' => 10]); return 0; // Success } ``` ================================================ FILE: acorn/directory-structure.md ================================================ --- date_modified: 2025-07-22 13:34 date_published: 2021-11-19 11:58 description: Acorn works with zero configuration by default. Optionally publish config files to use Laravel's familiar directory structure in WordPress. title: Acorn Application Directory Structure authors: - alwaysblank - ben - rafaucau - QWp6t --- # Acorn Application Directory Structure ## Zero-config setup Out of the box, Acorn will [use its own configs](https://github.com/roots/acorn/tree/main/config), and it will keep the application cache and logs in the standard WordPress cache directory: ```plaintext [wp-content]/ # wp-content directory ("app" if you're using Bedrock) ├── cache/ │ └── /acorn/ # Private application storage ("storage" directory) │ ├── app/ # Files generated or used by the application │ ├── framework/ # Files generated or used by Acorn (never edit) │ └── logs/ # Application logs └── themes/ └── [theme]/ # Theme directory (e.g., "sage") ├── app/ # Core application code ├── public/ # Built application assets (never edit) ├── resources/ # Uncompiled source assets and views │ └── views/ # Application views to be compiled by Blade └── vendor/ # Composer packages (never edit) ``` ## Traditional setup Acorn also supports a more traditional Laravel-esque structure. We recommend this approach if you are adding Acorn/Laravel packages and want to have more control over your app. ::: tip If you've installed Acorn from your Bedrock project root, Acorn's `config/` directory will conflict with Bedrock's. We recommend using [Radicle](/radicle/) to avoid this.

There are no conflicts with the `config/` directory if you've installed Acorn from your theme. ::: ```plaintext root/ # Base directory for your Acorn application (e.g., "sage") ├── app/ # Core application code ├── config/ # Application configuration ├── public/ # Built application assets (never edit) ├── resources/ # Uncompiled source assets and views │ └── views/ # Application views to be compiled by Blade ├── storage/ # Private application storage │ ├── app/ # Files generated or used by the application │ ├── framework/ # Files generated or used by Acorn (never edit) │ └── logs/ # Application logs └── vendor/ # Composer packages (never edit) ``` You can manually create a `config/` directory, or you can automatically set up the traditional structure with WP-CLI (see below). If you have a `config/` directory, you can drop your desired config files in there. any that are missing (such as `app.php`) will just be pulled from [Acorn's config directory](https://github.com/roots/acorn/tree/main/config). ### WP-CLI commands for setting up the traditional structure You can automatically set up the traditional structure via WP-CLI: ```shell $ wp acorn acorn:init storage && wp acorn vendor:publish --tag=acorn ``` Alternatively, you can choose to only copy the config files. ```shell $ wp acorn vendor:publish --tag=acorn ``` ## Advanced directory modifications You can modify the path for any Acorn directory by defining the following constants: - `ACORN_BASEPATH` - `ACORN_APP_PATH` - `ACORN_CONFIG_PATH` - `ACORN_STORAGE_PATH` - `ACORN_RESOURCES_PATH` - `ACORN_PUBLIC_PATH` ================================================ FILE: acorn/eloquent-models.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2025-10-01 00:00 description: Use Laravel's Eloquent ORM in WordPress with Acorn. Create models for WordPress posts, users, and custom tables with relationships, scopes, and clean query syntax. title: Using Eloquent Models in WordPress authors: - ben --- # Using Eloquent Models in WordPress Acorn brings Laravel's powerful Eloquent ORM to WordPress, allowing you to interact with WordPress data using clean, expressive syntax. Create models for posts, users, custom tables, and more with relationships, scopes, and all the Eloquent features you love. We recommend referencing the [Laravel docs on Eloquent](https://laravel.com/docs/13.x/eloquent) for a complete understanding of the ORM. ## Creating your first model Since Acorn doesn't include the `make:model` command, you'll need to create model files manually. Create a new PHP file in the `app/Models/` directory with the following structure. ## WordPress post model Here's an example of an Eloquent model for WordPress posts: ```php belongsTo(User::class, 'post_author'); } public function meta() { return $this->hasMany(PostMeta::class, 'post_id'); } public function scopePublished($query) { return $query->where('post_status', 'publish'); } public function scopeOfType($query, $type) { return $query->where('post_type', $type); } } ``` ### Key considerations for WordPress models When creating models for WordPress tables, keep these points in mind: - **Table names**: WordPress tables don't follow Laravel naming conventions, so explicitly set the `$table` property - **Primary keys**: WordPress uses `ID` (uppercase) instead of `id`, so set `$primaryKey = 'ID'` - **Timestamps**: WordPress handles timestamps differently, so set `$timestamps = false` and handle dates manually - **Table prefixes**: WordPress table prefixes are handled automatically by WordPress's database configuration ## WordPress user model ```php hasMany(Post::class, 'post_author'); } public function meta() { return $this->hasMany(UserMeta::class, 'user_id'); } } ``` ## Post meta model ```php belongsTo(Post::class, 'post_id'); } } ``` ## Using models in your application ### Basic queries ```php // Get all published posts $posts = Post::published()->get(); // Get posts of a specific type $pages = Post::ofType('page')->published()->get(); // Get a post with its author $post = Post::with('author')->find(123); // Create a new post $post = Post::create([ 'post_title' => 'Hello World', 'post_content' => 'This is my first post using Eloquent!', 'post_status' => 'publish', 'post_type' => 'post', 'post_author' => get_current_user_id(), ]); ``` ### Working with relationships ```php // Get a post's author $post = Post::find(123); $author = $post->author; // Get an author's posts $user = User::find(1); $posts = $user->posts()->published()->get(); // Get post meta $post = Post::with('meta')->find(123); foreach ($post->meta as $meta) { echo $meta->meta_key . ': ' . $meta->meta_value; } ``` ## Custom tables You can also create models for custom database tables: ```php Boot Acorn in your own theme or plugin Add the following in your theme's `functions.php` file, or in your main plugin file: ```php 'https://roots.io/acorn/docs/installation/', 'link_text' => __('Acorn Docs: Installation', 'domain'), ] ); } add_action('after_setup_theme', function () { Application::configure() ->withProviders([ App\Providers\ThemeServiceProvider::class, ]) ->boot(); }, 0); ``` ### Advanced booting Acorn provides several additional configuration methods that can be chained before booting. Here's a comprehensive example with explanations: ```php add_action('after_setup_theme', function () { Application::configure() ->withProviders([ // Register your service providers App\Providers\ThemeServiceProvider::class, ]) ->withMiddleware(function (Middleware $middleware) { // Configure HTTP middleware for WordPress requests $middleware->wordpress([ Illuminate\Cookie\Middleware\EncryptCookies::class, Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, Illuminate\Session\Middleware\StartSession::class, Illuminate\View\Middleware\ShareErrorsFromSession::class, Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, Illuminate\Routing\Middleware\SubstituteBindings::class, ]); // You can also configure middleware for web and API routes // $middleware->web([...]); // $middleware->api([...]); }) ->withExceptions(function (Exceptions $exceptions) { // Configure exception handling // $exceptions->reportable(function (\Throwable $e) { // Log::error($e->getMessage()); // }); }) ->withRouting( // Configure routing with named parameters web: base_path('routes/web.php'), // Laravel-style web routes api: base_path('routes/api.php'), // API routes wordpress: true, // Enable WordPress request handling ) ->boot(); }, 0); ``` ## Add the autoload dump script Acorn has a function that should be added to the `scripts` section of your `composer.json` file for the `post-autoload-dump` event. To automatically configure this script, run the following command: ```shell $ wp acorn acorn:install ``` Select **Yes** when prompted to install the Acorn autoload dump script. ::: warning `wp acorn` commands won't work if your theme/plugin that boots Acorn hasn't been activated and will result in the following message: **Error: 'acorn' is not a registered wp command.** :::
Manually adding Acorn's post autoload dump function Open `composer.json` and add Acorn's `postAutoloadDump` function to Composer's `post-autoload-dump` event in the `scripts`: ```json "scripts": { //... "post-autoload-dump": [ "Roots\\Acorn\\ComposerScripts::postAutoloadDump" ] } ```
## Server requirements Acorn's server requirements are minimal, and mostly come from WordPress and [Laravel 13's requirements](https://laravel.com/docs/13.x/deployment#server-requirements). - PHP >=8.3 with extensions: Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer, XML - WordPress >= 5.4 - [WP-CLI](https://wordpress.org/cli/) ================================================ FILE: acorn/laravel-cache-alternative-to-wordpress-transients.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2023-01-30 17:32 description: Use Laravel's caching system instead of WordPress Transients. Acorn supports multiple cache drivers including Redis, Memcached, and files. title: Laravel Cache as an Alternative to WordPress Transients authors: - ben --- # Laravel Cache as an Alternative to WordPress Transients Acorn provides [Laravel integration with WordPress](/acorn/), which means that certain Laravel components are able to be used within your WordPress site. Compared to WordPress transients API, [Laravel Cache](https://laravel.com/docs/13.x/cache) provides a more standardized and developer-friendly approach to caching data. It also has a wider range of cache storage options, compared to the WordPress Transients API, which only supports storing data in the WordPress database. ::: tip Review the [Laravel Cache docs](https://laravel.com/docs/13.x/cache) to get a more detailed understanding about how it works, along with the various ways that the cache can be configured ::: ## Storing data in the cache ```php use Illuminate\Support\Facades\Cache; Cache::put('key', 'value', $minutes); ``` ## Retrieving data from the cache ```php use Illuminate\Support\Facades\Cache; $value = Cache::get('key'); ``` ## Removing items from the cache ```php use Illuminate\Support\Facades\Cache; Cache::forget('key'); ``` You can also use Acorn's WP-CLI integration to interact with the cache: ```shell $ wp acorn cache:clear ``` ================================================ FILE: acorn/laravel-redis-configuration.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2025-10-27 10:00 title: Laravel Redis Configuration for Acorn description: Configure Redis with Laravel and Acorn in WordPress. Enable high-performance caching using PhpRedis or Predis with your WordPress sites. authors: - ben - Log1x --- # Laravel Redis Configuration for Acorn Acorn provides [Laravel integration with WordPress](/acorn/), which means that Laravel's Redis setup can be configured to work on your WordPress sites. We recommend referencing the [Laravel docs on Redis](https://laravel.com/docs/13.x/redis) for a complete understanding of the integration. ## Requirements The [PhpRedis PECL extension](https://github.com/phpredis/phpredis), or the [`predis/predis`](https://github.com/predis/predis) package are required in order to use Redis. ## Configuration Add the Laravel Redis package as a dependency: ```shell $ composer require illuminate/redis ``` Update `config/app.php` to add the Redis Service Provider: ```diff Roots\Acorn\Providers\AcornServiceProvider::class, Roots\Acorn\Providers\RouteServiceProvider::class, Roots\Acorn\View\ViewServiceProvider::class, + Illuminate\Redis\RedisServiceProvider::class, ``` Update `config/app.php` to add the Redis facade: ```diff 'aliases' => Facade::defaultAliases()->merge([ // 'ExampleClass' => App\Example\ExampleClass::class, + 'Redis' => Illuminate\Support\Facades\Redis::class ])->toArray(), ``` ================================================ FILE: acorn/logging.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2021-10-21 13:21 description: Acorn provides Laravel's logging services for WordPress. Configure multiple channels and send logs to files, syslog, Slack, and custom handlers. title: Laravel Logging in WordPress authors: - ben --- # Laravel Logging in WordPress ::: tip We recommend referencing the [Laravel docs on Logging](https://laravel.com/docs/13.x/logging) ::: The location of your application logs depends on your [directory structure](/acorn/docs/directory-structure/). For zero-config setups, logs live at `[wp-content]/cache/acorn/logs/`. For traditional setups, logs live at `storage/logs/`. ## Basic PHP logging example ```php use Illuminate\Support\Facades\Log; Log::debug('👋 Howdy'); ``` ## Basic Blade logging example ```blade {{ logger('👋 Howdy') }} ``` ================================================ FILE: acorn/package-development.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2021-10-21 13:21 title: Developing Packages for Acorn description: Use the Acorn Example Package as a template for creating custom packages and reusable functionality for WordPress with Laravel architecture. authors: - ben - Log1x --- # Developing Packages for Acorn We have an [Acorn Example Package](https://github.com/roots/acorn-example-package) repo that can be used as a template for creating your own Acorn packages. It's similar to some of the other Laravel package templates out there, but more specific to Acorn. Creating Acorn packages is useful for when you want to reuse specific functionality on your Acorn-powered WordPress sites, or open-sourcing functionality that's not tied directly to your site. You can think of Acorn packages similiar to WordPress plugins, or any other dependency. Packages are installed by Composer, just like Acorn is. ::: tip We recommend referencing the [Laravel docs on Packages](https://laravel.com/docs/13.x/packages) ::: ## Creating an Acorn package From the [roots/acorn-example-package](https://github.com/roots/acorn-example-package) repo, click the **Use this template** button to create a new repo with the template. After cloning your new repo, run the configure script to replace the placeholder names with your own: ```shell $ php configure.php ``` The script will prompt you for your vendor name, package name, namespace, and other details. You can also run it non-interactively: ```shell $ php configure.php --no-interaction --author-name="Your Name" --author-email="you@example.com" --vendor-slug="your-vendor" --vendor-namespace="YourVendor" --package-slug="your-package" --class-name="YourPackage" --package-description="Your package description" ``` To preview changes without modifying any files, use `--dry-run`: ```shell $ php configure.php --dry-run ``` ## Developing an Acorn package Once your package is created, clone your new git repo somewhere on your machine that's accessible from a WordPress site with Acorn installed. To work on a package locally, you can require it by defining a new local repository from the `composer.json` file used for your site/theme: ```json "repositories": [ { "type": "path", "url": "./packages/vendor-name/example-package" } ], ``` Replace `./packages/vendor-name/example-package` above with the path to your local package, along with the correct names. Then require the package in your project: ```shell $ composer require vendor-name/example-package ``` Then run the Acorn WP-CLI command to discover your package: ```shell $ wp acorn package:discover ``` ```plaintext INFO Discovering packages. vendor-name/example-package ...... DONE roots/sage ....................... DONE ``` ::: tip If you haven't already, run `php configure.php` from the root of your package to replace the placeholder names ::: ================================================ FILE: acorn/rendering-blade-views.md ================================================ --- date_modified: 2026-02-02 12:00 date_published: 2023-02-21 11:30 description: Render Blade templates anywhere in WordPress using the `view()` helper function. Examples for Gutenberg blocks, ACF blocks, and email notifications. title: Rendering Blade Views in WordPress authors: - ben - chuckienorton - rafaucau - strarsis - talss89 --- # Rendering Blade Views in WordPress You can use the `view()` helper function from Acorn to render Blade templates anywhere in your WordPress site. ## Rendering blocks with Blade templates ### First-party blocks In the following example we'll render a `vendor/example` block with `resources/views/blocks/example.blade.php`: ```php register_block_type('vendor/example', [ 'render_callback' => function ($attributes, $content) { return view('blocks/example', compact('attributes', 'content')); }, ]); ``` In the following example register an ACF block named `example` and render it with `resources/views/blocks/example.blade.php`: ### ACF blocks with Blade templates ```php acf_register_block_type([ 'example', 'render_callback' => function ($block) { echo view('blocks/example', ['block' => $block]); }, ]); ``` ### Existing blocks with Blade templates In the following example we'll render the `core/buttons` block with `resources/views/blocks/button.blade.php`: ```php add_filter('register_block_type_args', function ($args, $name) { if ($name === 'core/buttons') { $args['render_callback'] = function ($attributes, $content) { return view('blocks/buttons', compact('attributes', 'content')); }; } return $args; }, 10, 2); ``` ### block.json `render` field with Blade templates If you're registering blocks using `block.json` with a `render` field pointing to a Blade template (e.g. `"render": "file:./render.blade.php"`), you can automatically handle the rendering with a single filter: ```php add_filter('register_block_type_args', function (array $args, string $name): array { if (empty($args['render_callback']) || ! ($args['render_callback'] instanceof \Closure)) { return $args; } $reflector = new \ReflectionFunction($args['render_callback']); $renderCallbackVariables = $reflector->getStaticVariables(); if (array_key_exists('template_path', $renderCallbackVariables) && str_ends_with($renderCallbackVariables['template_path'], '.blade.php')) { $args['render_callback'] = function ($attributes, $content, $block) use ($renderCallbackVariables) { return view() ->file($renderCallbackVariables['template_path'], compact('attributes', 'content', 'block')) ->render(); }; } return $args; }, 1, 2); ``` ## Rendering emails with Blade templates The following example uses the `resources/views/emails/welcome.blade.php` template file customizing the new WordPress user notification emails: ```php add_filter('wp_new_user_notification_email', function ($wp_new_user_notification_email, $user, $blogname) { $key = get_password_reset_key($user); $encoded_user_login = rawurlencode($user->user_login); $password_reset_link = network_site_url('wp-login.php?action=rp&key='.$key.'&login='.$encoded_user_login, 'login'); $message = view('emails/welcome', compact('user', 'blogname', 'password_reset_link'))->render(); $wp_new_user_notification_email['message'] = $message; $wp_new_user_notification_email['headers'] = ['Content-Type: text/html; charset=UTF-8']; return $wp_new_user_notification_email; }, 10, 3); ``` ================================================ FILE: acorn/routing.md ================================================ --- date_modified: 2025-03-07 09:00 date_published: 2024-06-03 15:00 description: Add Laravel's routing system to WordPress with Acorn. Create custom routes with parameters, controllers, and middleware for advanced applications. title: Laravel Routing in WordPress authors: - ben --- # Laravel Routing in WordPress ::: tip See [Laravel's routing documentation](https://laravel.com/docs/10.x/routing) to better understand how routing works in Acorn ::: Acorn allows you to use Laravel's routing functionality on your WordPress sites, and will automatically handle Laravel routes defined in the `routes/web.php` file if it exists. Routes are an easier way to implement virtual pages in WordPress. ## Basic routing example ### Create the route file Create `routes/web.php` with the following: ```php name('welcome'); ``` ### Create the view file Create `resources/views/welcome.blade.php` with the following: ```blade @extends('layouts.app') @section('content')

Welcome

@endsection ``` ## Update Acorn's configuration Find where `Application::configure` is used in your setup. On a Sage theme, this would be `functions.php`. Add `->withRouting(web: base_path('routes/web.php'))`: ```diff Application::configure() ->withProviders([ App\Providers\ThemeServiceProvider::class, ]) + ->withRouting(web: base_path('routes/web.php')) ->boot(); ``` See [Advanced booting](/acorn/docs/installation/#advanced-booting) for more examples. ## Configuring SEO elements Since registered routes are dynamic, WordPress is not aware of how to handle some SEO elements and functionality: * Setting the canonical URL * Setting the `` * Adding SEO-related meta data * Adding pages to the sitemap [Laravel's `Route` facade allows you to access information about the route](https://laravel.com/docs/11.x/routing#accessing-the-current-route), which can be used with hooks to populate this data: ```php /** * Set the page <title> for the welcome route */ add_filter('pre_get_document_title', function ($title) { $name = Route::currentRouteName(); if ($name === 'welcome') { return 'Welcome Page'; } return $name; }); ``` ## Advanced routing features For more complex applications, you can use: - **[Controllers, Middleware, and HTTP Kernel](controllers-middleware-kernel.md)** - Organize route logic with controllers, filter requests with middleware, and customize the HTTP kernel - **[Eloquent Models](eloquent-models.md)** - Work with WordPress data using Laravel's ORM in your controllers ### Using controllers Instead of defining route logic directly in your routes file, you can organize it into controller classes: ```php <?php use Illuminate\Support\Facades\Route; use App\Http\Controllers\PostController; Route::get('/api/posts', [PostController::class, 'index']); Route::get('/api/posts/{id}', [PostController::class, 'show']); ``` ### Applying middleware Protect routes with middleware for authentication, rate limiting, and more: ```php Route::middleware('auth')->group(function () { Route::post('/api/posts', [PostController::class, 'store']); Route::put('/api/posts/{id}', [PostController::class, 'update']); }); ``` ## Route caching If you're using routes then you should enable [Laravel's route cache](https://laravel.com/docs/10.x/routing#route-caching) during your deployment process: ```shell $ wp acorn route:cache ``` ================================================ FILE: acorn/upgrading-acorn.md ================================================ --- date_modified: 2026-03-22 12:00 date_published: 2023-01-13 13:12 description: Learn how to upgrade Acorn to the latest version with guidance on breaking changes, dependency requirements, and configuration updates. title: Upgrading Acorn to the Latest Version authors: - ben - chrillep - joshf --- # Upgrading Acorn to the Latest Version ## Upgrading to v6.x from v5.x Acorn v6 includes Laravel v13 components, whereas Acorn v5 includes Laravel v12 components. ### Upgrading dependencies Acorn v6 requires PHP >= 8.3. Update the `roots/acorn` dependency in your `composer.json` file to `^6.0`: ```shell $ composer require roots/acorn ^6.0 -W ``` The `-W` flag is required to upgrade the included Laravel dependencies. ::: warning If any packages/dependencies have conflicts while updating, try removing and then re-requiring them after Acorn is bumped to 6.x. ::: ### Breaking changes #### Cache, session, and Redis prefix separators The default prefix/cookie separators have changed from underscores to hyphens to match Laravel 13 defaults. This means: - **Cache prefix**: `laravel_cache_` → `laravel-cache-` - **Session cookie**: `laravel_session` → `laravel-session` - **Redis prefix**: `laravel_database_` → `laravel-database-` This will **invalidate existing caches and log out all sessions** unless you have explicitly set these values via environment variables (`CACHE_PREFIX`, `SESSION_COOKIE`, `REDIS_PREFIX`). To preserve existing behavior, add these to your `.env`: ```plaintext CACHE_PREFIX=your_app_name_cache_ SESSION_COOKIE=your_app_name_session REDIS_PREFIX=your_app_name_database_ ``` #### Mail configuration The SMTP `encryption` key has been replaced with `scheme`: ```diff 'smtp' => [ 'transport' => 'smtp', - 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'scheme' => env('MAIL_SCHEME'), ], ``` If you are using the `MAIL_ENCRYPTION` environment variable, rename it to `MAIL_SCHEME`. If you use [`roots/acorn-mail`](https://github.com/roots/acorn-mail), bump it to `^2.0` — earlier versions read the removed `encryption` key and will silently ignore `MAIL_SCHEME`: ```shell $ composer require roots/acorn-mail ^2.0 ``` #### Logging configuration The stderr channel's `with` key has been renamed to `handler_with`: ```diff 'stderr' => [ 'driver' => 'monolog', 'handler' => StreamHandler::class, - 'with' => [ + 'handler_with' => [ 'stream' => 'php://stderr', ], ], ``` ### Config changes If you have published Acorn's configs, you should review and update them based on the latest versions in the [Acorn repo](https://github.com/roots/acorn/tree/main/config). Notable changes include: - **cache.php**: New `serializable_classes` option - **session.php**: New `serialization` option - **database.php**: New Redis retry/backoff keys, SQLite `transaction_mode`, SSL CA guard updated for PHP 8.5 - **mail.php**: New `resend` and `roundrobin` mailers, `retry_after` on failover, `markdown` section removed - **services.php**: Postmark and Resend env variable names updated - **All configs**: `(string)` casts added to `env()` calls per Laravel 13 conventions ## Upgrading to v5.x from v4.x Acorn v5 includes Laravel v12 components, whereas Acorn v4 includes Laravel v10 components. ### Upgrading dependencies Acorn v5 requires PHP >= 8.2. Update the `roots/acorn` dependency in your `composer.json` file to `^5.0`: ```shell $ composer require roots/acorn ^5.0 -W ``` The `-W` flag is required to upgrade the included Laravel dependencies. ::: warning If any packages/dependencies have conflicts while updating, try removing and then re-requiring them after Acorn is bumped to 5.x. ::: ### Breaking changes The most significant change in v5 is how Acorn is booted. The `bootloader()` helper has been deprecated in favor of using `Application::configure()`. This change aligns Acorn with Laravel 11's new application configuration system, providing a more fluent and powerful way to configure your application. You'll need to import the Application class at the top of your file: ```php use Roots\Acorn\Application; ``` Then update your bootstrapping code: ```diff - add_action('after_setup_theme', fn () => \Roots\bootloader()->boot(), 0); + add_action('after_setup_theme', function () { + Application::configure() + ->withProviders([ + App\Providers\ThemeServiceProvider::class, + ]) + ->boot(); + }, 0); ``` If you have previously registered service providers through either `composer.json` (`extra.acorn.providers`) or `config/app.php`, you'll need to migrate these to the new configuration method. All providers should now be registered using `withProviders()` when configuring the application. Remove any provider configurations from your composer.json and config files, and instead register them directly in your bootstrapping code: ```php Application::configure() ->withProviders([ App\Providers\ThemeServiceProvider::class, App\Providers\ExampleServiceProvider::class, ]) ->boot(); ``` ### Routing Acorn v5 introduces support for Laravel’s routing features within WordPress. If you previously used Livewire, you may encounter an error such as `Route [livewire.update] not defined`, or experience other routing-related issues. To resolve this, and to enable routing, ensure your application is properly configured by adding the `withRouting` method: ```diff Application::configure() ->withProviders([ App\Providers\ThemeServiceProvider::class, ]) + ->withRouting(wordpress: true) ->boot(); ``` ### Config changes If you have published Acorn's configs, you should review and update them based on the latest versions in the [Acorn repo](https://github.com/roots/acorn/tree/main/config). ## Upgrading to v4.x from v3.x Acorn v4 includes Laravel v10 components, whereas Acorn v3 includes Laravel v9 components. ### Upgrading dependencies Acorn v4 requires PHP >= 8.1. Update the `roots/acorn` dependency in your `composer.json` file to `^4.0`: ```shell $ composer require roots/acorn ^4.0 -W ``` The `-W` flag is required to upgrade the included Laravel dependencies. ::: warning If any packages/dependencies have conflicts while updating, try removing and then re-requiring them after Acorn is bumped to 4.x. ::: ### Config changes If you previously published Acorn's config(s), you will need to update them based on the configs in the [Acorn repo](https://github.com/roots/acorn/tree/main/config) ([history](https://github.com/roots/acorn/commits/main/config?since=2023-11-01&until=2024-01-31)). You mainly need the [new provider changes](https://github.com/roots/acorn/blob/v4.0.0/config/app.php#L160-L169) if you published `config/app.php`. ```diff + use Roots\Acorn\ServiceProvider; - 'timezone' => get_option('timezone_string', 'UTC'), + 'timezone' => get_option('timezone_string') ?: 'UTC', - 'providers' => [ + 'providers' => ServiceProvider::defaultProviders()->merge([ - - /* - * Framework Service Providers... - */ - Illuminate\Auth\AuthServiceProvider::class, - Illuminate\Broadcasting\BroadcastServiceProvider::class, - Illuminate\Bus\BusServiceProvider::class, - // ... - Roots\Acorn\Providers\AcornServiceProvider::class, - Roots\Acorn\Providers\RouteServiceProvider::class, - Roots\Acorn\View\ViewServiceProvider::class, /* * Package Service Providers... */ /* * Application Service Providers... */ // App\Providers\ThemeServiceProvider::class, - ], + ])->toArray(), ``` ### Breaking changes The breaking changes this time are minimal and should not impact most users. Service providers should now extend Illuminate: ```diff - use Roots\Acorn\ServiceProvider; + use Illuminate\Support\ServiceProvider; ``` View Composer `Arrayable` trait uses property [`Composer::$except`](https://github.com/roots/acorn/blob/70d179955cddc61f0c6101717af2fdf88cf38831/src/Roots/Acorn/View/Composer.php#L35-L54) instead of `Arrayable::$ignore`. ```diff class Alert extends Composer { use Arrayable; - $ignore = ['token']; + $except = ['token']; } ``` Asset Contract adds [`relativePath()` method](https://github.com/roots/acorn/blob/70d179955cddc61f0c6101717af2fdf88cf38831/src/Roots/Acorn/Assets/Contracts/Asset.php#L38). So if you're implementing this contract, you'll need to update it. (Most users will not be impacted by this.) ```diff class MyAsset implements Asset { + relativePath(string $base_path): string + { + // ... + } } ``` ## Upgrading to v3.x from v2.x Acorn v3 includes Laravel v9 components, whereas Acorn v2 includes Laravel v8 components. ### Upgrading dependencies Acorn v3 requires PHP >= 8.0.2. Update the `roots/acorn` dependency in your `composer.json` file to `^3.0`: ```shell $ composer require roots/acorn ^3.0 -W ``` The `-W` flag is required to upgrade the included Laravel dependencies. ### Theme/application Acorn v2 is typically booted in your WordPress theme's `functions.php` file. Look for the line that includes `\Roots\bootloader()`, and replace it with `\Roots\bootloader()->boot()`. ```diff -\Roots\bootloader(); +\Roots\bootloader()->boot(); ``` We highly recommend removing the exception from bootloader to prevent service providers from silently skipping on local dev, a change that was introduced in Acorn v3.1.0 ([PR #266](https://github.com/roots/acorn/pull/266)) and Sage v10.5.1 ([PR #3121](https://github.com/roots/sage/pull/3121/files)). Replace the original bootloader method: ```php try { \Roots\bootloader()->boot(); } catch (Throwable $e) { wp_die('You need to install Acorn to use this theme.'), ... } ``` With the new one: ```php if (! function_exists('\Roots\bootloader')) { wp_die( __('You need to install Acorn to use this theme.', 'sage'), '', [ 'link_url' => 'https://roots.io/acorn/docs/installation/', 'link_text' => __('Acorn Docs: Installation', 'sage'), ] ); } add_action('after_setup_theme', fn () => \Roots\bootloader()->boot(), 0); ``` You can also remove the theme support added for Sage if you are working on a Sage-based WordPress theme: ```diff -add_theme_support('sage'); ``` #### Target class [sage.view] does not exist Some setups may require changes if you run into the following error: ```plaintext Target class [sage.view] does not exist ``` In this case, edit the `ThemeServiceProvider` and make sure it extends `SageServiceProvider` and has `parent::` calls to `register()` and `boot()` if they are present: ```diff # app/Providers/ThemeServiceProvider.php namespace App\Providers; -use Roots\Acorn\ServiceProvider; +use Roots\Acorn\Sage\SageServiceProvider; -class ThemeServiceProvider extends ServiceProvider +class ThemeServiceProvider extends SageServiceProvider { /** * Register any application services. * * @return void */ public function register() { - // + parent::register(); } /** * Bootstrap any application services. * * @return void */ public function boot() { - // + parent::boot(); } } ``` After doing so, you may need to delete [Acorn's application cache directory](https://roots.io/acorn/docs/directory-structure/). By default, this is located in `[wp-content|app]/cache/acorn/`. Reference the [Acorn v3 upgrade pull request on the Sage repo](https://github.com/roots/sage/pull/3097) to see a full diff. #### Target class [assets.manifest] does not exist Some setups may require changes if you run into the following error: ```plaintext Target class [assets.manifest] does not exist ``` This error can be fixed by copying over the latest changes to the [`config/app.php` file](https://github.com/roots/acorn/blob/67cce76e6ca13e28acaced3333d77e2f779b07a3/config/app.php) from Acorn. ================================================ FILE: acorn/using-livewire-with-wordpress.md ================================================ --- date_modified: 2025-03-06 07:00 date_published: 2024-03-05 16:41 description: Use Laravel Livewire with Acorn to create reactive, dynamic components in WordPress themes and plugins without complex JavaScript frameworks. title: Using Livewire with WordPress authors: - ben - Log1x --- # Using Livewire with WordPress With the release of Acorn v4 came the final implementations needed for [Livewire](https://livewire.laravel.com/) support alongside your Acorn-powered WordPress themes and plugins. In this guide, we will walk through installing Livewire and using a component in a [Sage 11](https://roots.io/sage/) theme. ## Installing Livewire Start by installing Livewire alongside where you installed Acorn: ```bash $ composer require livewire/livewire ``` Once installed, Livewire requires you have an `APP_KEY` set in your environment. You can generate this using Acorn's CLI: ```bash $ wp acorn key:generate ``` ## Enqueueing Livewire Adding the Livewire styles and scripts can be done using the `@livewireStyles` and `@livewireScripts` directives. This can be done by [manually inserting](https://livewire.laravel.com/docs/installation#manually-including-livewires-frontend-assets) them inside of `resources/views/layouts/app.blade.php`: ```blade <head> ... @livewireStyles </head> <body> ... @livewireScripts </body> ``` ## Update Acorn's configuration Find where `Application::configure` is used in your setup. On a Sage theme, this would be `functions.php`. On a Radicle setup, this would be in `mu-plugins/00-acorn-boot.php`. Add `->withRouting(wordpress: true)`: ```diff Application::configure() ->withProviders([ App\Providers\ThemeServiceProvider::class, ]) + ->withRouting(wordpress: true) ->boot(); ``` See [Advanced booting](/acorn/docs/installation/#advanced-booting) for more examples. ## Creating a Component For this example, we will create a simple searchable **Post List** component. Start by generating the component using Acorn's CLI: ```sh $ wp acorn make:livewire PostList ``` Inside of `app/Livewire/PostList.php`, we can create a `$query` property to hold our search term and perform a simple `get_posts()` with the query if it is not empty: ```php <?php namespace App\Livewire; use Livewire\Attributes\Url; use Livewire\Component; class PostList extends Component { /** * The search query. */ #[Url] public string $query = ''; /** * Render the component. * * @return \Illuminate\View\View */ public function render() { $posts = $this->query ? get_posts([ 'post_type' => 'post', 'post_status' => 'publish', 's' => $this->query, ]) : []; $posts = collect($posts); return view('livewire.post-list', compact('posts')); } } ``` In `resources/views/livewire/post-list.blade.php`, we can add some simple markup consisting of an `<input>` for the search `$query` and a loop of any found posts: ```php <div> <input wire:model.live="query" type="text" placeholder="Search posts..." > @if ($query) @if ($posts) <p>Found {{ $posts->count() }} result(s) for "{{ $query }}"</p> <ul> @foreach ($posts as $post) <li> <a href="{{ get_permalink($post->ID) }}"> {{ $post->post_title }} </a> </li> @endforeach </ul> @else <p>No results found for "{{ $query }}"</p> @endif @else <p>Start typing to search...</p> @endif </div> ``` Once done, you can add the Livewire component into one of your existing Blade views/templates: ```php <livewire:post-list /> ``` To learn more about Livewire, head over to the official [Livewire Documentation](https://livewire.laravel.com/docs/quickstart). ================================================ FILE: acorn/wp-cli.md ================================================ --- date_modified: 2026-03-10 12:00 date_published: 2021-11-19 11:58 description: Acorn provides WP-CLI commands similar to Laravel's `artisan` for managing WordPress. Clear caches, compile views, and run administrative tasks. title: WP-CLI Commands for Acorn authors: - alwaysblank - ben - QWp6t --- # WP-CLI Commands for Acorn Acorn comes with WP-CLI commands similar to Laravel's `artisan` CLI. ## Available commands | Command | Description | | --- | --- | | `wp acorn about` | Display basic information about your application | | `wp acorn clear-compiled` | Remove the compiled class file | | `wp acorn completion` | Dump the shell completion script | | `wp acorn db` | Start a new database CLI session | | `wp acorn env` | Display the current framework environment | | `wp acorn help` | Display help for a command | | `wp acorn list` | List commands | | `wp acorn migrate` | Run the database migrations | | `wp acorn optimize` | Cache framework bootstrap, configuration, and metadata to increase performance | | `wp acorn test` | Run the application tests | | `wp acorn acorn:init` | Initializes required paths in the base directory | | `wp acorn acorn:install` | Install Acorn into the application | | `wp acorn cache:clear` | Flush the application cache | | `wp acorn cache:forget` | Remove an item from the cache | | `wp acorn config:cache` | Create a cache file for faster configuration loading | | `wp acorn config:clear` | Remove the configuration cache file | | `wp acorn db:seed` | Seed the database with records | | `wp acorn db:table` | Display information about the given database table | | `wp acorn db:wipe` | Drop all tables, views, and types | | `wp acorn key:generate` | Set the application key | | `wp acorn make:command` | Create a new Artisan command | | `wp acorn make:component` | Create a new view component class | | `wp acorn make:composer` | Create a new view composer class | | `wp acorn make:controller` | Create a new controller class | | `wp acorn make:job` | Create a new job class | | `wp acorn make:middleware` | Create a new HTTP middleware class | | `wp acorn make:migration` | Create a new migration file | | `wp acorn make:model` | Create a new Eloquent model class | | `wp acorn make:provider` | Create a new service provider class | | `wp acorn make:queue-batches-table` | Create a migration for the batches database table | | `wp acorn make:queue-failed-table` | Create a migration for the failed queue jobs database table | | `wp acorn make:queue-table` | Create a migration for the queue jobs database table | | `wp acorn make:seeder` | Create a new seeder class | | `wp acorn migrate:fresh` | Drop all tables and re-run all migrations | | `wp acorn migrate:install` | Create the migration repository | | `wp acorn migrate:refresh` | Reset and re-run all migrations | | `wp acorn migrate:reset` | Rollback all database migrations | | `wp acorn migrate:rollback` | Rollback the last database migration | | `wp acorn migrate:status` | Show the status of each migration | | `wp acorn optimize:clear` | Remove the cached bootstrap files | | `wp acorn package:discover` | Rebuild the cached package manifest | | `wp acorn queue:clear` | Delete all of the jobs from the specified queue | | `wp acorn queue:failed` | List all of the failed queue jobs | | `wp acorn queue:flush` | Flush all of the failed queue jobs | | `wp acorn queue:forget` | Delete a failed queue job | | `wp acorn queue:listen` | Listen to a given queue | | `wp acorn queue:monitor` | Monitor the size of the specified queues | | `wp acorn queue:pause` | Pause job processing for a specific queue | | `wp acorn queue:prune-batches` | Prune stale entries from the batches database | | `wp acorn queue:prune-failed` | Prune stale entries from the failed jobs table | | `wp acorn queue:restart` | Restart queue worker daemons after their current job | | `wp acorn queue:resume` | Resume job processing for a paused queue | | `wp acorn queue:retry` | Retry a failed queue job | | `wp acorn queue:retry-batch` | Retry the failed jobs for a batch | | `wp acorn queue:work` | Start processing jobs on the queue as a daemon | | `wp acorn route:cache` | Create a route cache file for faster route registration | | `wp acorn route:clear` | Remove the route cache file | | `wp acorn route:list` | List all registered routes | | `wp acorn schedule:clear-cache` | Delete the cached mutex files created by scheduler | | `wp acorn schedule:interrupt` | Interrupt the current schedule run | | `wp acorn schedule:list` | List all scheduled tasks | | `wp acorn schedule:run` | Run the scheduled commands | | `wp acorn schedule:test` | Run a scheduled command | | `wp acorn schedule:work` | Start the schedule worker | | `wp acorn vendor:publish` | Publish any publishable assets from vendor packages | | `wp acorn view:cache` | Compile all of the application's Blade templates | | `wp acorn view:clear` | Clear all compiled view files | ================================================ FILE: bedrock/auditing-wordpress-vulnerabilities-with-composer.md ================================================ --- date_modified: 2026-05-03 12:00 date_published: 2026-05-03 12:00 description: Audit WordPress plugins and themes for known vulnerabilities with Composer using WP Sec Adv, a security advisory repository sourced from Wordfence Intelligence. title: Auditing WordPress Vulnerabilities with Composer authors: - ben --- # Auditing WordPress Vulnerabilities with Composer `composer audit` reports known vulnerabilities for PHP packages on Packagist, but it has no awareness of WordPress plugin and theme advisories. [WP Sec Adv](https://github.com/typisttech/wpsecadv) closes that gap by exposing WordPress security data — sourced from the [Wordfence Intelligence](https://www.wordfence.com/help/wordfence-intelligence/v3-accessing-and-consuming-the-vulnerability-data-feed/) feed — as a Composer repository. Once added, Composer treats WordPress advisories the same as any other: - `composer audit` reports known vulnerabilities in installed WordPress packages - `composer require` and `composer update` block installation of vulnerable packages - Advisories include CVEs, severity ratings, and links to vulnerability reports The advisory data refreshes twice daily. ## Adding the repository From your Bedrock project root: ```shell $ composer repo --append add wpsecadv composer https://repo-wpsecadv.typist.tech ``` Composer will now check for WordPress vulnerabilities during `install`, `require`, `update`, and `audit`. ## Package support WP Sec Adv matches advisories to Composer packages by slug, with built-in support for: - [WordPress plugin and theme packages](https://wp-packages.org/) - [WordPress core packages](https://wp-packages.org/wordpress-core) Unrecognized vendors still attempt to match against known plugin and theme slugs, so custom mirrors and private registries work too. ## Ignoring advisories Not every advisory requires immediate action. Composer lets you acknowledge specific advisories with a documented reason: ```json { "config": { "audit": { "ignore": { "CVE-2026-3589": { "apply": "block", "reason": "Waiting for upstream fix in v1.2.3. Allow during updates but still report in audits" } } } } } ``` Every exception is tracked in `composer.json`, keeping your security posture intentional rather than reactive. ## Auditing in CI Pair WP Sec Adv with a CI step to audit your lockfile on every push. For GitHub Actions: ```yaml - name: Audit run: composer audit --locked ``` This gives you continuous vulnerability monitoring for both PHP and WordPress dependencies with no additional tooling. ::: tip WP Sec Adv is maintained by [Tang Rufus](https://github.com/tangrufus). If it's useful to your projects, consider [sponsoring his work](https://github.com/sponsors/tangrufus). ::: ================================================ FILE: bedrock/bedrock-with-ddev.md ================================================ --- date_modified: 2024-07-09 18:30 date_published: 2023-02-19 12:16 description: Set up DDEV for Bedrock WordPress development using Docker. Configure docroot to `web/` directory and adjust DDEV services for Bedrock's structure. title: Bedrock Local Development with DDEV authors: - ben --- # Bedrock Local Development with DDEV [DDEV](https://ddev.readthedocs.io/en/stable/) is a local PHP development environment. In this guide you will learn how to setup a Bedrock-based WordPress site with DDEV. ## Setting up a Bedrock site ```shell $ ddev config --project-type=wordpress --docroot=web --create-docroot ``` ```shell $ ddev composer create roots/bedrock ``` ## Configure environment variables Bedrock requires [environment variables to be configured](https://roots.io/bedrock/docs/installation/#getting-started) in order to get started. The `.env` file must be configured with DDEV's database settings along with your home URL. Update the following values in your `.env` file: ```dotenv DB_NAME='db' DB_USER='db' DB_PASSWORD='db' DB_HOST='db' WP_HOME="${DDEV_PRIMARY_URL}" WP_SITEURL="${DDEV_PRIMARY_URL}/wp" ``` After configuring the environment variables, run `ddev start`. Your site will be accessible at `https://ddevtest.ddev.site/`. ================================================ FILE: bedrock/bedrock-with-devkinsta.md ================================================ --- date_modified: 2023-02-19 12:16 date_published: 2023-02-19 12:16 description: Set up DevKinsta for Bedrock WordPress development. Configure site settings and document root to work with Bedrock's unique `wp/` and `app/` directories. title: Bedrock Development with DevKinsta authors: - ben --- # Bedrock Development with DevKinsta [DevKinsta](https://kinsta.com/devkinsta/?kaid=OFDHAJIXUDIV) is a local WordPress development environment. In this guide you will learn how to setup a Bedrock-based WordPress site with DevKinsta. ## Create a new site 1. Create a new site from the DevKinsta interface using the **Custom site** option 2. Select the **Empty site** option In this guide, we'll use `example` as the site name. ## Installing Bedrock from the terminal Navigate to the site path for your DevKinsta site: ```shell $ cd ~/DevKinsta/public/example ``` Once you are in the `example/` folder for your DevKinsta site, either install Bedrock with Composer or clone your existing git repository into this directory: ```shell $ composer create-project roots/bedrock ``` Your folder structure should now look like this: ```plaintext # @ ~/DevKinsta/ . ├── kinsta ├── logs ├── nginx_sites ├── private ├── public │   └── example │   ├── bedrock │   └── index.html ├── ssl └── wp ``` ## Configure environment variables Bedrock requires [environment variables to be configured](https://roots.io/bedrock/docs/installation/#getting-started) in order to get started. The `.env` file in the `app/bedrock/` directory must be configured with Local's database settings along with your home URL. Update the following values in your `.env` file: ```dotenv DB_NAME='example' DB_USER='root' DB_PASSWORD='password' DB_HOST='devkinsta_db' WP_HOME='http://example.local' ``` Make sure to populate the `DB_PASSWORD` based on the provided password in the DevKinsta interface for your site. ## Set the webroot in DevKinsta's site config DevKinsta's site config is located at `~/DevKinsta/nginx_sites/example.conf`. Open this file and modify the`root` path: ```diff -root /www/kinsta/public/example; +root /www/kinsta/public/example/bedrock/web; ``` You will need to restart your site after making these changes, and then your site will be accessible at `http://example.local`. ================================================ FILE: bedrock/bedrock-with-lando.md ================================================ --- date_modified: 2026-03-08 10:00 date_published: 2023-02-19 12:16 description: Set up Lando for Bedrock WordPress development. Configure webroot to `web/` directory and adjust Lando settings for Bedrock's unique folder structure. title: Bedrock Local Development with Lando authors: - ben - james0r --- # Bedrock Local Development with Lando [Lando](https://lando.dev/) is a local development environment. In this guide you will learn how to setup a Bedrock-based WordPress site with Lando. ## Configuring a Lando recipe for Bedrock After [installing Bedrock](/bedrock/docs/installation/), you can either use `lando init` to create the recipe, or you can just drop in the contents of the recipe file that you will find below within a file called `.lando.yml`. To use the CLI, run `lando init --recipe wordpress` and answer the following prompts: * From where should we get your app's codebase? **current working directory** * Where is your webroot relative to the init destination? **web** * What do you want to call this app? **bedrock** Or, just drop in the following `.lando.yml` file in the root of your Bedrock directory: ```yaml # .lando.yml name: bedrock recipe: wordpress config: webroot: web services: appserver: type: php:8.3 # Bedrock requires PHP >= 8.3 ``` ## Configure environment variables Bedrock requires [environment variables to be configured](https://roots.io/bedrock/docs/installation/#getting-started) in order to get started. The `.env` file must be configured with Lando's database settings along with your home URL. Update the following values in your `.env` file: ```dotenv DB_NAME='wordpress' DB_USER='wordpress' DB_PASSWORD='wordpress' DB_HOST='database' WP_HOME='https://bedrock.lndo.site' ``` ## Setup trusted certificates Make sure to follow the instructions in the Lando docs for [Trusting the CA](https://docs.lando.dev/config/security.html#trusting-the-ca) to avoid warnings on your browser when visiting your site. ## Start your Lando site Run `lando start`, and then your site will be accessible from `https://bedrock.lndo.site/`. ================================================ FILE: bedrock/bedrock-with-local.md ================================================ --- date_modified: 2026-03-10 17:00 date_published: 2023-02-19 12:16 description: Configure Local for Bedrock WordPress development. Adjust document root to `web/` directory and configure Local for Bedrock's structure. title: Using Bedrock with Local authors: - ben - ethanclevenger91 --- # Using Bedrock with Local [Local](https://localwp.com/), previously known as Local by Flywheel, is one of the many local development tools available for WordPress developers. In this guide you will learn how to configure Local for a Bedrock-based WordPress site. Bedrock sites are structured in a way that your [entire WordPress site is managed from a git repository](https://roots.io/bedrock/docs/folder-structure/). Local's workflow isn't friendly towards this approach, but it's still possible to configure Local to work with Bedrock sites. ## Create a new site Create a new site from the Local interface. In this guide, we'll use `bedrock` as the site name. ## Installing Bedrock from the terminal From your new Local site, click **Open site shell**. When the terminal opens, you should be under `/Local Sites/bedrock/app/public`. First, remove the default WordPress installation that is in the public folder: ```shell rm -rf * rm .htaccess ``` This will remove all content of the public folder. Now install Bedrock with Composer into the public directory or clone your existing git repository into this directory: ```shell composer create-project roots/bedrock . ``` ## Configure environment variables Bedrock requires environment variables to be configured in order to get started. First, copy the example environment file: ```shell cp .env.example .env ``` The `.env` file must be configured with Local's database settings along with your home URL. Update the following values in your `.env` file: ```plaintext DB_NAME='local' DB_USER='root' DB_PASSWORD='root' WP_HOME='https://bedrock.local' ``` For Local WP these are the default DB credentials. If you changed them manually, then you need to change them here accordingly. The `WP_HOME` should be the website URL we configured in Local - in our case here it's `bedrock.local`. ## Set the webroot in Local's site config Local's site config is located at `~/Local Sites/bedrock/conf/nginx/site.conf.hbs`. Open this file and append `/web` to the server root: ```diff server { listen {{port}}; - root "{{root}}"; + root "{{root}}/web"; ``` You will need to restart your site after making these changes, and then your site will be accessible at `https://bedrock.local`. ================================================ FILE: bedrock/bedrock-with-valet.md ================================================ --- date_modified: 2023-03-08 8:55 date_published: 2023-02-19 12:16 description: Set up Laravel Valet for Bedrock WordPress development on macOS. Configure Valet drivers and local domains for seamless development workflow. title: Using Bedrock with Laravel Valet authors: - ben --- # Using Bedrock with Laravel Valet [Laravel Valet](https://laravel.com/docs/10.x/valet) is a local development environment. In this guide you will learn how to setup a Bedrock-based WordPress site with Valet. Valet supports Bedrock out of the box, along with traditional WordPress installations, Laravel apps, Drupal sites, and more. Since Valet is very lightweight, it is a great local development setup for folks that are working on several WordPress sites at any given time. See the [Valet installation docs](https://laravel.com/docs/10.x/valet#installation) for information on how to install Valet. You will also want to install the Valet WP-CLI command: ```shell $ wp package install aaemnnosttv/wp-cli-valet-command:@stable ``` ## Setting up a Bedrock site To create a new Bedrock site for Valet, navigate to Valet sites directory and use the `wp valet` command: ```shell $ cd ~/Sites/valet ``` ```shell $ wp valet new bedrock --project=bedrock ``` You should now be able to access your new site at `https://bedrock.test`. If you hit a 404, make sure that you have ran `valet park` from your Valet sites directory first. ### Bedrock multisite #### Subdomain installs * `wp valet new bedrock-multisite --project=bedrock` * Add to `config/application.php` in Bedrock: ```php Config::define('WP_ALLOW_MULTISITE', true); ``` * Visit `https://bedrock-multisite.test/wp/wp-admin/network.php` to install the network and select subdomain install * Add to `.env`: `DOMAIN_CURRENT_SITE=bedrock-multisite.test` * Update `config/application.php` again with full multisite constants: ```php /** * Multisite */ Config::define('WP_ALLOW_MULTISITE', true); Config::define('MULTISITE', true); Config::define('SUBDOMAIN_INSTALL', true); Config::define('DOMAIN_CURRENT_SITE', env('DOMAIN_CURRENT_SITE')); Config::define('PATH_CURRENT_SITE', env('PATH_CURRENT_SITE') ?: '/'); Config::define('SITE_ID_CURRENT_SITE', env('SITE_ID_CURRENT_SITE') ?: 1); Config::define('BLOG_ID_CURRENT_SITE', env('BLOG_ID_CURRENT_SITE') ?: 1); ``` * Add the Bedrock multisite URL fixer plugin: `composer require roots/multisite-url-fixer` * Link any subdomains to current site with Valet: ```shell $ valet link test.bedrock-multisite ``` ```shell $ valet link site2.bedrock-multisite ``` #### Subfolder / subdirectory installs * Copy the [Bedrock multisite subdirectory driver](https://gist.github.com/QWp6t/1e055482d722e2b02dfff1bb21698194) into `~/.valet/Drivers/` * `wp valet new bedrock-multisite --project=bedrock` * Add to `config/application.php` in Bedrock: ```php Config::define('WP_ALLOW_MULTISITE', true); ``` * Visit `https://bedrock-multisite.test/wp/wp-admin/network.php` to install the network and select subfolder install * Add to `.env`: `DOMAIN_CURRENT_SITE=bedrock-multisite.test` * Update `config/application.php` again with full multisite constants: ```php /** * Multisite */ Config::define('WP_ALLOW_MULTISITE', true); Config::define('MULTISITE', true); Config::define('SUBDOMAIN_INSTALL', false); Config::define('DOMAIN_CURRENT_SITE', env('DOMAIN_CURRENT_SITE')); Config::define('PATH_CURRENT_SITE', env('PATH_CURRENT_SITE') ?: '/'); Config::define('SITE_ID_CURRENT_SITE', env('SITE_ID_CURRENT_SITE') ?: 1); Config::define('BLOG_ID_CURRENT_SITE', env('BLOG_ID_CURRENT_SITE') ?: 1); ``` * Add the Bedrock multisite URL fixer plugin: `composer require roots/multisite-url-fixer` (Optional) * * * Thank you to [Evan Mattson](https://discourse.roots.io/u/aaemnnosttv) for contributing Bedrock's driver to Valet, and for creating the [Valet WP-CLI command](https://github.com/aaemnnosttv/wp-cli-valet-command). Thank you to [Craig](https://discourse.roots.io/u/QWp6t) for the multisite subdirectory driver. ================================================ FILE: bedrock/compatibility.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2020-02-20 09:25 description: If plugins or themes work with regular WordPress but not Bedrock, it's usually due to hardcoded paths, not Bedrock itself. Solutions included. title: WordPress Plugin Compatibility with Bedrock authors: - alwaysblank - ben - QWp6t --- # WordPress Plugin Compatibility with Bedrock Bedrock does certain things a bit differently than the default WordPress installation, but it does so by leveraging functionality that WordPress Core provides. If a plugin or theme works with a vanilla WordPress install and not with Bedrock, the plugin or theme is likely at fault: In most cases, it is hard-coding assumptions about directory structure or file location and ignoring the systems WordPress has in place to determine those things dynamically. This type of issue will often arise not only on Bedrock, but also on sites that have [installed WordPress in its own directory](https://wordpress.org/support/article/giving-wordpress-its-own-directory/) or [moved the wp-content folder](https://developer.wordpress.org/apis/wp-config-php/#moving-wp-content-folder). Common issues include: - Assuming the content directory is `wp-content`. - Assuming WordPress is not in a subdirectory. - [Trying to include wp-load.php](https://ottopress.com/2010/dont-include-wp-load-please/). If you reach a WordPress error page on a non-development environment that says `"Sorry, you are not allowed to access this page."`, then the plugin or theme could be conflicting with Bedrock's use of `DISALLOW_FILE_MODS`. If you run into an issue with a specific theme or plugin, please contact their authors first and link them to this page. ================================================ FILE: bedrock/composer.md ================================================ --- date_modified: 2023-08-16 12:45 date_published: 2015-09-06 07:42 description: Bedrock treats WordPress core, plugins, and themes as Composer dependencies. Use WP Packages to require plugins and automate updates efficiently. title: WordPress Dependencies with Composer authors: - ben - Log1x - swalkinshaw - TangRufus - EHLOVader --- # WordPress Dependencies with Composer Bedrock uses [Composer](https://getcomposer.org/) to manage dependencies. Any 3rd party library is considered a dependency, including WordPress itself and any plugins. ## Adding WordPress plugins with Composer [WP Packages](https://wp-packages.org/) is already registered in the `composer.json` file so any plugins from the [WordPress Plugin Directory](https://wordpress.org/plugins/) can easily be required. To add a plugin, add it under the `require` directive or use `composer require <namespace>/<packagename>` from the command line. If the plugin is from WordPress.org, then the namespace is always `wp-plugin`: ```shell $ composer require wp-plugin/akismet ``` `plugins` and `mu-plugins` are ignored in Git by default since Composer manages them. If you want to add something to those folders that *isn't* managed by Composer, you need to update `.gitignore` to allow them to be added to your repository: `!web/app/plugins/plugin-name` ### Force a plugin to be a mu-plugin To force a regular `wordpress-plugin` to be treated as a `wordpress-muplugin`, you can update the `installer-paths` config to tell Bedrock to install it in the `mu-plugins` directory. In the following example, Akismet will be installed in the `mu-plugins` directory: ```yaml ... "extra": { "installer-paths": { "web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin", "wp-plugin/akismet"], "web/app/plugins/{$name}/": ["type:wordpress-plugin"], "web/app/themes/{$name}/": ["type:wordpress-theme"] }, "wordpress-install-dir": "web/wp" }, ... ``` #### Configuring multiple mu-plugins To configure more than one regular plugin to be installed to `mu-plugins`, add additional strings to the same array value for the `web/app/mu-plugins/{$name}/` JSON key, for example: ```yaml ... "web/app/mu-plugins/{$name}/": [ "type:wordpress-muplugin", "wp-plugin/akismet", "wp-plugin/turn-comments-off" ], ... ``` ## Updating WordPress and WordPress plugin versions with Composer Updating your WordPress version, or the version of any plugin, is best achieved by re-requiring the dependencies to install the latest versions or specific versions: ```shell $ composer require roots/wordpress -W ``` ```shell $ composer require wp-plugin/akismet ``` ```shell $ composer require roots/wordpress:6.8.3 -W ``` ### Automating WordPress updates Tools like [Dependabot](https://dependabot.com/) and [Renovate](https://www.mend.io/free-developer-tools/renovate/) can be used to automate updates of your Composer dependencies in Bedrock, including WordPress itself. The Bedrock repo [uses Renovate to bump WordPress versions](https://github.com/roots/bedrock/blob/e14658bbae2c64df9605168a9c7932e5e10a9dd8/.github/renovate.json) when new versions become available. ## Adding WordPress themes with Composer Themes can also be managed by Composer but should only be done so under two conditions: 1. You're using a parent theme that won't be modified at all 2. You want to separate out your main theme and use that as a standalone package Under most circumstances, we recommend keeping your main theme as part of your repository. Just like plugins, WP Packages maintains a Composer mirror of the WP theme directory. To require a theme, just use the `wp-theme` namespace: ```shell $ composer require wp-theme/twentytwentythree ``` ## Recommended resources [WordPress with Composer resources](https://roots.io/composer-wordpress-resources/) for more extensive documentation and background information: - [📝 Composer in WordPress from Rarst](https://composer.rarst.net/) - [📝 `roots/wordpress` Composer Package](https://roots.io/announcing-the-roots-wordpress-composer-package/) - [📝 Using Composer with WordPress](https://roots.io/using-composer-with-wordpress/) - [📝 WordPress Plugins with Composer](https://roots.io/wordpress-plugins-with-composer/) - [🎥 Using Composer With WordPress screencast](https://www.youtube.com/watch?v=2cFRQA1_GY0) (2013) - [📝 Private or Commercial WordPress Plugins as Composer Dependencies](https://roots.io/bedrock/docs/private-or-commercial-wordpress-plugins-as-composer-dependencies/) ================================================ FILE: bedrock/configuration.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-06 07:42 description: Bedrock replaces `wp-config.php` with modern configuration files. Set global config in `application.php` and override per environment as needed. title: Configuring Bedrock for WordPress authors: - ben - Log1x - mZoo - swalkinshaw --- # Configuring Bedrock for WordPress The file to modify for configuration options is `config/application.php`. This is the file that contains what `wp-config.php` usually would. The root `web/wp-config.php` is required by WordPress and is only used to load the other main configs. Nothing else should be added to it. Bedrock's base configuration options are production-standard safe settings and used in all environments except where specifically overridden. To override configuration settings based on environments: - Use an existing environment config in `config/environments` or create a new one. Bedrock will `require` any file in the `config/environments` directory with a filename matching the `WP_ENV` environment variable. This environment variable can be set in a few ways: - in the `.env` file as described in our [installation docs](installation.md) - via [Trellis config](/trellis/docs/wordpress-sites/) if you're using Trellis - or as a last resort, hardcoding it in `config/application.php` - Bedrock comes with `development.php` and `staging.php` configs included. If you create an additional environment, configure it with a matching PHP file in `config/environments`. - The [`development.php` file](https://github.com/roots/bedrock/blob/master/config/environments/development.php) sets `WP_DEBUG_DISPLAY` to `true`, so WordPress will display PHP errors in the browser when your `WP_ENV` is `development`. Bedrock 1.9.0 (2018-09-17) introduced [`roots/wp-config`](https://github.com/roots/wp-config/blob/master/docs/why.md) ([discussion](https://github.com/roots/bedrock/pull/380)). `Config::define` is a static method that overrides the application options (WP) with environment specific options where they are defined, defaulting to the application options set in `config/application.php`. ================================================ FILE: bedrock/converting-wordpress-sites-to-bedrock.md ================================================ --- date_modified: 2025-10-24 12:00 date_published: 2025-10-24 12:00 description: Convert traditional WordPress sites to Bedrock using Lithify. Automatically updates database references and file paths for Bedrock's directory structure. title: Converting WordPress Sites to Bedrock authors: - ben - MWDelaney --- # Converting WordPress Sites to Bedrock [Lithify](https://github.com/MWDelaney/lithify) is a WordPress plugin that adds a WP-CLI command to convert traditional WordPress sites into Bedrock-style installations. Created by [MWDelaney](https://github.com/MWDelaney), Lithify automates the database changes needed to make your existing WordPress site work with Bedrock's improved folder structure. Converting an existing WordPress site to Bedrock typically involves manual database updates and file path changes. Lithify handles this conversion process automatically, updating your database to work seamlessly with Bedrock's modern architecture. ## Prerequisites Before starting the conversion, you'll need: - A fresh Bedrock installation - A database backup of your existing WordPress site - Your existing site's plugins, themes, and uploads directories ## Create a new Bedrock site Create a new Bedrock installation: ```bash $ composer create-project roots/bedrock example.com ``` ```bash $ cd example.com ``` ## Update WordPress version Update Bedrock's WordPress version to match your current installation. For example, if your site runs WordPress 6.8.2, update `composer.json`: ```json "roots/wordpress": "6.8.2" ``` ## Copy your content files Copy your WordPress `plugins`, `themes`, `mu-plugins`, and `uploads` directories into the Bedrock `web/app` directory. ## Add Lithify as a dependency Install Lithify using Composer: ```bash $ composer require mwdelaney/lithify ``` ## Import your database Navigate to your Bedrock directory and import your WordPress database: ```bash $ wp db import example.sql ``` ## Run the conversion Activate Lithify and run the conversion command: ```bash $ wp plugin activate lithify ``` ```bash $ wp lithify ``` ### What Lithify does When you run `wp lithify`, the plugin: - Updates file path references from `wp-content` to `app` - Adjusts plugin and theme paths to match Bedrock's structure - Ensures upload paths work correctly with the new organization - Verifies that WordPress core references point to the correct locations The conversion is non-destructive—your existing content and configuration remain intact while gaining all the benefits of Bedrock's modern development workflow. ================================================ FILE: bedrock/deployment.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-10-15 16:17 description: Bedrock deployments require running `composer install` to fetch dependencies. Learn deployment workflows for various hosting platforms and CI/CD tools. title: Deploying WordPress with Bedrock authors: - alwaysblank - ben - knowler - Log1x - noplanman - swalkinshaw --- # Deploying WordPress with Bedrock Running `composer install` from the Bedrock folder must be part of your deployment process. ## Supported deployment tools These tools include supporting deploying Bedrock out of the box: - [Trellis](https://roots.io/trellis/) – Recommended if self-hosting WordPress or [hosting with Kinsta](https://kinsta.com/?kaid=OFDHAJIXUDIV). Other methods need to account for setting the `WP_ENV` [environment variable](environment-variables.md) to `production` when your site is in a production environment. ::: warning Note Bedrock's [Disallow Indexing mu-plugin](https://github.com/roots/bedrock-disallow-indexing) will prevent indexing of a site when `WP_ENV` is not set to `production`. ::: ================================================ FILE: bedrock/disable-plugins-based-on-environment.md ================================================ --- date_modified: 2023-04-04 11:30 date_published: 2018-05-15 12:00 description: Use Bedrock Plugin Disabler to prevent specific plugins from loading in certain environments. Disable debug tools in production or heavy plugins locally. title: Disable Plugins Based on Environment authors: - ben - luke - owi --- # Disable Plugins Based on Environment Bedrock supports defining an environment with the `WP_ENV` environment variable. A typical setup for a project could contain several different environments: * `development` for local development * `staging` for a staging environment * `production` for the live/production environment In some cases, you may want to enforce certain plugins to be deactivated on one or more of your environments. The [Bedrock Plugin Disabler](https://github.com/lukasbesch/bedrock-plugin-disabler) mu-plugin package by [@luke](https://discourse.roots.io/u/luke) can be used to configure a list of disabled plugins in your Bedrock environment configs located in `config/environments/`. Install the mu-plugin with Composer: ```shell $ composer require lukasbesch/bedrock-plugin-disabler ``` This package requires defining a `DISABLED_PLUGINS` constant with an array of plugin filenames to be disabled. ## Disabling plugins on local development The most common use-case is disabling caching plugins on local development. We'll cover disabling WP Rocket and WP Super Cache in the following example. Open `config/environments/development.php` and add the `DISABLED_PLUGINS` constant: ```php Config::define('DISABLED_PLUGINS', [ 'wp-rocket/wp-rocket.php', 'wp-super-cache/wp-cache.php', ]); ``` ================================================ FILE: bedrock/environment-variables.md ================================================ --- date_modified: 2023-02-16 20:55 date_published: 2015-09-06 07:42 description: Bedrock uses `.env` files for environment-specific settings like database credentials. Keep sensitive data out of Git with environment variables. title: WordPress Environment Variables in Bedrock authors: - alwaysblank - ben - Log1x - swalkinshaw - tristanbes --- # WordPress Environment Variables in Bedrock Bedrock tries to separate config from code as much as possible and environment variables are used to achieve this. The benefit is there's a single place (`.env`) to keep settings like database or other 3rd party credentials that aren't committed to your repository. [PHP dotenv](https://github.com/vlucas/phpdotenv) is used to load the `.env` file. All variables are then available in your app by the built-in `getenv`, `$_SERVER`, or `$_ENV` methods. However, we use the [env](https://github.com/oscarotero/env) library and its `env` function which handles simple type coercion (such as converting the string `'True'` to the boolean `true`). We recommend you use `env` as well for reading environment variables. Currently, the following env vars are required: - `WP_HOME` - `WP_SITEURL` The following vars are required if `DATABASE_URL` is not set: - `DB_USER` - `DB_NAME` - `DB_PASSWORD` ::: tip Note There is also the `DATABASE_URL` which is optional. ::: ## WP_ENV Although it isn't required (if not defined elsewhere, Bedrock will default to `production`), `WP_ENV` is used by several pieces of the Roots stack, as well as software outside of it, to modify behavior based on environment. There are three values you can set for `WP_ENV` that Bedrock will understand: - `production` - `staging` - `development` Make sure that these are set correctly in your different environments. ### WP_ENVIRONMENT_TYPE Bedrock also infers [`WP_ENVIRONMENT_TYPE`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) based on `WP_ENV`. `WP_ENVIRONMENT_TYPE` was introduced in WordPress 5.5.0. ================================================ FILE: bedrock/folder-structure.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-06 07:42 description: Bedrock organizes WordPress differently. `wp-content` renamed to `app/`, WordPress core isolated in `wp/` directory for improved project structure. title: Bedrock WordPress Folder Structure authors: - ben - Log1x - mZoo - swalkinshaw --- # Bedrock WordPress Folder Structure ```plaintext ├── composer.json # → Manage versions of WordPress, plugins & dependencies ├── config # → WordPress configuration files │ ├── application.php # → Primary WP config file (wp-config.php equivalent) │ └── environments # → Environment specific configs │ ├── development.php # → Development config │ └── staging.php # → Staging config ├── vendor # → Composer packages (never edit) └── web # → Web root (document root on your webserver) ├── app # → wp-content equivalent │ ├── mu-plugins # → Must use plugins │ ├── plugins # → Plugins │ ├── themes # → Themes │ └── uploads # → Uploads ├── wp-config.php # → Required by WP (never edit) ├── index.php # → WordPress view bootstrapper └── wp # → WordPress core (never edit) ``` The organization of Bedrock is similar to putting WordPress in its own subdirectory but with some improvements: - In order not to expose sensitive files in the web root, Bedrock moves what's required into a `web/` directory including the `wp/` source, and the `wp-content` source. - `wp-content` has been named `app` to better reflect its contents. It contains application code and not just "static content". It also matches up with other frameworks such as Symfony and Rails. - `wp-config.php` remains in the `web/` because it's required by WP, but it only acts as a loader. The actual configuration files have been moved to `config/` for better separation. - `vendor/` is where the Composer managed dependencies are installed to. - `wp/` is where WordPress core lives. It's also managed by Composer but can't be put under `vendor` due to WP limitations. ================================================ FILE: bedrock/installation.md ================================================ --- date_modified: 2026-03-08 16:09 date_published: 2015-10-15 12:29 description: Install Bedrock with PHP 8.3+ and Composer. Configure environment variables in `.env` file and set document root to `web/` directory to access WordPress. title: Installing the Bedrock WordPress Boilerplate authors: - ben - Log1x - swalkinshaw --- # Installing the Bedrock WordPress Boilerplate ## What is Bedrock? Bedrock is a [WordPress boilerplate](https://roots.io/bedrock/). ### Why use Bedrock? - Better folder structure - Dependency management with [Composer](https://getcomposer.org) - Easy WordPress configuration with environment specific files - Environment variables with [Dotenv](https://github.com/vlucas/phpdotenv) - Autoloader for mu-plugins (use regular plugins as mu-plugins) ## Requirements - PHP >= 8.3 - [Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos) ## Installing Bedrock with Composer Create a new Bedrock project: ```shell $ composer create-project roots/bedrock ``` ## Getting Started - Create a `.env` file with the following environment variables (see `.env.example` as an example): - Database variables - `DB_NAME` - Database name - `DB_USER` - Database user - `DB_PASSWORD` - Database password - `DB_HOST` - Database host - Optionally, you can define `DATABASE_URL` for using a DSN instead of using the variables above (e.g. `mysql://user:password@127.0.0.1:3306/db_name`) - `WP_ENV` - Set to environment (`development`, `staging`, `production`) - `WP_HOME` - Full URL to WordPress home (https://example.com) - `WP_SITEURL` - Full URL to WordPress including subdirectory (https://example.com/wp) - `AUTH_KEY`, `SECURE_AUTH_KEY`, `LOGGED_IN_KEY`, `NONCE_KEY`, `AUTH_SALT`, `SECURE_AUTH_SALT`, `LOGGED_IN_SALT`, `NONCE_SALT` - Generate with [wp-cli-dotenv-command](https://github.com/aaemnnosttv/wp-cli-dotenv-command) - Generate with [our WordPress salts generator](https://roots.io/salts.html) - Add theme(s) in `web/app/themes/` as you would for a normal WordPress site - Run the test suite with `composer test` (see [Testing Bedrock with Pest](/bedrock/docs/testing/)) - Set the document root on your webserver to Bedrock's `web` folder: `/path/to/site/web/` - Access WordPress admin at `https://example.com/wp/wp-admin/` ### Multisite Bedrock is multisite network compatible, but needs the [roots/multisite-url-fixer](https://github.com/roots/multisite-url-fixer) mu-plugin on subdomain installs to make sure admin URLs function properly. This plugin is not _needed_ on subdirectory installs but will work well with them. From your Bedrock directory: ```shell $ composer require roots/multisite-url-fixer ``` ================================================ FILE: bedrock/local-development.md ================================================ --- date_modified: 2026-03-08 16:07 date_published: 2018-12-28 13:54 description: Bedrock supports various local development tools including Trellis, Laravel Valet, Local, DDEV, Lando, and DevKinsta for flexible WordPress development. title: Local WordPress Development with Bedrock authors: - ben - Log1x - swalkinshaw --- # Local WordPress Development with Bedrock Bedrock can be used with most local development setups. [Trellis](https://roots.io/trellis/) is our WordPress LEMP stack that supports Bedrock out of the box. We also have guides for using Bedrock with some popular setups: - [Bedrock with DDEV](/bedrock/docs/bedrock-with-ddev/) - [Bedrock with DevKinsta](/bedrock/docs/bedrock-with-devkinsta/) - [Bedrock with Lando](/bedrock/docs/bedrock-with-lando/) - [Bedrock with Local](/bedrock/docs/bedrock-with-local/) - [Bedrock with Valet](/bedrock/docs/bedrock-with-valet/) For test setup and commands, see [Testing Bedrock with Pest](/bedrock/docs/testing/). Additionally, [WP-CLI's server command](https://developer.wordpress.org/cli/commands/server/) can be used with Bedrock (the `docroot` for the server is set in Bedrock's [`wp-cli.yml`](https://github.com/roots/bedrock/blob/master/wp-cli.yml)) MAMP, XAMPP, and others setups work with Bedrock once the [virtual host is configured](configuration.md). ================================================ FILE: bedrock/mu-plugin-autoloader.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-06 07:42 description: Bedrock's autoloader lets you install regular plugins as must-use plugins via Composer, ensuring critical plugins always load without user control. title: WordPress Must-use Plugin Autoloader authors: - ben - Log1x - swalkinshaw --- # WordPress Must-use Plugin Autoloader Bedrock includes an autoloader that enables standard plugins to be required just like must-use plugins. The autoloaded plugins are included after all mu-plugins and standard plugins have been loaded. An asterisk (*) next to the name of the plugin designates the plugins that have been autoloaded. To remove this functionality, just delete `web/app/mu-plugins/bedrock-autoloader.php`. This enables the use of mu-plugins through Composer if their package type is `wordpress-muplugin`. You can also override a plugin's type like the following example: ```json "installer-paths": { "web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin", "roots/wp-stage-switcher"], "web/app/plugins/{$name}/": ["type:wordpress-plugin"], "web/app/themes/{$name}/": ["type:wordpress-theme"] }, ``` [wp-stage-switcher](https://github.com/roots/wp-stage-switcher) is a package with its type set to `wordpress-plugin`. Since it implements `composer/installers` we can override its type. ================================================ FILE: bedrock/patching-wordpress-plugins-with-composer.md ================================================ --- date_modified: 2025-10-13 12:00 date_published: 2025-10-13 12:00 description: Use Composer patches to modify WordPress plugins without forking. Resolve dependency conflicts and apply fixes to third-party plugins in Bedrock projects. title: Patching WordPress Plugins with Composer authors: - ben --- # Patching WordPress Plugins with Composer When managing WordPress plugins through Composer in Bedrock, you may encounter situations where you need to modify third-party plugin code. Common scenarios include resolving dependency conflicts, fixing bugs before official updates are released, or adapting plugins for your specific environment. Rather than forking plugins or manually editing vendor code, Composer patches provide a maintainable solution that persists across updates. ## Installing the patches plugin Add the [cweagans/composer-patches](https://github.com/cweagans/composer-patches) package to your project: ```shell $ composer require cweagans/composer-patches ``` Enable the plugin in your `composer.json`: ```json "config": { "allow-plugins": { "cweagans/composer-patches": true } } ``` ## Creating a patch file Patches are standard unified diff files. You can create them using Git or the `diff` command. ### Using Git to create a patch The easiest method is to make changes to the plugin and generate a diff. First initialize a temporary Git repo for the plugin: ```shell $ cd web/app/plugins/example-plugin ``` ```shell $ git init ``` ```shell $ git add . && git commit -m "Base plugin" ``` Make your changes to the plugin files, then generate the patch and clean up: ```shell $ git diff > ../../../../patches/example-plugin-fix.patch ``` ```shell $ rm -rf .git ``` ### Example: resolving PSR library conflicts A common issue in WordPress is multiple plugins bundling the same PSR libraries with different versions, causing conflicts. Here's a real-world example of commenting out conflicting `psr/log` class registrations: ```diff diff --git a/vendor/composer/jetpack_autoload_classmap.php b/vendor/composer/jetpack_autoload_classmap.php index 1855b18..d52edf3 100644 --- a/vendor/composer/jetpack_autoload_classmap.php +++ b/vendor/composer/jetpack_autoload_classmap.php @@ -194,11 +194,11 @@ return array( 'version' => '3.1.3', 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/class-plugins-handler.php' ), - 'Psr\\Log\\LoggerInterface' => array( - 'version' => '1.1.4.0', - 'path' => $vendorDir . '/psr/log/Psr/Log/LoggerInterface.php' - ), + // 'Psr\\Log\\LoggerInterface' => array( + // 'version' => '1.1.4.0', + // 'path' => $vendorDir . '/psr/log/Psr/Log/LoggerInterface.php' + // ), ``` Save this patch file to a `patches/` directory in your project root. ## Configuring patches in `composer.json` Add your patches to the `extra.patches` section of `composer.json`: ```json "extra": { "patches": { "vendor/package-name": [ { "description": "Brief description of patch", "url": "patches/example-plugin-fix.patch" } ] } } ``` ## Applying patches Once configured, patches are automatically applied when you install or update dependencies: ```shell $ composer install ``` You'll see output confirming patches are being applied: ```plaintext - Applying patches for vendor/package-name patches/example-plugin-fix.patch (Brief description of patch) ``` ## When to use patches Composer patches are ideal for: * **Dependency conflicts** - Removing bundled libraries that conflict with your main dependencies * **Bug fixes** - Applying fixes before official plugin updates are available * **Environment adjustments** - Modifying plugins for specific hosting requirements * **Temporary workarounds** - Addressing issues while waiting for upstream fixes ::: tip Document your patches clearly. Include the reason for each patch in the description and maintain separate patch files for different issues to make future maintenance easier. ::: ## Maintaining patches across updates When updating plugins, patches are reapplied automatically. If a patch fails to apply (usually because the plugin code changed), Composer will show an error. You'll need to: 1. Review the plugin changes 2. Update or remove the patch as needed 3. Test that your fix is still necessary ================================================ FILE: bedrock/patching-wordpress-with-composer.md ================================================ --- date_modified: 2026-03-10 12:00 date_published: 2026-03-10 12:00 description: Apply patches to WordPress core in Bedrock using Composer. Fix bugs or apply upstream changes before official releases without modifying core files directly. title: Patching WordPress with Composer authors: - ben --- # Patching WordPress with Composer Sometimes you need to patch WordPress core — to fix a bug before an official release, suppress noisy deprecation notices, or backport a change from an open pull request. Composer patches let you apply these changes in a maintainable way that persists across installs and deploys. ## Installing the patches plugin Add the [cweagans/composer-patches](https://github.com/cweagans/composer-patches) package to your project: ```shell $ composer require cweagans/composer-patches ``` Enable the plugin in your `composer.json`: ```json "config": { "allow-plugins": { "cweagans/composer-patches": true } } ``` ## Which package to patch Bedrock's `roots/wordpress` is a metapackage — it doesn't contain any files. The actual WordPress core files are in the `roots/wordpress-no-content` package, which is installed to your `wordpress-install-dir` (typically `web/wp`). Patches must target `roots/wordpress-no-content`, and patch file paths should be relative to the package root (e.g., `wp-includes/load.php`, not `web/wp/wp-includes/load.php`). ## Creating a patch file Create a `patches/` directory in your project root. Patches are standard unified diff files with paths relative to the WordPress root. ### Example: suppressing deprecated notices ```diff --- a/wp-includes/load.php +++ b/wp-includes/load.php @@ -607,7 +607,7 @@ function wp_debug_mode() { } if ( WP_DEBUG ) { - error_reporting( E_ALL ); + error_reporting( E_ALL & ~E_DEPRECATED ); if ( WP_DEBUG_DISPLAY ) { ini_set( 'display_errors', 1 ); ``` Save this as `patches/wordpress.patch`. ## Configuring patches in `composer.json` Add your patches to the `extra.patches` section of `composer.json`: ```json "extra": { "patches": { "roots/wordpress-no-content": [ { "description": "Suppress E_DEPRECATED notices", "url": "patches/wordpress.patch" } ] } } ``` ## Applying patches Patches are automatically applied when you install or update dependencies: ```shell $ composer install ``` You'll see output confirming patches are being applied: ```plaintext - Patching roots/wordpress-no-content - Applying patch patches/wordpress.patch (Suppress E_DEPRECATED notices) ``` To force patches to reapply, reinstall the package: ```shell $ composer reinstall roots/wordpress-no-content ``` ## Maintaining patches across updates When WordPress is updated, patches are reapplied automatically. If a patch fails to apply (usually because the patched code changed in the new version), Composer will show an error. You'll need to: 1. Review the WordPress changes 2. Update or remove the patch as needed 3. Test that your fix is still necessary — the issue may have been resolved upstream ::: warning WordPress core patches are version-specific. After updating WordPress, always verify that your patches still apply cleanly and are still needed. ::: ================================================ FILE: bedrock/private-or-commercial-wordpress-plugins-as-composer-dependencies.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2018-08-02 14:04 description: Add paid and private plugins to Bedrock through Composer using private Git repositories, custom Composer repos, or services like WP Packages. title: Private or Commercial WordPress Plugins as Composer Dependencies authors: - MWDelaney - strarsis --- # Private or Commercial WordPress Plugins as Composer Dependencies Bedrock (and by extension Trellis) uses Composer to manage its dependencies, which includes WordPress themes and plugins. This is great for version control as many WordPress plugins are easily available via [WP Packages](https://wp-packages.org/), but what happens when you need to add a private, commercial, or paid plugin to your site? This guide will explain a simple way to add private plugins to your site via Composer. There are many ways to add private or paid plugins to your Bedrock-based project. Popular methods include: * Private Git repositories * [SatisPress](https://github.com/cedaro/satispress) * [Private Packagist](https://packagist.com/) * [Toran Proxy](https://toranproxy.com/) For the purposes of this document we will focus only on the first option: **private Git repositories**. We welcome contributed guides covering these methods or others. **Note:** We recommend hosting private and commercial plugins in private Git repositories. GitHub [offers private repositories](https://github.com/pricing) for free and BitBucket includes them in its [limited free plan](https://bitbucket.org/product/pricing). The following guide assumes you’re using a GitHub private repository. ## Create a private GitHub repository for your plugin [Create the repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/quickstart-for-repositories) as normal and clone the empty repository to your computer. ```shell $ git clone git@github.com:YourGitHubUsername/example-plugin.git ``` ## Create `composer.json` In your empty repository, create a file named `composer.json` with the following content (edited to include your correct user, repository, and plugin information): ```json { "name": "YourGithubUsername/example-plugin", "description": "", "keywords": ["wordpress", "plugin"], "homepage": "https://github.com/YourGitHubUsername/example-plugin", "authors": [ { "name": "Original Plugin Author's Name", "homepage": "https://originalpluginurl.com" } ], "type": "wordpress-plugin", "require": { "php": ">=8.0" } } ``` ::: tip Composer can create a skeleton `composer.json` for you: Just run `composer init` in your empty directory. ::: ## Copy plugin files into your repository Copy all the plugin’s files into your new repository. ## Commit your plugin to Git and push your changes to GitHub Run each of the following commands from your repository directory: Add all of your plugin’s files to Git. ```shell $ git add . ``` Commit your changes ::: tip Include the plugin’s version number in your commit message so that you can easily reference it later! ::: ```shell $ git commit . ``` ## Tag the release Composer will need a way to know the version of a plugin. Fortunately, it understands [git tags](https://getcomposer.org/doc/articles/versions.md#tags) and can interpret them correctly if they use [semantic versioning](https://semver.org/), so we’ll be using those. Let’s assume you’re pushing SearchWP version 2.9.14. That means we’ll be creating the 2.9.14 tag. Remember, tags are tied to commits, so be sure to commit all your changes _before_ creating the tag. ```shell $ git tag 2.9.14 ``` Push your changes, and your tags to GitHub: ```shell $ git push --tags ``` ::: tip Tags pushed to GitHub will automatically be turned into “Releases,” a feature of GitHub. You can also [create releases](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) manually on the GitHub website. ::: ## Edit your Bedrock `composer.json` file, add your repository and plugin In your Bedrock site’s `composer.json` * Add a new your GitHub repository to the `repositories` section referencing your GitHub repository: ```json "repositories": [ ... { "type": "vcs", "url": "git@github.com:YourGitHubUsername/example-plugin.git" } ... ], ``` * Add your plugin to the `require` section using the version number you named your `release` after: ```json "require": { ... "YourGitHubUsername/example-plugin": "2.9.14", ... }, ``` ## Update your dependencies Run `composer update` in your Bedrock directory to get your new plugin. [**Join the discussion on Roots Discourse**](https://discourse.roots.io/t/private-or-commercial-wordpress-plugins-as-composer-dependencies/13247) ================================================ FILE: bedrock/server-configuration.md ================================================ --- date_modified: 2026-03-08 10:00 date_published: 2018-12-21 18:24 description: Configure Nginx or Apache for Bedrock by setting document root to `web/` directory. Includes complete server configuration examples and rewrite rules. title: Server Configuration for Bedrock authors: - ben - Lachlan_Arthur - Log1x - swalkinshaw --- # Server Configuration for Bedrock Bedrock can run on any webserver that supports Composer and PHP >= 8.3. The document root for your site must be pointed to Bedrock's `web` folder. ## Nginx configuration for Bedrock If you aren't using a Nginx-based setup that already supports Bedrock, such as Valet or [Trellis](https://roots.io/trellis/), you'll need to configure Nginx with the following rules: ```nginx server { listen 80; server_name example.com; root /srv/www/example.com/web; index index.php index.htm index.html; # Prevent PHP scripts from being executed inside the uploads folder. location ~* /app/uploads/.*.php$ { deny all; } location / { try_files $uri $uri/ /index.php?$args; } } ``` ### Nginx multisite config Multisite installations on Nginx need additional rewrites depending on the type of multisite install. #### Subdomain multisite rewrites ```nginx rewrite ^/(wp-.*.php)$ /wp/$1 last; rewrite ^/(wp-(content|admin|includes).*) /wp/$1 last; ``` #### Subfolder multisite rewrites ```nginx if (!-e $request_filename) { rewrite /wp-admin$ $scheme://$host$uri/ permanent; rewrite ^(/[^/]+)?(/wp-.*) /wp$2 last; rewrite ^(/[^/]+)?(/.*.php) /wp$2 last; } ``` ## Apache configuration for Bedrock Make sure the `DocumentRoot` is set to the `web` folder: ```apache <VirtualHost *:80> DocumentRoot /var/www/html/bedrock/web DirectoryIndex index.php index.html index.htm <Directory /var/www/html/bedrock/web> Options -Indexes # .htaccess isn't required if you include this <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] </IfModule> </Directory> </VirtualHost> ``` You can also add the suggested `.htaccess` file from WordPress at `web/.htaccess`: ```apache # BEGIN WordPress <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] </IfModule> # END WordPress ``` ## Managed WordPress hosts and Bedrock If you're using a [supported WordPress host](deployment.md#supported-wordpress-hosts) such as Kinsta, then contact support and ask them to set your document root to the `web` folder. Sometimes you can't change the document root on hosted web server. In this case, you can create an `.htaccess` file at the root of your project with the following content: ```apache RewriteEngine on RewriteCond %{REQUEST_URI} !web/ RewriteRule ^(.*)$ /web/$1 [L] ``` ================================================ FILE: bedrock/testing.md ================================================ --- date_modified: 2026-03-08 16:05 date_published: 2026-03-08 16:05 description: Learn how to run tests in Bedrock using Pest (powered by PHPUnit), create feature tests, and run the suite locally and in CI. title: Testing Bedrock with Pest authors: - ben --- # Testing Bedrock with Pest Bedrock includes a minimal testing setup based on [Pest](https://pestphp.com/) (powered by PHPUnit). ## Running tests Run the test suite from your Bedrock root: ```shell $ composer test ``` ## Default test structure Bedrock ships with these testing files: - `phpunit.xml.dist` - PHPUnit configuration used by Pest - `tests/Pest.php` - Pest bootstrap and shared test configuration - `tests/Feature/ExampleTest.php` - Example test ## Writing tests Add tests anywhere under `tests/`: ```php <?php test('home URL is configured', function () { expect(env('WP_HOME'))->not->toBeEmpty(); }); ``` Then run: ```shell $ composer test ``` ## Scope of the default setup The default setup is intentionally minimal and framework-agnostic: - It gives you a ready-to-run PHP testing baseline - It does **not** include WordPress core integration test bootstrap or database test provisioning If you need deeper WordPress integration testing, you can extend this baseline with your preferred tooling. ================================================ FILE: bedrock/wp-cron.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-06 07:42 description: Disable WordPress's unreliable internal cron with `DISABLE_WP_CRON` in Bedrock and set up proper system cron jobs for scheduled tasks. title: Managing WP Cron in Bedrock authors: - ben - Log1x - swalkinshaw --- # Managing WP Cron in Bedrock Bedrock allows you to disable the internal WP Cron via the `DISABLE_WP_CRON` environment variable. If you enable this setting and disable WP Cron, you'll need to manually set a cron job like the following in your crontab file: ```plaintext */5 * * * * curl https://example.com/wp/wp-cron.php ``` ================================================ FILE: netlify.toml ================================================ [[redirects]] from = "/" to = "https://roots.io/" status = 301 force = true [[redirects]] from = "/docs/*" to = "/:splat" status = 301 force = true [[redirects]] from = "/acorn/" to = "https://roots.io/acorn/docs/installation/" status = 301 force = true [[redirects]] from = "/acorn/2.x/*" to = "https://roots.io/acorn/docs/:splat" status = 301 force = true [[redirects]] from = "/bedrock/" to = "https://roots.io/bedrock/docs/installation/" status = 301 force = true [[redirects]] from = "/bedrock/master/*" to = "https://roots.io/bedrock/docs/:splat" status = 301 force = true [[redirects]] from = "/examples/*" to = "https://roots.io/" status = 301 force = true [[redirects]] from = "/sage/" to = "https://roots.io/sage/docs/installation/" status = 301 force = true [[redirects]] from = "/sage/10.x/*" to = "https://roots.io/sage/docs/:splat" status = 301 force = true [[redirects]] from = "/trellis/" to = "https://roots.io/trellis/docs/installation/" status = 301 force = true [[redirects]] from = "/trellis/master/*" to = "https://roots.io/trellis/docs/:splat" status = 301 force = true [[redirects]] from = "/getting-started/" to = "https://roots.io/" status = 301 force = true [[redirects]] from = "/getting-started/macos/" to = "https://roots.io/" status = 301 force = true [[redirects]] from = "/getting-started/ubuntu-linux/" to = "https://roots.io/" status = 301 force = true [[redirects]] from = "/getting-started/windows/" to = "https://roots.io/" status = 301 force = true [[redirects]] from = "/sage/10.x/installing-packages/" to = "https://roots.io/acorn/docs/available-packages/" status = 301 force = true [[redirects]] from = "/acorn/2.x/installing-packages/" to = "https://roots.io/acorn/docs/available-packages/" status = 301 force = true [[redirects]] from = "/sage/10.x/available-packages/" to = "https://roots.io/acorn/docs/available-packages/" status = 301 force = true [[redirects]] from = "/sage/10.x/package-development/" to = "https://roots.io/acorn/docs/package-development/" status = 301 force = true [[redirects]] from = "/trellis/master/languages/" to = "https://roots.io/trellis/docs/guides/install-wordpress-language-files/" status = 301 force = true [[redirects]] from = "/trellis/master/deploys/" to = "https://roots.io/trellis/docs/deployments/" status = 301 force = true ================================================ FILE: sage/adding-linting.md ================================================ --- date_modified: 2023-03-12 19:25 date_published: 2023-01-23 19:40 description: Set up ESLint, Prettier, and Stylelint in Sage to enforce code quality standards, consistent formatting, and best practices for theme development. title: Adding ESLint, Prettier, and Stylelint authors: - ben - chrillep --- # Adding ESLint, Prettier, and Stylelint ::: tip We recommend enabling linting Sage 10 no longer includes linting styles or scripts out of the box. We highly recommend adding and configuring [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), and [Stylelint](https://stylelint.io/) based on your needs. ::: Bud has several extensions that can be added to your theme dependencies to help with linting. To add ESLint, Prettier, and Stylelint to your theme, run: ``` yarn add @roots/bud-eslint -D yarn add @roots/bud-prettier -D yarn add @roots/bud-stylelint -D yarn add @roots/eslint-config -D ``` Add `scripts` to `package.json` for better access to linting your scripts and styles: ```json ... "scripts": { "lint": "yarn lint:js && yarn lint:css", "lint:js": "eslint resources/scripts", "lint:css": "stylelint \"resources/**/*.{css,scss,vue}\"", "test": "yarn lint", } ... ``` Then create new files for `.eslintrc.cjs`, `.prettierrc`, and `.stylelintrc`. `.eslintrc.cjs`: ```javascript module.exports = { root: true, extends: ['@roots/eslint-config/sage'], }; ``` `.prettierrc`: ```json { "bracketSpacing": false, "jsxBracketSameLine": true, "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "all", "useTabs": false } ``` `.stylelintrc`: ```json { "extends": [ "@roots/sage/stylelint-config", "@roots/bud-tailwindcss/stylelint-config" ] } ``` ================================================ FILE: sage/blade-templates.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2018-02-07 09:46 description: Sage uses Laravel's Blade for powerful templating. Learn template inheritance with `@extends`, layouts with `@yield`, and passing data to WordPress views. title: Using Blade Templates in Sage authors: - alwaysblank - ben - Log1x --- # Using Blade Templates in Sage Sage uses [Laravel's Blade](https://laravel.com/docs/10.x/blade) templating engine. ::: tip The Blade templating language is described in much more depth in the [Laravel docs](https://laravel.com/docs/10.x/blade), which we recommend you read for a full understanding of how it works. Nearly everything described there should work in Sage. ::: The following are some of the Blade features you're likely to find yourself using regularly. ## Including One of the primary features of Blade is the `@include` directive (which also has a few useful variants). `@include` allows you to use a Blade file in any other Blade file, and creates a new scope for each included file. Variables define in a given view will cascade down to views that it `@includes`, but you can also pass data directly to Blade templates by passing a keyed array as the second argument to the `@include()` directive. The key names will become the variable names that their values are assigned to. ```blade @include('partials.example-partial', ['variableName' => 'Variable Value'] <!-- /resources/views/partials/example-partial.blade.php --> <h1>{{ $variableName }}</h1> <!-- <h1>Variable Value</h1> --> ``` ## Layouts A layout is a special kind of template that can be extended. It's useful when you have a lot of HTML content surrounding something you want to be dynamic—for instance the header and footer of a site. ```blade <!-- resources/views/layouts/app.blade.php --> <html> <body> <header> @section('header') @include('partials.nav.primary') @show </header> <main> @yield('content') </main> </body> </html> <!-- resources/views/page.blade.php --> @extends('layouts.app') @section('header') @parent @include('partials.nav.page') @endsection @section('content') <h1>{{ $title }}</h1> <div>{!! $content !!}}</div> @endsection ``` The extending view (`page.blade.php` in this case) can then "insert" its content into these sections to be rendered. ## Passing data to templates The best way to handle passing data to templates is to use [Composers](composers.md), which allow you to separate data handling and manipulation from the view where that data is used. With Composers, you can bind data to _any_ Blade template file. You can also pass data directly to Blade templates when `@include`ing them by passing a keyed array as the second argument to the `@include()` directive. The key names will become the variable names that their values are assigned to. ```blade @include('partials.example-partial', ['variableName' => 'Variable Value']) <!-- /resources/views/partials/example-partial.blade.php --> <h1>{{ $variableName }}</h1> <!-- <h1>Variable Value</h1> --> ``` ## WP-CLI utility If you need to clear or compile Blade templates, you can do so with WP-CLI: ### Compile all Blade templates ```shell $ wp acorn view:cache ``` ### Clear all Blade templates ```shell $ wp acorn view:clear ``` ## Additional resources * [Rendering Blade views for blocks, emails, and more](/acorn/docs/rendering-blade-views/) ================================================ FILE: sage/bootstrap.md ================================================ --- date_modified: 2025-02-27 14:30 date_published: 2022-02-24 10:25 description: Add Bootstrap CSS framework to Sage themes. Install Bootstrap via npm and integrate Bootstrap styles, grid system, and components into WordPress theme development. title: How to Use Bootstrap with Sage authors: - ben - code23_isaac - diomededavid - MWDelaney - kellymears - talss89 - taylorgorman --- # How to Use Bootstrap with Sage ::: warning Setup Sass first See [how to use Sass](./sass.md) before you follow this guide ::: ## Install Bootstrap Add Bootstrap as a dependency: ```shell $ npm install --save bootstrap @popperjs/core ``` Add Bootstrap to `resources/css/app.scss`: ```scss @import "bootstrap/scss/bootstrap"; ``` ::: tip Bootstrap's Vite docs See [Bootstrap's Vite docs](https://getbootstrap.com/docs/5.2/getting-started/vite/) for more information. ::: ================================================ FILE: sage/compatibility.md ================================================ --- date_modified: 2023-04-26 10:35 date_published: 2018-04-25 13:52 description: Known compatibility issues between WordPress plugins and Sage starter theme, including solutions, workarounds, and alternative plugin recommendations. title: WordPress Plugin Compatibility with Sage authors: - alwaysblank - ben - jure - Log1x --- # WordPress Plugin Compatibility with Sage A list of currently known compatibility issues with any WordPress plugins and Sage. Also take a look at the [Acorn compatibility](/acorn/docs/compatibility/) docs. ## Adding support for plugins ### WooCommerce WooCommerce support for Sage can be added by using the [generoi/sage-woocommerce](https://github.com/generoi/sage-woocommerce) package. ## Known issues with plugins - Disqus Comment System is [not compatible with Sage](https://github.com/roots/sage/issues/2035#issuecomment-369673419). There is a [Laravel-based solution](https://github.com/yajra/laravel-disqus) which may work (untested). ================================================ FILE: sage/compiling-assets.md ================================================ --- date_modified: 2026-03-22 11:00 date_published: 2015-09-01 18:19 description: Sage uses Vite for fast asset compilation with HMR support. Includes custom plugin for hot module replacement in WordPress block editor during development. title: Compiling Assets in Sage with Vite authors: - alwaysblank - ben - kero - Log1x - octoxan - toddsantoro --- # Compiling Assets in Sage with Vite [Vite](https://vite.dev/) is front-end build tool used in Sage. Sage also uses the Laravel Vite plugin, along with Laravel's Vite facade for referencing Vite assets in PHP and Blade template files. Because of this, [Laravel's Vite documentation](https://laravel.com/docs/13.x/vite) also applies to Sage. ## Available build commands - `npm run build` — Build assets - `npm run dev` — Start dev server (requires updating `vite.config.js` with your local dev URL) ## Theme assets What files are built and how is controlled from the `vite.config.js` file in the root of the theme. The configuration will generate the following files: - `app.css` - The primary stylesheet for the theme. - `app.js` - The primary JavaScript file for the theme. - `editor.css` - Styles used by the editor when creating/editing posts. - `editor.js` - JavaScript for the block editor, i.e. block styles and variants. It will also copy any files in the `images` or `fonts` directories under `/resources/` into the `public` directory with the other compiled files, but does not optimize or compress them. This is handled by the `assets` option on `laravel-vite-plugin` in `vite.config.js`. ### Assets in Blade template files Use the [`Vite::asset` method](https://laravel.com/docs/13.x/vite#blade-processing-static-assets) to call assets from Blade template files: ```blade <img src="{{ Vite::asset('resources/images/example.svg') }}"> ``` ### Assets in CSS You can reference images in CSS using the included Vite alias for images. ```css .background { background-image: url("@images/example.svg"); } ``` ### Assets in PHP #### Get the URL of the asset ```php use Illuminate\Support\Facades\Vite; $asset = Vite::asset('resources/images/example.svg'); ``` #### Get the contents of the asset ```php use Illuminate\Support\Facades\Vite; $asset = Vite::content('resources/images/example.svg'); ``` ================================================ FILE: sage/components.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2021-10-21 13:21 description: Components in Sage provide a structured approach for creating reusable view elements with scoped data, ideal for frequently reused theme components. title: Creating Blade Components in Sage authors: - alwaysblank - bbuilds - ben - code23_isaac - Log1x --- # Creating Blade Components in Sage Fundamentally, Components don't do anything you couldn't also accomplish with [partials](blade-templates.md) and [Composers](composers.md), but they provide system of interaction and a mental model that can be more intuitive. Like Composers and Blade templates, Components are an extension of the Laravel feature, so the [Laravel documentation](https://laravel.com/docs/7.x/blade#components) applies. Generally a Component consists of: 1) A Blade template in `/resources/views/components/`. 2) A Composer-like class in `/app/View/Components/`. The easiest way to create a component is with WP-CLI: ```shell $ wp acorn make:component ExampleComponent ``` Sage also ships with some examples. ::: tip You can also create [inline](https://laravel.com/docs/7.x/blade#inline-component-views) and [anonymous](https://laravel.com/docs/7.x/blade#anonymous-components) components, which forgo the template or class respectively. These would need to be created manually (the WP-CLI command only creates "traditional" components). ::: ## Usage A Component in action in a Blade template will look something like this: ```blade <x-example-component title="Example Component" :image-id="$image"/> ``` The template for that Component might look like this: ```blade <div {{ $attributes }}> <h3>{!! $title !!}</h3> @if($imageElement) <figure>{!! $imageElement !!}</figure> @endif </div> ``` In turn, the class might look like this: ```php namespace App\View\Components; use Roots\Acorn\View\Component; class ExampleComponent extends Component { public $title; public $imageElement; protected $imageId; public function __construct($title, $imageId = null) { $this->title = $title; $this->imageId = $imageId; $this->imageElement = $this->getImage(); } protected function getImage() { if (!is_numeric($this->imageId)) { return false; } return wp_get_attachment_image($this->imageId, 'medium_large'); } } ``` ## Argument and attribute names The names of the arguments in the definition of your Component's `__construct()` method must match the names of the attributes you use to pass data to your Component tag. ::: warning Note Component constructor arguments should be specified using `camelCase`, while `kebab-case` should be used when referencing the argument names in your HTML attributes. [Laravel documentation](https://laravel.com/docs/10.x/blade#casing) ::: In the above example ```blade <x-example-component title="A Component"/> ``` will work, but ```blade <x-example-component theTitle="A Component"/> ``` will throw an error. The attributes used to pass data to your Component tag can be in any order, so long as the names are correct: ```blade <x-example-component title="The Title" :image-id="$image"/> <x-example-component :image-id="$image" title="The Title"/> ``` These are equivalent. ## Passing data By default, anything passed to an attribute on a Component tag will be treated as a string. So if you do this: ```blade <x-example-component title="$variable"/> ``` Your component will treat that as a string containing `$variable`, _not_ whatever the contents of `$variable` is. If you need to pass non-string data, just prefix your attribute with a colon, and its value will be evaluated as PHP: ```blade <x-example-component :title="$variable"/> <x-example-component :title="get_my_title()"/> <x-example-component :title="TITLE_CONSTANT"/> ``` ::: warning Note Because your argument is now evaluated as PHP, you _don't_ want to pass a simple string, or PHP will try and evaluate it: ```blade <x-example-component :title="Uh oh"/> ``` This will throw an error when it tries to evaluate `Uh oh` as PHP. ::: ## Data in views The view for your Component (in the above example, `/resources/views/components/example-component.blade.php`) does _not_ receive the arguments you pass to the Component tag; The data it has access to is limited to any `public` properties you've set on your class. So remember to set those properties, or your view won't have the data you need! ## Other attributes In the Component tag, you use attributes to pass data to your component, but you can also add other, arbitrary attributes as well. These attributes will be put in an "attribute bag" which you can then access in your Component view with the special `$attributes` variable. If you echo the variable it will print out each attribute and its value, which is very useful for things like passing CSS selectors to your Components: ```blade <x-example-component title="Styled Component" class="bg-color-red text-color-white"/> <!-- yields... --> <div class="bg-color-red text-color-white"> ... ``` You can do many other things with attributes that are described in the [Laravel documentation](https://laravel.com/docs/7.x/blade#managing-attributes). ================================================ FILE: sage/composers.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2021-10-21 13:21 description: Use composers to pass scoped data to any Blade view in Sage. Bind variables to templates, partials, and components for organized theme development. title: View Composers in Sage WordPress Theme authors: - alwaysblank - ben - code23_isaac - Log1x --- # View Composers in Sage WordPress Theme Composers, also sometimes called View Composers, are essentially identical to the [Laravel system of the same name](https://laravel.com/docs/7.x/views#view-composers). They allow you to pass data to views (blade templates), scoping that data to that view (and any views it subsequently includes). If you're familiar with [Sage 9's data filters](https://roots.io/sage/docs/blade-templates/#passing-data-to-templates), or the [Controller package](https://github.com/soberwp/controller) often used with Sage 9, then Composers are a similar concept, but much more powerful: Instead of only allowing data binding to top-level WordPress templates, Composers allow you target _any_ view. ## Construction ::: warning Note Composers are autoloaded, which means their naming needs to conform to the [PSR-4 standard](https://www.php-fig.org/psr/psr-4/). ::: If you're using WP-CLI, you can create composers from the command line: ```shell wp acorn make:composer ExampleComposer ``` This would create a Composer called `ExampleComposer` in `app/View/Composers/`. If you're not using WP-CLI, the most basic Composer looks like this: ```php // app/View/Composers/ExampleComposer.php namespace App\View\Composers; use Roots\Acorn\View\Composer; class ExampleComposer extends Composer {} ``` This composer doesn't do anything yet, though, so let's give it some functionality. ```php class ExampleComposer extends Composer { /** * This tells the Composer that it should bind data to the 'example' * partial. */ protected static $views = [ 'partials.example', ]; /** * This will make the variable `$roots` available in the 'example' partial * with the value described here. */ public function with() { return [ 'roots' => "Tools for modern WordPress development", ]; } } ``` Because that variable is scoped to `example.blade.php`, we'll also see the following behavior: ```blade <!-- resources/views/content.blade.php --> {{ $roots }} <!-- Throws an error because the variable is not defined --> @include('partials.example') ``` ```blade <!-- resources/views/partials/example.blade.php --> <h1>{{ $roots }}</h1> <!-- <h1>Tools for modern WordPress development</h1> --> @include('partials.example2') ``` ```blade <!-- resources/views/partials/example2.blade.php --> <div>{{ $roots }}</div> <!-- <div>Tools for modern WordPress development</div> --> <!-- Variable is defined in this context because it inherits it from example.blade.php --> ``` ## Data sources We've seen how data can be bound to views, but we only returned a hard-coded string. Usually you'll want something more involved than that. ### WordPress Composers are executed in a context where WordPress functions like `get_the_ID()` and `the_post()` will return expected values, so you can retrieve data from WordPress much like you normally would. ### Inherited data Inside of a Composer, you can easily access data that has been passed to or inherited by the view through the `data` property: ```php class Example2 extends Composer { ... public function with() { return [ 'better_roots' => str_replace( 'modern', '*awesome*', $this->data->get('roots') ), ]; } } ``` ```blade <!-- resources/views/partials/example2.blade.php --> <div>{{ $better_roots }}</div> <!-- <div>Resources for *awesome* WordPress development</div> --> ``` ### "Automatic" view selection You can always define what view a Composer will be bound to using the `$views` property to list the name(s) of the views. However, if your Composer will target only a single view, you can save yourself a few lines of code. Sage will attempt to match Composers to views based on some simple file path logic: If your view and Composer share the same path segments and name, they'll be automatically bound together. For example, if your view is a partial at `/resources/views/partials/page-header.blade.php`, a Composer at `/app/View/Composers/PageHeader.php` will be automatically bound to it. In other words: - Match paths below `/resources/views` and `/app/View`. - Convert the `kebab-case` of view file names to the `PascalCase` of Composers. ### `with()` vs `override()` You've seen `with()` used above to pass data to views, but it has a more aggressive sibling calling `override()` which does the same thing--except that it will replace data inherited by, or passed to, the view while `with()` will not. ```blade <!-- /resources/views/page.blade.php --> @include('partials.example', ['roots' => "Resources for modern WordPress development"]) <!-- /resources/views/partials/example.blade.php --> <h1>{{ $roots }}</h1> <!-- <h1>Resources for modern WordPress development</h1> --> ``` Using `with()`: ```php class Example extends Composer { public function with() { return [ 'roots' => "An amazing stack!", ]; } } ``` ```blade <!-- /resources/views/partials/example.blade.php --> <h1>{{ $roots }}</h1> <!-- <h1>Resources for modern WordPress development</h1> --> <!-- The same output! --> ``` Using `override()`: ```php class Example extends Composer { public function override() { return [ 'roots' => "An amazing stack!", ]; } } ``` ```blade <!-- /resources/views/partials/example.blade.php --> <h1>{{ $roots }}</h1> <!-- <h1>An amazing stack!</h1> --> ``` ================================================ FILE: sage/configuration.md ================================================ --- date_modified: 2024-01-17 08:22 date_published: 2015-09-01 19:02 description: Configure Sage theme features in `setup.php`. Register menus, define sidebars, enable theme support for WordPress features, and set configuration values. title: Configuring the Sage WordPress Theme authors: - alwaysblank - ben - Log1x --- # Configuring the Sage WordPress Theme ## Introduction All of the configuration for Sage lives inside of `app/setup.php`. Each option is documented allowing for you to easily familiarize yourself with the options configured out of the box. ## Theme Configuration Configuration specific to WordPress resides in the `app/setup.php` file. In this file, you will find the default enqueued stylesheets and scripts, the supported theme features added with `add_theme_support`, and the registration hooks for navigation menus and sidebars. By default, Sage is configured to: - Enqueue `app.css` and `app.js` on the frontend. - Enqueue `editor.css` and `editor.js` in the Gutenberg editor. - Add theme support for common functionality. - Register a default navigation menu called `primary_navigation`. - Register a primary and footer Sidebar widget area. ### `theme.json` Sage ships with a starter `theme.json` that is generated from the build based on your Tailwind config. See the [Gutenberg docs](/sage/docs/gutenberg/) for further information. ================================================ FILE: sage/deployment.md ================================================ --- date_modified: 2025-10-30 11:30 date_published: 2015-09-01 19:29 description: Deploy Sage themes by building assets for production, running `composer install` for dependencies, and ensuring PHP version consistency across environments. title: Deploying the Sage WordPress Theme authors: - alwaysblank - ben - kero - Log1x - MWDelaney --- # Deploying the Sage WordPress Theme ::: warning PHP versions must match Make sure the PHP version of your development environment matches the PHP version of your production environment, or you may hit a fatal error due to your Composer dependencies requiring a different PHP version. ::: ## Deploying a Sage-based WordPress theme 1. Build theme assets (`npm run build`) 2. Install Composer dependencies (`composer install --no-dev --optimize-autoloader`) 3. Upload all files and folders in your theme except the `node_modules` directory to your host ## Optimization Similar to deploying a Laravel app, Acorn supports an `optimize` command that will cache your configuration and views. This command should be run as part of your deployment process: ```shell $ wp acorn optimize ``` ## Server configuration ::: tip Using Trellis or Radicle? If you are using [Trellis](/trellis/) to provision your production environment, or you are using [Radicle](/radicle/), you can **skip** this section. ::: ### Securing Blade templates Due to the nature of WordPress, any file residing in the theme folder is publicly accessible. By default, webservers will return any requests made to a `*.blade.php` template as plain-text. **This can create an opening for potential security risks as well as unwanted snooping.** To prevent this from happening, we will need to add configuration to the web server to deny access to the file extension. #### Nginx If you are using Nginx, add the following to your site configuration before the final location directive: ```nginx location ~* \.(blade\.php)$ { deny all; } ``` #### Apache If you are using Apache, add the following to your virtual host configuration or the `.htaccess` file at the root of your web application: ```apache <FilesMatch ".+\.(blade\.php)$"> # Apache 2.4 <IfModule mod_authz_core.c> Require all denied </IfModule> # Apache 2.2 <IfModule !mod_authz_core.c> Order deny,allow Deny from all </IfModule> </FilesMatch> ``` ## Deploying Sage with Trellis If you use [Trellis](https://roots.io/trellis/), you can build your assets locally (or on a CI server), then copy them to the remote server during deployment. [See the `build-before.yml` example hook](https://github.com/roots/trellis/blob/master/deploy-hooks/build-before.yml) in Trellis. ## Deploying Sage on Kinsta [Kinsta supports Bedrock and Trellis](https://kinsta.com/blog/bedrock-trellis/?kaid=OFDHAJIXUDIV), so deploying Sage with Trellis on [Kinsta](https://kinsta.com/?kaid=OFDHAJIXUDIV) is possible by following a few extra steps. ================================================ FILE: sage/fonts-setup.md ================================================ --- date_modified: 2025-02-27 14:30 date_published: 2023-02-20 11:30 description: Set up custom fonts in Sage using `theme.json`. Define font families that work in both theme frontend and WordPress block editor for consistent typography. title: Setting Up Custom Fonts in Sage authors: - ben --- # Setting Up Custom Fonts in Sage Sage includes an empty `resources/fonts/` directory for you to use for any fonts you want to use in your theme. ## Add your fonts The first step to setting up a font in Sage is to add the `woff2` file to the `resources/fonts/` directory. Since [woff2 usage](https://caniuse.com/?search=woff2) is so high, you probably don't need to consider any other font file formats. For this example, we're going to download [Public Sans from the google-webfonts-helper](https://gwfh.mranftl.com/fonts/public-sans?subsets=latin). The [google-webfonts-helper](https://gwfh.mranftl.com/) is a good resource for quickly grabbing font files and their CSS from Google Fonts. ```plaintext resources ├── css │   ├── app.css │   ├── fonts.css # Create this file │   └── editor.css ├── fonts │   └── public-sans-v14-latin-regular.woff2 ├── images ├── js └── views ``` ## Add the CSS You can place the CSS for your web fonts wherever you'd like. We recommend creating a `css/fonts.css` file and then importing it from `app.css` and `editor.css`: ```css @import './fonts.css'; ``` Define your `@font-face` in `css/fonts.css`: ```css @font-face { font-display: swap; font-family: 'Public Sans'; font-style: normal; font-weight: 400; src: url('@fonts/public-sans-v18-latin-regular.woff2') format('woff2'), } ``` ## Add the font to your Tailwind theme Open `app.css` and add the new font family: ```css @theme { --font-sans: "Public Sans", sans-serif; } ``` See the [Tailwind CSS docs on customizing fonts](https://tailwindcss.com/docs/font-family#customizing-your-theme) for more information. ================================================ FILE: sage/functionality.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-01 19:05 description: The `app/` directory contains theme functionality. Sage is a starter theme, so modify files in `app/` to implement custom features for your WordPress site. title: Adding Theme Functionality in Sage authors: - alwaysblank - ben - jure - Log1x --- # Adding Theme Functionality in Sage The `app/` directory contains all the theme functionality. Since Sage is a starter theme, it’s okay for you to modify files within `app/` to meet the needs of the site you’re building. Most of the PHP code in Sage is namespaced and autoloaded, so make sure to use namespaced functions and classes. If you aren't familiar with these methods, see our blog posts on: * [Namespacing and Autoloading](/namespacing-and-autoloading/) * [Upping PHP Requirements in Your WordPress Themes and Plugins](/upping-php-requirements-in-your-wordpress-themes-and-plugins/) ## The `app/` directory - `app/setup.php` — Enqueue stylesheets and scripts, register support for theme features with `add_theme_support`, register navigation menus and sidebars. See [Theme Configuration and Setup](configuration.md). - `app/filters.php` — Add WordPress filters in this file. Filters included by default: - `excerpt_more` — add "… Continued" to excerpts. - `app/Providers` — The place for any [Service Providers](https://laravel.com/docs/10.x/providers) you care to define for your theme. Comes with `ThemeServiceProvider` that adds no functionality but provides a template for your own Service Providers. - `app/View` — The place for view-related code, i.e. Composers and Components. For more information, see the documentation on [Composers](composers.md) and [Components](components.md). ================================================ FILE: sage/gutenberg.md ================================================ --- date_modified: 2025-02-27 14:00 date_published: 2021-10-21 13:21 description: Sage includes full WordPress block editor support with HMR for editor styles, ensuring consistent styling between editor and frontend with `theme.json` integration. title: Gutenberg Block Editor Support in Sage authors: - alwaysblank - ben - joshf - Log1x - strarsis --- # Gutenberg Block Editor Support in Sage Sage includes two assets that are enqueued when working with the WordPress block editor, also known as Gutenberg: * `resources/js/editor.js` * `resources/css/editor.css` Any styles added to `editor.css` will only be applied to the block editor. ::: warning All blocks must have version 3 support Sage's `editor.css` expects the block editor to be iframed. If you have any blocks that don't support version 3, then the block editor won't be iframed. ::: <small>([Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/#version-3-wordpress-6-3))</small> ## `theme.json` generator Sage includes a `theme.json` file for configuring the WordPress editor. It's generated during [asset builds](compiling-assets.md) automatically and accounts for settings from your Tailwind setup. ::: tip theme.json spec Reference [the `theme.json` documentation](https://developer.wordpress.org/block-editor/how-to-guides/themes/global-settings-and-styles/) for the full specification. ::: Due to Sage including a `theme.json` file, this means [trying to use `add_theme_support()` to configure the editor](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/) will not work. Tailwind CSS colors, font families, and sizes are generated on the theme build for `theme.json`. See the [Sage docs on Tailwind CSS](/sage/docs/tailwind-css/) for more information. This can be disabled in `vite.config.js`. ================================================ FILE: sage/installation.md ================================================ --- date_modified: 2025-02-27 13:45 date_published: 2015-08-29 18:09 description: Install Sage WordPress starter theme by running `composer create-project roots/sage`. Start modern WordPress theme development with Sage foundation. title: Installing Sage WordPress Starter Theme authors: - alwaysblank - ben - Jacek - Lachlan_Arthur - Log1x - QWp6t - TangRufus --- # Installing Sage WordPress Starter Theme Install Sage using Composer from your WordPress themes directory: ```shell $ composer create-project roots/sage your-theme-name ``` To install the latest development version of Sage, add `dev-main` to the end of the command: ```shell $ composer create-project roots/sage your-theme-name dev-main ``` ## Build assets - Edit the `base` path in `vite.config.js` - Run `npm install` from the theme directory to install dependencies - Run `npm run build` to compile assets You must build theme assets in order to access your site. Failing to build the assets will result in the error: ```plaintext Vite manifest not found at [/path/to/sage/public/build/manifest.json] cannot be found. ``` ================================================ FILE: sage/localization.md ================================================ --- date_modified: 2025-12-22 13:00 date_published: 2018-04-24 09:47 description: Generate translation files for Sage themes with custom build scripts. Create and load language files for multilingual WordPress sites with proper loading. title: Localizing the Sage WordPress Theme authors: - alwaysblank - ben - bonakor - jure - Log1x - strarsis --- # Localizing the Sage WordPress Theme ## Generating language files Run `yarn translate:pot` from your theme directory to generate the language files. Then open the generated `.pot` file with [Poedit](https://poedit.com/), select "Create new translation", save `.mo` & `.po` files in the `resources/lang` folder with the correct name syntax (eg. `fr_FR`, `en_US`). When adding/removing translations in templates, run `yarn translate:update`, then select "Catalog > Update from a POT file" in Poedit. ## Loading language files Add the following to `app/setup.php`: ```php add_action('after_setup_theme', function () { load_textdomain( 'sage', get_template_directory() . '/resources/lang/' . determine_locale() . '.mo' ); }); ``` Make sure language files exist in the `resources/lang` directory. ## Polylang and Sage - Install [BenjaminMedia/wp-polylang-theme-strings](https://github.com/BenjaminMedia/wp-polylang-theme-strings) - Replace `__()` with `pll__()` in your templates Need to also translate strings from the `app/` folder? See [`Sage_Polylang_Theme_Translation`](https://github.com/roots/sage/issues/1875#issuecomment-380076482). ================================================ FILE: sage/sass.md ================================================ --- date_modified: 2026-03-22 11:15 date_published: 2023-06-06 17:30 description: Enable Sass in Sage by renaming `app.css` to `app.scss` for advanced CSS preprocessing with variables and mixins. title: Using Sass with Sage WordPress Theme authors: - ben - carlosfaria - code23_isaac - diomededavid - MWDelaney - kellymears - talss89 --- # Using Sass with Sage WordPress Theme Remove Tailwind CSS dependencies: `npm uninstall -D @tailwindcss/vite tailwindcss` Delete the contents of `resources/css/app.css` and `resources/css/editor.css`. Add the `sass` extension: ```shell $ npm install -D sass ``` In the `resources/css` directory, rename `app.css` to `app.scss` and rename `editor.css` to `editor.scss`. ```plaintext app.css -> app.scss editor.css -> editor.scss ``` Modify `vite.config.js` to remove the Tailwind plugin, reference the new file extensions, and disable the Tailwind CSS `theme.json` generation: ```diff import { defineConfig } from 'vite' -import tailwindcss from '@tailwindcss/vite'; import laravel from 'laravel-vite-plugin' import { wordpressPlugin, wordpressThemeJson } from '@roots/vite-plugin'; export default defineConfig({ base: '/app/themes/sage/public/build/', plugins: [ - tailwindcss(), laravel({ input: [ - 'resources/css/app.css', + 'resources/css/app.scss', 'resources/js/app.js', - 'resources/css/editor.css', + 'resources/css/editor.scss', 'resources/js/editor.js', ], assets: ['resources/images/**', 'resources/fonts/**'], wordpressThemeJson({ - disableTailwindColors: false, - disableTailwindFonts: false, - disableTailwindFontSizes: false, + disableTailwindColors: true, + disableTailwindFonts: true, + disableTailwindFontSizes: true, }), ], ``` Modify the `@vite()` directive in `resourves/views/layouts/app.blade.php` to use `app.scss` instead of `app.css`: ```diff - @vite(['resources/css/app.css', 'resources/js/app.js']) + @vite(['resources/css/app.scss', 'resources/js/app.js']) ``` Modify the `block_editor_settings_all` filter in `app/setup.php` to use `editor.scss` instead of `editor.css`; ```diff add_filter('block_editor_settings_all', function ($settings) { - $style = Vite::asset('resources/css/editor.css'); + $style = Vite::asset('resources/css/editor.scss'); $settings['styles'][] = [ 'css' => "@import url('{$style}')", ]; return $settings; }); ``` ================================================ FILE: sage/structure.md ================================================ --- date_modified: 2025-02-27 13:50 date_published: 2021-10-21 13:21 description: Sage's directory structure provides organized folders for scalable development. `resources/` for views, `app/` for functionality, `config/` for settings. title: Sage WordPress Theme Structure authors: - alwaysblank - ben - jure - Log1x - MWDelaney --- # Sage WordPress Theme Structure ## Introduction The default Sage structure is intended to provide a sane starting point for both small and large WordPress sites alike. Where a file or class is located is ultimately decided by you. As long as Composer can autoload the class or you have modified the necessary paths in your [configuration](configuration.md), things should work as expected. ```plaintext themes/your-theme-name/ # → Root of your Sage based theme ├── app/ # → Theme PHP │ ├── Providers/ # → Service providers │ ├── View/ # → View models │ ├── filters.php # → Theme filters │ └── setup.php # → Theme setup ├── public/ # → Built theme assets (never edit) ├── resources/ # → Theme assets and templates │ ├── css/ # → Theme stylesheets │ ├── fonts/ # → Theme fonts │ ├── images/ # → Theme images │ ├── js/ # → Theme JavaScript │ └── views/ # → Theme templates │ ├── components/ # → Component templates │ ├── forms/ # → Form templates │ ├── layouts/ # → Base templates │ └── partials/ # → Partial templates ├── vendor/ # → Composer packages (never edit) ├── composer.json # → Autoloading for `app/` files ├── functions.php # → Theme bootloader ├── index.php # → Theme template wrapper ├── node_modules/ # → Node packages (never edit) ├── package.json # → Node dependencies and scripts ├── screenshot.png # → Theme screenshot for WP admin ├── style.css # → Theme meta information └── vite.config.js # → Vite configuration ``` ## The root directory ### The `app/` directory The majority of your theme functionality lives in the `app` directory. By default, this directory is namespaced under `App` and is automatically loaded by Composer using the [PSR-4 autoloading standard](https://www.php-fig.org/psr/psr-4/). See our blog post on [Namespacing and Autoloading](/namespacing-and-autoloading/) if you aren't familiar with these methods. This directory is covered more in [Functionality](/sage/docs/functionality/). ### The `public/` directory The `public` directory contains the compiled assets for your theme. This directory will never be manually modified. ### The `node_modules/` directory The `node_modules` directory contains your [Node](https://nodejs.org/) dependencies used to build your assets during development or for production. This folder is automatically generated and should not be modified. ::: danger Don’t upload node_modules Under no circumstances should there ever be a need to upload this folder or any of its contents to a live production server. It is a security risk. ::: ### The `resources/` directory The `resources` directory contains your Blade views as well as the un-compiled assets of your theme such as CSS, JavaScript, images, and fonts. ### The `vendor/` directory The `vendor` directory contains your [Composer](https://getcomposer.org/) dependencies and autoloader. This directory is automatically generated and should not be modified. ================================================ FILE: sage/tailwind-css.md ================================================ --- date_modified: 2025-02-27 14:00 date_published: 2023-03-13 11:00 description: Sage generates `theme.json` from Tailwind configuration automatically, making Tailwind color palette, font families, and sizes available in WordPress block editor. title: Using Tailwind CSS with Sage Theme authors: - ben - MWDelaney --- # Using Tailwind CSS with Sage Theme Sage includes support for Tailwind CSS out of the box, along with some helpful functionality for integrating your Tailwind config into the WordPress block editor. ## Generating `theme.json` from your Tailwind setup Sage includes a `theme.json` file for configuring the WordPress editor. It's generated during [asset builds](compiling-assets.md) automatically and accounts for settings from your `app.css` file. This functionality is handled by the [`@roots/vite-plugin`](https://github.com/roots/vite-plugin) included in Sage. To modify this behavior, open `vite.config.js` and edit the `wordpressThemeJson` plugin's configuration. ### Default color palette Rather than [manually defining the editor colors](https://developer.wordpress.org/themes/global-settings-and-styles/settings/color/) by adding them to `theme.json`, your Tailwind config will be used to generate colors for the WordPress editor. Tailwind’s [default color palette](https://tailwindcss.com/docs/colors) is a good starting point for sites that don’t already have color/branding guidelines to follow. ### Sizes and font families In addition to including Tailwind’s color palette for the WordPress editor, Sage will also configure the editor with Tailwind’s font families and font sizes. ================================================ FILE: sage/theme-templates.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-01 19:12 description: Sage's `resources/views/` directory contains Blade templates following WordPress template hierarchy. Extend templates using standard WordPress conventions. title: WordPress Theme Templates in Sage authors: - alwaysblank - ben - Log1x --- # WordPress Theme Templates in Sage The `resources/views/` directory contains files that you can further extend with the normal [WordPress Template Hierarchy](https://developer.wordpress.org/themes/classic-themes/basics/template-hierarchy/): - `404.blade.php` – Error 404 page - `index.blade.php` – Archive page (used by blog page, category archives, author archives and more) - `page.blade.php` – Single page - `search.blade.php` – Search results page - `single.blade.php` – Single post page - `template-custom.blade.php` – An example single page template All templates are wrapped by a base file in the `layouts/` directory: - `app.blade.php` – The base template which wraps the base markup around all template files ::: warning Note The `app` layout contains all the content generated by Blade templates, but is itself wrapped by the `index.php` in the root of the theme. ::: These files include templates from the `resources/views/partials/` directory which is where you'll be making most of your customizations: - `comments.blade.php` – Markup for comments - `content-page.blade.php` – Markup included from `resources/views/page.blade.php` - `content-search.blade.php` – Markup included from `resources/views/search.blade.php` - `content-single.blade.php` – Markup included from `resources/views/single.blade.php` - `content.blade.php` – Markup included from `resources/views/index.blade.php` - `entry-meta.blade.php` – Post entry meta information included from `resources/views/content-single.blade.php` - `footer.blade.php` – Footer markup included from `resources/views/app.blade.php` - `header.blade.php` – Header markup included from `resources/views/app.blade.php` - `page-header.blade.php` – Page title markup included from most of the files in the `resources/views/` directory - `sidebar.blade.php` – Sidebar markup included from `resources/views/app.blade.php` ## Extending templates The normal [WordPress Template Hierarchy](https://developer.wordpress.org/themes/classic-themes/basics/template-hierarchy/) is still intact. Here’s some examples: - Copy `index.blade.php` to `author.blade.php` for customizing author archives - Copy `index.blade.php` to `home.blade.php` for customizing the Home page if you’re showing the latest posts (under Reading Settings) instead of a static front page - Copy `index.blade.php` to `archive-gallery.blade.php` for customizing the archive page for a custom post type registered as `gallery` - Copy `page.blade.php` to `front-page.blade.php` for customizing the static front page - Copy `page.blade.php` to `page-about.blade.php` for customizing a page called About ================================================ FILE: sage/use-blade-icons.md ================================================ --- date_modified: 2024-04-24 13:00 date_published: 2022-03-18 20:49 description: Install and use blade-icons in Sage for SVG icon components in Blade templates. Simplifies icon management with clean component syntax. title: How to Use blade-icons with Sage authors: - altan - ben --- # How to Use blade-icons with Sage The [blade-icons](https://github.com/driesvints/blade-icons) package allows you to easily use SVG's in your Blade views. Besides being able to use your own SVG's, you can also add one of the many third party icon sets, such as: * [Blade Bootstrap Icons](https://github.com/davidhsianturi/blade-bootstrap-icons) * [Blade Font Awesome](https://github.com/owenvoke/blade-fontawesome) * [Blade Heroicons](https://github.com/driesvints/blade-heroicons) * [Blade Simple Icons](https://github.com/ublabs/blade-simple-icons) [![Screenshot of blade-icons home page](https://cdn.roots.io/app/uploads/use-blade-icons.png)](https://blade-ui-kit.com/blade-icons) ## Installation From the same directory where you've installed Acorn (typically your site root or your Sage theme folder), add `blade-icons` as a Composer dependency: ```shell $ composer require blade-ui-kit/blade-icons ``` Then publish the configuration file: ```shell $ wp acorn vendor:publish --tag=blade-icons ``` ## Configuration From the published `config/blade-icons.php` file, we recommend setting the default set to point to your theme directory: ```php <?php return [ 'sets' => [ 'default' => [ 'path' => 'web/app/themes/sage/resources/images/icons', # Relative path to the new directory 'prefix' => 'icon', ], ], ]; ``` ## Adding icons Add a new directory inside `resources/images/` named `icons/` and place your SVG icons in this directory. ## Using icons in Blade views From your Blade views you can now use the provided Blade component, or the `@svg` directive: ```blade <x-icon-example-icon /> @svg('example-icon') ``` ## Adding icon sets `blade-icons` supports a **lot** of different icon sets through packages made through the community. The [Blade icons search](https://blade-ui-kit.com/blade-icons#search) allows you to quickly find a new icon to use in your project. To add aditional icon sets, require them as Composer dependencies the same as you did for the `blade-icons` package. In this example, we'll add the `blade-heroicons` package: ```shell $ composer require blade-ui-kit/blade-heroicons ``` Now Heroicons can be referenced in any of the supported methods from inside your Blade views: ```blade <x-heroicon-s-menu /> @svg('heroicon-s-menu') {{ svg('heroicon-s-menu) }} ``` ## Caching icons in production It's recommended to enable icon caching to optimize performance by running `wp acorn icons:cache` during deployment. If you are using Trellis, modify the `deploy_build_after` hook within your `deploy-hooks/build-after.yml` file: ```yml - name: Cache Blade UI Icons command: wp acorn icons:cache args: chdir: "{{ deploy_helper.new_release_path }}" ``` ## Additional information The [blade-icons README](https://github.com/driesvints/blade-icons) covers how to pass attributes, set default classes, and more. ================================================ FILE: sage/woocommerce.md ================================================ --- date_modified: 2025-06-26 11:00 date_published: 2025-06-26 11:00 description: Set up WooCommerce in Sage themes for eCommerce functionality. Configure templates, declare theme support, and integrate WooCommerce styling with Sage. title: Setting Up WooCommerce with Sage Theme authors: - aitor - csorrentino - ben - strarsis - YourRightWebsite --- # Setting Up WooCommerce with Sage Theme [WooCommerce](https://woocommerce.com/) is compatible with Sage's Blade templates with the correct setup. ## Add the sage-woocommerce package The [`generoi/sage-woocommerce`](https://github.com/generoi/sage-woocommerce) package adds functionality to allow Blade templates to work on Acorn and Sage powered WordPress sites: ```shell $ composer require generoi/sage-woocommerce ``` ### Publish the templates to your theme Add the required `single-product.blade.php` and `archive-product.blade.php` views to your theme: ```shell $ wp acorn vendor:publish --tag="woocommerce-template-views" ``` You can now edit these templates from `resources/views/woocommerce/`. ## Update WooCommerce default pages In WooCommerce 9.x+, the default pages (Shop, Cart, Checkout) are created with block-based content by default. These do not use classic templates. Remove the default blocks from these pages and replace them with the relevant shortcodes: * `[woocommerce_cart]` * `[woocommerce_checkout]` ## Disable "Coming soon mode" In WooCommerce 9.x+, Coming soon mode is enabled by default for all stores from **Settings > Site visibility**. Until it’s done, WooCommerce keeps the Shop page in a special unpublished state that behaves differently depending on whether you’re logged in or not: * Logged-in users see the correct template * Logged-out users are shown a block-based fallback, ignoring the theme's templates entirely ================================================ FILE: trellis/ansible.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2022-02-28 22:16 description: Understand how Trellis leverages Ansible for WordPress automation. Learn key concepts like playbooks, roles, tasks, and variables used for server management. title: How Trellis Uses Ansible for WordPress authors: - swalkinshaw --- # How Trellis Uses Ansible for WordPress Since Trellis is powered by Ansible, the best way to understand Trellis is to understand Ansible itself. Even knowing a few just key Ansible concepts will help you learn Trellis and how to customize it to fit your needs. Ansible's own [documentation](https://docs.ansible.com/projects/ansible/latest/user_guide/index.html) is very comprehensive and should be considered as an extension of Trellis' documentation. However, since Ansible itself is unopinionated, this will explore some key concepts and how they apply to Trellis. ## Playbooks At the highest level, Trellis provides a few playbooks which execute _tasks_ organized into _roles_. Trellis' playbooks are found in the root of Trellis itself: * [`dev.yml`](https://github.com/roots/trellis/blob/master/dev.yml) - provisions a development server. This playbook assumes that your local Trellis project files have been synced to a virtual machine and automatically installs WordPress. * [`server.yml`](https://github.com/roots/trellis/blob/master/dev.yml) - provisions a remote (non-dev) server. This playbook assumes you will be deploying sites separately and does not attempt to install WordPress. * [`deploy.yml`](https://github.com/roots/trellis/blob/master/deploy.yml) - deploys a single site to an environment * [`rollback.yml`](https://github.com/roots/trellis/blob/master/deploy.yml) - rolls back a previously deployed release * [`xdebug-tunnel.yml`](https://github.com/roots/trellis/blob/master/xdebug-tunnel.yml) - opens or closes the PHP Xdebug tunnel ## Roles Each playbook listed above contains a list of roles to run. A role's main purpose is to group a collection of tasks to run within the `tasks` directory. All of Trellis' roles are found under the top-level [`roles`](https://github.com/roots/trellis/tree/master/roles) directory. Additionally, there are some 3rd party community roles used from Ansible Galaxy which are specified in the [`galaxy.yml`](https://github.com/roots/trellis/blob/master/galaxy.yml) file. Roles in Trellis usually contain one of more of these subfolders: * `defaults` - variables defined with low precedence * `tasks` - tasks to be executed - the main functional part of roles * `templates` - templates in Jinja format which are used in tasks ## Inventory In Ansible, [inventory](https://docs.ansible.com/projects/ansible/latest/user_guide/intro_inventory.html#intro-inventory) is a list of defined hosts in your infrastructure. For most Trellis projects, this list of hosts is usually one development virtual machine, one staging server (optional), and one production server. If you look at the default inventory files in [`hosts`](https://github.com/roots/trellis/tree/master/hosts) directory, you'll see three files named after the standard environments: `development`, `staging`, `production`. Here's what an inventory file in Trellis looks like: ```ini [production] your_server_hostname [web] your_server_hostname ``` Each host is under two groups: the environment (`production` in this case) and `web`. These groups can be used for any semantic grouping you want, but in Trellis you at least need those two built-in ones. ## Group variables The "group" naming isn't the most clear, but as shown above, these refer to Ansible's concept of "inventory groups". And since Trellis' inventory hosts are named for environments, "group vars" are really just _environment specific variables_. Though they can also be used for any semantic grouping of inventory hosts for more advanced use cases. Note: the `all` group (in `group_vars/all`) is special and applies to all groups. ## Variables All variables in Ansible can be considered _global_. Even if a variable is defined within a role (eg: `roles/nginx/defaults/main.yml`), it can be referenced or re-defined in a `group_vars` file. Once a role is included in a playbook, their variables (in `defaults` or `vars`) are available globally. ### Example As an example, let's say you wanted to change PHP's max execution time in development to be higher than in production. [`php_max_execution_time`](https://github.com/roots/trellis/blob/40b949a910373398e3fda06105287e0edf24051a/roles/php/defaults/main.yml#L10) is found in [`roles/php/defaults/main.yml`](https://github.com/roots/trellis/blob/master/roles/php/defaults/main.yml). We can apply two things we learned above: 1. variables are global 2. group vars can be used to define environment specific values Taking advantage of Ansible's [variable precendence](https://docs.ansible.com/projects/ansible/latest/user_guide/playbooks_variables.html#understanding-variable-precedence), we'll just override the variable by re-defining it in `group_vars/development/php.yml`: ```yaml php_max_execution_time: 500 ``` ================================================ FILE: trellis/cli.md ================================================ --- date_modified: 2024-09-11 10:00 date_published: 2023-04-05 07:42 description: Use the Trellis CLI to manage WordPress projects via the `trellis` command. Simplifies provisioning servers, deploying sites, and common Trellis tasks. title: Trellis CLI Command-Line Interface authors: - swalkinshaw --- # Trellis CLI Command-Line Interface trellis-cli is a command-line interface (CLI) to manage Trellis projects via the `trellis` command. The CLI provides a more consistent and integrated experience and includes: * Automatic Python Virtualenv integration for easier dependency management * Smart autocompletion (based on your defined environments and sites) * One-command cloud server creation (DigitalOcean, Hetzner Cloud) * Better Ansible Vault support for encrypting files * (New) Built-in virtual machine support for development environments and much more. ## Installation ### Quick Install (macOS, Linux via Homebrew) ```shell $ brew install roots/tap/trellis-cli ``` ### Script We also offer a quick script version: ```shell $ curl -sL https://roots.io/trellis/cli/get | bash ``` ### Manual Install trellis-cli provides binary releases for a variety of OSes. These binary versions can be manually downloaded and installed. 1. Download the [latest release](https://github.com/roots/trellis-cli/releases/latest) or any [specific version](https://github.com/roots/trellis-cli/releases) 2. Unpack it (eg: `tar -zxvf trellis_1.2.1_Linux_x86_64.tar.gz`) 3. Find the `trellis` binary in the unpacked directory, and move it to its desired destination (eg: `mv trellis_1.2.0_Darwin_x86_64/trellis /usr/local/bin/trellis`) 4. Make sure the above path is in your `$PATH` ### Dev/unstable install (macOS, Linux via Homebrew) ```shell $ brew uninstall roots/tap/trellis-cli ``` ```shell $ brew install --HEAD roots/tap/trellis-cli-dev ``` ```shell $ brew upgrade --fetch-HEAD roots/tap/trellis-cli-dev ``` ## Usage Run `trellis` for the complete usage and help. For subcommand documentation, run `trellis <command> -h`. ### Commands | Command | Description | | --- | --- | | `alias` | Generate WP CLI aliases for remote environments | | `check` | Checks if the required and optional Trellis dependencies are installed | | `db` | Commands for database management | | `deploy` | Deploys a site to the specified environment | | `dotenv` | Template .env files to local system | | `server` | Commands for cloud server management (DigitalOcean, Hetzner Cloud) | | `exec` | Exec runs a command in the Trellis virtualenv | | `galaxy` | Commands for Ansible Galaxy | | `info` | Displays information about this Trellis project | | `init` | Initializes an existing Trellis project | | `key` | Commands for managing SSH keys | | `logs` | Tails the Nginx log files for an environment | | `new` | Creates a new Trellis project | | `open` | Opens user-defined URLs (and more) which can act as shortcuts/bookmarks specific to your Trellis projects | | `provision` | Provisions the specified environment | | `rollback` | Rollback the last deploy of the site on the specified environment | | `shell-init` | Prints a script which can be eval'd to set up Trellis' virtualenv integration in various shells | | `ssh` | Connects to host via SSH | | `valet` | Commands for Laravel Valet | | `vault` | Commands for Ansible Vault | | `vm` | Commands for managing development virtual machines | | `xdebug-tunnel` | Commands for managing Xdebug tunnels | ## Configuration There are four ways to set configuration settings for trellis-cli and they are loaded in this order of precedence: 1. global config (`$HOME/.config/trellis/cli.yml`) 2. project config (`trellis.cli.yml`) 3. project config local override (`trellis.cli.local.yml`) 4. env variables The global CLI config (defaults to `$HOME/.config/trellis/cli.yml`) and will be loaded first (if it exists). Next, if a project is detected, the project CLI config will be loaded if it exists at `trellis.cli.yml` (within your `trellis` directory). A Git ignored local override config is also supported at `trellis.cli.local.yml`. Finally, env variables prefixed with `TRELLIS_` will be used as overrides if they match a supported configuration setting. The prefix will be stripped and the rest is lowercased to determine the setting key. Note: only string, numeric, and boolean values are supported when using environment variables. Current supported settings: | Setting | Description | Type | Default | | --- | --- | -- | -- | | `allow_development_deploys` | Whether to allows deploy to the `development` env | boolean | false | | `ask_vault_pass` | Set Ansible to always ask for the vault pass | boolean | false | | `check_for_updates` | Whether to check for new versions of trellis-cli | boolean | true | | `database_app` | Database app to use in `db open` (Options: `tableplus`, `sequel-ace`)| string | none | | `load_plugins` | Load external CLI plugins | boolean | true | | `open` | List of name -> URL shortcuts | map[string]string | none | | `virtualenv_integration` | Enable automated virtualenv integration | boolean | true | | `server` | Options for cloud server management | Object | see below | | `vm` | Options for dev virtual machines | Object | see below | ### `server` | Setting | Description | Type | Default | | --- | --- | -- | -- | | `provider` | Cloud provider (Options: `digitalocean`, `hetzner`)| string | `digitalocean` | ### `vm` | Setting | Description | Type | Default | | --- | --- | -- | -- | | `manager` | VM manager (Options: `auto` (depends on OS), `lima`)| string | "auto" | | `ubuntu` | Ubuntu OS version (Options: `18.04`, `20.04`, `22.04`, `24.04`)| string | `24.04` | | `hosts_resolver` | VM hosts resolver (Options: `hosts_file`)| string | `hosts_file` | | `images` | Custom OS image | object | Set based on `ubuntu` version | #### `images` | Setting | Description | Type | Default | | --- | --- | -- | -- | | `location` | URL of Ubuntu image | string | none | | `arch` | Architecture of image (eg: `x86_64`, `aarch64`) | string | none | Example config: ```yaml ask_vault_pass: false check_for_updates: true load_plugins: true open: site: "https://mysite.com" admin: "https://mysite.com/wp/wp-admin" server: provider: digitalocean virtualenv_integration: true vm: manager: auto ubuntu: 24.04 ``` Example env var usage: ```shell $ TRELLIS_ASK_VAULT_PASS=true trellis provision production ``` ================================================ FILE: trellis/composer-authentication.md ================================================ --- date_modified: 2026-03-11 12:00 date_published: 2021-09-06 16:48 description: Set up Composer authentication in Trellis to access private packages, commercial plugins, and authenticated repositories during deployment. title: Composer Authentication authors: - ben - swalkinshaw - TangRufus --- # Composer Authentication Many paid WordPress plugins also offer Composer support. Typically, this is accomplished by adding the plugin repository to your composer.json file: ```json "repositories": [ { "type":"composer", "url":"https://example.com" } ] ``` The actual plugin download is usually protected behind an authentication layer. This allows the plugin developer to restrict access to the plugin via Composer. The authentication credentials are stored in an auth.json file. However, when using such plugins in a Trellis project, it is generally considered bad practice to implement this via [deploy hooks](https://discourse.roots.io/t/interactive-console-authentication-for-3rd-party-repository-on-deploy/8592/2) or adding the `auth.json` to your version control. Trellis supports authentication for multiple Composer repositories, via the Ansible [Vault](/trellis/docs/vault/#steps-to-enable-ansible-vault) functionality, on a per environment configuration. ## Supported authentication types | Type | Description | | --- | --- | | `http-basic` | HTTP basic authentication (username/password) | | `bearer` | HTTP Bearer token authentication | | `github-oauth` | GitHub OAuth token | | `gitlab-oauth` | GitLab OAuth token | | `gitlab-token` | GitLab personal/deploy token | | `bitbucket-oauth` | Bitbucket OAuth consumer key/secret | | `custom-headers` | Custom HTTP header authentication | ## HTTP Basic If `type` is omitted, it defaults to `http-basic` for backward compatibility. ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: http-basic, hostname: example.com, username: my-username, password: my-password } ``` If the private repository doesn't use a password (because the username contains an API key for example), you can omit `password`: ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: http-basic, hostname: example.com, username: apikey } ``` ## Bearer ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: bearer, hostname: example.com, token: my-token } ``` ## GitHub OAuth ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: github-oauth, hostname: github.com, token: my-github-token } ``` ## GitLab OAuth ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: gitlab-oauth, hostname: gitlab.com, token: my-gitlab-oauth-token } ``` ## GitLab Token ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: gitlab-token, hostname: gitlab.com, token: my-gitlab-token } ``` ## Bitbucket OAuth ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: bitbucket-oauth, hostname: bitbucket.org, consumer_key: my-consumer-key, consumer_secret: my-consumer-secret } ``` ## Custom Headers For private repositories that use custom HTTP headers for authentication: ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: custom-headers, hostname: repo.example.org, headers: ["API-TOKEN: my-api-token"] } ``` Multiple headers can be specified: ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: custom-headers, hostname: repo.example.org, headers: ["API-TOKEN: my-api-token", "X-CUSTOM-HEADER: value"] } ``` ## Multiple repositories Multiple private Composer repositories can be configured together: ```yaml # group_vars/<env>/vault.yml vault_wordpress_sites: example.com: composer_authentications: - { type: http-basic, hostname: example.com, username: my-username, password: my-password } - { type: github-oauth, hostname: github.com, token: my-github-token } - { type: bearer, hostname: private-registry.com, token: my-token } ``` ## Requirements - Passwords and tokens should not be stored as plain text, as described in the [Vault](/trellis/docs/vault/) documentation ================================================ FILE: trellis/configuring-php.md ================================================ --- date_modified: 2025-04-01 00:00 date_published: 2025-04-01 00:00 description: Configure PHP settings in Trellis by overriding default values. Adjust memory limits, max execution time, upload sizes, and other PHP directives per site. title: Configuring PHP Settings in Trellis authors: - dalepgrant --- # Configuring PHP Settings in Trellis Trellis will setup PHP and extensions suitable for a WordPress environment out of the box, but you may want to customise for your own setups. For example, you may want to change the version of PHP or add an extension. ::: tip Note If you make a change to the PHP settings, you will need to [reprovision](https://roots.io/trellis/docs/local-development/#re-provisioning) your environments before seeing any changes. You can use `--tags=php` to only run the required role(s). Be sure to reprovision all the environments you need to. ::: ## Change the version of PHP In [`group_vars/all/main.yml`](https://github.com/roots/trellis/blob/master/group_vars/all/main.yml), set the value of `php_version` to the version you're working with. e.g. to use PHP 8.1: `php_version: "8.1"` As of [#1560](https://github.com/roots/trellis/pull/1560) Trellis supports 7.4 & 8.1 up to 8.4. Newer versions may work when released but may not have been tested by the community yet - you can help by testing yourself and reporting your progress on [Roots Discourse](https://discourse.roots.io/). ## Changing the default extensions Trellis will look for a version-specific override file before falling back to the default PHP extensions. If you'd like to change the extensions installed on your environments, duplicate the [`roles/php/vars/version-specific-defaults.yml`](https://github.com/roots/trellis/blob/master/roles/php/vars/version-specific-defaults.yml) file and rename it to the version of PHP you are targetting. ### Example To target PHP 8.4, duplicate and rename so that your folder looks like this: ```diff ├── ...other folders... └── roles/ ├── ...other folders... └── php/ ├── ...other folders... └── vars/ + ├── 8.4.yml └── version-specific-defaults.yml ``` In your `8.4.yml` file you can then set the extensions you'd like to use. Include all extensions from the defaults file unless you have a good reason not to, remembering that [WP requires some extensions](https://make.wordpress.org/hosting/handbook/server-environment/#php-extensions) to work. ```yaml php_extensions_default: php8.4-bcmath: "{{ apt_package_state }}" php8.4-example: "{{ apt_package_state }}" # etc. ``` ================================================ FILE: trellis/cron-jobs.md ================================================ --- date_modified: 2026-03-10 12:00 date_published: 2026-03-10 12:00 description: How Trellis manages WordPress cron jobs with system cron, including configuration options for single sites and Multisite, plus adding custom cron jobs via deploy hooks. title: Cron Jobs in Trellis authors: - ben - chrillep --- # Cron Jobs in Trellis Trellis sets `DISABLE_WP_CRON` to `true` by default and replaces WP-Cron with a system cron job. This is more reliable than WP-Cron, which depends on site traffic to trigger scheduled tasks. ## How it works When you provision a server, Trellis automatically: 1. Sets `DISABLE_WP_CRON` to `true` in your site's `.env` file 2. Creates a system cron job that runs `wp cron event run --due-now` on a schedule This means WordPress scheduled events (like publishing scheduled posts, checking for updates, and running plugin tasks) are handled by the server's cron daemon instead of relying on page visits. ## Configuration ### Cron interval The default cron interval is every 15 minutes. You can customize it per-site with the `cron_interval` option in `wordpress_sites.yml`: ```yaml wordpress_sites: example.com: cron_interval: '*/5' ``` The value follows standard [cron schedule syntax](https://en.wikipedia.org/wiki/Cron#Overview) for the minute field. ### Disabling system cron If you want to use WP-Cron instead of the system cron, set `disable_wp_cron` to `false` in your site's `env` configuration: ```yaml wordpress_sites: example.com: env: disable_wp_cron: false ``` This will re-enable WP-Cron and remove the system cron job on the next provision. ### Multisite For Multisite installations, Trellis creates a separate cron job that iterates over all sites in the network. The default interval for Multisite is every 30 minutes, configurable with `cron_interval_multisite`: ```yaml wordpress_sites: example.com: multisite: enabled: true cron_interval_multisite: '*/15' ``` You can disable the Multisite cron job while keeping `disable_wp_cron` enabled by setting `multisite.cron` to `false`: ```yaml wordpress_sites: example.com: multisite: enabled: true cron: false ``` ## Adding custom cron jobs Trellis doesn't have a built-in configuration option for custom cron jobs, but you can add them using Ansible's [cron module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/cron_module.html) with [deploy hooks](/trellis/docs/deployments/#hooks). ### During provisioning To add a custom cron job during provisioning, create a task file and include it via a hook or custom role. For example, create `roles/custom-crons/tasks/main.yml`: ```yaml - name: Schedule database backup cron: name: "{{ item.key }} database backup" minute: '0' hour: '3' user: "{{ web_user }}" job: "cd {{ www_root }}/{{ item.key }}/current && wp db export /tmp/{{ item.key }}-backup.sql > /dev/null 2>&1" cron_file: "{{ item.key | replace('.', '_') }}-db-backup" loop: "{{ wordpress_sites | dict2items }}" loop_control: label: "{{ item.key }}" ``` ### During deploy If you only use the deploy portion of Trellis (e.g., you don't have root access to run full provisioning), you can manage cron jobs through deploy hooks. This is useful for keeping cron job definitions in sync with your deployed code. Create a task file at `deploy-hooks/cron-jobs.yml`: ```yaml - name: Schedule custom task cron: name: "{{ site }} custom task" minute: '0' hour: '*/6' user: "{{ web_user }}" job: "cd {{ deploy_helper.current_path }} && wp eval-file scripts/custom-task.php > /dev/null 2>&1" ``` ::: tip Always set `user` explicitly. Without it, the cron job is added to the crontab of whichever user Ansible connects as (often the `deploy` user), which can cause WP-CLI permission issues. Use `{{ web_user }}` to match how Trellis manages its own WordPress cron jobs. ::: Then add the hook to your configuration in `group_vars/all/deploy-hooks.yml` (or `group_vars/all/main.yml`): ```yaml deploy_finalize_after: - "{{ playbook_dir }}/roles/deploy/hooks/finalize-after.yml" - "{{ playbook_dir }}/deploy-hooks/cron-jobs.yml" ``` ::: warning When overriding `deploy_finalize_after`, make sure to keep the default Trellis hook (`roles/deploy/hooks/finalize-after.yml`) as the first item in the list. Omitting it will skip the default tasks that refresh WordPress settings and reload php-fpm. ::: ### Cron module options The Ansible `cron` module supports the following scheduling options: | Option | Description | Default | |-----------|--------------------------------------|---------| | `minute` | Minute (0-59, `*`, `*/N`) | `*` | | `hour` | Hour (0-23, `*`, `*/N`) | `*` | | `day` | Day of month (1-31, `*`, `*/N`) | `*` | | `month` | Month (1-12, `*`, `*/N`) | `*` | | `weekday` | Day of week (0-6, Sunday=0, `*`) | `*` | | `job` | The command to run | | | `name` | Description of the cron entry | | | `user` | The user the cron job runs as | | | `state` | `present` or `absent` | `present` | ## Verifying cron jobs Trellis stores its provisioned cron jobs as files in `/etc/cron.d/`. To list them: ```bash ls /etc/cron.d/wordpress-* ``` To view a specific cron file: ```bash cat /etc/cron.d/wordpress-example_com ``` Cron jobs added without `cron_file` (including deploy hook examples that use `user`) are stored in the specified user's crontab instead. To view those: ```bash sudo crontab -u web -l ``` ## Related documentation - [Managing WP-Cron in Bedrock](/bedrock/docs/wp-cron/) - [Deployments and deploy hooks](/trellis/docs/deployments/) ================================================ FILE: trellis/database-access.md ================================================ --- date_modified: 2023-06-06 15:00 date_published: 2016-11-27 11:34 description: Access Trellis WordPress databases using GUI tools like Sequel Pro or TablePlus. Configure SSH tunnels for secure connections without phpMyAdmin. title: WordPress Database Access with Trellis authors: - ben - huubl - Log1x - MWDelaney - mZoo - swalkinshaw - TangRufus --- # WordPress Database Access with Trellis Accessing your databases with client software like [Sequel Pro](https://www.sequelpro.com/), [Sequel Ace](https://sequel-ace.com/) and [TablePlus](http://tableplus.com/) is straight forward with [`trellis-cli`](https://github.com/roots/trellis-cli). Run the following from any directory within your project: ## Sequel Pro (or Sequel Ace): ```shell $ trellis db open --app=sequel-pro production example.com ``` ## TablePlus ```shell $ trellis db open --app=tableplus production example.com ``` ::: tip SSH Password? Because Trellis provisions remote environments to use [SSH keys](/trellis/docs/ssh-keys/) rather than passwords, the password field or prompt is left blank. ::: ## Connection details To access database passwords, run: ```shell $ trellis vault view <environment> | grep "db_password" ``` ### Remote servers * Connection type: SSH * MySQL host: `127.0.0.1` * Username: `example_com` * Password: `example_dbpassword` * SSH Host: `example.com` * SSH User: `web` ================================================ FILE: trellis/debugging-php.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2016-11-07 16:30 description: Debug WordPress PHP code with Trellis's built-in Xdebug support in development. Configure your IDE for step debugging, breakpoints, and inspection. title: Debugging PHP in Trellis with Xdebug authors: - ben - Log1x - swalkinshaw --- # Debugging PHP in Trellis with Xdebug There are many ways to go about debugging a PHP application, and one of the most effective ways is using a debugger. One of the most powerful tools in the PHP community to go about doing this is [Xdebug](https://en.wikipedia.org/wiki/Xdebug). ## What is Xdebug? Xdebug enables you to do the following: - debug and profile PHP applications and scripts - interactively debug running code - measure the performance of your application - see the state of your application at a point in time Xdebug gives you all sorts of visibility into the internals of your application, like what variable values are at a certain point in time, what functions are taking a long time to execute, as well as what the return values of functions are. It gives you the ability to step through the execution of your application function by function, or even line by line if you really want to. ## Installation Trellis is configured with Xdebug and ready to rock out of the box in development. All you have to do is select a compatible debugger. Xdebug is designed to be used with a DBGP-compatible debugger in order to interface with Xdebug on your site. [PHPStorm](https://www.jetbrains.com/phpstorm/) comes with support for this out of the box and [Sublime Text](https://github.com/martomo/SublimeTextXdebug), [Visual Studio Code](https://github.com/felixfbecker/vscode-php-debug), and [Vim](https://github.com/vim-vdebug/vdebug) have plugins available. ## Configuration The variables used in the `roles/xdebug` role directly correlate to the configuration options used by Xdebug itself. For example, Xdebug has the option `xdebug.scream` to disable PHP error suppression using the `@` symbol before function calls. The corresponding Trellis variable would be `xdebug_scream`. You can see all the available configuration options in `roles/xdebug/defaults/main.yml` and read about how they're used in Xdebug on their [documentation page](https://xdebug.org/docs/all_settings). Trellis ships with pretty sane defaults, but this gives you the option to override if necessary. To change those variables, it's recommended you set them in `group_vars/<environment>/php.yml`. ## Using Xdebug in production While we default to installing Xdebug in development, installing it in any other environment is "opt-in." **It is not recommended to use Xdebug in production**, but it _can_ be extremely useful in debugging production-like environments. For example, if there's an issue you're encountering in Production, but cannot reproduce in Development, it's likely the problem lies with something specific to your VPS provider. Duplicating your production environment and sanitizing the data using something like [WP Hammer](https://github.com/10up/wp-hammer) will allow you to debug your production environmment without affecting it. This is where trellis-cli's `xdebug-tunnel` commands comes in. ### `trellis xdebug-tunnel`: Xdebug + SSH tunnels Xdebug gives a lot of visibility into your application that you do not want to give to anyone. Because of this, you want to restrict access to who is allowed to initiate a debugging session. The way we go about doing that is by creating a remote SSH tunnel from the VPS to your local computer. `trellis xdebug-tunnel` makes it trivial to set up the connection by installing Xdebug if it is not already on the remote host as well as establishing the SSH tunnel between your server and your computer. By default, Trellis configures Xdebug to look for a debugging session on the server's localhost port 9000: ```yaml # roles/xdebug/defaults/main.yml xdebug_remote_host: localhost xdebug_remote_port: 9000 ``` Because your debugger is located on your computer and not the server, Xdebug would attempt to communicate with `localhost:9000` unsuccessfully and proceed with the request as normal. Using `trellis xdebug-tunnel open` creates a tunnel from the server's `localhost:9000` to your computer's `localhost:9000`, bridging the gap and allowing the two to communicate. ### Establishing the tunnel First, let's look at the command we'll be using to create the tunnel: ```shell $ trellis xdebug-tunnel <action> <host> ``` The argument `action` can be `open` or `close` and `host` is the hostname, IP, or inventory alias in your `hosts/<environment>` file. Provided this hosts file: ```plaintext # let's pretend hosts/staging some_inventory_hostname ansible_ssh_host=12.34.56.78 [staging] some_inventory_hostname [web] some_inventory_hostname ``` You would execute: ```shell $ trellis xdebug-tunnel open some_inventory_hostname ``` This script runs the `xdebug-tunnel.yml` playbook with the necessary variables to install Xdebug on the environment as well as establish the tunnel. To close the tunnel, as well as disable Xdebug, run: ```shell $ trellis xdebug-tunnel close some_inventory_hostname ``` This will remove the `/etc/php/8.0/fpm/conf.d/20-xdebug.ini` symlink, effectively disabling it for that environment while leaving xdebug installed. It also closes the SSH connection. If you don't use inventory aliases in your host files, you can also use an ip address directly instead of the alias. For example, if your hosts file looks like this: ```plaintext [staging] 12.34.56.78 [web] 12.34.56.78 ``` You can do this: ```shell $ trellis xdebug-tunnel open 12.34.56.78 ``` You must specify the `host` exactly the same when opening and closing the tunnel. It would cause an error to open the tunnel with a `host` of `some_inventory_hostname` then close with a host of `12.34.56.78`. This is because the tunnel socket is created using the host parameter you pass: ```shell /tmp/trellis-xdebug-{{ provided host }} ``` ================================================ FILE: trellis/deploy-to-digitalocean.md ================================================ --- date_modified: 2026-04-03 10:00 date_published: 2019-01-07 10:05 description: Deploy Trellis WordPress sites to DigitalOcean servers. Create servers, configure settings, and automate WordPress deployment to DigitalOcean. title: Deploying Trellis to DigitalOcean authors: - ben --- # Deploying Trellis to DigitalOcean [DigitalOcean](https://www.digitalocean.com/?refcode=09f11cfb5bac) is a cloud infrastructure provider that offers virtual servers (droplets) that can handle most normal WordPress sites when provisioned with Trellis. To provision a server, Trellis requires a server running a bare/stock version of Ubuntu 24.04 LTS. ::: tip ℹ️ If you [signup for DigitalOcean](https://www.digitalocean.com/?refcode=09f11cfb5bac) through the Roots referral link you will receive a free $200 in credit for 2 months, and you help cover the costs of our hosting. ::: ## Creating a new server Trellis CLI comes with a `trellis server create` command to automatically create and provision a server for a specified environment: ```shell $ trellis server create production ``` ::: warning This command requires a [DigitalOcean personal access token](https://cloud.digitalocean.com/account/api/tokens/new). ::: If the `DIGITALOCEAN_ACCESS_TOKEN` environment variable is not set, the command will prompt for one. DigitalOcean is the default provider. You can also set it explicitly with the `--provider` flag or in your `trellis.cli.yml`: ```yaml server: provider: digitalocean ``` ### Quick start (region and size will be prompted) ```shell $ trellis server create production ``` ![Screenshot of trellis server create example](https://cdn.roots.io/app/uploads/deploy-to-digitalocean-trellis-droplet-create.png) The remote server playbook will run and provision your server with PHP, Nginx, and everything else included in Trellis. ### Additional options The command help file can be accessed by passing the `--help` flag: ```shell $ trellis server create --help ``` <details> <summary>trellis server create --help</summary> ```plaintext Usage: trellis server create [options] ENVIRONMENT Creates a server on a cloud provider for the environment specified. Only remote servers (for staging and production) are currently supported. This command requires a DigitalOcean personal access token. Link: https://cloud.digitalocean.com/account/api/tokens/new If the DIGITALOCEAN_ACCESS_TOKEN environment variable is not set, the command will prompt for one. Create a production server (region and size will be prompted): $ trellis server create production Create a 1gb server in the nyc3 region: $ trellis server create --region=nyc3 --size=s-1vcpu-1gb production Create a server but skip provisioning: $ trellis server create --skip-provision production Arguments: ENVIRONMENT Name of environment (ie: production) Options: --provider Cloud provider (digitalocean, hetzner) --region Region to create the server in --image (default: ubuntu-24-04-x64) Server image (ie: Linux distribution) --size Server size/type --skip-provision Skip provision after server is created --ssh-key Path to SSH public key to be added on the server -h, --help show this help ``` </details> ## Changes made after running the command After creating a new server, your local project will have a modified hosts file for the environment that you provisioned: ```diff [production] -your_server_hostname +159.89.191.207 [web] -your_server_hostname +159.89.191.207 ``` ## Deploying Once your server is provisioned you’ll want to perform the first deploy. If you try to visit your site before deploying you’ll see a server 500 error. ```shell $ trellis deploy production ``` After the first deploy is done, you can now either install WordPress by visiting the site or even import an existing database. ================================================ FILE: trellis/deploy-to-hetzner-cloud.md ================================================ --- date_modified: 2026-04-03 10:00 date_published: 2026-04-03 10:00 description: Deploy Trellis WordPress sites to Hetzner Cloud servers. Create servers, configure settings, and automate WordPress deployment to Hetzner Cloud. title: Deploying Trellis to Hetzner Cloud authors: - ben --- # Deploying Trellis to Hetzner Cloud [Hetzner Cloud](https://hetzner.cloud/?ref=V6DnI7GDHM4N) is a cloud infrastructure provider offering virtual servers with competitive pricing that can handle most normal WordPress sites when provisioned with Trellis. ::: tip ℹ️ Sign up for [Hetzner Cloud](https://hetzner.cloud/?ref=V6DnI7GDHM4N) through the Roots referral link to receive $20 in cloud credits. ::: ## Creating a new server Trellis CLI comes with a `trellis server create` command to automatically create and provision a server for a specified environment: ```shell $ trellis server create --provider=hetzner production ``` ::: warning This command requires a [Hetzner API token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/). ::: If the `HCLOUD_TOKEN` environment variable is not set, the command will prompt for one. To avoid passing `--provider` every time, set Hetzner as your default provider in `trellis.cli.yml`: ```yaml server: provider: hetzner ``` Then you can simply run: ```shell $ trellis server create production ``` ### Quick start (region and size will be prompted) ```shell $ trellis server create production ``` The remote server playbook will run and provision your server with PHP, Nginx, and everything else included in Trellis. ### Additional options The command help file can be accessed by passing the `--help` flag: ```shell $ trellis server create --help ``` ## Changes made after running the command After creating a new server, your local project will have a modified hosts file for the environment that you provisioned: ```diff [production] -your_server_hostname +49.13.25.100 [web] -your_server_hostname +49.13.25.100 ``` ## Deploying Once your server is provisioned you'll want to perform the first deploy. If you try to visit your site before deploying you'll see a server 500 error. ```shell $ trellis deploy production ``` After the first deploy is done, you can now either install WordPress by visiting the site or even import an existing database. ================================================ FILE: trellis/deploy-with-github-actions.md ================================================ --- date_modified: 2025-11-16 11:00 date_published: 2023-04-05 11:00 description: Deploy Trellis WordPress sites with GitHub Actions using `setup-trellis-cli`. title: Deploying Trellis with GitHub Actions authors: - ben - swalkinshaw --- # Deploying Trellis with GitHub Actions The [`roots/setup-trellis-cli` GitHub Action](https://github.com/roots/setup-trellis-cli) can be used for setting up continuous deploys for Trellis based WordPress sites. ::: warning This guide requires that you already have a repo on GitHub with your WordPress site along with the `trellis` directory committed to it ::: ## Setup the GitHub action ### Add the Ansible Vault password Add a GitHub secret for `ANSIBLE_VAULT_PASSWORD` that contains the value of your `.vault_pass` file. Either manually add it at **Settings > Secrets and variables > Actions**, or use the GitHub CLI to automatically add it: ```bash $ gh secret set ANSIBLE_VAULT_PASSWORD -b $(cat trellis/.vault_pass) ``` ### Generate a SSH key The GitHub Action runner needs to SSH into your remote Trellis server. The easiest way to get setup is by using Trellis CLI: ```shell $ trellis key generate ``` After running this command you'll have: * A new file in `trellis/public_keys` — make sure to commit this addition * A deploy key added to your repo automatically (**Settings > Deploy keys**) * Two new repository secrets added to your repo automatically: `TRELLIS_DEPLOY_SSH_KNOWN_HOSTS` and `TRELLIS_DEPLOY_SSH_PRIVATE_KEY` Further information can be found on the [`roots/setup-trellis-cli` README](https://github.com/roots/setup-trellis-cli#ssh-known-hosts). ## Add a workflow for deploying The setup-trellis-cli repo contains some example workflows including: * [Basic deploy](https://github.com/roots/setup-trellis-cli/blob/main/examples/basic.yml) * [Deploy with a Sage-based theme](https://github.com/roots/setup-trellis-cli/blob/main/examples/sage.yml) These examples are configured to deploy a Trellis site to the production environment when the `main` branch is pushed to. Copy the relevant example to your repo at `.github/workflows/deploy.yml`. If you site uses a Sage-based theme, make sure to modify the `cache-dependency-path` to point to the `package-lock.json` file in your theme directory. ================================================ FILE: trellis/deployments.md ================================================ --- date_modified: 2025-02-21 17:30 date_published: 2015-09-07 20:44 description: Trellis provides zero-downtime WordPress deployment with atomic deploys. Customize each deployment step with hooks for builds, migrations, and cleanup tasks. title: WordPress Deployments with Trellis authors: - ben - dalepgrant - dougjq - Log1x - MWDelaney - swalkinshaw - TangRufus --- # WordPress Deployments with Trellis Trellis allows zero-downtime WordPress deployment out of the box with a little configuration. Hooks let you customize what happens at each step of the deploy process. Trellis deploys your site from a Git repository. In your `wordpress_sites.yml` file, found in the `group_vars/<environment>` directory, make sure the `repo` and `branch` keys are set correctly: - `repo` - Git URL of your Bedrock-based WordPress project (in SSH format: `git@github.com:org/repo-name.git`) - `branch` - Git branch to deploy (default: `master`) ```diff wordpress_sites: example.com: ... - repo: git@github.com:example/example.com.git + repo: git@github.com:org/repo-name.git - branch: master + branch: main ``` [Read more about WordPress Sites in Trellis](/trellis/docs/wordpress-sites/) ::: tip Using DigitalOcean? Read our guide on [deploying Trellis to DigitalOcean](https://roots.io/trellis/docs/deploy-to-digitalocean/) ::: ## Deploying Run the following from any directory within your project: ```shell $ trellis deploy <environment> ``` ::: warning Note **Trellis does not automatically "install" WordPress on remote servers**. It's normal and expected to see the WordPress install screen the first time you deploy. It's up to you to either import an existing database or install a fresh site. ::: ::: warning Note **About zero-downtime deploys**. Database migrations to a new schema are not included as part of a Trellis deploy. This means that if you need to migrate your database (for example, to account for new plugins), you may need to expect downtime depending on how you manage your database. Modify your deploy process to account for database migrations as with any other framework. ::: ## Rollbacks Run the following from any directory within your project: ```shell $ trellis rollback <environment> ``` Manually specify a different release using `--release=12345678901234` as such: ```shell $ trellis rollback --release=12345678901234 <environment> ``` By default Trellis stores five previous releases, not including the current release. See `deploy_keep_releases` in [Options - Remote Servers](wordpress-sites.md) to change this setting. ## Hooks Trellis deploys let you customize what happens at each step of the atomic deployment process. A single deploy has the following steps in order: 1. `initialize` - creates the site directory structure (or ensures it exists) 2. `update` - clones the Git repo onto the remote server 3. `prepare` - prepares the files/directories in the new release path (such as moving the repo subtree if one exists) 4. `build` - builds the new release by copying templates, files, and folders 5. `share` - symlinks shared files/folders to new release 6. `finalize` - finalizes the deploy by updating the `current` symlink (atomic deployments) Each step has a `before` and `after` hook. The hooks are variables that you can define with a list of custom task files to be included and run when the hook fires. The hook variables available are: - `deploy_before` - `deploy_initialize_before` - `deploy_initialize_after` - `deploy_update_before` - `deploy_update_after` - `deploy_prepare_before` - `deploy_prepare_after` - `deploy_build_before` - `deploy_build_after` - `deploy_share_before` - `deploy_share_after` - `deploy_finalize_before` - `deploy_finalize_after` - `deploy_after` ### Default hooks By default, Trellis defines and uses three hooks: - `deploy_build_after` runs `composer install`. - `deploy_finalize_before` checks the WordPress installation. - `deploy_finalize_after` refreshes WordPress settings and reloads php-fpm. The default deploy hooks are defined in `roles/deploy/defaults/main.yml`: ```yaml deploy_build_before: - '{{ playbook_dir }}/deploy-hooks/build-before.yml' deploy_build_after: - '{{ playbook_dir }}/roles/deploy/hooks/build-after.yml' # - "{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-build-after.yml" deploy_finalize_before: - '{{ playbook_dir }}/roles/deploy/hooks/finalize-before.yml' deploy_finalize_after: - '{{ playbook_dir }}/roles/deploy/hooks/finalize-after.yml' ``` The `deploy_build_before` definition and the commented path under `deploy_build_after` offer examples of using hooks for custom tasks, as described below. ### Custom tasks To use a deploy hook, define or override the hook variable somewhere within your `group_vars` directory, such as in `group_vars/all/main.yml`. If you end up defining many hooks, you may want to create a new file such as `group_vars/all/deploy-hooks.yml`. Each deploy hook variable is a list of task files to be included and run when the hook fires. We suggest keeping your hooked task files in a top level `deploy-hooks` folder. Here are some example hook variable definitions: ```yaml # Defining a hook that Trellis does not already use by default deploy_before: - '{{ playbook_dir }}/deploy-hooks/deploy-before.yml' # Overriding a hook that Trellis already uses by default deploy_build_after: - '{{ playbook_dir }}/roles/deploy/hooks/build-after.yml' - '{{ playbook_dir }}/deploy-hooks/build-after.yml' - '{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-build-after.yml' ``` The second example above demonstrates overriding the `deploy_build_after` hook that Trellis already uses by default. The first include file in this hook's list is `roles/deploy/hooks/build-after.yml`, which is the task file Trellis usually executes. If you omit a hook's default file when overriding an existing hook variable, the default file's tasks will no longer execute. The second include file in the `deploy_build_after` example above, `deploy-hooks/build-after.yml`, is an example of adding a custom task file that would run on every deploy, regardless the site being deployed. The third include file, <code>deploy-hooks/sites/{{ site }}-build-after.yml</code>, demonstrates how you could use a `{{ site }}` variable to include a file based on the name of the site being deployed, e.g., `example.com-build-after.yml`. ================================================ FILE: trellis/existing-projects.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2018-08-23 09:56 title: Adding Trellis to Existing WordPress Projects description: Get started on existing Trellis projects. Clone the repository, install dependencies, set up Ansible Vault, and provision your local development environment. authors: - ben - Log1x - MWDelaney - TangRufus --- # Adding Trellis to Existing WordPress Projects The majority of the Trellis documentation focuses on setting up new projects. If you are collaborating on, or taking over an existing project, the process is a little different. ::: tip Note This documentation presumes your project follows the [Roots Example Project](https://github.com/roots/roots-example-project.com) recommendations. ::: ## Gather Information To work on an existing Trellis project you need the following: - Git repository access - The Ansible Vault password - Permissions for provisioning and deployment - Your site's development URL - A database dump and a copy of the project's `/uploads` folder ### Git Repository Access Roots recommends that Trellis projects be kept in private Git repositories. Make sure you have permission and access to the project's Git repository and any dependent plugin or theme repositories. ### Ansible Vault password Trellis stores passwords and other sensitive data in [encrypted vault files](vault.md). Retrieve the project's vault password from someone who already works on the project. ### Permission for provisioning and deployment If you need to [provision this project's remote servers](remote-server-setup.md) or [deploy the project](deployments.md) to staging or production, add your SSH keys to the necessary remote servers either by accessing the server directly, or by having someone who already has access [add your SSH keys to the Trellis configuration](ssh-keys.md) and re-provision the server. ### Your site's development URL Review the project's `trellis/group_vars/development/wordpress_sites.yml` and note its URL: ```yaml wordpress_sites: example.com: site_hosts: - canonical: example.test # <-- this is the development URL ``` ## Clone Your Project ```shell $ git clone git@github.com:YourOrganization/example.com.git ``` ## Ansible Vault Determine whether your vault files are encrypted by looking at the `vault.yml` files in `trellis/group_vars/` ```yaml $ANSIBLE_VAULT;1.1;AES256 343163646662643438323831343332626234333233386666333162383265663 3132306538383762336332376165383530633838643937320a6363343238643 363065366664316364646561613163653866623566303235666537343437643 6638363265383831390a6631663239373833636133623333666363643166383 6237663637353638653266616562616535623465636265316231613331 etc. ``` If any of the `vault.yml` files look like the example above, follow the [vault instructions](vault.md) to configure your Ansible Vault and vault password. ## Create Your Development VM Run the following from any directory within your project: ```shell $ trellis up ``` Confirm you can access the development site at the development URL noted earlier. ## Import the database Retrieve an export of the current project’s database. ::: tip Note For easy access during the import process, place the database export in your local project’s `site` directory. ::: Run the following from any directory within your project: ```shell $ trellis ssh development ``` Navigate to the web root: ```shell $ cd /srv/www/example.com/current ``` Import the database with wp-cli: ```shell $ wp db import example.com.sql ``` If the export is not from another development environment, search-and-replace the site's URL with wp-cli: ```shell $ wp search-replace http://example.com http://example.test ``` ## Import the Uploads Retrieve a copy of the current project’s `uploads` directory and place it in your local project's `site/web/app` directory. ================================================ FILE: trellis/fastcgi-caching.md ================================================ --- date_modified: 2025-02-27 15:48 date_published: 2015-09-06 07:42 description: Trellis offers built-in FastCGI caching with Nginx microcaching. No WordPress plugin required, with configurable cache skipping for eCommerce pages. title: FastCGI Caching for WordPress in Trellis authors: - ben - catgofire - Log1x - swalkinshaw - vdrnn --- # FastCGI Caching for WordPress in Trellis You can enable caching for your site by changing the cache settings under each site key. Using caching provides substantial speed improvement once pages are cached. The full settings looks like this: ```yaml cache: enabled: false duration: 30s skip_cache_uri: /wp-admin/|/wp-json/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml skip_cache_cookie: comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in ``` The `duration` parameter control how long your pages will stay in the cache. You should generally keep this value low (the default is 30 seconds), unless your content doesn't change frequently. Lowering the duration to `1s` will make the cache more like a DDOS protection; meaning that if you have a sudden spike of traffic, only one request will hit the back-end per second instead of the full load. The whole setup is "micro-cache" oriented, so there is no means of flushing the cache. The `skip_cache_uri` is a regex that will be used to tell Nginx **not** cache pages matching it. **Use it if you have sections of your site that you don't want cached (like shopping carts)**. Override the global `nginx_skip_cache_uri` in `group_vars/all/main.yml` or override `skip_cache_uri` under `cache` to vary it per [WordPress site](wordpress-sites.md). The default value is shown above. The `skip_cache_cookie` is a regex that will disable the cache when a cookie match it. Useful for disabling the cache for certain users. Already cached content will continue being served if your back-end (PHP-FPM) goes down. ## Cache-Control Headers As of [Trellis v1.24.0](https://github.com/roots/trellis/releases/tag/v1.24.0), Nginx now respects Cache-Control headers sent by WordPress and plugins. This means applications can control their own caching behavior by setting appropriate Cache-Control headers in their HTTP responses. For e-commerce sites using WooCommerce, Easy Digital Downloads, or similar plugins that properly set Cache-Control headers on dynamic pages (like cart, checkout, and account pages), this can simplify cache configuration by reducing the need for extensive `skip_cache_uri` and `skip_cache_cookie` settings. ## Example cache configurations ### WooCommerce Disable the cache for `/store/`, `/cart/`, `/my-account/`, `/checkout/`, `/addons/`, and when items are in the cart: ```yaml cache: enabled: true skip_cache_uri: /wp-admin/|/wp-json/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml|/store.*|/cart.*|/my-account.*|/checkout.*|/addons.* skip_cache_cookie: comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_ ``` Alternatively, if you're using a recent version of WooCommerce that sets appropriate Cache-Control headers, you may be able to simplify this configuration by relying on those headers instead of extensive URI and cookie patterns. ### Easy Digital Downloads Disable the cache for `/checkout/` and when items are in the cart: ```yaml cache: enabled: true skip_cache_uri: /wp-admin/|/wp-json/|/checkout/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml skip_cache_cookie: comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|edd_items_in_cart ``` Like with WooCommerce, if your version of Easy Digital Downloads sets Cache-Control headers correctly, you may be able to simplify this configuration. ================================================ FILE: trellis/install-wordpress-language-files.md ================================================ --- date_modified: 2023-09-27 14:05 date_published: 2021-09-08 00:29 description: Configure WordPress language file installation in Trellis for multi-language sites. Automate translation downloads for core, plugins, and themes. title: Installing WordPress Language Files in Trellis authors: - strarsis - hooley --- # Installing WordPress Language Files in Trellis ## Current state of language management ### Locking in language versions? With Composer (Bedrock site) the plugin versions are already locked-in. So it naturally makes sense to also lock-in the plugin languages. The [Composer WordPress language packs project](https://wp-languages.github.io/) by Koodimonni offers composer packages for plugin languages. However, providing all languages for each plugin version results in a very large amount of packages. Because of this practical issue, the site offers language packages for only a subset of plugins, so chances are, that languages for some plugins your site is using are not available there (e.g. [Redirection plugin](https://wordpress.org/plugins/redirection/)) ### Using custom Composer installers There are some custom Composer installers which simply download the latest languages for core, plugins and themes. However, this approach doesn't allow any locking-in of language versions. One may end up with different translations for same site release. - [wplang](https://github.com/bjornjohansen/wplang) - [Composer Auto Language Updates](https://github.com/Angrycreative/composer-plugin-language-update) ## Using `wp language` command on Trellis deploys At the time of writing there is no way yet for locking-in languages of core, plugins and themes (for all plugins and themes), so we are stuck with installing the latest language available. The best approach is using the official mechanisms, which would be the `wp language` subcommand. ### Setup deploy hooks We use the [`finalize-after` deploy hook](/trellis/docs/deployments/#hooks) for installing, activating and updating the core/plugins/themes languages of a site for the languages `en_GB`, `de_DE_formal` and `de_DE`: `deploy-hooks/sites/example.com-finalize-after.yml`: ```yaml # Install + activate languages - name: Install core languages en_GB de_DE command: wp language core install en_GB de_DE args: chdir: "{{ deploy_helper.current_path }}" - name: Install (and activate) core language de_DE_formal command: wp language core install de_DE_formal --activate args: chdir: "{{ deploy_helper.current_path }}" - name: Install plugins languages en_GB de_DE de_DE_formal command: wp language plugin install --all en_GB de_DE de_DE_formal args: chdir: "{{ deploy_helper.current_path }}" - name: Install themes languages en_GB de_DE de_DE_formal command: wp language theme install --all en_GB de_DE de_DE_formal args: chdir: "{{ deploy_helper.current_path }}" # Update installed languages - name: Update installed core languages command: wp language core update args: chdir: "{{ deploy_helper.current_path }}" - name: Update plugins languages command: wp language plugin --all update args: chdir: "{{ deploy_helper.current_path }}" - name: Install themes languages command: wp language theme --all update args: chdir: "{{ deploy_helper.current_path }}" ``` (All these `wp` commands are idempotent, they only install/update when it is required.) In the first part the required languages are installed for core, plugins and themes. In the second part all the installed languages of core, plugins and themes are updated. #### Removing no longer needed languages Note: If you ever want to remove a language, you can do this here, too. Ideally the language is removed before updating as this removes an unnecessary update of a language that is removed anyway. - [`wp language core uninstall <language> <language> ...`](https://developer.wordpress.org/cli/commands/language/core/uninstall/) - [`wp language plugin uninstall --all <language> <language> ...`](https://developer.wordpress.org/cli/commands/language/plugin/uninstall/) - [`wp language theme uninstall --all <language> <language> ...`](https://developer.wordpress.org/cli/commands/language/theme/uninstall/) ### Initial deploy (non-setup site) Many `wp` commands including `wp language` don't work on a WordPress site that is installed but not had been set up yet. ```plaintext Error: The site you have requested is not installed. Run `wp core install` to create database tables. ``` For making an initial deploy possible, the WordPress site has to be setup at the beginning of deploy. You may overwrite everything using a transfer script or backup/restore plugin, etc. This is only important here for being able to install/update the languages on initial deploy. `deploy-hooks/finalize-after.yml`: ```yaml - name: Install WP (required for installing languages on non-transferred site) command: wp core {{ project.multisite.enabled | default(false) | ternary('multisite-install', 'install') }} --allow-root --url="{{ site_env.wp_home }}" {% if project.multisite.enabled | default(false) %} --base="{{ project.multisite.base_path | default('/') }}" --subdomains="{{ project.multisite.subdomains | default('false') }}" {% endif %} --title="{{ project.site_title | default(site) }}" --admin_user="{{ project.admin_user | default('admin') }}" --admin_password="{{ vault_wordpress_sites[site].admin_password }}" --admin_email="{{ project.admin_email }}" args: chdir: "{{ deploy_helper.current_path }}" register: wp_install changed_when: "'WordPress is already installed.' not in wp_install.stdout and 'The network already exists.' not in wp_install.stdout" ``` ### Add deploy hooks For making trellis actually using these new deploy hooks, they need to be added: `groups_vars/all/main.yml`: ```yaml # Deploy hooks deploy_build_before: - "{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-build-before.yml" # build + upload theme assets deploy_build_after: - "{{ playbook_dir }}/roles/deploy/hooks/build-after.yml" # built-in deploy_finalize_before: - "{{ playbook_dir }}/roles/deploy/hooks/finalize-before.yml" # built-in deploy_finalize_after: - "{{ playbook_dir }}/roles/deploy/hooks/finalize-after.yml" # built-in - "{{ playbook_dir }}/deploy-hooks/finalize-after.yml" # finish site setup for installing languages - "{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-finalize-after.yml" # install + update languages ```` ### Improve performance / prevent "translation downtimes" By default, for each new release the languages would have to be reinstalled, during that time the site can appear partially untranslated (falling back to English by default, or to the language setup by a language fallback plugin). For preventing this "translation downtime" and for making a language update instead of install possible, the `languages` folder can be added to `project_copy_folders`, so it is copied between releases. `group_vars/all/main.yml`: ```yaml project_copy_folders: - vendor - web/app/languages # copy languages between releases ``` ## Language fallback By experience, for languages with a formal and non-formal variation, plugins are often only translated for one of these variations. It is possible to fall back at least to the non-formal variation by using a [language fallback plugin](https://wordpress.org/plugins/language-fallback/). With such a plugin installed and enabled, when a string is not translated in current language, it is looked up from the fallback language first, instead of falling back immediately to English. ================================================ FILE: trellis/installation.md ================================================ --- date_modified: 2026-03-06 13:00 date_published: 2015-10-15 12:20 description: Install Trellis for WordPress projects. Complete setup instructions covering requirements, dependencies, project initialization, and initial configuration. title: Installing Trellis for WordPress authors: - ben - Log1x - MWDelaney - nikitasol - swalkinshaw - TangRufus - MWDelaney --- # Installing Trellis for WordPress ## What is Trellis? [Trellis](https://roots.io/trellis/) is a tool to create WordPress web servers and deploy WordPress sites. Trellis lets you create and manage servers that are production ready, performance optimized and based on best practices that are continuously improved. Trellis is self-hosting done right since you benefit from the community and experience of Roots. ### Why use Trellis? You’ll get a complete WordPress server [running all the software](#software-installed) you need configured according to the best practices that are fully customizable. <details> <summary>Trellis features</summary> #### Ansible Trellis is powered by [Ansible](https://docs.ansible.com/projects/ansible/latest/index.html) for configuration management. You don’t have to use brittle and confusing Bash scripts or worry about commands you found to copy and paste. You get the benefit of Ansible [documentation](https://docs.ansible.com/projects/ansible/latest/user_guide/index.html), its extensive library of [modules and plugins](https://docs.ansible.com/ansible/latest/collections/all_plugins.html), and the community ecosystem of [Galaxy roles](https://galaxy.ansible.com/). #### Local development Trellis comes with [Lima](https://lima-vm.io/) support for local development environments that run on isolated virtual machines. This means you don't have to worry about polluting your local OS with software that might break or conflict with other tools you use. However, using Lima is optional and you're free to use other local dev tools as well, or even none at all. #### Customizable While Trellis gives you everything for a standard WordPress server out of the box, it's completely customizable as well. This is what makes Trellis different from managed hosting or even tools like SpinupWP that automatically setup WordPress servers. Thanks to Ansible's YAML based configuration, Trellis is "infrastructure as code" so you can easily see exactly what Trellis installs on your server and customize if you want. #### Portable without vendor-lock in Trellis servers can be run on _any_ hosting platform; traditional dedicated server hosting or cloud platforms. All Trellis needs is a server running a plain Ubuntu operating system. This means you can easily migrate hosting providers making your infrastructure much more flexible and portable. You can even "disconnect" your server from Trellis if you want and just manage your server manually. Trellis isn't required to keep your server running (but we do recommend it!). #### Cost effective Managed WP hosting can make your life easier, but it can also be extremely expensive and is often overkill for simpler WordPress sites. Trellis lets you run performant sites on extremely cheap servers ($5-10/month) and even supports running multiple sites on a single server for more efficiency. #### Community backed Since Trellis is open-source, we get the leverage of Roots and our community to continuously improve the defaults over time. We are constantly learning better settings and defaults for WordPress servers, and then we apply them to Trellis. #### Development and production parity Unlike many other solutions for WordPress server hosting, Trellis aims to have [parity between your development and production environments](https://roots.io/twelve-factor-10-dev-prod-parity/). Trellis comes setup to run locally with Lima so you can test your WordPress sites with full confidence that they'll work once you deploy to production. #### CLI Trellis has its own [CLI](cli.md) that makes managing your local and remote servers much easier. It also enables powerful CI/CD workflows like our [setup-trellis-cli](https://github.com/roots/setup-trellis-cli/) [GitHub action that can be used for continuous deploys](/trellis/docs/deploy-with-github-actions/). #### Zero-downtime deploys Trellis has atomic, zero-downtime deploys built-in that are completely configurable with a powerful hook system. You can deploy and rollback releases with a single command thanks to trellis-cli too. </details> ### Trellis servers are production-ready Trellis provisions a base Ubuntu 24.04 server by installing and configuring the following software: * PHP 8.3+ * Nginx (including HTTP/2, HTTP/3, and optional FastCGI micro-caching) * MariaDB (a drop-in MySQL replacement) * SSL support (scores an A+ on the [Qualys SSL Server Test](https://www.ssllabs.com/ssltest/)) * Let's Encrypt for free SSL certificates * Composer * WP-CLI * sSMTP (mail delivery) * Memcached * Fail2ban and ferm In addition to configuring common services like ntp, sshd, etc. ## System requirements * macOS or Linux ::: warning Windows users Windows is not supported at this time. A community-maintained fork of trellis-cli with Windows/WSL support is available at [qwatts-dev/trellis-cli](https://github.com/qwatts-dev/trellis-cli). ::: ## Install Trellis CLI ```shell $ brew install roots/tap/trellis-cli ``` ## Create a new project with Trellis Choose a descriptive project name (and use it in place of the default example.com). We recommend the domain of the site for uniqueness. ```shell $ trellis new example.com ``` After you've created a project, the folder structure for a Trellis project will look like this: ```plaintext example.com/ # → Root folder for the project ├── trellis/ # → Your server configuration (a customized install of Trellis) └── site/ # → A Bedrock-based WordPress site └── web/ ├── app/ # → WordPress content directory (themes, plugins, etc.) └── wp/ # → WordPress core (don't touch! - managed by Composer) ``` Check out the following files to review the basic site configuration: * `trellis/group_vars/development/wordpress_sites.yml` * `trellis/group_vars/production/wordpress_sites.yml` ## Start your development environment ```shell $ trellis vm up ``` This command will start the Lima environment and provision the server. Once it's done, you can visit your development site at the URL you chose when you ran `trellis new`. [Read more about Local Development](/trellis/docs/local-development/) ## Configure your environments Trellis pre-configures most of your site's settings, but you'll need to fill in a few gaps in the [WordPress Sites](/trellis/docs/wordpress-sites/) configuration. ## Encrypt your vault files You probably want to encrypt your vault files, which hold automatically-generated passwords and other sensitive information. [Read more about Vault](/trellis/docs/vault/) ## Provision your production server Before deploying to production, you'll need to provision your server. [Read more about provisioning](/trellis/docs/remote-server-setup/) ```shell $ trellis provision production ``` ## Deploy to production Ready to deploy your site to production? [Read more about deployments](/trellis/docs/deployments/) ```shell $ trellis deploy production example.com ``` ================================================ FILE: trellis/local-development.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-10-15 12:24 description: Trellis uses Lima VM's for local development. Trellis uses Ansible to automatically provision virtual machines running complete WordPress environments. title: Local WordPress Development with Trellis authors: - ben - fullyint - IanEdington - Log1x - MWDelaney - swalkinshaw - TangRufus --- # Local WordPress Development with Trellis Trellis has an official integration with Lima for development environments using virtual machines. Other options include: * [Laravel Valet](#laravel-valet) * [Nothing!](#nothing) ## Lima Trellis integrates with Lima to automatically run the Ansible provisioning. Provisioning in development uses the `dev.yml` Ansible playbook to create a Lima virtual machine running your WordPress site. Follow these steps to get a development server running: 1. Configure your site(s) based on the [WordPress Sites docs](wordpress-sites.md) and read the [development specific](wordpress-sites.md#development) ones. 2. Make sure you've edited both `group_vars/development/wordpress_sites.yml` and `group_vars/development/vault.yml`. 3. Run `trellis vm start` from anywhere in your project. Then let Lima and Ansible do their thing. After roughly 5 minutes you'll have a virtual machine running and a WordPress site automatically installed and configured. To access the VM, run `trellis vm shell`. Sites can be found at `/srv/www/<site name>` on the VM. Note that each WP site you configured is synced between your local machine (the host) and the Lima VM. Any changes made to your host will be synced instantly to the VM. There's no need to manually sync files or deploy to the VM. Composer and WP-CLI commands need to be run on the virtual machine for any post-provision modifications. Front-end build tools should be run from your host machine and not the Lima VM. ### WordPress installation Trellis installs WordPress on your first `trellis vm start` with `admin` as the default user. You can override this by defining `admin_user`, as noted in the [WordPress sites options](wordpress-sites.md#options). ### Re-provisioning Re-provisioning is always assumed to be a safe operation. When you make changes to your Trellis configuration, you should provision the VM again to apply the changes: Run the following from your project's `trellis` directory: ```shell $ trellis provision development ``` You can also provision with specific tags to only run the relevant roles: Run the following from your project's `trellis` directory: ```shell $ trellis provision --tags=users development ``` ### Usage There's 5 commands for working with VMs: * `trellis vm start` - create or start a VM * `trellis vm stop` - stop a running VM * `trellis vm delete` - delete a stopped VM * `trellis vm shell` - open a shell/terminal on the VM * `trellis vm sudoers` - configure sudoers to avoid the need for `sudo` Run `trellis vm <command> -h` for details on each command. For default use cases, `trellis vm start` can be run without any customization first. It will create a new virtual machine (using Lima) from a generated config file (`project/trellis/.trellis/lima/config/<name>.yml`). The site's `local_path` will be automatically mounted on the VM and your `/etc/hosts` file will be updated. Note: run `trellis vm sudoers -h` to make `/etc/hosts` file updates passwordless: ```bash $ trellis vm sudoers | sudo tee /etc/sudoers.d/trellis ``` Under the hood, those commands wrap equivalent `limactl` features. You can always run `limactl` directly to manage your VMs. ### Configuration: For the common use case, the default configuration should be all that's needed which is why config options are limited to start with. We will offer more customization over time. The CLI [config file](cli.md#configuration) (global or project level) supports a new `vm` option. The only useful config option right now is `ubuntu` for setting the Ubuntu version. Here's an example of specifying 20.04: ```yml vm: ubuntu: 20.04 ``` Note: this must be changed _before_ creating the VM, otherwise you'll need to delete it first and re-create it. ### Integration details When you first run `trellis vm start`, the CLI will do the following: 1. Generate a Lima config file (`.trellis/lima/example.com.yml`) based on your Trellis project's development site 2. Create the Lima instance by running `limactl start --name=example.com .trellis/lima/example.com.yml` 3. Generate an Ansible inventory/hosts file for the VM (`.trellis/lima/inventory`) 4. Add your sites hosts to your `/etc/hosts` file Knowing how the CLI and Lima interact can help with troubleshooting and debugging. Issues with the VM itself are usually related to Lima, and the underlying `limactl` command can be run manually to try and isolate the issue. Tip: run `limactl list` to see all Lima instances and their statuses. ### Ansible inventory As detailed above, trellis-cli will automatically generate and manage a VM specific inventory file. There is no need to manually edit the `hosts/development` file as it won't be used. Commands like `trellis provision` will automatically detect and specify the Lima inventory file. If you need to run an Ansible command manually against the VM host, the `--inventory-file` flag needs to be set: ```bash ansible-playbook dev.yml --inventory-file=.trellis/lima/inventory ``` #### SSH port One reason why the inventory file needs to be generated each time a VM is created or started is due to SSH port forwarding. Lima will find a free _local_ port and use it to forward to port 22 on the VM. The inventory file references this forwarded port and Ansible will use that for its SSH connection. It's recommended to use `trellis vm shell` to SSH to the VM and open a shell/terminal since you don't need to worry about hosts or ports. To connect manually via SSH, run `limactl show-ssh -f config <instance name>` or `limactl show-ssh <instance name>` to view the SSH config in various formats. There is no need to edit your `hosts/development` file unless you were manually using it in a non-standard setup. As mentioned in the [Ansible inventory](#ansible-inventory) section above, trellis-cli generates a separate inventory file. ## Other non-Lima options While Trellis offers integrated Lima development environments, it is completely optional. There are other local development options as well. Most of these options mean you're using Trellis for your production servers but something else entirely in development which is why it's not recommended. ### Laravel Valet [Valet](https://laravel.com/docs/10.x/valet) can be used in development if you're already using it for Laravel projects or want a lighter-weight solution than a full virtual machine. However, be warned that doesn't guarantee [development and production parity](https://roots.io/twelve-factor-10-dev-prod-parity/). Using Valet locally means you aren't using Trellis _at all_ in development. trellis-cli does offer some basic Valet integration as well. Run `trellis valet` for more information. ### Nothing That's right... nothing! You might not care about a local development environment. Or you might only want to use Trellis for deploying to managed servers. Trellis is quite flexible and supports these uses cases as well. ================================================ FILE: trellis/mail.md ================================================ --- date_modified: 2026-04-04 07:00 date_published: 2015-09-06 07:42 description: Trellis uses Mailpit in development to capture outgoing emails. Configure production mail delivery with SMTP settings in the `mail.yml` configuration file. title: WordPress Mail Configuration in Trellis authors: - ben - fullyint - jbicha - Log1x - MWDelaney - mZoo - swalkinshaw - TangRufus --- # WordPress Mail Configuration in Trellis Trellis' mail functionality is separated between development and staging/production since you usually want different behaviour out of them. ## Development Dealing with emails in development is never fun. The two common solutions are: - Ignore it and hope it works fine on production - Set up real SMTP credentials to send emails Enter [Mailpit](https://github.com/axllent/mailpit). It's a simple tool which captures outgoing email and lets you view them from a web UI. And after that you can optionally "release" them which would actually send the email. ![Mailpit Preview](https://cdn.roots.io/app/uploads/trellis-mailpit-preview.png) Mailpit is automatically set up in development. You can access it at `http://example.test:8025` (replacing the domain with yours that you set up for the WP site host). ::: warning Note Mail will be automatically captured but you won't ever see it unless you access the Mailpit UI at the address above. ::: Another benefit of using Mailpit is that if you are using real SMTP credentials in development, you can ensure you don't accidentally send emails to real email addresses which might exist in your database. ::: warning Note This is not the case if you have an active WordPress plugin that is configured to send mail. You'll need to disable the mail plugin on development to ensure you don't accidentally send emails to real email addresses. You could also hook into `phpmailer_init` in WordPress for non-production environments to prevent emails from being sent out. Using a service like [Mailtrap](https://mailtrap.io/) is another option.\*\* ::: Trellis is using the [Mailpit role](https://github.com/roots/ansible-role-mailpit). See that `README` for any extra configuration options although none should be required as Trellis integrates it automatically. ## Remote servers (staging/production) Outgoing mail is handled by [msmtp](https://marlam.de/msmtp/), a lightweight SMTP client. Trellis uses the [msmtp role](https://github.com/roots/ansible-role-msmtp) to configure it. In order to send external emails, you'll need to configure an SMTP server. We always suggest using an external email service rather than your own because it's very difficult to set up a proper email server. Some suggested services: - [Sendgrid](https://www.twilio.com/en-us/sendgrid) - [Mailgun](https://www.mailgun.com/) - [Amazon SES](https://aws.amazon.com/ses/) All of these offer around 10k+ emails for free per month. Once you have SMTP credentials, configure them in `group_vars/all/mail.yml`. - `mail_smtp_server`: hostname:port - `mail_hostname`: hostname for mail delivery - `mail_user`: username - `mail_password`: password or "API key" (define in `group_vars/all/vault.yml`) **Note:** Trellis sends emails through SMTP, which requires a username and password. Some email service providers refer to `mail_password` as an "API key", even though it is not actually used to access the email service provider's API. If you prefer to send email through your email service provider's API (instead of via SMTP), you will need to use a plugin. ### Example ```yaml mail_smtp_server: smtp.example.com:587 mail_hostname: example.com mail_user: admin@example.com mail_password: '{{ vault_mail_password }}' # Define this in group_vars/all/vault.yml ``` If your SMTP settings are invalid, WordPress will return the following error message: ```plaintext Could not instantiate mail function. ``` To fix this error, update your SMTP settings so that they're valid and then re-provision the remote server. ================================================ FILE: trellis/multiple-sites.md ================================================ --- date_modified: 2026-03-10 12:00 date_published: 2026-03-10 12:00 description: Learn how to structure your Trellis projects when managing multiple WordPress sites, including shared and separate Trellis configurations. title: Managing Multiple Trellis Sites authors: - ben --- # Managing Multiple Sites Trellis supports hosting multiple WordPress sites on a single server out of the box. But when managing several sites, how you organize your project directories matters. There are two common approaches. ## Shared Trellis A single Trellis instance manages multiple Bedrock sites on one server: ```plaintext projects/ # → Root folder ├── trellis/ # → Single Trellis managing all sites ├── example.com/ # → First Bedrock site └── another.com/ # → Second Bedrock site ``` Each site is defined in `wordpress_sites.yml` with its own `local_path` pointing to the corresponding directory. See the [WordPress Sites](/trellis/docs/wordpress-sites/) docs for configuration details. This approach works well when: - Sites share the same server and server configuration - You want to minimize infrastructure costs by running multiple sites on one server - You want a single place to manage provisioning and deploys ## Separate Trellis per site Each site gets its own Trellis instance with independent server configuration: ```plaintext example.com/ # → First project ├── trellis/ └── site/ another.com/ # → Second project ├── trellis/ └── site/ ``` This approach works well when: - Sites need different server configurations (PHP versions, Nginx settings, etc.) - Sites are hosted on different servers or providers - You want fully independent infrastructure per site - Different teams manage different sites The trade-off is more duplication of Trellis configuration, but you get full isolation between projects. ================================================ FILE: trellis/multisite.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-06 07:42 description: Set up WordPress multisite on Trellis by configuring Bedrock for multisite installation before provisioning. Supports subdomain and subdirectory networks. title: WordPress Multisite Setup with Trellis authors: - ben - evance - iamkarex - jmslbam - JulienMelissas - Log1x - MWDelaney - nathanielks - ned - Simeon - swalkinshaw --- # WordPress Multisite Setup with Trellis Trellis assumes your WordPress configuration already has multisite set up. If not, ensure the following values are placed somewhere before calling `Config::apply()` in Bedrock's `config/application.php` and **before** provisioning your server: ```php /* Multisite */ Config::define('WP_ALLOW_MULTISITE', true); Config::define('MULTISITE', true); Config::define('SUBDOMAIN_INSTALL', false); // Set to true if using subdomains Config::define('DOMAIN_CURRENT_SITE', env('DOMAIN_CURRENT_SITE')); Config::define('PATH_CURRENT_SITE', env('PATH_CURRENT_SITE') ?: '/'); Config::define('SITE_ID_CURRENT_SITE', env('SITE_ID_CURRENT_SITE') ?: 1); Config::define('BLOG_ID_CURRENT_SITE', env('BLOG_ID_CURRENT_SITE') ?: 1); ``` You'll also need to update the multisite settings under your environment directory (`group_vars/<environment>/wordpress_sites.yml`): ```yaml multisite: enabled: true subdomains: false # Set to true if you're using a subdomain multisite install ``` You may also want to define the `env` dictionary for more multisite specific settings such as `DOMAIN_CURRENT_SITE` or `PATH_CURRENT_SITE`. ```yaml env: domain_current_site: store1.example.com ``` That `env` will be merged in with Trellis' defaults so you don't need to worry about re-defining all of the properties. Here's an example of a complete entry set up for multisite: ```yaml # group_vars/production/wordpress_sites.yml wordpress_sites: example.com: site_hosts: - canonical: example.com local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root) admin_email: admin@example.com multisite: enabled: true subdomains: true ssl: enabled: false cache: enabled: false env: domain_current_site: store1.example.com ``` After provisioning your remote server and deploying your sites, you'll need to install WordPress as a final step in your staging and production environments. SSH into your server as the `web` user with `ssh web@<domain>` and in the `/srv/www/<domain>/current/` directories run the following WP-CLI command to install WordPress: ```shell $ wp core multisite-install --title="site title" --admin_user="username" --admin_password="password" --admin_email="you@example.com" ``` You may notice that your network's main site URLs contain `/wp/` before the post's or page's pathnames. This is a problem in WP core which occurs when WordPress is located in a subdirectory, as is the case with Bedrock. See issue [Bedrock issue #250](https://github.com/roots/bedrock/issues/250) for details, along with the site URL fix plugin in the [Multisite Fixes](https://github.com/felixarntz/multisite-fixes) plugin collection for a solution. If you use [Let's Encrypt](ssl.md#let-s-encrypt) as your SSL provider and your multisite install uses subdomains, currently you have to generate individual certificates for each of your subdomains, but this may change soon as Let's Encrypt will begin issuing [wildcard certificates in January of 2018](https://letsencrypt.org/2017/07/06/wildcard-certificates-coming-jan-2018.html). You can generate SSL certificates for your subdomains if you know these subdomains in advance while provisioning your server. To do this, define multiple `canonical` entries under `site_hosts` in your corresponding `wordpress_sites.yml` file like this: ```yaml site_hosts: - canonical: example.com redirects: - www.example.com - canonical: subdomain.example.com redirects: - www.subdomain.example.com ``` ================================================ FILE: trellis/nginx-includes.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2020-02-05 16:24 description: Customize Nginx configuration in Trellis by placing files in `includes.d/` subdirectories. Add custom rules, headers, and configuration per WordPress site. title: Custom Nginx Includes in Trellis authors: - alwaysblank - ben - fullyint - Log1x - swalkinshaw - dalepgrant --- # Custom Nginx Includes in Trellis The Nginx site conf generated by Trellis is designed to work with a wide range of WordPress installations, but your sites may require customization. For example, perhaps you use custom `rewrite`s to redirect legacy URLs, or maybe you would like to proxy some requests to another server. You may create custom Nginx `include` files or use child templates to extend the Trellis Nginx conf templates. ## `include` files You may put your Nginx customizations in Ansible/Jinja2 templates, making full use of variables and logic. Store your template files in a new `nginx-includes` directory in your project's `trellis/` directory (e.g., sibling to `server.yml` playbook). If you prefer to name this directory `some-other-name`, you may define `nginx_includes_templates_path: some-other-name` in your `group_vars/all/main.yml`. Trellis will recurse `nginx-includes` and template any files ending in `*.conf.j2` to the `/etc/nginx/includes.d/` directory on the remote, preserving the subdirectory structure. If you want to edit, add to, or delete your Nginx include files, edit them on the local machine and re-run the playbook. ::: tip Append `--tags nginx-includes` to your command to run only the relevant portion of the playbook. ::: ### Default By default in Trellis, a WordPress site's Nginx conf will include any `nginx-includes` files found in a subdirectory named after the site. Only the directories that match sites in your `wordpress_sites.yml` will be templated to the remote by default, with the addition of `all/`. To illustrate, suppose you have two sites managed by Trellis, defined in `wordpress_sites` as follows: ```yaml wordpress_sites: site1: ... site2: ... ``` You could organize your `nginx-includes` templates in corresponding subdirectories: ```plaintext trellis/ nginx-includes/ site1/ rewrites.conf.j2 proxy.conf.j2 site2/ rewrites.conf.j2 ``` You could also have an "all" directory, which would apply conf to all sites: ```plaintext trellis/ nginx-includes/ all/ rewrites.conf.j2 ``` The above directory structure would be templated to the remote server as follows: ```plaintext / etc/ nginx/ includes.d/ site1/ rewrites.conf proxy.conf site2/ rewrites.conf ``` To explicitly walk through the example, consider just the `site1` site key in the `wordpress_sites` list. The corresponding `nginx-includes/site1/` directory contains two confs which are templated to the remote's `/etc/nginx/includes.d/site1/`. The primary Nginx conf for `site1` will have a statement `include includes.d/site1/*.conf;`, thus including `rewrites.conf` and `proxy.conf`. This `include` directive is located inside the primary `server` block, just before the primary `location` block. Explore the [Child templates](#child-templates) section below for more options if this default does not satisfy your needs. ::: warning Note This default `include` directive per site will not recurse subdirectories within `includes.d/site1` so if you place templates in `nginx-includes/site1/somedir/*.conf.j2`, they will be templated to the remote's `includes.d/site1/somedir/*.conf` but will not be included by default. See the [Child templates](#child-templates) section below for how you could include such confs. ::: ### File cleanup By default, Trellis will remove from the remote's `includes.d` directory any `*.conf` file that lacks a corresponding template in your local machine's `nginx-includes`. If removing config files results in an empty directory, that directory is not removed. If you prefer to leave all conf files on the remote, you may disable this file cleanup by defining `nginx_includes_d_cleanup: false` in `group_vars/all/main.yml`. ### Deprecated templates directory The original implementation of Trellis Nginx includes required template files to be stored in `roles/wordpress-setup/templates/includes.d`. That directory is now deprecated and will no longer function beginning with Trellis 1.0. Please move your templates to a directory named `nginx-includes` in the root of this project. It is preferable to store user-created templates separate from Trellis core files. Trellis Nginx includes were originally made possible thanks to @chriszarate in [#242](https://github.com/roots/trellis/pull/242). ## Child templates You may use child templates to override any `block` in the two Nginx conf templates: - [`roles/nginx/templates/nginx.conf.j2`](https://github.com/roots/trellis/blob/master/roles/nginx/templates/nginx.conf.j2) is templated to the server as `/etc/nginx/nginx.conf` - [`roles/wordpress-setup/templates/wordpress-site.conf.j2`](https://github.com/roots/trellis/blob/master/roles/wordpress-setup/templates/wordpress-site.conf.j2) is templated to the server as `/etc/nginx/sites-available/example.com.conf` (per each site) Create your child templates following the [Jinja template inheritance](https://jinja.palletsprojects.com/en/stable/templates/#template-inheritance) docs and the guidelines below. ::: tip Once you have set up your child templates, append `--tags nginx-includes` to your command to run only the Nginx conf portions of the playbook. ::: ### Designate a child template You will need to inform Trellis of the child templates you have created. #### `nginx_conf` Use the `nginx_conf` variable to designate your child template for `nginx.conf.j2`. Given that this template applies to all sites, it would be appropriate to define the variable in a `group_vars/<environment>/main.yml` file (including `group_vars/all/main.yml`). ```yaml nginx_conf: nginx-includes/nginx.conf.child ``` The example above designates a child template in the `nginx-includes` path on your local machine (i.e., the default path for `nginx_includes_templates_path` variable; see [`include` files](#include-files) section above). You may choose a different path and assign the template any name and file extension you wish. When using the `nginx-includes` path, however, avoid using a filename that matches the `*.conf.j2` pattern required for `include` files described above. #### `nginx_wordpress_site_conf` Use the `nginx_wordpress_site_conf` variable to designate your child template for `wordpress-site.conf.j2`, which is used for each of your sites. To designate a global child template for all your sites, you could define the variable in a `group_vars/<environment>/main.yml` file. ```yaml nginx_wordpress_site_conf: nginx-includes/wordpress-site.conf.child ``` You may designate a child template per site by defining the variable in `group_vars/<environment>/wordpress_sites.yml`. ```yaml wordpress_sites: example.com: ... nginx_wordpress_site_conf: nginx-includes/example.com.conf.child ... ``` ### Create a Child Template Create your child templates at the paths you designated in the `nginx_conf` and `nginx_wordpress_site_conf` variables described above. [Child templates](https://jinja.palletsprojects.com/en/stable/templates/#child-template) must include two elements: - an `{% extends 'base_template' %}` statement - one or more `{% block block_name %}` blocks #### Child Template Example – Simple Here is an example child template that replaces the `http_begin` block in the `nginx.conf.j2` base template. ```jinja {% extends 'roles/nginx/templates/nginx.conf.j2' %} {% block http_begin -%} server_names_hash_bucket_size 128; server_names_hash_max_size 512; {% endblock %} ``` The path for your base template – referenced in your `extends` statement – must be relative to the `server.yml` playbook (i.e., relative to the Trellis root directory). #### Child Template Example – Complex The first block in the example child template below augments the content of the `fastcgi_basic` block from the `wordpress-site.conf.j2` base template. It inserts <code>{{ super() }}</code>, which represents the original block content from the base template, then adds an extra `fastcgi_param`. The second block in the example rewrites the `redirects_https` block, omitting the `ssl_enabled` conditional and adding a new `listen 8080` directive. ```jinja {% extends 'roles/wordpress-setup/templates/wordpress-site.conf.j2' %} {% block fastcgi_basic -%} {{ super() }} fastcgi_param HTTPS on; {%- endblock %} {% block redirects_https %} # Redirect to https server { listen 80; listen 8080; server_name {{ site_hosts | join(' ') }}{% if item.value.multisite.subdomains | default(false) %} *.{{ site_hosts_canonical | join(' *.') }}{% endif %}; {{ self.acme_challenge() -}} location / { return 301 https://$host$request_uri; } } {% endblock -%} ``` You'll notice that these blocks use indentation and [whitespace control](https://jinja.palletsprojects.com/en/stable/templates/#whitespace-control) (e.g., `-%}`) parallel to their counterparts in the base template `wordpress-site.conf.j2`. This will achieve the best formatting of templated conf files on the server. ## Sites templates You may use sites templates to add new sites confs to Nginx in addition to the standard WordPress confs. They are also Ansible/Jinja2 templates and thus can make full use of variables and logic. Create your sites templates following the guidelines below. ::: tip Once you have set up your sites templates, append `--tags nginx-sites` to your command to run only the Nginx sites portions of the playbook. ::: ### Default By default in Trellis, a "no-default" site Nginx conf is included. Its purpose is to drop requests to unknown server names, preventing host header attacks and other potential problems. The `nginx_sites_confs` variable contains the list of confs to be templated to the server's `sites-available` folder. Its default value only registers the default site (whose template resides in `roles/nginx/templates/no-default.conf.j2`): ```yaml nginx_sites_confs: - src: no-default.conf.j2 ``` Each entry to this variable also has an `enabled` parameter, which can be omitted, and defaults to `true`. It controls whether the conf is linked to the server's `sites-enabled` folder, and thus activated. The above default is equivalent to: ```yaml nginx_sites_confs: - src: no-default.conf.j2 enabled: true ``` However, you might want to add other sites for specific purposes. ### Designate a site template You will need to inform Trellis of the sites templates you have created. #### `nginx_sites_confs` Use the `nginx_sites_confs` variable to designate your new site template. Given that this template applies to all environments, it would be appropriate to define the variable in a `group_vars/<environment>/main.yml` file (including `group_vars/all/main.yml`). Remember to keep the default site for security purposes if you don't have a specific reason to override it. ```yaml nginx_sites_confs: - src: no-default.conf.j2 - src: nginx-includes/example.conf.site.j2 ``` The example above designates a site template in the `nginx-includes` path on your local machine (i.e., the default path for `nginx_includes_templates_path` variable; see [`include` files](#include-files) section above). You may choose a different path and assign the template any name and file extension you wish. When using the `nginx-includes` path, however, avoid using a filename that matches the `*.conf.j2` pattern required for `include` files described above. ### Create a site template Create your site templates at the paths you designated in the `nginx_sites_confs` variable described above. Templates should start with an <code># {{ ansible_managed }}</code> statement to indicate that the file is [managed by ansible](https://docs.ansible.com/projects/ansible/latest/reference_appendices/config.html). #### Template example Here is an example site template that hosts nginx default page, listening on `example.com` non-standard port 8080. ```nginx # {{ ansible_managed }} server { listen 8080; server_name example.com; root /var/www/html; index index.html index.htm index.nginx-debian.html; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } } ``` ### File cleanup By default, Trellis will remove from the remote's `site-enabled` directory any link to a site conf file that has its `enabled` attribute set to `false`. There is no cleanup of the confs in `sites-available`, they're only made mute by being disabled. This example shows the addition of the above site template, while also disabling Trellis' default site. ```yaml nginx_sites_confs: - src: no-default.conf.j2 enabled: false - src: nginx-includes/example.conf.site.j2 ``` ================================================ FILE: trellis/passwords.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-06 07:42 description: Manage passwords in Trellis for MySQL root, admin users, sudoer access, and WordPress databases. Store securely in Ansible Vault for production environments. title: Password Management in Trellis authors: - alwaysblank - ben - fullyint - Log1x - swalkinshaw --- # Password Management in Trellis There are a few places you'll want to set/change passwords: `group_vars/<environment>/vault.yml` - `vault_mysql_root_password` - `vault_users.*.password` - `vault_wordpress_sites.*.env.db_password` `group_vars/development/vault.yml` - `vault_wordpress_sites.admin_password` `group_vars/all/vault.yml` - `vault_mail_password` For staging/production environments, it's best to randomly generate longer passwords using something like [random.org](https://www.random.org/passwords/). You may be concerned about setting plaintext passwords in a Git repository, and you should be. We strongly recommend you encrypt these passwords before committing them to your repo. Trellis is structured to make it easy to enable [Ansible Vault](vault.md) to encrypt select files. Alternatively, you could try an option such as [git-crypt](https://github.com/AGWA/git-crypt). ::: warning Note Any type of server configs such as this playbook should always be in a **private** Git repository. ::: ================================================ FILE: trellis/python.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2022-02-28 22:16 description: Install and configure Python for using Trellis. Python is required for Ansible automation that powers WordPress server provisioning and deployment. title: Python Requirements for Trellis authors: - swalkinshaw --- # Python Requirements for Trellis Trellis' main requirement is Python because Ansible is built with Python. This page documents the best way to install Python on your computer, how to manage Python package dependencies (like Ansible), common issues to avoid, and using trellis-cli to make your life easier. When dealing with Trellis and Python, there's three key points: 1. Make sure you have a stable version of Python 3 and pip installed 2. Use [trellis-cli](https://github.com/roots/trellis-cli) since it handles dependencies for you 3. **Never** use `sudo` when installing packages with `pip` ## Python 2 vs Python 3 Python 2 reached end-of-life in 2019 and hasn't been maintained since then. For that reason, newer version of Trellis (and trellis-cli) only support Python 3. Unlike most languages that have a single version installed at a time, and only offer an "unversioned" single binary path (such as just `node`), Python can be more confusing because most operating systems treat them separately with `python3` and `python` (which can be version 2 or 3 depending on your setup). Regardless of the OS, it's still possible to symlink `python3` to `python` for convenience as well. ## Installing Python ### macOS Newer versions of macOS like Monterey and Big Sur come with both versions 2 and 3. Annoyingly though, the unversioned `python` is 2.7x while `python3` needs to be explicitly used for Python 3. While using the system Python on macOS should work fine, the main downside is that the versions are only updated when macOS itself has a new major version. If you want to have more control over Python versions, we recommend using a tool like [pyenv](https://github.com/pyenv/pyenv) or [asdf](https://github.com/asdf-community/asdf-python) to install specific versions globally (or even per project if that's needed). We **do not recommend** installing Python from Homebrew. This might be surprising since it goes against most guides and recommendations but we believe using Python from Homebrew will cause more problems long-term due to its newer "feature" of auto-upgrading packages as described in [this article](https://justinmayer.com/posts/homebrew-python-is-not-for-you/). After Python is installed and working, you'll also need to ensure pip is installed. If `pip` or `pip3` does not exist, it can be installed like this: ```shell $ python3 -m ensurepip ``` ### Ubuntu Ubuntu 20.04 comes default with Python 3 available as `python3` only. There's no "unversioned" `python`. The [`python-is-python3`](https://packages.ubuntu.com/focal/python-is-python3) package exists solely as an easy way to symlink `/usr/bin/python` to `python3`. ```shell $ sudo apt-get install -y python3 python-is-python3 python3-pip ``` ## Installing and managing dependencies Once you have Python working, the next step is ensuring you can install Trellis' dependencies. They are always declared in the [`requirements.txt`](https://github.com/roots/trellis/blob/master/requirements.txt) file, but mainly this involves installing Ansible. [pip](https://pypi.org/project/pip/) is Python's package installer and what Trellis recommends using. But this is where trellis-cli comes in! ### trellis-cli and Virtualenv We **strongly recommend** using trellis-cli whenever possible since it will make your life managing dependencies and installing Ansible much easier. trellis-cli uses [Virtualenv](https://virtualenv.pypa.io) to manage dependencies _per_ project. It creates a "virtual environment" within each Trellis project so the dependencies are completely isolated. This allows different projects to have different versions of Ansible installed for example. trellis-cli automatically creates a virtualenv and installs dependencies via pip at two points: 1. when a new project is created with `trellis new` 2. when `trellis init` is run for an existing project Once the virtualenv exists, all other `trellis` commands automatically use it. This means running `trellis deploy production` will activate the virtualenv (within the CLI, for the lifetime of the command) and use the version of Ansible within the virtual environment. When using trellis-cli, you should almost never have to use `pip` manually yourself. There's a more advanced Virtualenv integration offered as well. See the [README](https://github.com/roots/trellis-cli#virtualenv) for more details. ### Manually using pip If you do need to run `pip` manually to install Ansible, here's a few tips: 1. **Never** use `sudo` with `pip`. It will only cause problems. 2. Make sure you're using the version of pip that corresponds to your Python version. If you're using Python 3, then you might need to use `pip3`. 3. Avoid installing `ansible` directly with pip. Instead run `pip install -r requirements.txt` within a Trellis project to ensure you're getting a supported version of Ansible. ================================================ FILE: trellis/redis.md ================================================ --- date_modified: 2026-03-02 12:00 date_published: 2025-10-24 12:00 description: Enable Redis in Trellis for WordPress object caching. Improve site performance by caching database queries and reducing load on MySQL database servers. title: Redis Object Caching for WordPress in Trellis authors: - ben - TangRufus --- # Redis Object Caching for WordPress in Trellis Trellis supports two types of caching that work together: - [**FastCGI Cache**](/trellis/docs/fastcgi-caching/) - Full page caching at the nginx level - **Object Cache** - Database query and transient caching (Redis or Memcached) These can be used independently or together for optimal performance. ## Configuration ### Maximum Performance (FastCGI + Object Cache) Enable both FastCGI page caching and Redis object caching: ```yaml wordpress_sites: example.com: cache: enabled: true # Enables FastCGI page caching duration: 30s # FastCGI cache duration object_cache: enabled: true # Enables object caching provider: redis # Use Redis (or memcached) database: 0 # Redis database number ``` ### FastCGI Cache Only (Default/Backward Compatible) Existing configurations continue to work unchanged: ```yaml cache: enabled: true # FastCGI page caching only duration: 30s skip_cache_uri: /wp-admin/|/wp-json/|/xmlrpc.php skip_cache_cookie: comment_author|wordpress_[a-f0-9]+ background_update: "on" ``` ### Object Cache Only If you need Redis object cache without page caching: ```yaml cache: enabled: false # Disable FastCGI page caching object_cache: enabled: true # Enable object caching provider: redis # Use Redis (or memcached) database: 0 # Redis database number ``` ## Cache Types ### FastCGI Cache (Page Caching) - **Purpose**: Caches complete HTML pages - **Performance**: Fastest possible page loads for cached content - **Best for**: High-traffic sites with mostly static content - **Location**: nginx level, bypasses PHP entirely ### Redis Object Cache - **Purpose**: Caches database queries, transients, and objects - **Performance**: Reduces database load significantly - **Best for**: Database-heavy sites, complex queries - **Features**: Persistent across page loads, shared data ### Memcached Object Cache - **Purpose**: Alternative to Redis for object caching - **Performance**: Similar to Redis, slightly different characteristics - **Best for**: When Redis isn't available or preferred ## WordPress Plugin Installation ### For Redis Object Cache Install a Redis object cache plugin in your WordPress site: 1. [Redis Object Cache](https://wordpress.org/plugins/redis-cache/) ```bash composer require wp-plugin/redis-cache ``` 2. After deployment, activate the plugin and enable object caching: ```bash wp plugin activate redis-cache ``` ```bash wp redis enable ``` 3. If your Redis server requires a password, add the following to your Bedrock config (`config/application.php`): ```php Config::define('WP_REDIS_PASSWORD', env('WP_REDIS_PASSWORD')); ``` ### For Redis Full-Site Cache Install [MilliCache](https://github.com/MilliPress/MilliCache) in your WordPress site: 1. Require the package: ```bash composer require millipress/millicache ``` 2. After deployment, activate the plugin: ```bash wp plugin activate millicache ``` 3. Verify the object cache is working: ```bash wp millicache test ``` ```bash wp millicache status ``` ### For Memcached Object Cache Install [Memcached Object Cache](https://github.com/Automattic/wp-memcached): ::: tip `reference` and `url` must be pinned to the same commit or tag. Do not reference a branch. For a different commit or tag, inspect that revision's `composer.json` in the `automattic/wp-memcached` repository and copy its `require` section here so the requirements match the version you're pinning. Replace `composer/installers` with `"koodimonni/composer-dropin-installer":"^1.4"`. ::: 1. Add the package repository: ```bash composer repo add wp-memcached '{"type":"package","package":{"name":"automattic/wp-memcached","type":"wordpress-dropin","version":"dev-master","dist":{"type":"file","url":"https://raw.githubusercontent.com/Automattic/wp-memcached/bb3b9f689dd99df66454b93ece34093556dc37f9/object-cache.php","reference":"bb3b9f689dd99df66454b93ece34093556dc37f9"},"require":{"koodimonni/composer-dropin-installer":"^1.4","php":">=7.4.0","ext-memcache":"*"}}}' --before wp-packages ``` 2. Configure the install location: ```bash composer config allow-plugins.koodimonni/composer-dropin-installer true ``` ```bash composer config --json extra.dropin-paths '{"web/app/":["type:wordpress-dropin"]}' ``` 3. Require the package (omit `--ignore-platform-req=ext-memcache` if already installed): ```bash composer require automattic/wp-memcached:dev-master --ignore-platform-req=ext-memcache ``` 4. Untrack the drop-in because it is now managed by Composer: ```bash echo 'web/app/object-cache.php' >> .gitignore ``` ## Configuration Examples ### Maximum Performance Setup ```yaml wordpress_sites: example.com: cache: enabled: true # FastCGI page caching duration: 60s object_cache: enabled: true # Object caching provider: redis # Using Redis database: 0 ``` ### Multiple Sites with Isolated Object Caches ```yaml # Site 1 wordpress_sites: site1.com: cache: enabled: true object_cache: enabled: true provider: redis database: 0 # Redis DB 0 # Site 2 wordpress_sites: site2.com: cache: enabled: true object_cache: enabled: true provider: redis database: 1 # Redis DB 1 ``` ### Mixed Cache Strategies ```yaml # High-traffic marketing site (page cache only) wordpress_sites: marketing.com: cache: enabled: true duration: 300s # 5-minute page cache # Database-heavy app (both caches) wordpress_sites: app.com: cache: enabled: true duration: 30s object_cache: enabled: true provider: redis # Adds object caching database: 0 ``` ## Customizing Redis Configuration ### Global Redis Settings You can customize Redis settings in `group_vars/all/main.yml`: ```yaml # Increase memory allocation (default: 256mb) redis_maxmemory: 512mb # Change eviction policy (default: allkeys-lru) redis_maxmemory_policy: allkeys-lru # Enable Redis password redis_requirepass: your_secure_password # Persistence settings redis_appendonly: "yes" # Enable AOF persistence ``` ### Advanced Configuration For more advanced Redis configuration, you can override any setting in `group_vars/all/main.yml`: ```yaml redis_extra_config: tcp-backlog: 511 tcp-keepalive: 300 supervised: systemd ``` ## Advanced Configurations ### Custom Redis Settings per Site ```yaml wordpress_sites: example.com: cache: enabled: true object_cache: enabled: true provider: redis host: 127.0.0.1 port: 6379 database: 0 password: secret_password prefix: custom_prefix_ ``` ### Memcached Sessions + Redis Object Cache ```yaml # In group_vars/all/main.yml memcached_sessions: true # Use Memcached for PHP sessions # In wordpress_sites.yml wordpress_sites: example.com: cache: enabled: true # FastCGI page cache object_cache: enabled: true # Redis object cache provider: redis database: 0 ``` This setup uses three different cache systems: - **Memcached**: PHP sessions - **Redis**: WordPress object cache - **FastCGI**: Page cache ## Monitoring Monitor Redis usage: ```bash redis-cli ``` ```bash redis-cli INFO memory ``` ```bash redis-cli INFO stats ``` ```bash redis-cli MONITOR ``` ================================================ FILE: trellis/remote-server-setup.md ================================================ --- date_modified: 2024-09-11 10:00 date_published: 2015-10-15 12:27 description: Set up remote servers for Trellis requiring bare Ubuntu 24.04 LTS installation on VPS or dedicated servers. Shared hosting is not supported. title: Remote Server Setup for WordPress with Trellis authors: - ben - fullyint - Log1x - MWDelaney - nicbovee - swalkinshaw - MWDelaney --- # Remote Server Setup for WordPress with Trellis Trellis can be used for setting up remote servers (offered by VPS/cloud service providers such as [DigitalOcean](/trellis/docs/deploy-to-digitalocean/) and [Hetzner Cloud](/trellis/docs/deploy-to-hetzner-cloud/)) to host your staging and production environments. ::: tip ℹ️ Sign up for [Hetzner Cloud](https://console.hetzner.com/refer?pk_campaign=referral-invite&pk_medium=referral-program&pk_source=reflink&pk_content=V6DnI7GDHM4N) through the Roots referral link to receive $20 in cloud credits. ::: Trellis CLI includes a `trellis server create` command that can automatically create and provision a server on a supported cloud provider: ```shell $ trellis server create production ``` This command requires a cloud provider API token. If the token environment variable is not set, the command will prompt for one. | Provider | Environment Variable | Token Link | | --- | --- | --- | | DigitalOcean | `DIGITALOCEAN_ACCESS_TOKEN` | [Create a DigitalOcean token](https://cloud.digitalocean.com/account/api/tokens/new) | | Hetzner Cloud | `HCLOUD_TOKEN` | [Create a Hetzner API token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/) | See the [CLI docs](/trellis/docs/cli/) for more details on configuring your cloud provider. ::: warning **Trellis cannot provision shared or managed hosts.** Trellis requires a bare server if you want to use it for provisioning. ::: ## Server requirements * Ubuntu 24.04 LTS * SSH access to the server You need a server running a bare/stock version of Ubuntu 24.04 LTS. If you're using a host such as DigitalOcean that lets you pick an OS to start with, then select the Ubuntu 24.04 option. You need to be able to connect to your Ubuntu server from your local computer via SSH. We *highly* suggest doing this via SSH keys so you don't have to specify a password every time. Many hosts offer to automatically add your SSH key when creating a server, so take advantage of that. Once you have a Ubuntu server up and running, you can provision it. ## Provisioning Provisioning a server means to set it up with the necessary software and configuration to run a WordPress site. For Trellis this means things like: installing MariaDB, installing Nginx, configuring Nginx, creating a database, etc. Trellis has two main [playbooks](https://docs.ansible.com/projects/ansible/latest/user_guide/playbooks_intro.html): `dev.yml` and `server.yml`. As mentioned in local development, Trellis automatically runs the `dev.yml` playbook for us. For remote servers, you provision a server via the `server.yml` playbook. This leaves you with a server *prepared* to run a WordPress site, but without the actual codebase yet. Before provisioning your server, there's a little more configuration to do. First determine the _environment_ you want to configure; after development, you'll likely be creating a `production` or `staging` environment. ### Configuration 1. Copy your `wordpress_sites` from your working development site in `group_vars/development/wordpress_sites.yml` to `group_vars/<environment>/wordpress_sites.yml`. 2. Modify your site and add the necessary settings for [remote servers](wordpress-sites.md#remote-servers) since they have a few more settings than local development. Also see the [Passwords docs](passwords.md). 3. Add your server hostname to `hosts/<environment>` (replacing `your_server_hostname`). 4. Specify public SSH keys for `users` in `group_vars/all/users.yml`. See the [SSH Keys docs](ssh-keys.md). 5. Consider setting `sshd_permit_root_login: false` in `group_vars/all/security.yml`. See the [Security docs](security.md). Now you're ready to provision your server. Ansible connects to the remote server via SSH so run the following command from your local machine: ```shell $ trellis provision <environment> ``` ### Re-provisioning Re-provisioning is always assumed to be a safe operation. When you make changes to your Trellis configuration, you should provision your remote servers again to apply the changes: Run the following from any directory within your project: ```shell $ trellis provision <environment> ``` You can also provision with specific tags to only run the relevant roles: Run the following from any directory within your project: ```shell $ trellis provision --tags users <environment> ``` ================================================ FILE: trellis/sage-integration.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2020-04-07 23:23 description: Use Trellis with Sage themes for complete WordPress development stack. Trellis handles server provisioning and deployment for Sage-based theme development. title: Sage Theme Integration with Trellis authors: - swalkinshaw --- # Sage Theme Integration with Trellis Trellis is designed to be theme-agnostic and is not tied to Roots' Sage theme at all. That doesn't mean it's hard though; all that's needed is a way to compile assets. ## Compiling production theme assets Sage, like many WordPress themes now, requires a step during deploys to compile production assets. Trellis includes an *example* `build-before` deploy hook designed to work with Sage. See [`deploy-hooks/build-before.yml`](https://github.com/roots/trellis/blob/master/deploy-hooks/build-before.yml). For Sage users, simply uncomment that built-in example hook file and deploy away. Assets will be compiled *locally* and then copied to the remote server. ## Other themes The Sage example deploy hook can also serve as a blueprint for many other themes that need to run an `npm` or `yarn` command for compiling assets. ## NVM If you use [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions for running themes locally, the easiest way to use it is via `$NVM_DIR/nvm-exec`. Example: ```yaml - name: Install npm dependencies command: $NVM_DIR/nvm-exec npm install delegate_to: localhost args: chdir: "{{ project_local_path }}/web/app/themes/mytheme" - name: Compile assets for production command: $NVM_DIR/nvm-exec npm run build:production delegate_to: localhost args: chdir: "{{ project_local_path }}/web/app/themes/mytheme" ``` ================================================ FILE: trellis/security.md ================================================ --- date_modified: 2026-03-05 00:00 date_published: 2015-09-06 07:42 description: Secure Trellis WordPress servers by disabling root SSH login, creating admin users with sudo access, configuring secure password authentication, and hardening WordPress runtime file permissions. title: WordPress Security Features in Trellis authors: - ben - fullyint - Log1x - QWp6t - swalkinshaw --- # WordPress Security Features in Trellis ## Locking down root The `sshd` role heightens your server's security by providing better SSH defaults. SSH password authentication will be disabled. We encourage you to disable SSH `root` login as well. You may adjust these two particular options in `group_vars/all/security.yml`. See the [`sshd` role `README.md`](https://github.com/roots/trellis/tree/master/roles/sshd) for more configuration options. ## Admin user The first provision via the `server.yml` playbook will create the `admin_user` and set up related [SSH Keys](ssh-keys.md). If you disable `root` login, subsequent connections will be made as the `admin_user`. ## Admin user sudoer password If `root` login is disabled and the `server.yml` playbook connects as the `admin_user`, it will invoke `sudo` using the password in `vault_users` (`group_vars/<environment>/vault.yml`). If you run the playbook with `--ask-become-pass`, Trellis will use the password you enter via the CLI. You are strongly encouraged to protect the sensitive `vault_users` information by enabling Ansible [Vault](vault.md). ## WordPress runtime hardening Trellis supports an opt-in hardening mode that separates the PHP-FPM runtime identity from the deploy user. When enabled, PHP runs as a dedicated user with write access limited to explicitly allowlisted paths. This reduces the impact of a compromised WordPress site by preventing PHP from modifying application code. By default, hardening is disabled and Trellis behaves as it always has — PHP-FPM runs as the `web_user`. ### Enabling hardening Add the following to `group_vars/all/main.yml` (or an environment-specific file like `group_vars/production/main.yml`): ```yaml wordpress_runtime_hardened: true ``` ### Configuration options | Variable | Default | Description | | --- | --- | --- | | `wordpress_runtime_hardened` | `false` | Enable runtime hardening mode | | `wordpress_runtime_user` | `www-data` | OS user that PHP-FPM runs as when hardened | | `wordpress_runtime_group` | `www-data` | OS group that PHP-FPM runs as when hardened | | `wordpress_runtime_writable_paths` | `["shared/uploads"]` | Paths the runtime user can write to (relative to the site root) | | `wordpress_runtime_cron_as_runtime_user` | `false` | Run WP-CLI cron as the runtime user instead of `web_user` | ### Using a custom runtime user For stronger isolation, use a dedicated user instead of `www-data`. The user and group must exist on the server before hardening is enabled — the playbook will fail fast if they don't. Define the user in `group_vars/all/users.yml`: ```yaml users: - name: php-app groups: - php-app keys: [] ``` Then configure the runtime variables: ```yaml wordpress_runtime_hardened: true wordpress_runtime_user: php-app wordpress_runtime_group: php-app ``` ### Per-site writable paths The global `wordpress_runtime_writable_paths` applies to all sites by default. You can override it for individual sites in your [WordPress Sites](/trellis/docs/wordpress-sites/) configuration: ```yaml wordpress_sites: example.com: runtime_writable_paths: - shared/uploads - current/web/app/cache ``` ### Cron user By default, WP-CLI cron jobs continue to run as the `web_user` even when hardening is enabled. To run cron as the runtime user instead: ```yaml wordpress_runtime_cron_as_runtime_user: true ``` This only takes effect when `wordpress_runtime_hardened` is also `true`. ================================================ FILE: trellis/server-logs.md ================================================ --- date_modified: 2023-01-31 17:40 date_published: 2018-04-24 09:59 description: Trellis site logs are located at `/srv/www/example.com/logs/` including Nginx access logs, error logs, and PHP-FPM logs for troubleshooting issues. title: Accessing Server Logs in Trellis authors: - ben - Log1x - swalkinshaw --- # Accessing Server Logs in Trellis ## Accessing logs Trellis CLI includes a `logs` command for quickly accessing logs. It automatically integrates with [GoAccess](https://goaccess.io/) when the `--goaccess` option is used. ```shell $ trellis logs [options] ENVIRONMENT [SITE] ``` | Description | Command | | -------------------------- | ------------------------------------ | | View production logs | `trellis logs production` | | View access logs only | `trellis logs --access production` | | View error logs only | `trellis logs --error production` | | View logs in GoAccess | `trellis logs --goaccess production` | | View the last 50 log lines | `trellis logs -n 50 production` | Run `trellis logs --help` for further information. ## Location of logs Server logs for Trellis sites can be found at `/srv/www/example.com/logs/`: - `/srv/www/example.com/logs/access.log` - `/srv/www/example.com/logs/error.log` Any server 500 errors or white screen issues should be debugged by viewing the error logs in the `/srv/www/example.com/logs/` directory. Trellis uses the [ansible-logrotate](https://github.com/nickhammond/ansible-logrotate) role to install and configure logrotate for sites and can be configured by editing [`group_vars/all/logrotate.yml`](https://github.com/roots/trellis/blob/master/group_vars/all/logrotate.yml). ================================================ FILE: trellis/ssh-keys.md ================================================ --- date_modified: 2025-10-16 10:00 date_published: 2015-09-06 07:42 description: Configure SSH keys in Trellis for secure server access. Add keys manually or automatically import SSH keys from GitHub users for team member access. title: SSH Key Management in Trellis authors: - ben - dalepgrant - evance - fullyint - knowler - Log1x - swalkinshaw - techieshark --- # SSH Key Management in Trellis Each Trellis playbook uses a specific SSH user to connect to your remote machines (or virtual machine in development). | Playbook | Default User | User Variable | Task | | ------------ | ----------------- | ------------- | ------------------------ | | `dev.yml` | Your local username | - | create development VMs | | `server.yml` | `root` or `admin` | `admin_user` | provision remote servers | | `deploy.yml` | `web` | `web_user` | deploy WordPress sites | This page reviews how to configure SSH users for the `server.yml` and `deploy.yml` playbooks. If you are looking for general SSH configuration options, see the [`sshd` role `README.md`](https://github.com/roots/trellis/tree/master/roles/sshd) . If you will be the only person provisioning and deploying, and your SSH public key is available at `~/.ssh/id_ed25519.pub`, you probably won't need to modify the Trellis defaults for `users`. ## The `users` Dictionary While provisioning, `server.yml` will create the `users` defined in `group_vars/all/users.yml`, assigning their `groups` and public SSH `keys`. The example below defines a single user. ```yaml users: - name: username groups: - primary_group - other_group keys: - "{{ lookup('file', '/path/to/local/file') }}" - https://github.com/username.keys ``` Specify the user's primary group first in the list of `groups`. List `keys` for anyone who will need to make an SSH connection as that user. `server.yml` can `lookup` keys in local files or retrieve them from remote host URLs. ::: tip GitHub example Using `https://github.com/<username>.keys` is a quick way to get all the public SSH keys you have on your GitHub account added onto the server. ::: If needed, you can define different users *per* environment by redefining `users` in any `group_vars/<environment>/users.yml` file. ## `server.yml` Trellis assumes that when you first create your server you've already added your SSH key to the `root` account. How this happens depends on your cloud provider but here's a few common ones: - *Digital Ocean*: gives you the option to automatically add your SSH key when creating your droplet - *AWS*: provides a `pem` file which needs to be converted to a `pub` file for use with Ansible (example: `ssh-keygen -y -f private_key1.pem > public_key1.pub `) `server.yml` will try to connect to your server as `root`. If the connection fails, `server.yml` will try to connect as the `admin_user` defined in `group_vars/all/users.yml` (default `admin`). If `root` login will be disabled on your server, it is critical for the `admin_user` to be defined in your list of `users`, with `sudo` first in this user's list of groups (see the [Security docs](security.md)). The default definition for the `admin_user` is shown below. ```yaml users: - name: '{{ admin_user }}' groups: - sudo keys: - "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}" # - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" # - https://github.com/username.keys admin_user: admin ``` - You may enable colleagues to run `server.yml` by adding their public SSH `keys` to the `admin_user`. - If your hosting provider disables root but provides a default user such as `ubuntu`, specify `admin_user: ubuntu`. - If you are trying to override the dynamic selection of `root` or `admin_user`, preferring to manually specify the Ansible remote user, review notes in the section [remote user variable precedence](https://github.com/roots/trellis/pull/274#issuecomment-121455761). ## `deploy.yml` The `deploy.yml` playbook deploys your site while connecting as the `web_user` (default `web`) because this user owns files in the web root, the deploy destination. The `web_group` must come first in the list of groups for the web_user. The default definition for the `web_user` is shown below. ```yaml web_user: web web_group: www-data users: - name: "{{ web_user }}" groups: - "{{ web_group }}" keys: - "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}" # - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" # - https://github.com/username.keys ``` You may enable colleagues to run `deploy.yml` by adding their public SSH `keys` to the `web_user`. See the example below. ## Example `users` The example below adds the SSH keys of GitHub users `swalkinshaw` and `retlehs` to `~/.ssh/authorized_keys` for `admin_user`. This enables `swalkinshaw` and `retlehs` to run `server.yml` to provision the servers. The example also adds their keys, and the keys of GitHub user `austinpray`, to `web_user`. This enables each of them to run `deploy.yml` to deploy sites. ```yaml users: - name: '{{ web_user }}' groups: - '{{ web_group }}' keys: - https://github.com/swalkinshaw.keys - https://github.com/retlehs.keys - https://github.com/austinpray.keys - name: '{{ admin_user }}' groups: - sudo keys: - https://github.com/swalkinshaw.keys - https://github.com/retlehs.keys - name: another_user groups: - some_group - some_other_group keys: - https://github.com/swalkinshaw.keys ``` The example above also demonstrates the option of creating `another_user` whose primary group is `some_group`, but who is also in `some_other_group`, and who has public SSH keys for `swalkinshaw`. ## Removing keys Removing a key from the configuration and re-provisioning the server does not remove the key from the server's `authorized_keys` file by default. To reset SSH keys, Trellis supports an opt-in "single use" `reset_user_ssh_keys` variable (as of [#1576](https://github.com/roots/trellis/pull/1576)): ```shell $ trellis provision --extra-vars reset_user_ssh_keys=true production ``` ::: tip Note This will first replace all keys on the remote with the _first found_ key from your defined users before re-adding the rest. Make sure you have access with the first key listed in your user dictionary before running this _just in case_ the process is interrupted. ::: ## Cloning remote repo using SSH agent forwarding All the SSH connections discussed above apply to Trellis connecting from your local machine to your server. It is a different type of connection, however, when Trellis clones a remote private repo during deployment. In this case, your remote server is allowed to forward your local machine's SSH credentials to the remote repo to authorize the connection. The Trellis `ansible.cfg` file enables this SSH agent forwarding with `ssh_args = -o ForwardAgent=yes`. You should not need auth tokens or private keys for the `web_user`. If you run into trouble cloning a remote repo during deploy, see [Using SSH agent forwarding](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/using-ssh-agent-forwarding) for tips and troubleshooting. ### macOS/OS X users Remember to import your SSH key password into Keychain. For macOS Monterey (12.0) and later, you should run: ```shell $ ssh-add --apple-use-keychain ``` For versions prior, you need to run: ```shell $ ssh-add -K ``` ================================================ FILE: trellis/ssl.md ================================================ --- date_modified: 2026-05-03 12:00 date_published: 2015-09-06 07:42 description: Enable HTTPS in Trellis with automatic Let's Encrypt certificates, manually provided SSL certificates, or self-signed certificates for local development. title: SSL Certificates in Trellis authors: - aitor - ben - dalepgrant - fullyint - joshf - Log1x - MWDelaney - qwatts-dev - runofthemill - swalkinshaw --- # SSL Certificates in Trellis HTTPS is now more important than ever. Strong encryption through HTTPS creates a safer and more secure web while protecting your site's users. Roots believes in security so we've always made SSL/HTTPS a priority in Trellis. Our implementation is designed to score an A+ on the [Qualys SSL Labs Test](https://www.ssllabs.com/ssltest/). In the past many people avoided going HTTPS for technical and convenience reasons: - Certificates were expensive - Annoying and complicated web-server configuration - HTTPS sites were much slower than HTTP Trellis has features to make it as easy, cheap, and painless as possible to use HTTPS giving you no excuse *not* to use it. There are three supported certificate *providers* in Trellis: - [Let's Encrypt](#lets-encrypt) - [Manual](#manual) - [Self-signed](#self-signed) HTTPS can be enabled on a per-site basis. However, by default, enabling SSL on a site will make that site HTTPS **only**. Meaning that all HTTP requests will be redirected to HTTPS with the proper HSTS headers set as well. Unless you have a good reason to change this default, you shouldn't. See the section on [HSTS](#hsts) for more details. CloudFlare Origin CA support can be added with [trellis-cloudflare-origin-ca](https://github.com/TypistTech/trellis-cloudflare-origin-ca). ## Configuration Any SSL provider starts with the same basic configuration. Add the following to a WP site: ```yaml # group_vars/production/wordpress_sites.yml (example) example.com: # rest of site config ssl: enabled: true provider: <name> ``` ### Let's Encrypt [Let's Encrypt](https://letsencrypt.org/) (LE) is a new Certificate Authority that is free, automated, and open. Unless you already have an SSL certificate purchased, Let's Encrypt should be your provider choice. Let's Encrypt is appropriate for your production and staging environments, but not for development (see [DNS records](#dns-records)). Trellis has complete automated integration. The only required setting is the `provider` itself: ```yaml # group_vars/production/wordpress_sites.yml (example) example.com: # rest of site config ssl: enabled: true provider: letsencrypt ``` There is one main difference between LE and other certificate authorities: their certificates expire every *90 days*. Trellis automates by running a cron-job so you never have to manually renew them or worry about them expiring like a paid certificate. ::: warning Note Let's Encrypt is ending support for v1 of their ACME protocol. If you are using a Trellis version older than `v1.2.0`, please see [here](https://discourse.roots.io/t/trellis-and-lets-encrypt-v1-end-of-life/16816) for more details. ::: #### DNS records Let's Encrypt verifies and creates certificates through a publicly accessible web server for *every* domain you want on the certificate. This means you need valid and working DNS records for every site host/domain you have configured for your WP site. ```yaml # group_vars/production/wordpress_sites.yml (example) mydomain.com: site_hosts: - canonical: mydomain.com redirects: - www.mydomain.com - mydomaintoredirect.com - www.mydomaintoredirect.com ssl: enabled: true provider: letsencrypt ``` In the example above, Trellis will try to automatically create 1 certificate with the following hosts: `mydomain.com`, `www.mydomain.com`, `mydomaintoredirect.com` and `www.mydomaintoredirect.com`. All you need to do is make sure those DNS records exist and point to the web server's IP. Trellis takes care of the rest. If you want "www" subdomains to redirect to your canonical domain, they MUST be included in redirects. #### Challenges Let's Encrypt certificate process looks roughly like: 1. Generate private account key 2. Generate private key for each site (could have multiple domains) 3. Generate CSR (Certificate Signing Request) for each site (single/multiple domains) 4. Request certificate from LE by sending them the account key and CSR 5. LE client creates a "challenge" file in the web root of your site 6. LE server verifies it can access the challenge file 7. LE server sends the certificate if the challenge succeeds The above steps is what Trellis handles automatically. #### Multiple servers Trellis' LE integration is designed by default for a single server. If you have multiple web servers behind a load balancer, you will *not* want this role/process running on all of them since it would generate different private and account keys for each one. This process is beyond the scope of the documentation right now. However, there are two variables which help for this process: - `letsencrypt_account_key_source_content` - `letsencrypt_account_key_source_file` You can use either of these to manually define an account key's contents or file. If one of these is set, it will be used and none will be automatically generated. It's also up to you to make sure you've manually registered your account key. See [https://gethttpsforfree.com/](https://gethttpsforfree.com/) for a simple site to do this. #### Staging Let's Encrypt has rate limits for their production/real certificates. While Trellis will prevent these rate limits from being hit, if you want to test out LE integration, you can use their staging server to get a "fake" certificate. ::: warning Note Note that browsers will display an error/warning that they don't recognize the Certificate Authority so this should only be used for testing purposes. ::: Just set the following variable: ```yaml # in a group_vars file letsencrypt_ca: "https://acme-staging-v02.api.letsencrypt.org" ``` #### Troubleshooting Let's Encrypt Trellis versions prior to [Jan 2017](https://github.com/roots/trellis/pull/630) did not detect some changes that should have triggered Let's Encrypt certificate regeneration. The most common example was users adding domain(s) to `site_hosts` (in `wordpress_sites`) and reporting that browsers gave privacy warnings for the new domains. Similar problems occurred for users switching from manual certificates to Let's Encrypt certificates. If you see similar privacy warnings after adjusting your SSL configuration in some way, these troubleshooting steps may help. 1. Update trellis to include [`roots/trellis#630`](https://github.com/roots/trellis/pull/630) 2. Set ssl `enabled: false` for affected sites in `group_vars/<environment>/wordpress_sites.yml` 3. Run `ansible-playbook server.yml -e env=<environment> --tags wordpress` 4. Reset ssl `enabled: true` for applicable sites in `group_vars/<environment>/wordpress_sites.yml` 5. Run `ansible-playbook server.yml -e env=<environment> --tags letsencrypt` ### Manual This provider means you're providing both the SSL certificate and private key. This was the original method included in Trellis. ```yaml # group_vars/production/wordpress_sites.yml (example) example.com: # rest of site config ssl: enabled: true provider: manual cert: ~/ssl/example.com.crt key: ~/ssl/example.com.key ``` `cert` and `key` are **local** relative paths to those files. They will be copied to the remote servers. This is done so your private key does not need to be stored in your Git repository for security reasons. ### Self-signed The self-signed provider **should only be used for development or internal server purposes**. Trellis will generate a "fake" (or "snake-oil") certificate which is not recognized by browsers. Browsers will prompt you with an error/warning that they don't recognize the Certificate Authority (which is yourself in this case). ```yaml # group_vars/development/wordpress_sites.yml (example) example.com: # rest of site config ssl: enabled: true provider: self-signed ``` #### Lima Trust the Lima VM's self-signed certificate so browsers and host-side tooling stop showing warnings: ```shell $ trellis vm trust ``` This pulls the cert and key out of the VM, exports them to `~/.local/share/trellis/ssl/<vm>-<hash>/`, and trusts the cert in the macOS login keychain and every Firefox profile. Re-runs are safe: if the cert is already trusted, the command reports it and does nothing. Firefox support requires `certutil` (install via `brew install nss` on macOS or `apt install libnss3-tools` on Linux). On Linux, pass `--trust-system` to also add the cert to the system trust store. Available flags: - `--site` — only trust the cert for the named site - `--no-export-key` — skip exporting the private key to the host To reverse trust entries added by this project: ```shell $ trellis vm untrust ``` To print the host paths of the exported cert and key per site: ```shell $ trellis vm trust paths ``` ## HSTS Trellis sets [HSTS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security) headers for better security. HSTS will ensure all traffic to your site is being served over HTTPS automatically. There are a few defaults set which you can override if need be: - `hsts_max_age` - how long the header lasts (default: `31536000` (1 year)) - `hsts_include_subdomains` - also make *all* subdomains be served over HTTPS (default: `false`) - `hsts_preload` - indicates the site owner's consent to have their domain preloaded (default: `false`) These variables are configured on a site's `ssl` object: ```yaml # group_vars/production/wordpress_sites.yml (example) example.com: # rest of site config ssl: enabled: true provider: letsencrypt hsts_max_age: 31536000 hsts_include_subdomains: true hsts_preload: true ``` ### Preload lists What is HSTS Preloading? > HSTS Preloading is a mechanism whereby a list of hosts that wish to enforce the use of SSL/TLS on their site is built into a browser. This list is compiled by Google and is utilised by Chrome, Firefox, Opera, Safari, IE11 and Edge. These sites do not depend on the issuing of the HSTS response header to enforce the policy, instead the browser is aleady aware that the host requires the use of SSL/TLS before any connection or communication even takes place. This removes the opportunity an attacker has to intercept and tamper with redirects that take place over HTTP. This isn't to say that the host needs to stop issuing the HSTS response header, this must be left in place for those browsers that don't use preloaded HSTS lists. > > - https://scotthelme.co.uk/hsts-preloading/ Using preloading is a two-step process: 1. Enable the `preload` option shown above by setting `hsts_preload: true` 2. Submit your site/domain to the official browser preload list: [https://hstspreload.org/](https://hstspreload.org/) More information: - [https://hstspreload.org/](https://hstspreload.org/) - [HSTS Preloading](https://scotthelme.co.uk/hsts-preloading/) ### `max-age` Trellis defaults to a long `max-age` of `31536000` seconds (1 year). You may want to test out HSTS with much shorter max-ages and then ramp up the value in stages until you're confident everything works. This deployment ramp up process is detailed here: [https://hstspreload.org/#deployment-recommendations](https://hstspreload.org/#deployment-recommendations) ### Disabling HSTS The only way to disable HSTS is to set the `max-age` header to `0`: ```yaml # group_vars/production/wordpress_sites.yml (example) example.com: # rest of site config ssl: enabled: true provider: letsencrypt hsts_max_age: 0 ``` ### `hsts_include_subdomains` HSTS should ideally be applied to all subdomains as well when possible. This means that if you have HSTS enabled on `example.com`, then *all- its subdomains (`*.example.com`) will also be forced over HTTPS. However, it's a common enough scenario for a subdomain to host another non-HTTPS site for various reasons (maybe it's externally managed and out of your control). For example, you might deploy a Trellis based WordPress site to `example.com` but also host another application from a subdomain such as `internalapp.example.com` which isn't secured by HTTPS. Since HSTS' `includeSubdomains` option would break any subdomains in those situations, Trellis _disables_ the `hsts_include_subdomains` option by default. ::: tip If you are in control of your domain and all its subdomains, we **highly recommend** you consider enabling the `includeSubdomains` option since it does provide stricter guarantees and security for your users. ::: This can be done by setting the `hsts_include_subdomains` option to `true` (either globally or a per-site basis). Per-site: ```yaml # group_vars/production/wordpress_sites.yml (example) example.com: # rest of site config ssl: enabled: true provider: letsencrypt hsts_include_subdomains: true ``` Globally: ```yaml # group_vars/production/main.yml nginx_hsts_include_subdomains: true ``` ## Client certificates You may want to set up TLS Client Authentication, especially when using [Cloudflare Authenticated Origin Pulls](https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/). To enable, simply set the `client_cert_url` variable to the URL of the certificate authority (CA) that will be used to verify clients with. ```yaml # group_vars/production/wordpress_sites.yml (example) example.com: # rest of site config ssl: # rest of ssl config client_cert_url: https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem ``` ## Performance Our HTTPS implementation uses all performance optimizations possible to ensure your sites remain fast despite the small overhead of SSL. This includes the following features: - HTTP/3 support with QUIC (fallback to HTTP/2 and HTTP/1.1 for older browsers) - SSL session cache - OCSP stapling - 1400 byte TLS records - Longer keepalives HTTP/3 requires UDP port 443 to be open. If you have a cloud or hardware firewall in front of your server (eg: AWS security groups, DigitalOcean cloud firewalls), ensure it allows UDP/443 inbound traffic. See [Is TLS Fast Yet?](https://istlsfastyet.com/) for more information on fast TLS/SSL. ## Browser support Since our implementation is designed to be modern and score an A+ on the [Qualys SSL Labs Test](https://www.ssllabs.com/ssltest/), this does mean that a few older browsers such as IE6 won't be able to access your site due to the cipher suites used. ================================================ FILE: trellis/troubleshooting.md ================================================ --- date_modified: 2023-01-27 13:17 date_published: 2015-09-06 07:42 description: Troubleshoot Trellis installations with debugging tips for Ansible errors, solutions for unresponsive machines, and fixes for common provisioning problems. title: Troubleshooting Common Trellis Issues authors: - ben - fullyint - Log1x - swalkinshaw - dalepgrant --- # Troubleshooting Common Trellis Issues ## Debugging Golden rule to debugging any failed command with Ansible: 1. Read the output logs and find the failed task. 2. Read through error message for the exact issue. 3. Re-run the command in `verbose` mode `ansible-playbook deploy.yml -vvvv -e "site=<domain> env=<environment>"` if necessary to get more details. 4. SSH into your server and manually run the command where Ansible failed. Example: if a Git clone task failed during deploys, then SSH into the server as the `web` user (which is what deploys use) and run the manual command such as `git clone <repo>`. This will give you a much better clue as to what's going wrong. ## Let's Encrypt SSL certificates See [Troubleshooting Let's Encrypt](ssl.md#troubleshooting-let-s-encrypt). ## Composer install: host key verification failed Sometimes a task that installs Composer dependencies gives an error `host key verification failed`. This can happen when the `known_hosts` file on your Lima VM or remote host is missing a key for one of the host `repositories` in the related `composer.json` file. Ensure that each host from `composer.json` has a key listed in `group_vars/all/known_hosts.yml` then try your `trellis provision development` or `trellis deploy` command again. ## SSH connections If you have trouble with SSH connections to your server, consider the tips below. You may also want to review information about [disabling `root` login](security.md#locking-down-root) and how to configure your server's SSH settings via the [`sshd` role](https://github.com/roots/trellis/tree/master/roles/sshd). ### SSH keys - [Generating a new SSH key and adding it to the ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) - [Testing your SSH connection](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection) - [Your local `ssh-agent` must be running](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/using-ssh-agent-forwarding#your-local-ssh-agent-must-be-running) (macOS users: remember to run `ssh-add -K`) - How to designate [SSH keys](ssh-keys.md) in Trellis SSH will automatically look for and try a default set of SSH keys, along with keys loaded in your `ssh-agent`. However, the SSH server will only let your SSH client try a limited number of keys before disconnecting (default: 6). If you have many SSH keys and the correct key is not being selected, you can force your SSH client to try only the correct key. Add this to your `~/.ssh/config` (with the correct path to your key): ```plaintext Host example.com IdentitiesOnly yes IdentityFile /users/username/.ssh/id_ed25519 ``` ### Host key change Your server may occasionally offer a different host key than what your local machine has on record in `known_hosts`. This could happen if you rebuild your server or if the `sshd` role configures your server to offer a stronger key. **Example 1** ```plaintext TASK [setup] ******************************************************************* System info: Ansible 2.2.1.0; Darwin Trellis at "Add `apt_packages_custom` to customize Apt packages" --------------------------------------------------- SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh fatal: [xxx.xxx.xxx.xxx]: UNREACHABLE! => {"changed": false, "unreachable": true} to retry, use: --limit @/Users/yourname/sites/example.com/trellis/deploy.retry ``` **Example 2** ```plaintext @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)! It is also possible that a host key has just been changed. The fingerprint for the ED25519 key sent by the remote host is SHA256:lv86hFykjn8pnOWE2WDWJo8Mzf6FTDMx/yWXOqzK5PU. ``` If this change in host keys is expected, then clear the old host key from your `known_hosts` by running the following command (with your real IP or host name). ```shell $ ssh-keygen -R 12.34.56.78 ``` Then try your Trellis playbook or SSH connection again. If the host key change is unexpected, cautiously consider why the host identification may have changed and whether you may be victim to a man-in-the-middle attack. ### `git clone` or `composer install` task hangs or fails The `sshd` role may cause your server's SSH client to request stronger host keys from hosts of git repos or composer packages. This could create the [host-key-change](#host-key-change) problem, but this time on your server instead of your local machine. Follow the same remediation steps, but on the server. Similarly, the `sshd` role may cause your server's SSH client to require stronger [ciphers, kex algorithms, and MACs](https://github.com/roots/trellis/tree/master/roles/sshd#ciphers-kexalgorithms-and-macs) than previously. If your `git clone` or `composer install` connections involve older systems that do not support the stronger protocols, you may need to add more options to `ssh_ciphers_extra`, `ssh_kex_algorithms_extra`, or `ssh_macs_extra`. ### Verbose output SSH connection issues are often difficult to resolve without verbose output. Use the `-vvvv` option with your `ansible-playbook` command: ```shell $ ansible-playbook server.yml -e env=production -vvvv ``` You may also use `-v`, `-vv`, and `-vvv` with manual SSH connections: ```shell $ ssh -v root@12.34.56.78 ``` ### Manual SSH If your `ansible-playbook` command is failing its SSH connection, it can be helpful to try a manual SSH connection to narrow down the problem. If manual SSH fails, try again with `-v` for [verbose output](#verbose-output). ```shell $ ssh -v root@12.34.56.78 ``` ### `Ciphers`, `KexAlgorithms`, or `MACs` The `sshd` role will most likely cause your SSH server to discontinue using some older and weaker protocols. If your connections involve older systems that do not support the stronger protocols configured by the `sshd` role, see [`Ciphers`, `KexAlgorithms`, and `MACs`](https://github.com/roots/trellis/tree/master/roles/sshd#ciphers-kexalgorithms-and-macs) for how to add back in any protocols you need. ## APT sources You may need to clean the APT sources to update a package, for example when [updating MariaDB mirrors](https://github.com/roots/trellis/issues/1575). You can set `apt_clean_sources: true` in `group_vars/all/main.yml` to run every provision, or to run this for one provision only, use: ```shell $ trellis provision --extra-vars apt_clean_sources=true production ``` ================================================ FILE: trellis/user-contributed-extensions.md ================================================ --- date_modified: 2024-10-12 16:15 date_published: 2015-09-06 07:42 description: Explore community-developed Ansible roles and extensions for Trellis that add functionality and features beyond the core WordPress server management. title: User Contributed Extensions for Trellis authors: - ben - Log1x - MWDelaney - strarsis - swalkinshaw - TangRufus - Xilonz --- # User Contributed Extensions for Trellis Extensions (or roles), developed by the community, that complement Trellis. Issues with extensions should be opened in their respective repositories. - [bedrock-site-protect](https://github.com/louim/bedrock-site-protect) — Add or remove htpasswd protection to your websites - [trellis-backup](https://discourse.roots.io/t/trellis-backup-an-ansible-role-for-local-backups/6497) — Set up automated backups to various locations using duplicity. - [trellis-backup](https://github.com/Xilonz/trellis-backup-role) — Set up automated backups to various locations using duply. - [trellis-cloudflare-origin-ca](https://typist.tech/portfolio-item/trellis-cloudflare-origin-ca/) — Add Cloudflare Origin CA to Trellis as SSL provider. - [trellis-newrelic-php](https://typist.tech/portfolio-item/trellis-newrelic-php/) — Install New Relic PHP agent on Trellis servers - [trellis-nixstats](https://github.com/Xilonz/trellis-nixstats/) — Install NIXStats agent on Trellis servers - [trellis-database-uploads-migration](https://github.com/valentinocossar/trellis-database-uploads-migration) — Ansible playbook for Trellis that manages database and uploads migration - [trellis-db-push-and-pull](https://github.com/hamedb89/trellis-db-push-and-pull) — Push and pull databases with Trellis and Ansible playbooks - [trellis-backup-during-deploy](https://github.com/ItinerisLtd/trellis-backup-during-deploy) - Backup WordPress database during Trellis deploys - [trellis-purge-kinsta-cache-during-deploy](https://github.com/ItinerisLtd/trellis-purge-kinsta-cache-during-deploy) - Purge Kinsta cache when Trellis deploys Bedrock - [trellis-cve-2018-6389](https://github.com/ItinerisLtd/trellis-cve-2018-6389) - Mitigate CVE-2018-6389 WordPress load-scripts / load-styles attacks - [trellis-disable-xml-rpc](https://github.com/ItinerisLtd/trellis-disable-xml-rpc) - Disable WordPress XML RPC on Trellis sites - [trellis-purge-wp-rocket-cache-during-deploy](https://github.com/ItinerisLtd/trellis-purge-wp-rocket-cache-during-deploy) - Purge WP Rocket cache when Trellis deploys Bedrock - [trellis_flush_rewrite_rules_during_deploy](https://github.com/ItinerisLtd/trellis_flush_rewrite_rules_during_deploy) - Resets WordPress' rewrite rules (based on registered post types, etc) during Trellis deploys - [trellis-slack-webhook-notify-during-deploy ](https://github.com/ItinerisLtd/trellis-slack-webhook-notify-during-deploy) - Sends a deployment complete message to a Slack channel when Trellis deploys Bedrock - [trellis_install_wp_cli_via_composer](https://github.com/ItinerisLtd/trellis_install_wp_cli_via_composer) - Install WP-CLI via composer on Trellis servers - [tiller-circleci-orb](https://github.com/ItinerisLtd/tiller-circleci-orb/) - Deploy Trellis, Bedrock and Sage(optional) via CircleCI - [trellis-cyberduck](https://github.com/ItinerisLtd/trellis-cyberduck) - Trellis commands for Cyberduck - [trellis-matomo](https://github.com/E-VANCE/trellis-matomo) - Install the latest on-premise version of Matomo with Trellis ================================================ FILE: trellis/vault.md ================================================ --- date_modified: 2026-03-10 17:00 date_published: 2015-11-01 14:32 description: Enable Ansible Vault in Trellis to encrypt sensitive data in `vault.yml`. Store passwords, API keys, and confidential variables securely in version control. title: Ansible Vault for Encrypting Secrets in Trellis authors: - ben - fullyint - Log1x - MWDelaney - mZoo - swalkinshaw - TangRufus --- # Ansible Vault for Encrypting Secrets in Trellis Some project variables contain sensitive data like passwords. Trellis keeps these variable definitions in separate files named `vault.yml`. We strongly recommend that you encrypt these `vault.yml` files using to avoid exposing sensitive data in your project repository. <details> <summary>vault.yml example</summary> To briefly demonstrate what vault does, consider this example `vault.yml` file. ```yaml # example vault.yml file -- unencrypted plain text my_password: example_password ``` You should replace the `example_password` then encrypt the file with Ansible Vault before committing it to your repo. The data would be safe in your repo because the encrypted file would look like this: ```yaml # example vault.yml file -- encrypted $ANSIBLE_VAULT;1.1;AES256 343163646662643438323831343332626234333233386666333162383265663 3132306538383762336332376165383530633838643937320a6363343238643 363065366664316364646561613163653866623566303235666537343437643 6638363265383831390a6631663239373833636133623333666363643166383 6237663637353638653266616562616535623465636265316231613331 etc. ``` </details> ## Encrypt your vault files ```shell $ trellis vault encrypt ``` ::: danger If you have unencrypted `vault.yml` files in your project's git history (e.g., passwords in plain text), you will most likely want to change the variable values in your `vault.yml` files before encrypting them and committing them to your repo. ::: ::: warning Don't forget your vault password Trellis automatically generates a vault password for you at `trellis/.vault_pass` (this file **will not** be added to your Git repository), and adds a reference to it to the `ansible.cfg` file. ::: Your Trellis commands will be exactly the same as before enabling vault, not requiring any extra flags. ### Adding additional vault files for encryption ```shell $ trellis vault encrypt -f path/to/file.yml ``` ## View an encrypted vault file You can view a vault file in your terminal with the following command: ```shell $ trellis vault view <environment> ``` ## Edit an encrypted vault file You can edit a vault file in your terminal with the following command: ```shell $ trellis vault edit group_vars/<environment>/vault.yml ``` ## Other vault commands `trellis-cli` provides a few basic commands that mirror with the official [Ansible Vault](https://docs.ansible.com/projects/ansible/latest/user_guide/vault.html) ones. - `trellis vault encrypt <args>` - `trellis vault view <args>` - `trellis vault edit <args>` - `trellis vault decrypt <args>` -- Avoid using the `decrypt` command. If your intention is to view or edit an encrypted file, use the `view` or `edit` commands instead. Any time you decrypt a file, you risk forgetting to re-encrypt the file before committing changes to your repo. Run `trellis vault` to see usage details. ## Working with vault variables Here are a few tips for working with [variables and vault](https://docs.ansible.com/projects/ansible/latest/tips_tricks/ansible_tips_tricks.html#keep-vaulted-variables-safely-visible) in Trellis. - Variables with sensitive data such as passwords are defined in files named `vault.yml`. - Each environment has its own `vault.yml` file: `group_vars/<environment>/vault.yml`. - There is also one `vault.yml` file applicable to all environments: `group_vars/all/vault.yml`. - Variables named with the `vault_` prefix are defined in the `vault.yml` files. - To view or edit an encrypted `vault.yml` file, use either `trellis vault view <file>` or `trellis vault edit <file>`. Avoid using the `decrypt` command. Any time you decrypt a file, you risk forgetting to re-encrypt the file before committing changes to your repo. You may want to employ a pre-commit hook ([example](https://reinteractive.com/articles/ansible-best-practices)) for added prevention. ## Sharing a project with vault-encrypted files Your repo with vault-encrypted files is secure from anyone being able to see or use the sensitive data in the `vault.yml` files. To grant a colleague access to the data, you will need to give your colleague your vault password to use in repeating the two password steps in the [Steps to Enable Ansible Vault](#encrypt-your-vault-files) above. It is still recommended to always keep your project in a private repo. ## Disabling Ansible Vault It is not recommended to disable Ansible Vault but you can disable it at any time. Simply run `ansible-vault decrypt <file1> <file2> <etc>`. If you then commit the unencrypted files to your repo, the sensitive data will be in your repo in plain text and will be difficult to remove from the git history. If you re-enable vault in the future, you may want to change all the sensitive data, encrypt with vault, then commit the revised and encrypted `vault.yml` files to your repo. ## Storing your password Without your password, either entered as a string or stored in your `vault_password_file` file (usually `.vault_pass` and configured in the `ansible.cfg` file), you will not be able to access the encrypted files. The `vault_password_file` should not ever be publicly accessible, or committed to version control. It's a good practice to backup this file on another physical or virtual drive, ideally also encrypted. ## Access recovery Should you lose access to your vault password, you you can either spin up a new server, or recreate or regenerate the `group_vars/(environment)/vault.yml` files and, on the servers, manually update the following to match new vault strings: ### admin root (sudo) password ```shell $ sudo passwd admin ``` ### root mysql password ```sql UPDATE mysql.user SET Password=PASSWORD('password_in_vault_file') WHERE USER='root' AND Host='localhost'; flush privileges; ``` ### WordPress database passwords ```sql UPDATE mysql.user SET Password=PASSWORD('password_in_vault_file') WHERE USER='example_com' AND Host='localhost'; flush privileges; ``` ## Additional resources [ansible-toolkit](https://github.com/dellis23/ansible-toolkit#atk-git-diff) provides a `atk-git-diff` command that allows you to do a `git diff` on encrypted files. ================================================ FILE: trellis/wordpress-sites.md ================================================ --- date_modified: 2024-06-19 13:17 date_published: 2016-03-28 21:10 description: Configure WordPress sites in Trellis through `wordpress_sites.yml`. Define domains, SSL certificates, cache configuration, and host multiple WordPress sites. title: Configuring WordPress Sites in Trellis authors: - ben - dalepgrant - fullyint - Log1x - mockey - MWDelaney - nathanielks - nlemoine - swalkinshaw - TangRufus --- # Configuring WordPress Sites in Trellis Everything in Trellis is built around the concept of "sites". Each Trellis managed server (local virtual machine or remote server) can support one or more WordPress sites. Trellis will automatically configure everything needed to host a WordPress site such as databases, Nginx confs, folder directories, etc based on the site's configuration. These sites are configured in YAML files for each environment such as `group_vars/development/wordpress_sites.yml`. There are two components and places to configure sites: - Basic settings in `group_vars/development/wordpress_sites.yml` - Passwords/secrets in `group_vars/development/vault.yml` ::: tip Note If you used Trellis CLI to create your project, the basic configuration settings will already be set for your main site. ::: ## Site configuration `wordpress_sites` is a top-level dictionary (object/hash) used to define all the sites you want. Here's an absolute bare-minimum site as an example for development: ```yaml # group_vars/development/wordpress_sites.yml wordpress_sites: example.com: site_hosts: - canonical: example.test local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root) admin_email: admin@example.test multisite: enabled: false ssl: enabled: false cache: enabled: false ``` Each site is defined by a "key" (`example.com` in this case). Trellis uses the key internally as the name of the site and as a default value in a lot of variables. We recommend naming your sites after their domain so it's descriptive. Nested under the name/key are the site specific configuration settings. You only need to define a variable/setting if you want to overwrite the default value which can be found below. ## Passwords/secrets When you add or edit a site in `wordpress_sites.yml`, you also need to edit `vault.yml` for the accompanying site/key. `vault.yml` simplifies the use of the Ansible Vault encryption feature for specific files. You never want to include plain-text passwords in a Git repository so we make it easier to optionally encrypt the `vault.yml` file while leaving the normal settings separate. See [Vault](vault.md) for more information on this. ```yaml # group_vars/development/vault.yml vault_wordpress_sites: example.com: admin_password: admin env: db_password: example_dbpassword ``` Notice the matching site keys in both `wordpress_sites` and `vault_wordpress_sites` for `example.com` which ties together these site settings. ## Options ### Common - `site_hosts` - List of hosts that Nginx will listen on. At least one is required. Each host item must specify a `canonical` host and may optionally specify a list of corresponding `redirects` (hosts). **Remember to set up DNS for every host listed.** You cannot use just an IP address. ```yaml # minimum required example.com: site_hosts: - canonical: example.com # multiple hosts and redirects are possible example.com: site_hosts: - canonical: example.com redirects: - www.example.com - site.com - canonical: example.co.uk redirects: - www.example.co.uk ``` - `local_path` - path targeting Bedrock-based site directory (*required*) - `current_path` - symlink to latest release (default: `current`) - `db_create` - whether to auto create a database or not (default: `true`) - `composer_authentications` - Composer auth setup. Useful for configuring access to private repositories. See the [Composer Authentication docs](/trellis/docs/composer-authentication/) (optional) - `ssl` - SSL options. See the [SSL docs](ssl.md) - `multisite` - Multisite options. See the [Multisite docs](multisite.md) - `cache` - Nginx FastCGI cache options. See the [Cache docs](fastcgi-caching.md) - `h5bp` - Nginx config files from [h5bp server config](https://github.com/h5bp/server-configs-nginx) to include - `cache_file_descriptors` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/directive-only/cache-file-descriptors.conf) (default: `not_dev`) - `extra_security` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/directive-only/extra-security.conf) (default: `true`) - `no_transform` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/directive-only/no-transform.conf) (default: `false`) - `x_ua_compatible` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/directive-only/x-ua-compatible.conf) (default: `true`) - `cache_busting` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/location/cache-busting.conf) (default: `false`) - `cross_domain_fonts` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/location/cross-domain-fonts.conf) (default: `true`) - `expires` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/location/expires.conf) (default: `false`) - `protect_system_files` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/location/protect-system-files.conf) (default: `true`) - `env` - environment variables - `disable_wp_cron` - Disable WP cron and use system's (default: `true`) - `wp_home` - `WP_HOME` constant (default: `<protocol>://${HTTP_HOST}`) - `wp_siteurl` - `WP_SITEURL` constant (default: `${WP_HOME}/wp`) - `wp_env` - environment (default: `env` via Ansible) - `db_name` - database name (default: `<site name>_<env>`) - `db_user` - database username (default: `<site name>`) - `db_password` - database password (*required*, in `vault.yml`) - `db_host` - database hostname (default: `localhost`) - `db_prefix` - database table prefix (defaults to `wp_` if not set) - `db_user_host` - hostname or ip range used to restrict connections to database (default: `localhost`) ### Development - `site_install` - whether to install WordPress or not (default: `true`) - `site_title` - WP site title (default: site name) - `admin_user` - WP admin user name (default: `admin`) - `admin_email` - WP admin email address (*required*) - `admin_password` - WP admin user password (*required* in `vault.yml`) - `initial_permalink_structure` - permalink structure applied at time of WP install (default: `/%postname%/`) ### Remote servers - `repo` - URL of the Git repo of your Bedrock project (*required*) - `repo_subtree_path` - relative path to your Bedrock/WP directory in your repo (above) if its not the root (like site/ in roots-example-project) - `branch` - the branch name, tag name, or commit SHA1 you want to deploy (default: `master`) - `env` - environment variables - `auth_key` - Generate (*required* in `vault.yml`) - `secure_auth_key` - Generate (*required* in `vault.yml`) - `logged_in_key` - Generate (*required* in `vault.yml`) - `nonce_key` - Generate (*required* in `vault.yml`) - `auth_salt` - Generate (*required* in `vault.yml`) - `secure_auth_salt` - Generate (*required* in `vault.yml`) - `logged_in_salt` - Generate (*required* in `vault.yml`) - `nonce_salt` - Generate (*required* in `vault.yml`) - `runtime_writable_paths` - list of paths (relative to the site root) the PHP-FPM runtime user can write to when [runtime hardening](/trellis/docs/security/#wordpress-runtime-hardening) is enabled (default: global `wordpress_runtime_writable_paths`) - `deploy_keep_releases` - number of releases to keep for rollbacks (default: 5)