> ## Content Index
> Fetch the complete content index at: https://blog.sudowl.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Moving a Blog to a New Domain Without Losing Google: 301s, Canonicals, Search Console
- URL: https://blog.sudowl.com/move-blog-new-domain-301-search-console/
- Published: 2026-10-24T16:00:00.000Z
- Updated: 2026-10-25T17:35:47.000Z
- Description: How I moved my Ghost blog and main site to a new domain: 301 per path, Ghost url, canonicals, sitemap, Search Console Change of Address, IndexNow and Bing.
- Author: Lukas
- Tags: Linux, nginx

This blog used to live at blog.diengdoh.com. Since 2026-10-19 it's blog.sudowl.com, and the main site moved from diengdoh.com to sudowl.com the same day. New brand, new domain, same posts.

A domain move is an easy way to throw away what Google has learned about your site. Do it per path, tell Google properly, and the signals move with you. Not instantly, but they move.

**TL;DR:** 301 every old URL to the same path on the new domain, never to the home page. Change the site URL in the app itself so canonicals, sitemap and RSS use the new domain. Keep the old domain, its DNS and its certificate running forever. Verify both properties in Search Console and start the Change of Address tool. Exclude the paths that must not move (ACME, verification files, webhooks). Keep staging out of the index. Ping IndexNow for Bing. Then wait, because nobody can tell you anything after a week.

## The redirect: same path, 301

The old blog domain is still a domain in my panel. Its HTTPS vhost doesn't proxy to Ghost anymore, it only has this:

```apache
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^ https://blog.sudowl.com%{REQUEST_URI} [R=301,L]

```

`%{REQUEST_URI}` keeps the path, so `/some-post/` on the old domain lands on `/some-post/` on the new one. The query string gets appended automatically by mod\_rewrite, you don't have to add it. And it's `R=301`, not the default 302\. A 302 says "temporarily somewhere else", which is not what you want Google to remember.

The ACME exclusion is there so Let's Encrypt can still renew the certificate for the old domain. That's important. Every old link out there is `https://`, and if the old cert expires, people get a browser warning before they ever see the redirect. So the old domain stays: DNS, cert, vhost. I will never delete it.

On a panel these lines go into the custom directives of the HTTPS vhost, see [Apache as a reverse proxy for Docker apps](/apache-reverse-proxy-docker-apps/).

## Gotcha: redirect to the home page is a soft 404

The lazy version is "everything on the old domain goes to the new home page". Google treats that as a soft 404 for every post. The old URL is gone, the new one is not its replacement, so the ranking of that post doesn't carry over. Per path or don't bother.

## Ghost: change the url, then recreate

Ghost runs in Docker for me, and the site URL is an environment variable in the compose file:

```yaml
services:
  ghost:
    image: ghost:6
    environment:
      url: https://blog.sudowl.com

```

Backup first (compose file and the content directory), then:

```bash
docker compose up -d

```

Not `docker restart ghost`. A restart keeps the old environment, only `up -d` recreates the container with the new value. Easy to miss and then you wonder why nothing changed.

Ghost uses that `url` for everything it generates: canonical tags, `og:url`, the sitemap, RSS, links between posts. Images in the database are stored with a `https://blog.sudowl.com` placeholder, so they follow on their own. What doesn't follow is anything you typed as an absolute link into a post. I grepped my markdown sources for the old domain before the move and there were zero hits, which was nice.

Check the canonical:

```bash
curl -s https://blog.sudowl.com/lower-dns-ttl-before-migration/ | grep -o '<link rel="canonical"[^>]*>'

```

It has to show the new domain. If it shows the old one, you're telling Google to index a URL that redirects.

Also update the stuff around the blog: my deploy scripts had the URL in them, and the stats script now reads the logs of both domains. Crossposts with a canonical to the old domain still work through the redirect, but I'll change those too.

## Verify every single URL

Don't test three posts by hand. Take the new sitemap, turn each URL back into the old one and check the redirect:

```bash
curl -s https://blog.sudowl.com/sitemap-posts.xml \
| grep -oE '<loc>[^<]+' | sed 's/<loc>//' \
| while read -r new; do
    old="${new/blog.sudowl.com/blog.diengdoh.com}"
    res=$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$old")
    [ "$res" = "301 $new" ] || echo "BAD $old -> $res"
  done

```

No output means every post redirects with a 301 to exactly its new URL. Do the same for the pages and tag sitemaps if you care about those.

## The main site: exceptions that must not move

The main site was more work, because not everything on the old domain belonged to the new one. Some paths had to keep working where they are:

- `.well-known/` for certificate renewals
- an old shop that lives in a subfolder of the old docroot and isn't part of the new brand
- an API path that a payment webhook and already-sent emails point at
- the Search Console verification file (more on that below)

So the old vhost got conditions before the redirect:

```apache
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/
RewriteCond %{REQUEST_URI} !^/old-shop/
RewriteCond %{REQUEST_URI} !^/tools/api/
RewriteCond %{REQUEST_URI} !^/google[0-9a-f]+\.html$
RewriteRule ^ https://sudowl.com%{REQUEST_URI} [R=301,L]

```

