# Quickstart

## 1. Set where articles live

Dashboard → your site → **Settings** → **Site and URLs** → **Articles live under**: the path your site serves them from, for example `/blog`. SEO for SaaS builds every canonical URL and every link between articles on it. If you leave it empty, articles live at the site root.

## 2. Collect three values

In the dashboard, open your site → **Delivery**:

- **Project ID**, from the **Connection** card.
- A **content key**: **Content API keys** → **Create key**. The dashboard shows it once, so store it as a secret.
- The **signing secret**, when you add the webhook (step 5): **Webhook** → **Signing secret** → **Reveal**.

```bash title=".env"
SEOFORSAAS_PROJECT_ID=YOUR_PROJECT_ID
SEOFORSAAS_CONTENT_KEY=      # Delivery → Content API keys → Create key
SEOFORSAAS_WEBHOOK_SECRET=   # Delivery → Webhook → Signing secret → Reveal
```

## 3. List your articles

Newest first, up to 100 per page. The TypeScript tab uses the [TypeScript client](/docs/api/client), one dependency-free file.

cURL:

```bash
curl "https://api.seoforsaas.dev/api/v1/projects/YOUR_PROJECT_ID/content?page=1&limit=100" \
  -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,
});

// Every published article, newest first; follows pagination for you.
const articles = await seoforsaas.listAllArticles();

// Or one page at a time (limit ≤ 100):
const { articles: first, pagination } = await seoforsaas.listArticles({ page: 1, limit: 20 });
```

Python:

```python
import os

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 list_all_articles() -> list[dict]:
    """Every published article, newest first."""
    articles: list[dict] = []
    page = 1
    while True:
        res = session.get(BASE, params={"page": page, "limit": 100}, timeout=10)
        res.raise_for_status()
        body = res.json()
        articles.extend(body["data"])
        if not body["pagination"]["hasNext"]:
            return articles
        page += 1
```

## 4. Read one article

Read it by slug, the value your article route receives. The response includes links to related articles.

cURL:

```bash
curl "https://api.seoforsaas.dev/api/v1/projects/YOUR_PROJECT_ID/content/by-slug/how-google-decides-what-to-show" \
  -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,
});

// The article and its related links, or null when there is none at this slug.
const article = await seoforsaas.getArticle("how-google-decides-what-to-show");
```

Python:

```python
import os
from urllib.parse import quote

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 get_article(slug: str) -> dict | None:
    """The article and its related links, or None when there is none at this slug."""
    res = session.get(f"{BASE}/by-slug/{quote(slug)}", timeout=10)
    if res.status_code == 404:
        return None
    res.raise_for_status()
    return res.json()["data"]
```

## 5. Refresh on publish

Add your URL in **Delivery** → **Webhook**. After you reveal the signing secret, we sign every delivery with [Standard Webhooks](https://www.standardwebhooks.com/). Before that, deliveries go out unsigned. Verify the signature, then rebuild or revalidate.

What we send:

```http
POST <your webhook URL>
content-type: application/json
webhook-id: msg_…
webhook-timestamp: 1759132800
webhook-signature: v1,…

{ "event": "content.created", "projectId": "YOUR_PROJECT_ID" }
```

TypeScript:

```ts
import { seoforsaasWebhook } from "./lib/seoforsaas/webhook";
import { refreshSite } from "./refresh-site"; // yours: purge a cache, or call a deploy hook

// Verifies the signature (401 when it fails), ignores the dashboard's test
// event, and calls you when an article is published, changed, or unpublished.
export const handleWebhook = seoforsaasWebhook(() => refreshSite());

// Mount it wherever your framework takes a fetch-style handler, for example
// Next.js: export const POST = handleWebhook;
```

Python:

```python
import os
from collections.abc import Callable, Mapping

from standardwebhooks.webhooks import Webhook, WebhookVerificationError

webhook = Webhook(os.environ["SEOFORSAAS_WEBHOOK_SECRET"])


def handle_seoforsaas_webhook(
    body: bytes, headers: Mapping[str, str], on_content_change: Callable[[dict], None]
) -> int:
    """The HTTP status to answer with.

    Pass the RAW body: the signature covers those bytes.
    """
    try:
        event = webhook.verify(body, dict(headers))
    except WebhookVerificationError:
        return 401
    if event["event"] in ("content.created", "content.updated"):
        on_content_change(event)  # your cache purge or deploy hook
    return 204
```

## 6. Render the article

The body is an array of typed [blocks](/docs/api/blocks). Render them with the [React components](/docs/components), or with your own markup from the block reference. The framework guides put all of this together: [Next.js](/docs/guides/nextjs), [Astro](/docs/guides/astro), [others](/docs/guides/other-frameworks).
