koala73/worldmonitor · error · Error
expected one location per sitemap entry: ${location}
Error message
expected one location per sitemap entry: ${location} What it means
getPublishedBatches parses a published sitemap's XML by regex, extracting exactly one <loc> element per <url> or <sitemap> entry. This error is thrown when an entry contains zero or multiple <loc> elements, meaning the sitemap served at `location` is malformed relative to the sitemap protocol and cannot be reliably submitted to IndexNow.
Solutions
- Fetch `location` with curl and inspect the raw XML; fix the offending <url>/<sitemap> entry so each contains exactly one <loc>.
- Regenerate the sitemap from the canonical generator (the same pipeline that declares SITEMAP_INDEX_MEMBERS) and redeploy, then purge CDN/edge cache for the sitemap URL.
- If a proxy or middleware rewrites the sitemap, exclude sitemap.xml paths from transformation.
- Confirm the deployed sitemap matches the built one — a partial deploy can leave an older/malformed file in place.
Example fix
// before (malformed served sitemap) <url><loc>/a</loc><loc>/a</loc></url> // after <url><loc>/a</loc></url>
Defensive patterns
Strategy: validation
Validate before calling
const xml = await res.text();
const entryMatch = xml.match(/<(url|sitemap)>([\s\S]*?)<\/(url|sitemap)>/);
if (entryMatch) {
const locs = [...entryMatch[0].matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/g)];
if (locs.length !== 1) throw new Error(`malformed sitemap entry in served XML: ${locs.length} <loc> elements`);
} Try / catch
try {
const batches = await getPublishedBatches(origin);
} catch (err) {
if (err instanceof Error && err.message.startsWith('expected one location per sitemap entry')) {
console.error(`Sitemap at origin is malformed; aborting IndexNow submit: ${err.message}`);
return;
}
throw err;
} Prevention
- Add a CI check that fetches the deployed sitemap and asserts one <loc> per entry before running IndexNow submission.
- Purge CDN cache for sitemap URLs on every sitemap-regenerating deploy.
- Never let proxies/middleware rewrite or inject content into /sitemap*.xml responses.
- Snapshot the generated sitemap in tests to catch generator regressions that duplicate or drop <loc>.
When it happens
Trigger: A fetched sitemap entry like `<url></url>` (no <loc>), `<url><loc>a</loc><loc>b</loc></url>` (two <loc>), or an entry whose text contains a stray `<loc>` tag passes the entry-pattern matchAll but yields `locations.length !== 1` in `/\<loc\>\s*([^<]+?)\s*<\/loc\>/g`.
Common situations: A stale or attacker-controlled reverse proxy / cached edge response returns a partially truncated sitemap; a CMS or sitemap generator was upgraded and now emits nested or duplicated <loc> tags; HTML error pages wrapped in matching tags get served in place of the XML.
Related errors
- invalid sitemap document: ${location}
- invalid sitemap entries: ${location}
- empty sitemap: ${location}
- invalid sitemap location: ${location}
- published sitemap tree exceeds 50 documents
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/fc8fe5571b47c96a.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/seo-indexnow-submit.mjs:191
seen.add(location);
if (seen.size > 50) throw new Error('published sitemap tree exceeds 50 documents');
const response = await fetchImpl(location, {
method: 'GET',
redirect: 'manual',
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
headers: { Accept: 'application/xml', 'User-Agent': USER_AGENT },
});
if (response.status !== 200) throw new Error(`${location} returned ${response.status}, expected direct 200`);
const source = (await response.text()).trim();
const root = /^(?:<\?xml[^?]*\?>\s*)?<(sitemapindex|urlset)\b[^>]*>([\s\S]*)<\/\1>\s*$/.exec(source);
if (!root) throw new Error(`invalid sitemap document: ${location}`);
const tag = root[1] === 'sitemapindex' ? 'sitemap' : 'url';
const entryPattern = new RegExp(`<!--[\\s\\S]*?-->|<${tag}>[\\s\\S]*?<\\/${tag}>`, 'g');
const entries = [...root[2].matchAll(entryPattern)].filter(([entry]) => !entry.startsWith('<!--'));
if (root[2].replace(entryPattern, '').trim()) throw new Error(`invalid sitemap entries: ${location}`);
const urls = entries.map(([entry]) => {
const locations = [...entry.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/g)];
if (locations.length !== 1) throw new Error(`expected one location per sitemap entry: ${location}`);
return decodeXml(locations[0][1].trim());
});
if (urls.length === 0) throw new Error(`empty sitemap: ${location}`);
if (location === `${origin}/sitemap.xml`
&& (root[1] !== 'sitemapindex' || urls.length !== SITEMAP_INDEX_MEMBERS.length
|| new Set(urls).size !== urls.length || urls.some(url => !SITEMAP_INDEX_MEMBERS.includes(url)))) {
throw new Error('published root sitemap members do not match the declared inventory');
}
if (root[1] === 'sitemapindex') {
pending.push(...urls);
} else {
for (const url of urls) {
const page = new URL(url);
if (page.protocol !== 'https:' || !hosts.has(page.host) || page.username || page.password || page.hash || page.pathname.endsWith('.xml')) {
throw new Error(`invalid published page URL: ${url}`);
}
pages.add(url);
}View on GitHub (pinned to 7d06c8633d)