> ## Documentation Index
> Fetch the complete documentation index at: https://thestacc.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Public Blog API

> The Public Blog API lets you fetch your published blog content from theStacc and display it on any website — including static sites hosted on Cloudflare…

The Public Blog API lets you fetch your published blog content from theStacc and display it on any website — including static sites hosted on Cloudflare Pages, Vercel, Netlify, or any other platform. No CMS required.

## When to use this

Use the Public Blog API when:

* Your site runs on **Cloudflare Pages**, **Vercel**, **Netlify**, or **GitHub Pages**
* You don't use a CMS like WordPress, Ghost, or Webflow
* Your site is built with a static site generator (Astro, Next.js, Nuxt, Hugo, Gatsby, 11ty, etc.)
* You want full control over how blog content is rendered on your site

If you use WordPress, Ghost, Webflow, or Zepio, you don't need this — use the [direct publishing integrations](/docs/content-seo/publishing) instead, which push posts straight into your CMS.

This is the **pull** side of theStacc's integrations: your site reads content from us. For an end-to-end walkthrough of wiring it into a specific framework, follow [Static Site Integration](/docs/developers/static-sites/overview).

## How it works

theStacc generates and stores your blog content. The Public Blog API exposes the **published** posts for one project as JSON. Your static site fetches them at **build time** and generates static HTML pages.

```
theStacc generates blog → stored in theStacc database
                                    ↓
Your site rebuilds → fetches blogs from Public Blog API
                                    ↓
Static HTML pages generated → deployed to your CDN
                                    ↓
Google crawls → sees full static HTML → indexes and ranks normally
```

Because content is fetched at build time (not in the browser), Google sees fully rendered HTML pages. **SEO is identical to any other static site** — Google has no idea the content came from an API.

To rebuild automatically whenever you publish or update a post, pair this with a [Deploy Hook](/docs/developers/deploy-hooks).

## Authentication

Every project gets one **Public API Key** for read-only public access. Find it in your project under **Settings > Publishing > API Access**. Click **Generate API Key** if you don't have one yet; **Reveal** and **Copy** to read it.

The key always starts with `pk_live_`. It is project-scoped: it only ever returns the published blogs that belong to that one project.

Include the key in every request — either as a query parameter or as a Bearer header:

```
# Query parameter
GET https://api.thestacc.com/blog/api/v1/public/blogs?api_key=YOUR_API_KEY
 
# Or as a header
GET https://api.thestacc.com/blog/api/v1/public/blogs
Authorization: Bearer YOUR_API_KEY
```

If both are supplied, the `Authorization: Bearer` header wins.

### This is a project key, not an agent key

The `pk_live_` Public API Key is **read-only**. It can list and read your published blogs — nothing more. It cannot create, edit, publish, or delete content, and it can't see drafts or unpublished posts. That's why it's safe to use in a static-site build.

It is a completely different credential from the **agent keys** (which start with `sk_agent_`) used by the [Model Context Protocol (MCP) server](/docs/developers/agent-keys-mcp). Agent keys are secret, write-capable (they can generate, edit, and publish blogs), are stored hashed, and live in the **Agent access** section right beside API Access. Never use one in place of the other — and never embed an `sk_agent_` key in a public build.

### Keep your key safe

Though the Public API Key is read-only, treat it as a credential. Store it in an environment variable in your build process and **never commit it to your repository**.

If a key is ever exposed, open **Settings > Publishing > API Access** and click **Rotate Key** to issue a new one (the old key stops working immediately), or **Revoke** to disable public access entirely. Any site still using the old key will stop fetching the moment you rotate or revoke, so update your build environment variable at the same time.

## Endpoints

All endpoints share the base URL `https://api.thestacc.com/blog/api/v1/public`.

### List published blogs

```
GET /blog/api/v1/public/blogs?api_key=YOUR_API_KEY
```

