API

Cache revalidation

Let Reblog tell your site to drop an article from its cache the moment it is published, edited, renamed, unpublished or deleted. One endpoint on your side, one secret, and stale articles stop happening.


If your site renders Reblog articles statically or with ISR, it keeps each rendered page until a later render succeeds. That one rule is the whole problem: when an article is deleted or renamed, the Content API starts answering 404 for its old handle, the render fails, and your site goes on serving the page it already had. Forever.

What you did in ReblogWhat your site keeps servingWhy
Deleted an articleThe old page, HTTP 200The API 404s, so no render can replace the cached page
Renamed an articleThe article at BOTH URLsThe new URL renders; the old one 404s and stays frozen
Edited a published articleThe old body, until the TTL expiresNothing tells the site the content moved

A shorter cache time does not fix this

A TTL only asks your site to try rendering again, and for a deleted or renamed article the try fails. The page has to be pushed out of the cache, which is what this endpoint does.

How it works#

You add one route to your site and set a shared secret. Reblog calls it whenever an article is published, edited, renamed, unpublished or deleted, with the exact list of paths that changed, and your site purges them. Delivery is on our side: a call your site refuses or misses is retried for an hour, so a deploy or an outage never loses a purge.

Renames and deletions are the point

The payload carries removed_handles: the URLs that no longer resolve. That is what lets you purge the address an article was renamed away from, and it is the information a polling site can never reconstruct.

Setup#

  1. 1

    Copy your secret

    Open your project, go to Integrations > Cache revalidation. It is already on and the signing secret is already there: reveal it and press copy. There is nothing to enable, nothing to invent and nothing to email around.

  2. 2

    Add the route to your site

    Create app/api/reblog/revalidate/route.ts with the handler below, and set the copied value as REBLOG_REVALIDATE_SECRET in your hosting environment (on Vercel: Project Settings, Environment Variables). Deploy.

  3. 3

    Press Save and send test

    Back in Integrations. A green result means the round trip works. From then on every publish, edit, rename, unpublish and delete purges itself.

One step, on your side

Revalidation is on for every project from the moment it is created, so the only thing standing between you and self-purging pages is the route below. Until it exists Reblog still calls, and your site answers 404 -- which shows up in the delivery log on the Integrations screen, so you can see it rather than wonder. If you ever need to change the secret, press the roll button in Integrations and update the environment variable: purges fail in between, and resume by themselves once the new value is live.

The route handler#

Eleven lines, and there is no package to install. Every call carries your secret in its Authorization header, so checking that header is the whole of it:

app/api/reblog/revalidate/route.ts
import { revalidatePath } from 'next/cache'

export async function POST(req: Request) {
  if (req.headers.get('authorization') !== `Bearer ${process.env.REBLOG_REVALIDATE_SECRET}`) {
    return new Response('Unauthorized', { status: 401 })
  }

  const { paths = [] } = await req.json()
  for (const path of paths) revalidatePath(path)

  return Response.json({ revalidated: paths.length })
}

The signed version, if you want it

Every call is also signed: X-Reblog-Signature is an HMAC of the timestamped body and X-Reblog-Timestamp lets you reject a replayed one. Checking them buys you nothing extra unless your secret leaks, which is why the short handler above is enough for most sites. The longer one below does check them.

The signed version, checking the HMAC and rejecting replays. This is the exact code running on reblog.so's own blog, which reads the Content API the same way your site does. Copy it as it is.

app/api/reblog/revalidate/route.ts
import crypto from 'crypto'
import { NextResponse } from 'next/server'
import { revalidatePath, revalidateTag } from 'next/cache'

const MAX_SIGNATURE_AGE_SECONDS = 300

const timingSafeEqual = (a: string, b: string) => {
  const bufA = Buffer.from(String(a))
  const bufB = Buffer.from(String(b))
  if (bufA.length !== bufB.length) return false
  return crypto.timingSafeEqual(bufA, bufB)
}

const isAuthorized = (request: Request, rawBody: string, secret: string) => {
  const bearer = (request.headers.get('authorization') || '').replace(/^Bearer\s+/i, '')
  if (bearer && timingSafeEqual(bearer, secret)) return true

  const signature = request.headers.get('x-reblog-signature') || ''
  const timestamp = request.headers.get('x-reblog-timestamp') || ''
  if (!signature || !timestamp) return false

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp))
  if (!Number.isFinite(age) || age > MAX_SIGNATURE_AGE_SECONDS) return false

  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
  return timingSafeEqual(signature, expected)
}

