# Sitemap

```text
GET https://api.seoforsaas.dev/api/v1/projects/{projectId}/content/sitemap
```

Every published article as `{ url, lastModified }`, newest change first. The shape is what sitemap generators take: Next.js returns it from `app/sitemap.ts` as is. One small request, no article bodies, and the same content key as every other read.

cURL:

```bash
curl "https://api.seoforsaas.dev/api/v1/projects/YOUR_PROJECT_ID/content/sitemap" \
  -H "Authorization: Bearer $SEOFORSAAS_CONTENT_KEY"
```

TypeScript:

```ts
import { createClient } from "./lib/seoforsaas/client";

const seoforsaas = createClient({
  projectId: process.env.SEOFORSAAS_PROJECT_ID,
  contentKey: process.env.SEOFORSAAS_CONTENT_KEY,
});

// [{ url, lastModified }], newest change first: add them to your sitemap as they are.
const entries = await seoforsaas.getSitemap();

// Next.js, app/sitemap.ts:
// export default async function sitemap() {
//   return [{ url: "https://example.com/" }, ...(await seoforsaas.getSitemap())];
// }
```

Python:

```python
import os
from xml.sax.saxutils import escape

import requests

BASE = f"https://api.seoforsaas.dev/api/v1/projects/{os.environ['SEOFORSAAS_PROJECT_ID']}/content"

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SEOFORSAAS_CONTENT_KEY']}"


def sitemap_urls() -> str:
    """The <url> elements for every published article, to put inside your <urlset>."""
    res = session.get(f"{BASE}/sitemap", timeout=10)
    res.raise_for_status()
    return "\n".join(
        f"<url><loc>{escape(e['url'])}</loc><lastmod>{e['lastModified']}</lastmod></url>"
        for e in res.json()["data"]
    )
```

## Response

```json
{
  "success": true,
  "data": [
    {
      "url": "https://example.com/blog/how-google-decides-what-to-show",
      "lastModified": "2026-09-28T10:14:03.000Z"
    },
    {
      "url": "https://example.com/blog/how-to-write-a-sitemap",
      "lastModified": "2026-09-12T08:00:00.000Z"
    }
  ]
}
```

| Field | |
|---|---|
| `url` | Absolute: your website URL plus the article's `seo.canonicalPath`, so it's always the URL the page names as canonical. |
| `lastModified` | ISO 8601, the article's [`updatedAt`](/docs/api/article). |

## Why the date matters

Google reads a sitemap's `lastmod` to decide what to recrawl, and uses it only "if it's consistently and verifiably accurate". Google may stop trusting the dates of a site that bumps them without changing the page, so a sitemap stamped with every build's date doesn't help.

`lastModified` moves only when a reader would see the change:

| Moves it | Leaves it |
|---|---|
| Publishing | A fact check that changed no text |
| A rewrite, ours or one you ask for, including one you approve from review | A rewrite still waiting in your review queue |
| A link we added to it, pointing at a newer article of yours | Republishing an article that is already live |
| | Internal changes that readers can't see |

Google counts a changed link as a significant change, so a link we add moves the date on purpose. An unpublished article leaves the list.

## Using it

- **Next.js**: [the guide's sitemap](/docs/guides/nextjs) spreads the entries next to your own pages.
- **Astro**: [the guide's config](/docs/guides/astro) gives each article the date through `@astrojs/sitemap`'s `serialize`.
- **Anything else**: write one `<url>` per entry, `<loc>` from `url` and `<lastmod>` from `lastModified`, as the Python example does.

The URLs are built on the website URL you added the site with (shown in **Settings** → **Site and URLs**). If you use them as they are (Next.js, the Python example), that must be the origin your pages use for canonicals, or the sitemap would list URLs your pages don't claim. The Astro setup matches dates by path, so it isn't affected.

Submit the sitemap once in Google Search Console (Sitemaps → Add a new sitemap). Google rereads it on its own from then on; there's no ping to send. A sitemap holds up to 50,000 URLs, and so does this list.