Returns blogs with `status: published`, sorted by `published_at` (newest first). By default it returns **metadata only** (no body content) for a fast, lightweight listing. Pass `include_content=true` to get the full HTML body for every post in the page.

**Query parameters:**

* `api_key` (required) — your project's `pk_live_` key (or pass it via the Authorization header instead)
* `limit` (optional) — number of blogs to return. Default `50`, minimum `1`, maximum `100`
* `offset` (optional) — pagination offset. Default `0`, minimum `0`
* `category` (optional) — return only blogs that include this category name
* `include_content` (optional) — set to `true` to include the full HTML body of each blog. Default `false`

Use `limit` and `offset` together to page through more than 100 posts. The `total` field in the response is the full count of published blogs (after any `category` filter), so you can compute how many pages remain.

**Response (default — without content):**

```json theme={null}
{
  "blogs": [
    {
      "id": "abc-123",
      "title": "10 SEO Tips for Small Businesses",
      "slug": "seo-tips-small-businesses",
      "excerpt": "Discover actionable SEO strategies that...",
      "meta_title": "10 SEO Tips for Small Businesses | Your Brand",
      "meta_description": "Discover 10 actionable SEO strategies...",
      "featured_image_url": "https://storage.thestacc.com/images/seo-tips.webp",
      "categories": ["SEO", "Marketing"],
      "tags": ["seo", "small-business", "organic-traffic"],
      "published_at": "2026-03-28T10:00:00Z"
    }
  ],
  "total": 42,
  "limit": 50,
  "offset": 0
}
```

When `include_content=true` is passed, each blog also includes a `content` field with the full HTML body.

### Get a single blog by slug

```
GET /blog/api/v1/public/blogs/:slug?api_key=YOUR_API_KEY
```

Returns the full blog for one slug (content is always included). This is the call your blog post template makes to render an individual page.

The single-blog response includes two fields the list response doesn't: `updated_at` and `word_count`. `word_count` is computed on the fly by stripping the HTML tags from the body and counting the remaining words, so it always reflects the current content.

**Response:**

```json theme={null}
{
  "id": "abc-123",
  "title": "10 SEO Tips for Small Businesses",
  "slug": "seo-tips-small-businesses",
  "excerpt": "Discover actionable SEO strategies that...",
  "content": "<h2>Why SEO matters</h2><p>Search engine optimization...</p>",
  "meta_title": "10 SEO Tips for Small Businesses | Your Brand",
  "meta_description": "Discover 10 actionable SEO strategies...",
  "featured_image_url": "https://storage.thestacc.com/images/seo-tips.webp",
  "categories": ["SEO", "Marketing"],
  "tags": ["seo", "small-business", "organic-traffic"],
  "published_at": "2026-03-28T10:00:00Z",
  "updated_at": "2026-03-29T14:30:00Z",
  "word_count": 1850
}
```

Returns `404` if the slug doesn't exist in this project or the matching blog isn't published.

### Get blog sitemap data

```
GET /blog/api/v1/public/blogs/sitemap?api_key=YOUR_API_KEY
```

Returns a lightweight list of all published blogs for generating your XML sitemap. Each entry includes only `slug`, `published_at`, and `updated_at` — just enough to build `<loc>` and `<lastmod>` entries.

This endpoint returns **up to 5,000 blogs** in a single response (newest first), with no pagination. If a project ever publishes more than 5,000 posts, the sitemap call returns the most recent 5,000. The `total` field is the number of blogs actually returned.

**Response:**

```json theme={null}
{
  "blogs": [
    {
      "slug": "seo-tips-small-businesses",
      "published_at": "2026-03-28T10:00:00Z",
      "updated_at": "2026-03-29T14:30:00Z"
    }
  ],
  "total": 42
}
```

## Content format

The `content` field is **HTML** — the same body HTML theStacc generates for your post. It includes:

* Headings (`h2`, `h3`, `h4`)
* Paragraphs, lists, and blockquotes
* Links (internal and external)
* Images with alt text
* Inline styles for formatting

