Skip to content

WordPressPart of: WordPress security

Staging with WordPress: host tools, plugins and the noindex trap

Which WordPress staging option to use, why a public staging copy needs both a password and noindex, the eight things that go wrong on push, and a wp-config snippet that makes staging safe by hostname.

getReport teamUpdated 27 Sept 202613 min read

A WordPress staging site is a private copy of your site where you test a theme update, a new plugin or a redesign before it reaches visitors. It solves one problem and creates two: the copy is often reachable by anyone who guesses staging. in front of your domain, and its database, including the checkbox that hides it from Google, will one day be pushed back over production. The safe setup is a host or plugin copy behind a password, with noindex keyed to the hostname so it can never travel to the live site. This guide ranks the options, lists the traps in order of cost, and is part of the WordPress security checklist.

Quick answer

  • A public staging copy needs both HTTP basic auth (or an IP allow-list) and noindex. Auth keeps people and bots out; noindex covers the day auth is off.
  • The report probes staging., dev. and test. in front of your domain. A copy that answers 200 without noindex is a security finding on the live site's report.
  • Managed-host staging is the least work and the safest push; a plugin clone lives on your own server and is public unless you protect it; local tools are for developers.
  • Never push the database of a live shop: "Discourage search engines" (blog_public), orders, comments and users created since the copy all travel with it.
  • Report the same page before and after: on production, then on staging, then on production after the push.
  • Never rely on robots.txt: a blocked URL can still be indexed, and Google cannot read a noindex on a page it may not fetch.

Why the staging trap is a WordPress trap

WordPress keeps "is this site visible to search engines" in the database, as blog_public in wp_options, set under Settings → Reading → "Discourage search engines from indexing this site". Tick it on staging and push that database to production, and every live page starts sending <meta name="robots" content="noindex, nofollow">. Nothing looks different in the browser. Traffic from Google fades over the following weeks.

The opposite failure is quieter. A copy at staging.example.com that nobody protected is crawled and indexed next to the real site. It competes for your own content, sends searchers to last month's prices, usually runs older code with debugging on, and shows the redesign to anyone before launch. Both failures come from the same fact: a staging copy is a real WordPress site, with real settings, on a real hostname.

How getReport checks it

When the report recognises WordPress, it requests the home page of three hostnames built from yours: staging., dev. and test. in front of the domain (without the www.). It reads at most the first 64 KB of each answer. A copy counts as open only if it answers HTTP 200 and neither the robots meta tag nor an X-Robots-Tag header contains noindex. A 401 from basic auth, a 403, a 404 or a DNS failure all count as "not found", which is what you want.

Two limits follow. Only those three prefixes are probed, so a copy at stage.example.com or example.com/staging-a1b2/ is not found; run the checker on that address directly. And noindex alone makes the finding pass, because the check is about what Google can index; auth is still required. The staging sites fix page has the short version.

On the staging copy itself, you want these findings to fail. They prove the copy is hidden:

The robots.txt finding should still pass on staging: a copy that blocks crawling hides its own noindex from Google.

The "Discourage search engines" finding opened on a WordPress fixture: the noindex, nofollow robots meta tag it emits, why the setting removes the site from Google, and the fix in Settings → Reading
On staging this finding is the goal; on production after a push it is the first thing to check.

If the discourage finding appears on the live site after a push, you have found the trap; WordPress "Discourage search engines" covers the recovery.

The report fetches from getReport's servers like a normal visitor, so it needs a public address. Private addresses and localhost (127.0.0.1, 10.x, 192.168.x, staging.local) are refused before any request, with "That address is not allowed"; the same rule stops the tool probing internal networks. A copy behind basic auth returns a 401, and the report describes that answer instead of your site. Lift the password for the minutes you test, with the noindex from step 4 in place, and put it back.

Step by step

1. Pick the staging option that matches who will use it

Managed-host staging (first choice for most sites). Most managed WordPress hosts create a copy with one click and offer a push back to live. Check whether yours protects the copy with auth, noindex or both; it is often only one. Read how the push works: some copy files and database together, some let you choose, and a database push overwrites whatever production gained since the copy.

Plugins. WP Staging clones the site into a subfolder of the same install, with its own table prefix in the same database, so it is public at that folder unless you protect it. Duplicator builds a package you deploy to any host. Both give you a copy fast; neither protects it for you.

Local (for developers). Local, DDEV and WordPress.com's Studio run a copy on your machine. Nothing is public, and the report cannot reach it; use the browser's DevTools and Lighthouse there.

