<?xml version="1.0" encoding="UTF-8"?>
<!--
  Main sitemap index. Hand-maintained: nothing regenerates this file.

  sitemap-general.xml lives here in public/. sitemap-lenders.xml,
  sitemap-rankings.xml and sitemap-rankings-md.xml are built by
  apps/pSEO-generator (generateSitemaps()) into R2 and served by the consumer
  worker's R2 allowlist in workers/app.ts.

  sitemap-local.xml is deliberately NOT listed here. Sitemap index files cannot
  nest — Google ignores an index referenced from another index — so it is a
  SIBLING of this file, listed separately in robots.txt and submitted on its own
  in Search Console.

  sitemap-rankings-md.xml was removed 2026-09-02. It listed the .txt markdown twin of every
  ranking page, and those responses now send Link: <html-url>; rel="canonical". Submitting a URL
  in a sitemap asks Google to index it while the canonical asks it not to — Google honours the
  canonical, so the entries only spent crawl budget to be told the HTML is authoritative. The
  markdown is still published and still discoverable for AI crawlers via llms.txt; it is R2 and
  the generator that own it, not the sitemap index.

  The seven sitemap-places-*.xml entries were removed 2026-08-15. They listed
  ~301,431 URLs on the retired /places/... scheme. The live equivalents are
  /local/<slug>/... and are covered by sitemap-local.xml.
  apps/consumer/scripts/generatePlacesSitemaps.ts still exists and would rewrite
  this file with those entries if it is ever run again.

  ⚠️ CORRECTED 2026-09-18. This paragraph said those URLs "301 to /local/places/...
  (a blind prefix-prepend rule in Cloudflare, not in this repo) and land on a 404".
  Checked against production: there was no redirect and no such Cloudflare rule —
  /places/market/64834 returned the app's own 404 (x-remix-catch, cfWorker;dur=607),
  having matched a legacy :geography/:metric route and paid a D1 lookup to fail.
  They now return 410 Gone from workers/app.ts before the handler runs, so crawlers
  are told the family is permanently gone and the database read never happens.
  See isRetiredPath in app/lib/crawlPolicy.ts.
-->
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://saferate.com/sitemap-general.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://saferate.com/sitemap-lenders.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://saferate.com/sitemap-rankings.xml</loc>
  </sitemap>
  <!--
    The three treasury sitemaps are SIBLINGS here, not a treasury index pointing
    at two children. A sitemap index referenced from another index is ignored,
    which is the same constraint that keeps sitemap-local.xml out of this file.

    They are ROUTES rather than files: treasury data is behind the TREASURY
    service binding and only the consumer worker is bound to it, so there is no
    generator that could write them into R2. See app/utils/sitemapXml.ts.

    Split by how often they change and how many URLs they hold, so Search
    Console's per-sitemap indexing report separates them:
      treasury             ~51 pages, content daily, URL set rarely
      treasury-rates       ~4,725, one fitted trading day each, one added a day
      treasury-securities  ~2,735, one per CUSIP, a few added a week
  -->
  <sitemap>
    <loc>https://saferate.com/sitemap-treasury.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://saferate.com/sitemap-treasury-rates.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://saferate.com/sitemap-treasury-securities.xml</loc>
  </sitemap>
  <!--
    HONEYPOT #3. Not a sitemap, and deliberately the only entry here that is not.

    The bait in sitemap-general.xml (added 2026-09-18) was taken within eight hours and has
    been taken 39 times since, by ~30 networks in 15 countries — residential and proxy space,
    almost never the same IP twice. So the mechanism works. What it could NOT tell us is WHICH
    list the harvester read, because there was only one bait to take.

    A distinct path per published list turns one boolean into attribution: a hit here means this
    index specifically was read and its entries followed without a robots.txt check. This is the root index, so a hit means the harvester started at the top rather than at a child it already knew.

    The discriminator is robots.txt, not the URL. `Disallow: /data/` predates all of these, so a
    compliant crawler reads this index, checks robots.txt and skips the entry. Cloudflare already
    blocks /data/* for unverified clients, so the trap is armed; this is only the bait.

    ⚠️ Search Console will warn that this file lists a URL blocked by robots.txt. That warning IS
    the mechanism — do not "fix" it by dropping the Disallow, which disarms the whole thing.

    ⚠️ Taking the bait proves "did not consult robots.txt", NOT "is a scraper". Verified
    ChatGPT-User and ClaudeBot have both fetched the existing bait and are waved through by our
    own allow rule; a user-initiated assistant fetch is documented as robots-exempt. Look at who
    it was before acting on a hit.
  -->
  <sitemap>
    <loc>https://saferate.com/data/sitemap-index-full.xml</loc>
  </sitemap>
</sitemapindex>