You can render it directly with `set:html` (Astro), `dangerouslySetInnerHTML` (React/Next.js), or `| safe` (Hugo). Apply your own CSS to match your site's design.

### The featured image is separate from the body

`featured_image_url` is a **standalone field**, not part of the `content` HTML. theStacc treats your post's hero image (or, if there's no dedicated hero, the first generated image) as a structured field so you can render it however you like — as a hero banner above the article, as a card thumbnail in your listing, or in your social/OG tags.

The body `content` is the article itself and is **not** prefixed with the featured image, so the hero won't appear twice. In your template, render `featured_image_url` as the hero and render `content` as the body. Any other images that belong inside the article stay inside `content` as normal `<img>` tags.

### Cross-post links are resolved for you

When theStacc writes a post that links to another one of your posts, it stores that link as an internal placeholder (`[INTERNAL:other-post-slug]`) rather than a hard URL, so the link survives no matter where you publish. The Public API **resolves these placeholders for you on every read** whenever it returns body content (the single-blog endpoint, and the list endpoint when `include_content=true`):

* If the referenced slug matches another **published** post in the same project, the placeholder becomes a real link to that post on your site, using the path convention `your-site.com/blog/<slug>/`. The base URL comes from the website connected to your project; if no site URL is set, the link is rendered as a root-relative `/blog/<slug>/` path.
* If the referenced post **isn't published yet** (or the slug doesn't match anything), the link is dropped but the anchor text is kept. You'll never receive a broken link or a raw `[INTERNAL:...]` placeholder.

Because resolution happens at read time, fixing or publishing the target post is reflected on your next fetch — you don't need to regenerate or republish the post that links to it.

This matches the `your-site.com/blog/<slug>/` URL pattern used throughout the [Static Site Integration](/docs/developers/static-sites/overview) guide. If your blog lives at a different path, you'll want to keep that path as `/blog/` (or adjust your routing accordingly) so resolved internal links point to the right place.

### Self-hosting images

Images referenced in `content` and `featured_image_url` are served from theStacc storage. For best performance and to avoid depending on our storage, many teams download these images during their build and rewrite the URLs to their own CDN. This is optional — the URLs work as-is.

## Rate limits

The Public Blog API is designed for **build-time** fetching, not real-time client requests. A typical static site build makes a handful of requests (a list or sitemap call, plus one per post). Fetch the content during your build, generate static pages, and serve those — don't call the API from client-side browser code on every page view.

If you have a very large catalog, page through the list endpoint with `limit`/`offset` rather than requesting everything at once, and use the lightweight `sitemap` endpoint (which returns just slugs and dates) when you only need URLs.

## Error handling

All errors return a JSON object with a `detail` field describing what went wrong:

```json theme={null}
{
  "detail": "Invalid API key. Check that your key is correct and hasn't been revoked. You can find your API key in Settings > Publishing > API Access."
}
```

**Status codes you may see:**

* `401` — Missing or invalid API key. The key wasn't supplied, or it doesn't match any project (often because it was rotated or revoked). Check the value in your build environment.
* `404` — Blog not found or not published. The slug doesn't exist in this project, or the matching post isn't published. (Only the single-blog endpoint returns this.)
* `500` — Server error. Transient — retry after a few seconds.

In your fetch helper, treat a non-`200` response as a failure: throw on the list/sitemap calls so a broken build is obvious, and return `null` on the single-blog call so you can render a 404 page for a missing slug.

## Next steps

* Follow [Static Site Integration](/docs/developers/static-sites/overview) for framework-specific setup (Astro, Next.js, Hugo, and more)
* Set up [Deploy Hooks](/docs/developers/deploy-hooks) so your site rebuilds automatically when new content is published
* Want an AI assistant or automation to create and publish posts on your behalf? That's a different, write-capable credential — see [Agent Keys & MCP](/docs/developers/agent-keys-mcp)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.