---
title: "Publish to Next.js and static sites"
description: "Connect a Next.js or static site to the Content API. Articles render as native pages on your domain, with no deploy per article."
---

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

# Publish to Next.js and static sites

RankControl publishes to Next.js sites through a pull model. Your site reads
articles from the Content API and renders them at your own URLs, for example
`yoursite.com/blog/article-slug`. RankControl never touches your repository
or your host. You add a few files once, and every article after that appears
on its own.

## Connect the destination

1. Go to **Brand Control → Publishing** and pick the
   **Headless / Static / Next.js** card.
2. Enter your site URL and the article path where articles should live, for
   example `/blog`. If a route already exists at that path, pick a different
   one so your existing pages are never overwritten.
3. Click **Create destination**.

The next screen shows your API key. Copy it right away: it is shown only
this once. You can always mint a new `read:content` key later under
**Settings → API**.

## Set up your site

The same screen gives you two paths:

- **Agent prompt.** One copyable prompt for Cursor, Claude Code, or any
  coding agent working in your repository. It contains the key, the
  endpoints, and instructions for every file below.
- **Manual setup.** The same files as copy-paste code blocks.

Either way, your site ends up with:

| File | What it does |
|---|---|
| `.env.local` | API URL, API key, and revalidate secret |
| `lib/rankcontrol.ts` | Fetches articles from the Content API |
| `app/[path]/[slug]/page.tsx` | Renders each article as a native page |
| `app/api/rankcontrol/revalidate/route.ts` | Lets new articles appear instantly |
| `next.config.js` | Allows article images from `cdn.imgcloud.org` |

If your site already lists its own posts, one more snippet merges
RankControl articles into your blog index and sitemap.

## Publish your first article

The Content API serves published articles only. Planned titles and drafts
stay private, so a site you just connected shows an empty list until you
publish something. An empty list means the setup works and there is nothing
to serve yet.

Open **Content** in RankControl, pick an article, and publish it. From
there:

1. The article becomes available on the Content API right away.
2. RankControl calls your revalidate route, so the article page and your
   article list refresh immediately.
3. A couple of minutes later, RankControl loads the article on your site to
   confirm it renders. An article that is not reachable yet is marked
   **Not live yet** on the **Content** screen, and the check retries on its
   own.

New articles need no deploy. Next.js renders an unknown slug on first
request by fetching the Content API. If you skipped the revalidate route,
pages refresh on a five minute timer instead.

> **Note**
>
> Fully static sites rebuild to publish. Paste your build hook URL on the
> destination (Cloudflare Pages, Netlify, and Vercel all provide one) and
> every published article triggers a rebuild.

## Count AI crawler visits

AI crawlers (GPTBot, ClaudeBot, PerplexityBot and others) fetch your HTML and
stop. They run no JavaScript, so a page script never sees them. A small
middleware in your site reports their visits to RankControl, and the
**AI Crawling** screen shows which crawlers read which articles.

Create `middleware.ts` at the project root. On Next.js 16 name the file
`proxy.ts` and keep the same export.

```ts
import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";

const AI_BOTS =
  /GPTBot|ChatGPT-User|PerplexityBot|ClaudeBot|Claude-Web|anthropic-ai|Google-Extended|Bingbot|Bytespider|CCBot|cohere-ai|Amazonbot|Meta-ExternalAgent|Applebot-Extended|Googlebot/;

export function middleware(req: NextRequest, event: NextFetchEvent) {
  const ua = req.headers.get("user-agent") ?? "";
  if (AI_BOTS.test(ua)) {
event.waitUntil(
  fetch(`${process.env.RANKCONTROL_API_URL}/api/site/crawl`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-rankcontrol-key": process.env.RANKCONTROL_REVALIDATE_SECRET ?? "",
    },
    body: JSON.stringify({ user_agent: ua, path: req.nextUrl.pathname }),
  }).catch(() => {})
);
  }
  return NextResponse.next();
}

export const config = { matcher: ["/((?!_next/|api/|.*\\..*).*)"] };
```

It uses the two variables you added in `.env.local`. Only requests from the
crawlers in the list are reported, and the report never blocks a response.
The agent prompt on your destination includes this file as step 8, with the
current crawler list filled in.

Verify the install with one request. It stores nothing:

```bash
curl -X POST "$RANKCONTROL_API_URL/api/site/crawl" \
  -H "x-rankcontrol-key: $RANKCONTROL_REVALIDATE_SECRET" \
  -H "content-type: application/json" \
  -d '{"user_agent":"install-check","path":"/","test":true}'
```

The answer is `{"ok":true,"test":true}`. The destination row then reads
"Crawler snippet: last report just now", and crawler visits appear under
**Analytics → AI Crawling** as they happen.

This works on Vercel, Netlify and Cloudflare Pages, for statically generated
pages too, because middleware runs on every request. It does not work for a
static export served from a storage bucket with no function layer.

## Not on Next.js?

Any static site generator works. The API is plain JSON:

- `GET /api/v1/articles` lists your published articles, paginated by
  cursor.
- `GET /api/v1/articles/{slug}` returns one article with its rendered HTML.

Authenticate both with your `read:content` key as a Bearer header, write
your pages at build time, and set a build hook on the destination. See the
[REST API page](/docs/api) for the full endpoint reference.

> **Note**
>
> Publishing requests, including the revalidate call, arrive from one fixed
> IP address. If a firewall guards your site, one rule lets RankControl
> through: see [the crawler page](/docs/bot).

Source: https://rctrl.com/docs/nextjs/index.mdx
