This audit started with my own question after an incident on a client's site: was any other domain mirroring this site, and was its SEO as good as it could be? On that client's site, Nginx had accepted a Host it did not recognize and served the full site anyway. The audit notes dated 6 September 2026 answered the mirror half with two pieces of news at once. There was no third-party mirror, but there were two public copies, and both of them were mine.
The more serious one was the production vercel.app alias of the Next project that served this site at the time. It answered 200 for all 508 URLs, sitemap and robots.txt included, with meta robots set to index,follow and no X-Robots-Tag header. The project was already using Deployment Protection, and that protection never touched the alias at all.
The client's incident cannot happen on Vercel
The first worry was a foreign domain whose A record points at a Vercel edge IP and ends up serving this site. That cannot happen on Vercel, and this was tested rather than assumed:
# summary of the test results, not raw output
foreign domain, A record to a Vercel edge IP TLS cut before any certificate is offered
foreign Host over plain HTTP 404 DEPLOYMENT_NOT_FOUND
SNI that does not match the Host 403, x-vercel-mitigated: denyThe reason is structural. Vercel routes strictly by the hostnames registered to a project, so an unknown Host being served the full site, as happened on the client's site, has no path here.
The search for third-party copies came back empty too. I searched eight verbatim sentences, eight short fragments, and a dozen or so article titles in quotes on Yahoo, which uses the Bing index, and on one other web search tool, and got zero results on any host other than my own. The Wayback Machine only holds five captures of robots.txt from 2018, and Common Crawl has none at all. Twenty-one look-alike domains all returned NXDOMAIN, and the only live one was a parked domain owned by someone else. A vercel.app subdomain with a similar name also turned out to belong to someone else, since its bundle references a different GitHub account and never mentions my name.
Two copies that turned out to be mine
What actually existed were two copies of my own. The measurements, summarized with host names masked:
# measurement summary, host names masked
production alias, Next project 200, meta robots index,follow, no X-Robots-Tag,
all 508 URLs + sitemap + robots.txt, canonical to apex
alias of the old CRA project 200, no meta robots, no canonical, robots.txt allow-all
team-name alias, 302 to Vercel SSO, x-robots-tag: noindex
main-branch alias,
and deployment URLsThe Next project's alias was a complete copy of this site. Its canonical did point to the apex, but a canonical is a hint that search engines may ignore, not an indexing ban like noindex. Even a correct canonical does not make a copy like this safe on its own.
The CRA project's alias is a different story. That project was built with Create React App in 2025 and holds the first version of my portfolio, so its content is not a copy of the current site. It was still public, though, under my old portfolio's title, with no meta robots, no canonical, and a robots.txt that allows everything.
Standard Protection skips production domains
The Next project was already on Standard Protection, with ssoProtection.deploymentType set to all_except_custom_domains. Read literally, the value means everything except custom domains, and a vercel.app alias is not a custom domain I added myself. That is the trap. Standard Protection's scope never covers production domains, and Vercel groups the production vercel.app alias as a production domain, listed right alongside this site's custom domain. So the production alias sits outside the protection together with the custom domain, while the team-name alias, the branch alias and the deployment URLs stay behind SSO, as the table shows.
The audit notes flag this part as an important correction: the advice to "turn on Deployment Protection" is wrong, and it changes nothing for this alias. According to the Vercel plan requirement recorded in the same audit, raising the scope to All Deployments needs the Pro plan. The notes do not say where that requirement came from, and no attempt to raise it was recorded.
The alias is listed as an ordinary domain
The way out turned out to be somewhere more ordinary. In the project API, the alias was listed as a domain with an empty redirect field, the same field the apex used to redirect to www. That meant its row could be edited under Settings > Domains and redirected 308 to www. Whether the edit dialog offers a status code choice had not been confirmed when that recommendation was written, so a PATCH to the project's domain endpoint in the Vercel API was noted as the fallback.
This redirect went on the list of actions that should not wait for the Astro rebuild, because it does not depend on the framework. On 8 September 2026 I ran it myself from my terminal. Afterwards the alias answered 308 to www at both the root and /blog, where it used to serve 508 URLs, while the root and /blog on www stayed at 200, undisturbed.
The replacement project gets its own public alias
The same problem nearly repeated itself in the replacement project. The plan for the Astro rebuild called for a new Vercel project with its own preview URL, and www would only move over from the old project once the Astro site was verified, so there would be no downtime and rolling back would just mean moving the domain back. What the plan had not accounted for at all: a new Vercel project automatically gets its own public production alias at *.vercel.app.
Left alone, the projection looked like this. With the allow-all robots.txt that the M1 checklist asked for, M1 being the milestone that builds the site shell, the alias would serve a complete copy of 476 articles with index, follow, and during the transition two sites would be live at once with the same content until the domain moved. The decision was for the Astro project to send X-Robots-Tag: noindex on every host other than www until the transition finished.
The guard is a single block in vercel.json. I have replaced the canonical domain below with www.example.com, and the rest is as written:
{
"source": "/(.*)",
"missing": [
{ "type": "host", "value": "www.example.com" }
],
"headers": [
{ "key": "X-Robots-Tag", "value": "noindex, nofollow" }
]
}The missing condition flips the usual logic: the header is added only when the request does not arrive on the www host. As long as www is still attached to the old project, every request that reaches the Astro project has to arrive on some other host, so all of them get noindex. Once the domain moves, requests to www no longer meet the condition, and the guard stops applying to www by itself. There is nothing to remember to remove.
The block has been in vercel.json since the first commit of the Astro repo on 8 September 2026, and the project's alias carried X-Robots-Tag: noindex, nofollow from the very first deploy, back when it held a single placeholder page and no real content. In M1 the site shell was verified against a real deployment on that alias, and noindex showed up on pages and assets alike.
Two general properties make this header a good fit for the job. X-Robots-Tag lives on the HTTP response, so it also applies to files that cannot carry a meta robots tag, such as images. Noindex is also only read when the crawler is allowed to fetch the page, so an allow-all robots.txt does not conflict with this guard. If the alias were blocked in robots.txt, crawlers would never see its noindex header.
One thing the guard does not do is make the alias private. Noindex only governs search engines, and the Astro project's alias stays public. That is why PUBLIC_GA_ID was deliberately left empty until the domain moved, so the GA4 property's first data would not be polluted by visits to the preview alias. For API keys I went the other way, knowing the consequences. The email service key and the AI model API key both stayed on the Astro project, so the CV delivery flow ran fully on the public preview alias, the alias could use the AI model quota, and both daily caps are shared with production.
The redirect that locked www
The www domain moved to the Astro project on 8 September 2026. The plan was to move only www. The apex was deliberately left in the old project, because the apex does not serve the site but sends a 308 to www, and moving it risked the apex starting to serve a copy.
That plan did not survive. Vercel refuses to release www from the old project while any redirect still points at it, and one of them was the apex's 308, set in that project's dashboard. So both domains had to move together, and the apex redirect needed a new home first. Its new home is the Astro project's vercel.json, again with the domain replaced by example.com:
"redirects": [
{
"source": "/(.*)",
"has": [{ "type": "host", "value": "example.com" }],
"destination": "https://www.example.com/$1",
"permanent": true
}
]permanent: true produces a 308, and the has host condition means this redirect never touches requests that already arrive on www. The apex was then moved between projects and kept answering 308 without a single DNS record being touched at the registrar.
The second redirect pointing at www was the audit fix itself, installed that same day: the old project's vercel.app alias with its 308 to www. Removing the alias with vercel alias rm made it 404, but it did not delete the domain entry along with its redirect setting, and neither vercel domains ls nor project inspect shows that field at all. The deadlock only came apart through the Vercel REST API, using the CLI's own token, which on macOS lives in ~/Library/Application Support/com.vercel.cli/auth.json.
# simplified, project name masked
GET /v9/projects/<project>/domains
every domain carries redirect and redirectStatusCode,
two fields that never show up in the CLI
PATCH the alias domain's redirect field to null
the lock opensWhat I removed was the redirect, not the domain, because a redirect can be put back.
After www moved, the Next project's alias could no longer redirect to www. That redirect had only been possible because www was still attached to the same project, since Vercel only allows a domain to redirect to a domain in the same project. The same rule had blocked the old CRA project's alias from the start, even though the earlier decision was to point that project at www with a 308 rather than delete it.
That left two options: keep it at 200, or remove the alias. I removed the alias, because the project and all of its deployments stay intact, and bringing it back only takes adding the alias again. By 8 September 2026 both old aliases returned 404, and no public copy remained. Verification on the live site after the domain move passed 14 out of 14, the apex kept its 308 to www, and the noindex guard switched itself off as designed.
That decision has a cost. Once the domain moved, the Next site could no longer serve as a comparison baseline, because its deployment URLs are behind Vercel SSO and its public alias has been removed.
What is still unanswered
One question was never answered in my notes: whether Google indexed either of the two old aliases. Yahoo with the Bing index and the other web search tool both returned zero results, but neither of them is Google's index. All my notes record for this is a planned check, a site: query for each alias to run in my own browser, and no result was ever written down.
For the next Vercel project, the order looks roughly like this. Measure every host the project answers on, not just the custom domain, because a production alias can serve the whole site with index,follow. Do not count on Standard Protection to close that alias, because Vercel counts it as a production domain. Put the noindex guard with a missing host condition in place before the new project's first deploy, so it switches itself off once the canonical domain moves. Before moving a domain between projects, check the redirect field on every project domain through the REST API, because the CLI does not show it.