Search documentation

Find pages, sections, and snippets across the Clog docs.

Redirects and link health

Sync the workspace redirect table to your edge or middleware, and report 404s back to Clog.

Clog doesn't host your site, so it can't issue 301s for you. Instead, it ships you the redirect table and accepts 404 reports back. The two ends of this loop are:

  • GET /external/redirects — pull the redirects your edge / middleware should replay.
  • POST /external/link-health — when your site serves a 404, report it back so the dashboard can resolve it.

Together they keep your URL structure healthy without manual coordination.

Where redirects come from

Two paths populate the workspace's redirect table:

  1. Auto-created on slug changes. Whenever a post or page slug changes, the backend writes a Permanent301 row from the old slug to the new one. You don't have to do anything — sync the table and the redirect appears.
  2. Manually created in the dashboard. The Redirects manager (/workspaces/[id]/redirects) lets editors create one-off entries or bulk-import a CSV. Useful for legacy URLs, vanity links, and one-way 410 Gone markers.

Sync redirects to your edge

GET /external/redirects returns a paginated list. Pull it on a schedule (at build, on a webhook-free poll, etc.) and replay matches locally — typically in your edge middleware so the 301 fires before your application code runs.

curl 'https://api.clog.dev/api/v1/external/redirects?pageSize=500&order=asc&orderBy=fromPath' \
  -H "X-API-Key: clog_live_xxx"

Response — a standard PaginatedResponse<RedirectResponse>:

{
  "data": [
    {
      "id": "...",
      "workspaceId": "...",
      "fromPath": "/posts/old-slug",
      "toPath": "/posts/new-slug",
      "type": "Permanent301",
      "hitCount": 12,
      "lastHitAt": "2026-05-22T10:11:12.000Z",
      "createdAt": "...",
      "updatedAt": "..."
    }
  ],
  "page": 1,
  "pageSize": 500,
  "order": "asc",
  "orderBy": "fromPath",
  "total": 87,
  "totalPages": 1
}
FieldNotes
fromPathAlways starts with /. Match against request.nextUrl.pathname (or its equivalent in your framework).
toPathStarts with / (relative) or http(s)://... (absolute). Pass through verbatim to the redirect response.
typePermanent301 (the default and most common), Temporary302, Temporary307, Gone410, UnavailableForLegalReasons451. Map each to the matching HTTP status.
hitCountThe dashboard's view of how often this redirect fired (Clog increments it from your lastHitAt updates — but v1 does not require you to report hits back, it's bookkeeping the dashboard does separately when you list).

Required permission: settings:read OR seo:read.

Filtering

ParamPurpose
searchCase-insensitive substring on fromPath and toPath.
typeOne of the RedirectType values above.
page, pageSize, order, orderByStandard pagination. orderBy allow-list: `createdAt

A Next.js edge-middleware example

middleware.ts
import { NextRequest, NextResponse } from 'next/server';

// Built at deploy time or fetched at startup — see the worked example for details.
const REDIRECTS: { fromPath: string; toPath: string; type: RedirectType }[] =
  await loadRedirectsFromClog();

const STATUS: Record<RedirectType, number> = {
  Permanent301: 301,
  Temporary302: 302,
  Temporary307: 307,
  Gone410: 410,
  UnavailableForLegalReasons451: 451,
};

export function middleware(req: NextRequest) {
  const hit = REDIRECTS.find(r => r.fromPath === req.nextUrl.pathname);
  if (!hit) return NextResponse.next();

  if (hit.type === 'Gone410' || hit.type === 'UnavailableForLegalReasons451') {
    return new NextResponse(null, { status: STATUS[hit.type] });
  }

  const dest = hit.toPath.startsWith('http')
    ? new URL(hit.toPath)
    : new URL(hit.toPath, req.nextUrl.origin);
  return NextResponse.redirect(dest, STATUS[hit.type]);
}

export const config = { matcher: '/((?!_next|api).*)' };

How often you re-sync depends on your edit cadence. For a build-time-only fetch (typical for marketing sites), you'll lose the auto-301 window between an editor renaming a slug and your next deploy — a 5–60 minute poll closes that gap. Webhooks aren't shipped in v1.

Report 404s back to Clog

When your site serves a 404, POST /external/link-health so the dashboard's Link health inbox sees it.

curl -X POST https://api.clog.dev/api/v1/external/link-health \
  -H "X-API-Key: clog_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "path": "/posts/missing-thing", "referrer": "https://example.com/sitemap" }'

Body shape:

{
  path: string;       // the missing path (no host, must start with /)
  referrer?: string;  // optional — the document that linked here, full URL
}

The endpoint upserts on (workspaceId, path) and increments hitCount. Repeat reports for the same path are explicitly safe — fire-and-forget.

Next.js — log every 404
// app/not-found.tsx (or your framework's 404 page)
export default async function NotFound() {
  await fetch('https://api.clog.dev/api/v1/external/link-health', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.CLOG_API_KEY!,
    },
    body: JSON.stringify({ path: usePathname() }),
  }).catch(() => { /* don't crash the 404 page on a reporting failure */ });

  return <h1>Not found</h1>;
}

Report from your server, not the browser. The API key is a workspace credential — never ship it to the client. Either fire the report from a route handler / middleware / Next.js Server Component, or proxy through your own backend.

The dashboard's Link health inbox lets an editor turn an entry into:

  • A redirect — creates a Redirect row on the spot (will surface on your next GET /external/redirects sync) and stamps resolvedBy = "redirect:<id>".
  • Fixed — the editor restored the page; nothing more to do.
  • Ignored — leave it as 404 deliberately.

All three set resolvedAt = now(). Resolution happens in the dashboard, not via the external API.

Permission

POST /external/link-health requires only workspace membership (any bound key works). It's intentionally low-friction — consumer sites need to report every 404 without per-route permission tuning.

A typical loop

  1. Editor renames /posts/old-slug/posts/new-slug in the dashboard. Backend writes a Permanent301 row.
  2. Your edge middleware re-syncs and starts replaying the redirect.
  3. An older crawler still requests /posts/old-slug → your middleware 301s it → all good.
  4. Some external site links to /posts/typo-slug → your site 404s → middleware (or your 404 page) POSTs to /external/link-health.
  5. Editor sees the typo in the Link health inbox, creates a redirect to /posts/correct-slug from there.
  6. Next sync picks up the new redirect; the 404 stops happening.

On this page