SEO for a One-Person Constellation of Sites — Same-Origin Sitemaps, Domain Properties, and a 308 Trap
A main site plus six product subdomains, and the sitemap only covers the main site. Why one sitemap can't cover the constellation, whether to use a Domain property or verify each origin, and a Cloudflare Pages .html trap that turns every sitemap entry into a redirect.
In the post about building this site I wrote one line: “RSS and the sitemap are generated automatically by the official integrations — no manual maintenance.” Today, adding SEO across the whole constellation, reality corrected that line — it’s only true for the main site.
My site isn’t one site, it’s a string of them: the main mahui.me (Astro / Cloudflare Pages), plus six product subdomains — pier.app.mahui.me, diskly.app.mahui.me, mtinker.app.mahui.me, typing.app.mahui.me, shellby.app.mahui.me, and www.yuwei.mahui.me, which is self-hosted on Docker + nginx. Each subdomain is a separate deployment in a separate repo. The main site’s sitemap auto-generated 89 pages, but not a single subdomain page is in it.
This post covers three things: why one sitemap can’t cover the constellation, how to organize it in Search Console, and one concrete trap I hit today.
Why one sitemap can’t cover the subdomains
This isn’t a misconfiguration — it’s a hard line in the sitemap protocol itself: a sitemap may only list URLs on the same origin (same host) as itself. Stuff pier.app.mahui.me URLs into mahui.me/sitemap-index.xml and Google simply ignores those cross-origin entries — they don’t belong there, and adding them does nothing.
The protocol leaves one “cross-site submission” exception: if two domains sit under the same Search Console property, or cross-declare via robots.txt, A’s sitemap can reference B. But that’s a heavy mechanism designed for large multi-site setups — overkill for a one-person constellation. The right answer is much simpler: one sitemap per origin, each declared in that origin’s robots.txt.
https://mahui.me/sitemap-index.xml ← main site, 89 pages, auto-generated
https://pier.app.mahui.me/sitemap.xml ← each product site has its own
https://diskly.app.mahui.me/sitemap.xml
...
The main one is generated at build time by @astrojs/sitemap; the product sites are mostly hand-written static pages, so the sitemap is a few hand-written lines — for a few-page marketing site, the maintenance cost rounds to zero.
Search Console: verify once, cover everything
The real “together or separate” question isn’t about sitemaps — it’s about how you organize Search Console. There are two property types, and picking wrong plants a chore for your future self:
- URL-prefix property: matches on exact “protocol + host”.
https://pier.app.mahui.me/is one,https://diskly.app.mahui.me/is another. Each one has to be verified separately. Seven origins in the constellation means seven verifications; every new product site is one more. - Domain property: matches on the bare domain and automatically covers all subdomains and all protocols under it. Verification is a single DNS TXT record.
For a constellation the answer is clear: create one mahui.me Domain property, verify once with a DNS TXT record, and mahui.me plus all its current and future *.mahui.me subdomains are covered. My DNS is already on Cloudflare, so adding one TXT record is a one-minute job.
So the precise answer to “together or separate” is two-layered: verify together (one Domain property covers all), submit sitemaps separately (the protocol requires same-origin), but every sitemap is submitted and managed under that same one Domain property, so there’s still a single console.
Cloudflare prepends things to your robots.txt
After deploying, I curled the live robots.txt and found a big block at the top that I never wrote:
# BEGIN Cloudflare Managed content
User-agent: *
Content-Signal: search=yes,ai-train=no,use=reference
Allow: /
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Google-Extended
Disallow: /
...
This is Cloudflare’s “block AI crawlers” feature — I’d enabled it in the dashboard, and it automatically prepends this managed block to every site’s robots.txt. The key is to separate two kinds of crawlers: GPTBot / ClaudeBot / CCBot / Google-Extended are training crawlers; blocking them does not affect Googlebot / Bingbot and the other search-indexing crawlers. Search crawling proceeds as normal, and my own User-agent: * Allow: / and Sitemap: declaration still apply. It looks alarming but doesn’t hurt indexing.
Incidentally, that Content-Signal: search=yes,ai-train=no is a newer declarative stance: allow search indexing, disallow training. It isn’t an enforcement mechanism, but it puts the reservation-of-rights intent somewhere machine-readable.
The 308 trap
Adding a sitemap to the Pier site, I found the repo already had a sitemap.xml (leftover from some earlier undeployed pass). The content looked fine, but every URL carried a .html suffix:
<loc>https://pier.app.mahui.me/mac-port-monitor.html</loc>
<loc>https://pier.app.mahui.me/changelog.html</loc>
The problem is that Cloudflare Pages serves clean URLs by default: hitting /changelog.html returns a 308 permanent redirect to the extensionless /changelog. Which means every single URL listed in the sitemap is an address that immediately bounces elsewhere.
That’s a real SEO loss. When Search Console crawls /changelog.html from the sitemap and finds it 308s away, it flags the URL as “Page with redirect” and refuses to index that URL — it wants the destination. The whole sitemap becomes a list of “please don’t index me” addresses, wasting crawl budget and muddying canonical attribution.
The fix is to write the canonical form (no .html) directly into the sitemap, aligning it with <link rel="canonical"> and with the address the user ultimately sees in the bar:
# a quick sed to strip .html off all 9 URLs
sed -i '' 's/\.html<\/loc>/<\/loc>/' website/sitemap.xml
The lesson is small but typical: a sitemap should list “the final URL you want indexed”, not “a URL that reaches the content”. On a host with redirect rules, those two are frequently not the same. Before listing anything, curl -I it and check for a 3xx.
Takeaways
- “Auto-generated, no maintenance” has a boundary. A framework’s automation only covers its own origin; every origin in a constellation has to be handled individually — the automation won’t cross a deployment boundary;
- The sitemap’s same-origin line dictates the architecture: split files by origin, consolidate management via a Search Console Domain property. Don’t try to cover everything with one file, and don’t reach for cross-site submission to do it;
- A Domain property is the default for a constellation. One DNS TXT verification covers every present and future subdomain; standing up individual URL-prefix properties is hoarding repeat work for yourself;
- A host’s redirect rules will pollute your sitemap. Pages clean URLs, trailing-slash normalization, www redirects — any of them can turn your listed addresses into “redirect sources”. Every class of URL you list is worth checking once for a 200.
The main site’s sitemap updated with today’s build, and the six product sites are each patched and deployed. All that’s left is one DNS verification — the one thing only I can do.
Comments