Skip to main content
This is the source of truth for integrating a RankBuddy-powered blog into a Next.js App Router app. AI coding agents should follow every required section below and substitute project-specific values from the user’s prompt (BLOG_PATH, SITE_URL, PROJECT_NAME).

SDK on npm

@rankbuddy/sdk — headless client, ISR helpers, React HTML renderer.

Article fields

Full reference for RankBuddyArticle and RankBuddyCluster.

What you are building

1

Routes

  • {BLOG_PATH} — article index (listing + topic strip)
  • {BLOG_PATH}/[slug] — article detail (HTML for humans and search)
  • {BLOG_PATH}/[slug].md — markdown twin for AI agents (via rewrite + Route Handler)
  • {BLOG_PATH}/topic/[clusterSlug] — topic hub (recommended)
  • {BLOG_PATH}/layout.tsx — passthrough layout for cache invalidation
2

Data

Server Components fetch published content via @rankbuddy/sdk ISR helpers (not raw fetch to RankBuddy).
3

SEO

Per-page generateMetadata using article seo.* fields, JSON-LD BlogPosting, and robots when seo.noIndex.
4

Styling

Clean Tailwind layout: readable prose for HTML body, card grid on index, clay accent #D97757, responsive and accessible.
Replace {BLOG_PATH} with the user’s path (e.g. /blog). Replace {SITE_URL} with their production origin (e.g. https://acme.com).

1. Install dependencies

Requires Next.js ≥ 14.2 and Node 18+. React is optional unless you use @rankbuddy/sdk/react.

2. Environment variables

Add to .env.local and your hosting provider:
Server-only. Never prefix with NEXT_PUBLIC_. Never import the client in Client Components with a secret key.

3. SDK client (server)

Create src/lib/rankbuddy.ts:
Set NEXT_PUBLIC_SITE_URL and RANKBUDDY_BLOG_PATH to match the user’s project, or hardcode values from the onboarding prompt.

4. Route map (App Router)

For {BLOG_PATH} = /blog:
Use a route group if needed (src/app/(marketing)/blog/...) — URLs stay /blog/....

Blog layout

src/app/blog/layout.tsx:
Keep the blog layout static in production. This prevents parent layouts from turning public blog pages into private, no-store responses and lets Vercel or the Next.js Full Route Cache serve cached HTML instantly.

5. Use SDK ISR helpers (required)

On Next.js, do not call rankBuddy.articles.getBySlug directly in Server Components. Use cached helpers so revalidateTag works:
See article fields for every property to render.

6. Blog index ({BLOG_PATH}/page.tsx)

Required behavior:
  • export const dynamic = "force-static" for public blog HTML.
  • export const revalidate = 3600 for first launch (see Caching); use false only with a revalidation strategy.
  • Fetch getArticlesList(rankBuddy, { limit: 24 }) and getClustersList(rankBuddy) in parallel.
  • Render an empty state when there are no posts.
  • Each card shows: coverImage, title, excerpt || description, publishedAt, readingTime, link to {BLOG_PATH}/{slug}.
  • Topic strip links to {BLOG_PATH}/topic/{cluster.slug}.
  • Static generateMetadata for the index (title, description, canonical, Open Graph).
Minimal styling: max-width container, responsive grid or stacked cards, subtle borders, hover states, sufficient color contrast.

7. Article page ({BLOG_PATH}/[slug]/page.tsx)

Required behavior:
  • export const dynamic = "force-static" and export const dynamicParams = true.
  • generateStaticParams — paginate getArticlesList (e.g. limit: 100) to prebuild slugs; wrap in try/catch so missing API key at build time does not fail CI.
  • generateMetadata — map from article fields:
Alternatively: await rankBuddy.seo.generateMetadata(slug) returns a Next-compatible metadata object. Page UI must include: Article body — choose one:
Hide duplicate <h1> in HTML if the page already renders post.title (e.g. [&>h1:first-child]:hidden). JSON-LD (recommended): BlogPosting + BreadcrumbList scripts with headline, description, image, datePublished, dateModified, url. Call notFound() when getArticleBySlug returns null.

8. Markdown endpoints (AI) — required

Serve raw markdown at {BLOG_PATH}/[slug].md so AI agents get clean source without HTML chrome. Reuse the same getArticleBySlug data cache — no separate cache tag or invalidation logic.

Rewrite (next.config.ts)

Add a rewrite (adjust {BLOG_PATH} if not /blog):
Replace /blog with the user’s {BLOG_PATH} (no trailing slash).

Shared helpers

Create src/lib/blog/articleMarkdown.ts. Centralizing canonical-URL logic here (instead of duplicating it in the HTML page and the Route Handler) guarantees alternates.canonical on the HTML page and Link: rel="canonical" on the .md response always agree:

Route Handler

Create src/app/api/rankbuddy/markdown/[slug]/route.ts:
Always wrap the SDK call in try/catch: an unhandled throw would fall back to Next.js’s default HTML error page, breaking the text/markdown contract for bots.

HTML discovery (generateMetadata)

On the article page, reuse the same helpers so the canonical URL can never drift between representations:

SEO rules (required)

  • Set X-Robots-Tag: noindex, nofollow on every .md response — prevents duplicate indexing; AI crawlers can still fetch the URL.
  • Do not add .md URLs to sitemap.xml.
  • Do not link to .md URLs in the UI — only the <link rel="alternate"> in metadata.
  • Serve the same content as HTML (from content.markdown / API source); do not maintain a separate AI-only text.
  • Don’t add Vary: Accept on the .md response — that header is only meaningful when the same URL negotiates representations by Accept. Here .md is a distinct URL, so it doesn’t apply.

Caching

The Route Handler calls getArticleBySlug — same blogArticleCacheTag(slug) as the HTML page. Existing revalidateTag(blogArticleCacheTag(slug)) after publish updates both representations. No changes to your revalidation helper are required. Keep the HTTP response itself no-store (see helper above) so a CDN can’t serve a stale copy past a publish.

9. Topic hub ({BLOG_PATH}/topic/[clusterSlug]/page.tsx)

Required behavior:
  • export const dynamic = "force-static" and export const dynamicParams = true.
  • generateStaticParams from getClusterSlugs(rankBuddy).
  • getClusterBySlug(rankBuddy, clusterSlug)notFound() if null.
  • getClusterArticles(rankBuddy, clusterSlug, { limit: 24 }) for the grid.
  • Metadata from cluster.name, cluster.notes, cluster.pillarKeyword, cluster.keywords.
  • Same card component as the index.

10. Caching

Published articles appear on your site within the revalidation window without extra infrastructure.

Production (on-demand)

Use this on {BLOG_PATH}/layout.tsx, {BLOG_PATH}/page.tsx, {BLOG_PATH}/[slug]/page.tsx, and {BLOG_PATH}/topic/[clusterSlug]/page.tsx. For dynamic pages, keep dynamicParams = true so newly published slugs can be generated after deploy. Pair with revalidateTag + targeted revalidatePath calls after publishes. Tags from @rankbuddy/sdk:
  • blogArticleCacheTag(slug)
  • blogListingCacheTag()
  • blogRelatedAllCacheTag()
  • blogRelatedCacheTag(slug)
  • blogTopicListingCacheTag(clusterSlug)
Do not use revalidatePath("{BLOG_PATH}", "layout") for routine article updates. It invalidates the whole blog subtree, so the next visitors and crawlers pay the full regeneration cost across many pages. Revalidate the exact index, article, topic, and sitemap paths instead.

Verify caching after deploy

Run two requests against the same article:
The HTML response should not include Cache-Control: private, no-store. On Vercel, repeat requests should show x-vercel-cache: HIT or a similarly cached response. If every request is MISS with private, no-store, a layout or route is still dynamic.

11. Base styling spec

Agents should ship production-ready pages without a design system dependency:
  • Font: system sans or project default
  • Accent: use the primary brand color from the onboarding prompt (Primary brand color: #RRGGBB). If none was detected from the site, RankBuddy defaults to #0F172A (Tailwind slate-900)
  • Index: card layout with 16:9 cover thumbnails, title, 2-line excerpt
  • Article: max-width ~48rem prose column, optional sidebar
  • Dark mode: support if the host app uses dark: classes
  • Images: alt from title, lazy loading, object-cover on cards
  • Accessibility: semantic <article>, <time dateTime>, focus states on links

12. Agent checklist

Before finishing, verify:
All three routes exist under the user’s {BLOG_PATH}.
RANKBUDDY_API_KEY is read only on the server.
SDK ISR helpers used instead of raw client list/get calls.
Every RankBuddyArticle SEO field in article fields is mapped in generateMetadata.
seo.noIndex sets robots correctly.
generateStaticParams tolerates missing API key at build time.
Empty index state explains that posts come from RankBuddy after publish.
Markdown .md endpoint at {BLOG_PATH}/[slug].md with noindex and alternates.types['text/markdown'] on the HTML page.
Production blog HTML is static/SSG and repeat requests are cache hits, not private, no-store dynamic responses.
User can follow Publish your first article after deploy.

Next step

Deploy, then publish from RankBuddy — see Publish your first article.