OptionWhere the copy livesPublic by defaultPush to liveReport can check it
Host stagingHost subdomainUsually protected, verifyBuilt in, read the DB caveatYes
WP StagingSubfolder of live siteYesPro versionYes, by full URL
DuplicatorAny server you chooseYesManualYes
Local / DDEV / StudioYour computerNoManualNo

2. Protect the public copy twice

Basic auth or an IP allow-list first. On nginx, in the staging server block:

nginx
# /etc/nginx/sites-enabled/staging.example.com
server {
    server_name staging.example.com;
    auth_basic "Staging";
    auth_basic_user_file /etc/nginx/staging.htpasswd;
    # Or instead of a password, only your office: allow 203.0.113.0/24; deny all;
    add_header X-Robots-Tag "noindex, nofollow" always;
    # ... the rest of the WordPress config
}

On Apache, in the staging site's .htaccess:

Apache
AuthType Basic
AuthName "Staging"
AuthUserFile /var/www/staging.htpasswd
Require valid-user
# Or instead of a password: Require ip 203.0.113.0/24
Header set X-Robots-Tag "noindex, nofollow"

Create the password file with htpasswd -c /etc/nginx/staging.htpasswd editor. The X-Robots-Tag header covers PDFs, images and cached pages that a meta tag misses, for the day auth is switched off "just for the client to look". The staging robots.txt must still allow crawling, or Google never sees the header.

3. Take a baseline, then test on staging

Run the report on the production page the change affects most (the home page for a theme, a product page for a WooCommerce plugin) and keep the link. Make the change on staging, and nothing else, so a difference has one cause. Run the same page on the staging hostname and compare module by module:

  • New findings that did not exist on production, and ones that disappeared.
  • The speed module's LCP element and lab metrics. A theme that turns the hero <img> into a CSS background often moves LCP by a second.
  • The staging hostname in absolute URLs: canonical, hreflang, og:url, og:image, JSON-LD and the sitemap. The canonical finding's technical detail shows the URL it found.

4. Make staging safe by hostname

One block in wp-config.php, above "That's all, stop editing", makes any copy at staging.example.com safe and does nothing on production, even when the same file is pushed back:

PHP
// wp-config.php: staging behaviour keyed to the hostname, safe to push to live.
if (isset($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] === 'staging.example.com') {
    define('WP_ENVIRONMENT_TYPE', 'staging');
    define('DISABLE_WP_CRON', true);           // no scheduled posts or jobs firing on the copy
}

Then a must-use plugin at wp-content/mu-plugins/staging-guard.php, which loads before ordinary plugins and cannot be deactivated by accident:

PHP
<?php
/**
 * Plugin Name: Staging guard
 * Description: noindex, no outgoing mail and no Woo webhooks when WP_ENVIRONMENT_TYPE is staging.
 */
if (wp_get_environment_type() !== 'staging') {
    return;
}

// Emit <meta name="robots" content="noindex, nofollow"> on every page (WordPress 5.7+).
add_filter('wp_robots', 'wp_robots_no_robots');

// The same instruction as a header, so feeds and cached responses carry it too.
add_action('send_headers', function () {
    header('X-Robots-Tag: noindex, nofollow', true);
});

// Short-circuit every email (WordPress 5.7+). Log the subject instead.
add_filter('pre_wp_mail', function ($null, $atts) {
    error_log('[staging] mail suppressed: ' . ($atts['subject'] ?? ''));
    return true;
}, 10, 2);

// Do not let WooCommerce deliver webhooks from the copy.
add_filter('woocommerce_webhook_should_deliver', '__return_false');

wp_get_environment_type() reads the constant from wp-config, so the guard is inactive on production even though the file is identical there. pre_wp_mail returning anything but null stops wp_mail() before it sends.

5. Know the eight things that go wrong on push

  1. The domain inside the database. A database copied from staging carries staging.example.com in siteurl, home, image URLs and builder data. Replace it with WP-CLI, which handles serialized data, not raw SQL, which corrupts it:
