roots/bedrock-disallow-indexing
A Roots Bedrock plugin that blocks search engine indexing on non-production environments by sending noindex headers and robots directives, helping keep staging and development sites out of search results.
Bedrock's mu-plugin autoloading makes installation a one-liner:
composer require roots/bedrock-disallow-indexing
Because Bedrock registers packages of type wordpress-muplugin automatically via composer/installers, the package lands in web/app/mu-plugins/ and is loaded on every request — no activation step, no functions.php edit. Confirm it landed with composer show roots/bedrock-disallow-indexing and check the web/app/mu-plugins/ directory for the symlinked/copied folder.
The first use case is almost always the same: you've just stood up a staging or review environment and want to make sure Google doesn't crawl it before launch. The package solves that by forcing blog_public to 0 and emitting an X-Robots-Tag: noindex header on non-production environments. Look first at:
config/environments/*.php — because the package keys off WP_ENV, this is where the decision lives..env — the WP_ENV value is the single source of truth.web/robots.txt vs. the dynamic robots output — verify which one is actually served.Verify it works in one command:
curl -sI https://staging.example.com/ | grep -i x-robots-tag
# X-Robots-Tag: noindex, nofollow
If you see that header, you're done. If not, jump to Gotchas.
Environment matrix. Bedrock typically ships development, staging, and production configs. The package is designed so that everything except production is noindexed. The pattern that matters is being explicit rather than relying on defaults:
// config/environments/production.php
define('WP_ENV', 'production');
And keep the WP_ENV value in .env for local overrides, with a CI check that production deploys always set WP_ENV=production (see Gotchas — this is the classic footgun).
Multi-environment pipelines. In a typical GitHub Actions / Forge / Deployer flow, the same composer artifact is promoted through environments. Because the package is environment-aware at runtime (not build time), you build once and deploy many — the staging deploy naturally noindexes, the production deploy naturally allows. This is the primary reason to prefer this package over hand-rolled blog_public logic in wp-config.php, which tends to get copy-pasted and drift.
Local dev parity. Developers get noindex for free in development, which means running SEO audits locally won't be polluted by real crawl results, and test content won't accidentally get published to search engines from a laptop using a tunnel (ngrok, Valet share, etc.).
Pre-launch checklist integration. A common pattern is to add a smoke test to your deploy verification:
# staging should be noindexed
test "$(curl -sI https://staging.example.com/ | grep -i x-robots-tag)" != ""
# production should NOT be noindexed
! curl -sI https://example.com/ | grep -qi x-robots-tag
Wire this into your post-deploy script so a misconfigured WP_ENV fails the deploy instead of silently deindexing production.
Paired with roots/bedrock-google-analytics-style mu-plugins. If you already use Roots mu-plugins, the pattern is consistent: composer-managed, environment-aware, zero configuration. Keep that convention — resist adding conditionals in application.php for this package specifically.
Integration with SEO plugins. Yoast and RankMath both read blog_public and respect it, so you generally get consistent behavior across the plugin's sitemap, canonical tags, and meta robots output without extra wiring. You should not need to configure the SEO plugin separately for staging.
WP_ENV is the whole ballgame. If WP_ENV is unset or misspelled (prod, Production, empty string), the package may treat the environment as non-production and noindex production — or, worse, behave inconsistently across requests if the constant is defined late. Set it in .env and pin it in the environment config. Add an assertion in wp-config.php:
if (!defined('WP_ENV')) {
define('WP_ENV', 'production'); // safe default
}
But prefer failing loudly in staging over guessing.
Noindex header ≠ deindexed. X-Robots-Tag and blog_public = 0 prevent future crawling, but pages already in Google's index remain until they're recrawled. For a site that was accidentally indexed on staging, you need Search Console's URL Removal tool in addition to this package. Don't assume flipping WP_ENV cleans things up retroactively.
Check what robots.txt is actually served. Bedrock serves web/robots.txt as a static file in many setups, and WordPress's dynamic robots_txt filter (which respects blog_public) is bypassed when a physical file exists. If your staging robots.txt says Allow: / while the header says noindex, you have two competing signals. Either delete the static file or make sure it's environment-aware.
The header can be cached and persisted. If you're behind Cloudflare, Fastly, or a Bedrock-style page cache (WP Rocket, etc.), an X-Robots-Tag: noindex header set on staging can follow you to production if the cache is shared or the DNS swap reuses the same CDN cache key. Invalidate the full cache on promotion and confirm the header is absent in production.
CDN and reverse-proxy stripping. Some proxies strip unknown X-Robots-Tag headers, and some WAF rule sets do as well. If the header vanishes between origin and client, test the origin directly with curl --resolve.
Authentication-protected environments. If staging is behind HTTP basic auth, crawlers can't reach it anyway — the package is a belt-and-braces measure there, not a substitute for auth. Both are worth keeping.
Extending the behavior. The package is intentionally small; if you need noindex on specific routes (e.g., API endpoints, PDFs) rather than the whole site, add your own mu-plugin that hooks wp_headers or send_headers and conditionally emits the tag, rather than trying to configure this one to do more. Keeping the "whole-environment noindex" concern separate from "route-level noindex" avoids surprising behavior when you later promote a route.
Composer autoload quirk. If you use composer install --no-dev in production and the package's autoloading relies on composer/installers mapping wordpress-muplugin, verify that the installer-paths in your root composer.json still includes web/app/mu-plugins/{$name}/. A custom installer-paths block that omits mu-plugins silently drops the package from production — which is the one environment where you'd rather it be present-but-inert than absent.
How can I help you explore Laravel packages today?