{"record":{"id":"fc8fe5571b47c96a","repo":"koala73/worldmonitor","slug":"expected-one-location-per-sitemap-entry-location","errorCode":null,"errorMessage":"expected one location per sitemap entry: ${location}","messagePattern":"expected one location per sitemap entry: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/seo-indexnow-submit.mjs","lineNumber":191,"sourceCode":"    seen.add(location);\n    if (seen.size > 50) throw new Error('published sitemap tree exceeds 50 documents');\n    const response = await fetchImpl(location, {\n      method: 'GET',\n      redirect: 'manual',\n      signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n      headers: { Accept: 'application/xml', 'User-Agent': USER_AGENT },\n    });\n    if (response.status !== 200) throw new Error(`${location} returned ${response.status}, expected direct 200`);\n    const source = (await response.text()).trim();\n    const root = /^(?:<\\?xml[^?]*\\?>\\s*)?<(sitemapindex|urlset)\\b[^>]*>([\\s\\S]*)<\\/\\1>\\s*$/.exec(source);\n    if (!root) throw new Error(`invalid sitemap document: ${location}`);\n    const tag = root[1] === 'sitemapindex' ? 'sitemap' : 'url';\n    const entryPattern = new RegExp(`<!--[\\\\s\\\\S]*?-->|<${tag}>[\\\\s\\\\S]*?<\\\\/${tag}>`, 'g');\n    const entries = [...root[2].matchAll(entryPattern)].filter(([entry]) => !entry.startsWith('<!--'));\n    if (root[2].replace(entryPattern, '').trim()) throw new Error(`invalid sitemap entries: ${location}`);\n    const urls = entries.map(([entry]) => {\n      const locations = [...entry.matchAll(/<loc>\\s*([^<]+?)\\s*<\\/loc>/g)];\n      if (locations.length !== 1) throw new Error(`expected one location per sitemap entry: ${location}`);\n      return decodeXml(locations[0][1].trim());\n    });\n    if (urls.length === 0) throw new Error(`empty sitemap: ${location}`);\n    if (location === `${origin}/sitemap.xml`\n      && (root[1] !== 'sitemapindex' || urls.length !== SITEMAP_INDEX_MEMBERS.length\n        || new Set(urls).size !== urls.length || urls.some(url => !SITEMAP_INDEX_MEMBERS.includes(url)))) {\n      throw new Error('published root sitemap members do not match the declared inventory');\n    }\n    if (root[1] === 'sitemapindex') {\n      pending.push(...urls);\n    } else {\n      for (const url of urls) {\n        const page = new URL(url);\n        if (page.protocol !== 'https:' || !hosts.has(page.host) || page.username || page.password || page.hash || page.pathname.endsWith('.xml')) {\n          throw new Error(`invalid published page URL: ${url}`);\n        }\n        pages.add(url);\n      }","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/seo-indexnow-submit.mjs#L173-L209","documentation":"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.","triggerScenarios":"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`.","commonSituations":"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.","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."],"exampleFix":"// before (malformed served sitemap)\n<url><loc>/a</loc><loc>/a</loc></url>\n\n// after\n<url><loc>/a</loc></url>","handlingStrategy":"validation","validationCode":"const xml = await res.text();\nconst entryMatch = xml.match(/<(url|sitemap)>([\\s\\S]*?)<\\/(url|sitemap)>/);\nif (entryMatch) {\n  const locs = [...entryMatch[0].matchAll(/<loc>\\s*([^<]+?)\\s*<\\/loc>/g)];\n  if (locs.length !== 1) throw new Error(`malformed sitemap entry in served XML: ${locs.length} <loc> elements`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const batches = await getPublishedBatches(origin);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('expected one location per sitemap entry')) {\n    console.error(`Sitemap at origin is malformed; aborting IndexNow submit: ${err.message}`);\n    return;\n  }\n  throw err;\n}","preventionTips":["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>."],"tags":["sitemap","xml","seo","validation"],"backgroundTag":"xml-parse-error","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}