export async function POST(request: Request) {
  const secret = process.env.REBLOG_REVALIDATE_SECRET
  if (!secret) {
    return NextResponse.json({ error: 'REBLOG_REVALIDATE_SECRET is not configured' }, { status: 503 })
  }

  const rawBody = await request.text()
  if (!isAuthorized(request, rawBody, secret)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const payload = JSON.parse(rawBody || '{}')
  if (payload.event === 'ping') {
    return NextResponse.json({ ok: true, event: 'ping', revalidated: [] })
  }

  const revalidated: string[] = []
  const listings = new Set<string>()

  for (const path of payload.paths || []) {
    if (typeof path !== 'string' || !path.startsWith('/')) continue
    // A literal path takes no `type` argument: that one is for route patterns
    // like '/blog/[slug]' and would not match this cache entry.
    revalidatePath(path)
    revalidated.push(path)
    // '/blog/how-to-x' also changes '/blog': the listing shows the article.
    const parent = path.split('/').slice(0, -1).join('/')
    if (parent) listings.add(parent)
  }
  for (const listing of listings) {
    revalidatePath(listing)
    revalidated.push(listing)
  }
  for (const tag of payload.tags || []) {
    if (typeof tag === 'string' && tag) revalidateTag(tag)
  }

  return NextResponse.json({ ok: true, event: payload.event || null, revalidated })
}

Not on Next.js?

The endpoint only has to do two things: check the secret, then purge the URLs in paths from whatever cache your site uses. On Cloudflare that is a cache purge call; on any other CDN it is the provider's purge API. Everything else in the snippet above is Next-specific detail.

What Reblog sends#

POSThttps://your-site.com/api/reblog/revalidate

Called by Reblog when a published article changes. You implement this endpoint; Reblog is the client.

Auth The signing secret Reblog generated for your project, sent as Authorization: Bearer <secret> and as an HMAC signature. Checking either one is enough.

Parameters

AuthorizationstringBearer <the secret shown in Integrations>. The simplest check: compare it to your env var.
X-Reblog-Eventstringarticle.published, article.updated, article.unpublished, article.deleted or ping.
X-Reblog-TimestampstringUnix seconds. Refuse anything older than a few minutes to block replays.
X-Reblog-Signaturestringsha256= followed by the HMAC-SHA256 of <timestamp>.<raw body>, keyed with the shared secret.
pathsstring[]Site-root-relative paths whose page changed. This is what you pass to revalidatePath.
handlesstring[]The same list as Reblog handles, without the leading slash.
removed_handlesstring[]Handles that no longer resolve: renamed away, or deleted. Use them to drop a route, or to install a 301 to the new URL.
tagsstring[]Cache tags, if you tag your fetches instead of purging by path.
eventstringSame value as the X-Reblog-Event header. A ping carries no paths: answer 200 and do nothing.
article_idstringThe article this call is about. Null for a ping.
langstringLanguage of the article that changed.
statusstringIts status after the change.

Responses

  • 200Purge accepted. Reblog records the delivery and moves on.
  • 401Secret or signature rejected. Reblog retries, so fix the secret rather than ignoring it.
  • 503Your endpoint has no secret configured. Reblog retries for an hour.
Body
{
  "event": "article.updated",
  "project_id": "7906b84e",
  "article_id": "665f0c2b9d1e4a0012ab34cd",
  "lang": "EN",
  "status": "published",
  "handles": ["blog/new-name", "blog/old-name"],
  "paths": ["/blog/new-name", "/blog/old-name"],
  "tags": ["reblog:article:blog/new-name", "reblog:project:7906b84e"],
  "removed_handles": ["blog/old-name"],
  "sent_at": "2026-09-07T10:00:00.000Z"
}

handles and paths include the article's linked translations, and, on a rename, the handle the article was moved away from. Purging every path in the list is always the correct behaviour.

Checking that it works#

  • Save and send test on the Integrations screen sends a ping and shows the HTTP status your site answered. A 401 means the two secrets differ; a 404 means the route is not deployed yet.
  • Rename a published article, then load its old URL. It should stop serving the article within seconds.
  • The Integrations screen lists the recent deliveries with their event, handles and response code, so you can see exactly what your site was told and when.
  • Connected an AI agent over MCP? It can run the same checks: get_revalidation_status reports the recent deliveries and whether an article is still owed a purge, and revalidate_article re-sends one (or, with no article, the test ping).

When Reblog stays quiet#

  • You switched cache revalidation off for the project, the project has no site URL, or the secret was pinned to an environment variable that is not set.
  • The change only touched a draft. A draft was never on your site, so there is nothing to purge.
  • The very first check for a project, which only records a starting point rather than purging your whole library at once.

The Content API is never cached in between

GET /api/external/articles answers Cache-Control: no-store, so no CDN between Reblog and your server can hold a stale copy of an article response. The only cache in play is your site's own, and this endpoint is how you clear it.