Shell
# Dry run first: counts replacements per table without changing anything
wp search-replace 'https://staging.example.com' 'https://example.com' --all-tables --skip-columns=guid --dry-run
wp search-replace 'https://staging.example.com' 'https://example.com' --all-tables --skip-columns=guid
  1. "Discourage search engines" travels. It is a database row. Untick it before the push, exclude wp_options, or key it to the hostname (step 4). wp option get blog_public must print 1 on production.
  2. WooCommerce orders. A test order on staging is a real row, and a database push overwrites the real orders placed on live since the copy. Never push a shop's database; push files and repeat settings by hand.
  3. Scheduled posts and cron. WP-Cron runs on staging whenever someone loads a page there, so plugins fire their jobs twice.
  4. Email from staging. Order confirmations and password resets reach real people with staging links in them.
  5. Licence keys. Premium plugins bind their key to a domain; some vendors allow a staging. subdomain, some do not.
  6. Uploads out of sync. Images added to live after the copy do not exist on staging, and a full push can drop them.
  7. Caches. A page cache copied with the site serves live's HTML until purged. After a push, purge the page cache, the object cache and the CDN on production too, or the report keeps showing the old tags.

6. After the push, report production again

On the live URL, the same page as the baseline:

  • "The page can be indexed" and "Search engines are not discouraged in Settings → Reading". If either fails, Settings → Reading first, then the SEO plugin's per-page settings.
  • The canonical names the live domain, and no finding mentions staging..
  • The SEO audit on two deep pages, because a per-page noindex set while testing is not site-wide.

Then compare the rest with the baseline: that is the before and after that tells you whether the change was worth it.

7. If staging got indexed

  1. Leave it reachable and add noindex (step 2 or 4), with robots.txt allowing crawling. A copy behind auth cannot show Google its noindex.
  2. In Search Console, verify the staging host and use Removals → New request → "Remove all URLs with this prefix". Removal hides the URLs for about six months while the noindex takes effect.
  3. Once the Pages report shows the URLs as excluded by noindex, put auth on. Not before.

Platform notes

Shared hosting with cPanel. There is usually no staging button. WP Staging into a subfolder is the pragmatic option: protect the folder with cPanel's Directory Privacy, which writes the Apache auth block, and adjust the step 4 check to the path, for example str_starts_with($_SERVER['REQUEST_URI'], '/staging-a1b2/').

WooCommerce. WooCommerce Subscriptions detects a changed site URL and enters a staging mode that stops renewal payments from the copy. It protects the copy, not the orders you might push back.

Not on WordPress? Preview deployments on Vercel, Netlify and similar hosts are public by URL. Vercel adds X-Robots-Tag: noindex to preview deployments; check any host with curl -sI https://preview-url/ | grep -i x-robots-tag. Build production from the production hostname so absolute URLs never carry the preview address.

Verify

  • On the live site's report: "No public staging copy found".
  • curl -sI https://staging.example.com/ returns 401 with auth on, and x-robots-tag: noindex, nofollow with auth off.
  • After a push: "The page can be indexed", the canonical shows the live domain, and curl -sI https://example.com/ | grep -i x-robots-tag prints nothing.

Common mistakes

  • Pushing the whole database because it is one click. Orders, comments, users and blog_public from staging replace production's.
  • Protecting staging with robots.txt only. The URLs show in results as bare links, and Google cannot see a noindex it may not fetch.
  • Auth without noindex. The day someone turns auth off for a preview, the copy is indexable. The hostname guard costs nothing.
  • Forgetting to put the password back. Make "restore auth" the last item of every test, and check for the 401.
  • A staging copy left online after launch. It ages, its plugins go unpatched, and it has the same users and passwords. Delete it when the work is done.

Questions people ask

Does a WordPress staging site affect SEO?

Only if Google can index it. An unprotected copy competes with the live site for its own content and can even be chosen as the canonical version. Behind a password, or with noindex on every response, it has no effect. The larger SEO risk is pushing staging's "Discourage search engines" setting to the live site.

Can I push only some changes from staging to live?

Yes, with most host tools and WP Staging Pro, which let you push files, the database or selected tables. Push files for theme and plugin changes, and repeat settings changes by hand on the live site. A selective database push is safe only for tables nothing on the live site writes to, which excludes orders, comments and users.

How do I make a WordPress staging site without a host tool?

Install WP Staging and create a clone, which lands in a subfolder of the same install, or package the site with Duplicator and deploy it to a subdomain. Then protect the copy with HTTP authentication and add the hostname guard from step 4. Developers can use Local, DDEV or Studio on their own machine instead.

Why does my staging site still appear in Google after I added a password?

Because Google can no longer fetch the pages, so it never sees they should be dropped. Remove the password for now, add noindex with crawling allowed, and request removal of the staging prefix in Search Console. When the Pages report lists the URLs as excluded by noindex, put the password back on.

Check your site before and after Check