Background
I run more than twenty sites on subdomains of a single domain — ocha., coffee., manga., cve.,
one per topic. They all publish daily, and a nightly Node.js job pulls search numbers for all of them.
The first decision you have to make in that setup is how to structure your Search Console properties.
Search Console (SC) only returns data per property, so the way you slice properties decides
what you are able to measure at all.
I got that slicing wrong once and misread my own dashboard for three months.
How it works
SC gives you two kinds of property:
-
Domain property — written
sc-domain:example.net. Verified with a DNS TXT record. Covers the domain and every subdomain under it, http and https alike. -
URL-prefix property — written
https://sub.example.net/. Covers only URLs under that prefix.
The tempting answer is "the domain property covers everything, so just use that." It does cover
everything, but it reports one aggregate number. To get per-site figures you have to write a
dimension filter on every single query. Go the other way and use only prefix properties, and you
lose the cross-domain view entirely.
So: own both. Prefix properties drive per-site operations; the domain property owns the
cross-site view and the sitemap registrations.
Implementation
Once you own both, the interesting part is how permissions fail.
The Sitemaps API lives under webmasters/v3 and takes the property as a URL-encoded path segment:
async function fetchSitemapList({ property, token, fetchImpl }) {
const url = `https://www.googleapis.com/webmasters/v3/sites/${encodeURIComponent(property)}/sitemaps`;
const r = await fetchImpl(url, {
headers: { Authorization: "Bearer " + token },
});
if (r.status === 403) return { error: "no-permission", entries: [] };
if (!r.ok) return { error: `http-${r.status}`, entries: [] };
const json = await r.json();
return { error: null, entries: json.sitemap || [] };
}
Two things matter here.
encodeURIComponent is not optional: property names contain colons and slashes, and without
encoding the path is parsed as a route and you get a 404.
The 403 branch is the real point. Returning only entries: [] makes "you lack permission"
indistinguishable from "no sitemap is registered." That is exactly the bug I shipped. A prefix
property my service account had never been added to returned siteUnverifiedUser 403, and my
report printed "sitemap not submitted" — while the domain property had it registered and Google
was crawling it perfectly happily. No error, no red line, just a confident wrong answer for three months.
The fix was to stop using three states and use four:
| status | meaning | ok | exit |
|---|---|---|---|
unknown |
could not measure (API 403, live fetch failed) | false | 1 |
missing |
genuinely not submitted | false | 1 |
errors / stale / warn
|
measured, and there is an action to take | false | 0 |
ok only |
nothing wrong | true | 0 |
Never collapse unknown into missing.
Gotchas
submittedUrlCount is not your current URL count. It is the count as of the last time Google
downloaded that sitemap. When re-crawling stalls, the two drift apart, and the API keeps returning 200.
So I now fetch the live sitemap.xml, count <loc> elements, and diff it against SC. Here is the
real run from 2026-09-03 across 23 properties (14 ok / 8 warn / 1 missing):
ocha live 381 / SC 318 (-63, 17%, last read 10 days ago)
coffee live 443 / SC 389 (-54, 12%, last read 9 days ago)
manga-deals live 192 / SC 153 (-39, 20%, last read 7 days ago)
amazon_site live 392 / SC 370 (-22, 6%, last read 4 days ago)
Before the diff existed, all four reported ok.
One threshold turns the check into noise. My first version warned on any difference, which
meant every site warned every day — sites that publish daily always drift by a few URLs. The rule
now needs either a gap of 20+ URLs, or a gap of 10%+ and a last-read older than 7 days.
A check that is always red is not a check.
Don't look at sitemap.xml alone. A sibling sitemap-news.xml on the same origin can be
broken without the site's row ever changing. In my data, anime.autoarticles.net/sitemap-news.xml
sat with one error while the site row stayed green. Enumerate siblings and report them separately —
but do not pull in sitemaps from other origins, or you inherit failures you cannot fix.
Expect flaky live fetches. Fetching the served sitemap fails intermittently (IPv6 in my case).
Retry up to three times before you are allowed to call it unknown, otherwise something is
un-measured every single night.
The result
A site that runs on this: https://cve.autoarticles.net
It keeps its sitemaps on the domain property and monitors sitemap.xml and sitemap-news.xml
as two independent rows.
Wrap-up
If you run a fleet on subdomains, own both property types. With only one of them, either the
per-site numbers or the cross-site numbers are structurally unavailable to you.
And give every measurement function a third state for "could not measure." The moment you flatten
a 403 into an empty array, your report stops telling you that nothing is wrong and starts telling
you that you cannot see whether anything is wrong. Those look identical on a dashboard, and I
stared at the second one for three months.
This article is about my own side project. It was written with AI assistance.
Top comments (0)