The paths are examples. The point: before you flip the switch, list every path on the old domain that something external calls. A webhook that suddenly gets a 301 usually doesn't follow it, it just fails.

The `www` variants redirect to the bare domain, and the German domain goes to `/de` on the new main site. Before the main site was ready, the new apex domain pointed to the blog with a 302, on purpose. That one really was temporary.

And the old domain's DNS has to keep running. It's still with the old DNS provider, and that's fine, but if that zone ever disappears, all the redirects disappear with it. If you move DNS at the same time, read [lower your DNS TTL before you migrate](/lower-dns-ttl-before-migration/) first.

## Search Console: Change of Address

This is the step that tells Google "this is a move, not a new site". The requirements:

- you're a verified owner of **both** properties, old and new
- the old site 301s to the new one, at least for the home page and the URLs Google samples
- both properties are site level, not a subfolder

I verified with the HTML file method. The file lives in one folder on the server and an Apache alias serves it on the domains that need it. For the main site that's the new domain and the old one, which is why it's in the exception list above. If the old domain redirects that file away, Google can't verify the old property and you can't start the move.

Funny part: the old main domain never had a Search Console property. I had to create it after the fact, just to be able to say "it moved". So if you're planning a move, set up the old property now, while it still serves its own content.

Then in the old property: Settings, Change of Address, pick the new property, validate, confirm. I started both moves on 2026-10-19\. Google says the move is processed over about 180 days and the redirects should stay at least that long. I'm keeping them forever, they cost nothing.

After that, submit the new sitemap in the new property. Googlebot fetched the new blog sitemap with a 200 on the same day.

## Gotcha: check the property name

I created the new property with a typo in the hostname the first time. Search Console doesn't check whether the hostname is real, it just creates the property, and of course verification can never work on it. Read the property name letter by letter before you start debugging your verification file.

## Staging: noindex, three ways

The new main site was built on a preview subdomain first. That one must never end up in Google next to the real site. It has:

- a `noindex` meta tag, controlled by an env variable in the app
- `X-Robots-Tag: noindex` as an Apache header
- `Disallow: /` in robots.txt

```apache
Header set X-Robots-Tag "noindex, nofollow"

```

## Gotcha: robots.txt Disallow hides your noindex

These two fight each other. If robots.txt blocks crawling, Google never fetches the page, so it never sees the `noindex`. It can still list the bare URL if something links to it. Nothing links to my staging, so I live with it, but the clean answer for staging is HTTP basic auth.

Second trap with that app: the noindex env variable had to be set as a build argument **and** at runtime, because the pages render dynamically. Set it in only one place and you can end up with indexable pages while the config looks right. Check the real HTML with curl, not the config.

## IndexNow and Bing

Google doesn't use IndexNow, but Bing and a few others do. You put a key file at the root of the site, then POST your URLs:

```bash
curl -s -o /dev/null -w '%{http_code}\n' \
  -H 'Content-Type: application/json' \
  -d '{"host":"blog.sudowl.com","key":"YOUR_KEY","urlList":["https://blog.sudowl.com/some-post/"]}' \
  https://api.indexnow.org/indexnow

```

The key file sits in the same alias folder as the Google file. My deploy script pings IndexNow for every new post. The first full run sent 134 URLs and got `202` back, which means accepted, key check still pending.

In Bing Webmaster Tools I added the new site and ran the Site Scan. It found two posts with an extra H1 and four meta titles that were too long. Fixed those. Bing also has its own site move setting, worth a look if Bing traffic matters to you.

## What I can say about traffic

Nothing yet. The move happened on 2026-10-19\. Rankings during a move go up and down for weeks, and anything I'd tell you now would be a guess. I'll look at the numbers again in a few weeks.

## Checklist

1. List every path on the old domain that something external calls.
2. Old Search Console property exists and is verified, before the move.
3. App URL changed (`docker compose up -d`, not restart), canonicals checked with curl.
4. 301 per path, ACME and exceptions excluded, every sitemap URL tested.
5. Old domain, DNS and cert stay. Forever.
6. New property verified, Change of Address started, sitemap submitted.
7. Staging noindex, verified in the real HTML.
8. IndexNow ping, Bing site added.

My blog had already been rebuilt once after [losing the database](/ghost-blog-rebuild-after-database-loss/), so I had every slug in markdown. If you don't have a list of your URLs, grab the sitemap before you move anything.

---

### Related posts

- [Bots vs. Real Readers: Counting Blog Visitors From the Access Log](https://blog.sudowl.com/bots-vs-real-readers-access-log/)
- [Let's Encrypt Kept Failing After a Server Move. The Culprit Was a Second A Record.](https://blog.sudowl.com/lets-encrypt-fails-duplicate-a-record/)
- [Caddy vs Nginx — When 5 Lines Beats certbot + Cron](https://blog.sudowl.com/caddy-vs-nginx-auto-https/)