# 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.

Updated 2026-09-25 · WordPress & WooCommerce · HTML version: https://getreport.app/guides/staging-with-wordpress-plugins-and-host-tools

A WordPress staging site solves one problem and creates two. It lets you test the theme update or the redesign without touching the live site. In return, it is a full copy of your site, often reachable by anyone who guesses `staging.` in front of your domain, and it carries a database that will one day be pushed back over production with everything you changed on it, including the checkbox that hides it from Google. This guide ranks the staging options for WordPress, lists the traps in order of how expensive they are, and gives you a wp-config snippet that makes a staging copy safe on the strength of its hostname alone. The general rules for any staging site are in [staging sites: testing changes before they go live](https://getreport.app/guides/staging-sites-testing-changes-before-they-go-live); this is the WordPress layer.

## 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 from staging to live blindly: "Discourage search engines" (`blog_public`), orders, comments and users created since the copy all travel with it.
- Before push: run the [WordPress checker](https://getreport.app/tools/wordpress-checker) on staging and expect noindex. After push: run it on production and expect "The page can be indexed".
- Key staging behaviour to the hostname in `wp-config.php`: noindex, no outgoing email, no cron, no WooCommerce webhooks.

## Why the staging trap is a WordPress trap

Other platforms keep "is this site public to search engines" in configuration. WordPress keeps it in the database, as `blog_public` in `wp_options`, edited through Settings → Reading → "Discourage search engines from indexing this site". When you tick it on staging, the staging database gains `blog_public = 0`. When you push that database to production, production gains it too, and every page on the live site starts sending `<meta name="robots" content="noindex, nofollow">`. Nothing looks different in the browser. Traffic from Google fades over the following days, and it takes a report or a Search Console alert to notice.

The opposite failure is quieter. A copy at `staging.example.com` that nobody protected is crawled, indexed and shown in results next to the real site. It competes with your own pages for your own content, it usually runs older code, and it shows anyone who looks the redesign 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

> **Free tool:** [WordPress health check](https://getreport.app/tools/wordpress-checker): Is your WordPress site indexed, up to date and free of the classic launch mistakes? Checks "Discourage search engines", default content and tagline, plain permalinks, attachment pages, the exposed admin user, a public staging copy and the core version, plus security basics.

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 (the `www.` is stripped first, so for `www.example.com` it tries `staging.example.com`). 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 page's robots meta tag nor an `X-Robots-Tag` header contains `noindex`. A 401 from basic auth, a 403 from an IP rule, a 404 or a DNS failure all count as "not found", which is what you want. If your live site itself lives on a hostname starting with `staging.`, `dev.` or `test.`, the probe is skipped.

> **Check: No public staging copy found.** A staging or dev copy that Google can index competes with the live site for its own content and often runs older, unpatched code. Anyone can find it.
>
> 1. Put the staging site behind HTTP authentication or an IP allow-list, or delete it when the launch is done.
> 2. At minimum switch on "Discourage search engines" there and add a noindex X-Robots-Tag header.

Two things follow from how the probe works. It only looks at those three prefixes, so a copy at `stage.example.com`, `new.example.com` or `example.com/staging-a1b2/` is not found by it; run the checker on that address directly instead. And noindex alone makes the finding pass, because the check is about what Google can index, not about who can look. Auth is still required; see step 2. The learn page on [staging sites](https://getreport.app/learn/staging-sites) has the short version of the finding.

On the staging copy itself, run the checker and read the same findings in reverse: you want them to fail.

> **Check: Search engines are not discouraged in Settings → Reading.** The WordPress setting adds a site-wide noindex, nofollow tag. It is meant for sites under construction and is often left on after launch, which removes the whole site from Google.
>
> 1. In wp-admin go to Settings → Reading and untick "Discourage search engines from indexing this site".
> 2. Then request indexing in Google Search Console; pages usually return within days to weeks.

> **Check: The page can be indexed.** To appear in search, a page must return HTTP 200, carry no noindex directive and be allowed in robots.txt. If any of the three fails, the page is invisible to searchers.
>
> 1. Open the technical detail to see which condition fails, then fix that one; the individual findings above have the steps.
> 2. After the fix, request indexing in Google Search Console to speed things up.

![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](https://getreport.app/guides/img/staging-with-wordpress-plugins-and-host-tools/discourage.webp "On staging this finding is the goal; on production after a push it is the first thing to check.")

The discourage check looks for a robots meta tag that contains both `noindex` and `nofollow`, which is exactly what the setting emits. On staging that is correct. If you push the database to production and this finding appears on the live site's report, you have found the trap; [WordPress "Discourage search engines"](https://getreport.app/guides/wordpress-discourage-search-engines) covers the recovery on a live site.

## 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 staging copy with one click, on a subdomain of your domain or on a host-owned domain, and offer a push back to live. The good ones protect the copy with basic auth or a login wall and set noindex; check which of the two yours does, because it is often only one. The push is where to read the fine print: some hosts push files and database together, some let you choose, and a database push overwrites whatever production gained since the copy was made. Prefer "push files only" plus manual settings changes on live when the change is a theme or a plugin.

**Plugins.** WP Staging clones the site into a subfolder of the same install (with its own table prefix in the same database), so it uses the live server's disk and PHP and is public at that folder unless you protect it. Duplicator builds a package (archive plus installer) you deploy to any host, which is a migration tool doing staging duty. Both give you a copy fast; neither protects it for you.

**Local (for developers).** Local, DDEV and WordPress.com's Studio run a full copy on your machine. Nothing is public and nothing can be indexed, which makes them the safest option and the only one where the report cannot help, because it cannot reach your laptop.

| Option | Where the copy lives | Public by default | Push to live | Report can check it |
| --- | --- | --- | --- | --- |
| Host staging | Host subdomain | Usually protected, verify | Built in, read the DB caveat | Yes |
| WP Staging | Subfolder of live site | Yes | Pro version | Yes, by full URL |
| Duplicator | Any server you choose | Yes | Manual | Yes |
| Local / DDEV / Studio | Your computer | No | Manual | No |

### 2. Protect the public copy twice

Basic auth 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;
    # ... 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
```

Create the password file with `htpasswd -c /etc/nginx/staging.htpasswd editor`. With auth on, the probe gets a 401 and the finding passes.

Then noindex, for the day auth is switched off "just for the client to look" and forgotten. Tick Settings → Reading → "Discourage search engines" on staging, or better, let the snippet in step 4 do it by hostname so it can never be pushed to production by accident. Do not block staging in `robots.txt`: a page Google cannot fetch is a page whose noindex Google cannot read, and blocked-but-indexed URLs linger.

### 3. Know the eight things that go wrong on push

1. **The domain inside the database.** Pushing a database copied from `staging.example.com` puts that hostname in `siteurl`, `home`, image URLs and builder data. Every host tool and plugin does a search-replace on push; confirm it handles serialized data, as described in the [WordPress migration checklist](https://getreport.app/guides/wordpress-site-migration-checklist).
2. **"Discourage search engines" travels.** The checkbox is a database row. Untick it before the push, or exclude `wp_options` from the push, or key it to the hostname (step 4).
3. **WooCommerce orders placed on staging.** A test order on staging is a real row in the staging database. Push the database and it appears in production's order list; worse, pushing overwrites the real orders placed on live since the copy. Never push the database of a shop; push files, then repeat the settings change by hand.
4. **Scheduled posts and cron.** WP-Cron runs on staging whenever someone loads a page there, so a post scheduled on the copy publishes on the copy, and plugins fire their scheduled jobs twice, once per site.
5. **Email from staging.** Order confirmations, form notifications and password resets sent from the copy reach real people with staging links in them.
6. **Licence keys.** Premium plugins and themes bind their key to a domain; many vendors allow a `staging.` subdomain, some do not, and a copy that cannot update is a copy that tests the wrong version.
7. **Uploads out of sync.** Images added to live after the copy do not exist on staging, so a page built on staging references files that do exist and misses the new ones, and vice versa after a full push.
8. **Caching plugins on staging.** A page cache copied with the site serves live's cached HTML, live URLs included, until purged. Purge on the copy after cloning and disable the cache while testing.

### 4. Make staging safe by hostname

One block in `wp-config.php`, above the line that says "That's all, stop editing", turns any copy at `staging.example.com` into a safe one 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, which loads before ordinary plugins and cannot be deactivated by accident, at `wp-content/mu-plugins/staging-guard.php`:

```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, images 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. The `wp_robots` filter with `wp_robots_no_robots` is the documented way to emit noindex, nofollow, and the report reads it the same way it reads the "Discourage" setting. `pre_wp_mail` returning anything but `null` stops `wp_mail()` before it sends.

### 5. Pre-push and post-push routine with the report

Before pushing, on the staging URL (the checker cannot log in, so switch auth off for the ten minutes of the check and switch it back on; with the hostname guard in place the copy stays noindex meanwhile):

- The WordPress checker shows "Discourage search engines is switched on" or "The page is set to noindex". Good: it is still hidden.
- The rest of the report is your test result: the SEO, speed and accessibility findings of the new theme or plugin, before it reaches anyone.

After pushing, on the live URL:

- "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 in the technical detail carries the live domain, not `staging.`.
- The [SEO audit](https://getreport.app/tools/seo-audit) on two deep pages, because "Discourage" is site-wide but a per-page noindex set while testing is not.

### 6. If staging got indexed

1. Leave it reachable for now and add noindex (step 4 or the checkbox). A copy behind auth cannot show Google its noindex.
2. In Search Console, add `staging.example.com` as a property, verify it and use Removals → New request for the staging URLs that show up in results; that hides them within about a day while Google recrawls.
3. Once the Pages report for the staging property shows the URLs as excluded by noindex, put auth on. Not before.
4. Add a canonical from staging to live only if you cannot do the above; noindex is the right tool for a copy that should not exist in search at all.

## Platform notes

### Managed hosts

Use the host's tool. Check two things in its documentation before the first push: whether the staging domain is behind auth or only noindex, and whether the push copies the database (then the eight traps apply) or only files. Most hosts let you choose per push.

### 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 block from step 2) and install the guard from step 4 with the subfolder's hostname check adjusted to the path, for example `str_starts_with($_SERVER['REQUEST_URI'], '/staging-a1b2/')`.

### Sites with WooCommerce

Never push the database. Test on staging, write down every setting you changed, push files, and repeat the settings on live. WooCommerce Subscriptions detects a changed site URL and enters a staging mode that stops renewals from the copy; it does this on the copy, not for the orders you might push back.

## Verify

- On the live site's report, the finding reads "No public staging copy found". Your copy is either behind auth (401) or answers with noindex, or does not exist.
- On the staging URL, the WordPress checker's first finding is the discourage or noindex finding, as a fail.
- `curl -sI https://staging.example.com/` returns `401` with auth on, and with auth off returns `x-robots-tag: noindex, nofollow`.
- After a push, the live report reads "The page can be indexed" and the canonical shows the live domain.

## Common mistakes

- **Pushing the whole database because it is one click.** Orders, comments, users and `blog_public` from staging replace production's. Push files; redo settings by hand.
- **Protecting staging with robots.txt only.** Google cannot read a noindex on a page it may not fetch, so the URLs stay indexed as "blocked". Use noindex, then auth.
- **Auth without noindex.** The day someone turns auth off for a client preview, the copy is indexable. The hostname guard costs nothing and covers that day.
- **Testing email on staging.** A form notification from the copy reaches a real customer with a staging link. Short-circuit `wp_mail` on the copy.
- **A staging copy left online after launch.** It ages, its plugins go unpatched, and it is the same site with the same passwords. Delete it when the work is done; the probe keeps checking those three hostnames on every report.
