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

  1. Fetch `location` with curl and inspect the raw XML; fix the offending <url>/<sitemap> entry so each contains exactly one <loc>.
  2. 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.
  3. If a proxy or middleware rewrites the sitemap, exclude sitemap.xml paths from transformation.
  4. 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

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


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)