roots/wordpress
Composer-managed WordPress package from Roots. Installs WordPress in a clean, modern directory structure with environment-based configuration, making core updates and dependencies easy to manage in version-controlled projects.
roots/wordpress is not a library you call — it is WordPress core itself, repackaged as a Composer package. Everything it does for you is installation and versioning: instead of wget-ing a tarball or clicking "Update" in wp-admin, core becomes a locked dependency in composer.json/composer.lock like any other package. Treat it as infrastructure, not an API.
Where to look first (in this order):
composer.json inside the package, not the PHP files — the only Roots-authored content is the package metadata (type: wordpress-core, the installer requirement, the extra block). The rest is verbatim WordPress.roots/bedrock's composer.json — this is the canonical usage example and the de-facto documentation. If you are not on Bedrock, copy its extra.wordpress-install-dir and config.allow-plugins blocks.roots/wordpress-core-installer — the Composer plugin that actually decides where core lands on disk. When installation "does nothing", this is almost always the culprit.Minimal steps (Bedrock-style project):
composer require roots/wordpress:^6.7
Then in the root composer.json:
{
"extra": {
"wordpress-install-dir": "web/wp"
},
"config": {
"allow-plugins": {
"composer/installers": true,
"roots/wordpress-core-installer": true
}
}
}
composer install
ls web/wp/wp-load.php # core is now on disk at the path you declared
First use case: a fresh site. composer create-project roots/bedrock my-site gives you core at web/wp, your writable content at web/app (WP_CONTENT_DIR), and wp-config.php kept outside the webroot in config/application.php. That separation — immutable core, versioned config, git-tracked content — is the entire value proposition. Once you've done one composer update roots/wordpress and watched git diff show only composer.lock, you understand the package.
Do not expect this package to scaffold a site, create wp-config.php, install a database, or register autoload classes. It ships files. Bedrock, Trellis, and Sage supply everything else.
composer outdated roots/wordpress # what's available
composer update roots/wordpress -W # -W: allow its installer deps to move too
git diff composer.lock # review: one version line should change
Then: run the test suite / smoke test on staging → deploy the artifact → verify integrity:
wp core version
wp core verify-checksums # confirms the on-disk files match the official release
Because core is a single locked package, a security release is a one-line change you can ship in minutes:
composer require roots/wordpress:6.7.2 --no-update
composer update roots/wordpress -W
That --no-update two-step is worth internalising: it lets you pin the exact patched version and keep the lockfile diff reviewable when other packages also depend on core constraints.
| Constraint | Behaviour | Use when |
|---|---|---|
^6.7 |
auto-minor + patch on composer update |
most sites; matches WordPress's own minor-auto-update policy |
6.7.* |
patches only | conservative client sites |
6.7.2 |
exact pin | regulated/audited environments |
Pair this with Renovate or Dependabot pointed at composer.json. A bot PR that bumps roots/wordpress is the cleanest possible core-update signal — one file changed, CI does the rest. Set the bot to group roots/* so a core bump doesn't arrive shredded across five PRs.
Two Packagist packages exist off the same release tags:
roots/wordpress — core plus the bundled wp-content (default themes, Akismet, hello.php, index.php).roots/wordpress-no-content — identical minus wp-content. Recent Bedrock releases scaffold with this one, because Bedrock already owns web/app as the content directory and a second wp-content is dead weight you then have to gitignore.To switch on an existing project, remove before you require — they are mutually exclusive and swapping them in one command will confuse the solver:
composer remove roots/wordpress
composer require roots/wordpress-no-content:^6.7
If you keep the full variant with a custom content dir, expect web/wp/wp-content to exist and be ignored by WordPress entirely; delete it in a deploy step to avoid confusion, or just migrate to -no-content.
The standard Roots pattern is a build artifact, not a server-side install:
# CI
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
# then rsync/ship the whole web/ tree (core included) to the target
Consequences to wire up deliberately:
web/wp/ is gitignored and built, never committed.~/.composer/cache in CI — core dists are ~20 MB per version and you'll fetch a fresh one on every build otherwise.composer install on the server (Trellis does this in its deploy hooks), you need Composer present and --no-dev set, and you must never run it concurrently with traffic in a way that leaves web/wp half-written. Artifact deploys sidestep this — prefer them.Core at a known path means WP-CLI works normally for diagnostics (wp core version, wp core verify-checksums, wp plugin list), and fails loudly for mutation: wp core update will try to write into a directory that your deploy will overwrite. Turn it off explicitly:
// config/application.php (Bedrock) — or wp-config.php
define('DISALLOW_FILE_MODS', true);
define('AUTOMATIC_UPDATER_DISABLED', true);
DISALLOW_FILE_MODS also blocks plugin/theme installs from wp-admin, which is correct — those go through composer require wpackagist-plugin/... with composer/installers routing them to web/app/plugins.
composer install in a fresh clone produces a working web/wp in seconds. That's the whole local bootstrap — no Docker image layer with a baked-in WordPress, no "download the zip and unzip into the right folder" README step. Whatever your local stack (Lando, DDEV, wp server, Trellis VM), it only needs to point at web/, and the first-run experience is deterministic across every developer's machine.
For agencies running many sites, the pattern is one Bedrock root per site, all requiring roots/wordpress with the same constraint. Bump the constraint across the portfolio in a scripted sweep, run each site's CI, deploy independently. The lockfile is the per-site audit record of exactly which core patch is live — invaluable when a CVE lands and you need to answer "who is still on 6.6.x?" with grep.
allow-plugins will bite you first. Composer 2.2+ refuses to run plugins non-interactively. If web/wp never appears after composer install, check for a "roots/wordpress-core-installer ... blocked by allow-plugins" message. Both composer/installers and roots/wordpress-core-installer must be allowed in the root composer.json, and CI needs it too — the flag is not inherited from your laptop's global config.
--no-plugins silently breaks placement. The install path is not magic; it's the wordpress-core installer plugin honouring extra.wordpress-install-dir. composer install --no-plugins or COMPOSER_NO_PLUGINS=1 downloads the package and drops it in vendor/roots/wordpress instead. Same for --no-scripts in some setups. If you harden CI, whitelist these two plugins rather than disabling plugins wholesale.
Never edit web/wp. It is regenerated wholesale on every update; there is no merge, no three-way diff. Anything you need to change lives in mu-plugins (web/app/mu-plugins, autoloaded by Bedrock), a normal plugin, or a filter in config/application.php. If you find yourself patching core, you've found a missing mu-plugin.
wordpress-install-dir is relative and unforgiving. It's resolved from the project root (where composer.json lives), not from vendor/ and not from the webroot. A typo gives you a site that 404s at /wp-admin with core sitting happily in the wrong folder — and composer install reports success. After the first install, ls the path before you debug anything else.
Composer's audit is blind to WordPress advisories. composer audit and roave/security-advisories track Packagist's advisory database, which does not model WordPress core CVEs the way it models PHP library CVEs. Do not treat a clean composer audit as "core is patched." Use WPScan, wp core verify-checksums against the current release, and a calendar reminder tied to WordPress's security-release cadence (patch releases land roughly monthly, plus out-of-band criticals).
Release-candidate tags need a stability flag. Betas and RCs are published as tags (6.8-RC1-style). Requiring one without minimum-stability: dev / an explicit stability suffix in the version constraint gets you a "could not find a matching version" error that reads like the tag doesn't exist. It exists; Composer is filtering it.
Bundled defaults disappear with -no-content. If you migrate to the no-content variant and your theme does get_template_directory() fallbacks, or a plugin assumes twentytwentyfour is present, you'll get fatals on fresh installs. Ship an explicit default theme in web/app/themes, or accept that "no active theme" is a state your site can be in.
composer update roots/wordpress -W can move more than you expect. The -W is usually necessary (the installer plugin's constraint can otherwise block the bump), but it expands the transaction. Always read the composer.lock diff; if three unrelated packages moved, split the commit so a rollback of core doesn't drag them along.
This repo is a mirror — don't send it pull requests. Core bugs belong upstream at WordPress/WordPress; packaging/installer bugs belong in roots/wordpress-core-installer or roots/bedrock. Issues filed against this repository for PHP behaviour in wp-includes will be closed. Knowing this saves you a wasted afternoon writing a patch that gets regenerated away.
Switching variants mid-project leaves orphans. composer remove then require the other package, but also delete the stale web/wp/wp-content (or the empty vendor remnant) by hand — the installer won't clean up files from the previous package version, and a leftover wp-content/index.php will quietly satisfy WP_CONTENT_DIR checks while serving nothing.
Debugging checklist for "the site won't boot after deploy":
web/wp/wp-load.php exist on the target? (composer install skipped or plugin blocked.)wp core version match the lockfile? (Stale artifact from cache.)WP_CONTENT_DIR still absolute and valid? (Path assumption broke when the release directory changed.)DISALLOW_FILE_MODS set, and did someone try to update anyway? (wp core verify-checksums will show modified files.)Extension points that actually exist: the variant package you choose, extra.wordpress-install-dir, extra.installer-paths from composer/installers (for plugins/themes alongside core), and the allow-plugins allowlist. That's the whole surface. Everything else you want to change about WordPress is a WordPress change — a filter, a mu-plugin, or a config constant — not a package concern.
How can I help you explore Laravel packages today?