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.{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
@rankbuddy/sdk/react.
2. Environment variables
Add to.env.local and your hosting provider:
3. SDK client (server)
Createsrc/lib/rankbuddy.ts:
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:
src/app/(marketing)/blog/...) — URLs stay /blog/....
Blog layout
src/app/blog/layout.tsx:
5. Use SDK ISR helpers (required)
On Next.js, do not callrankBuddy.articles.getBySlug directly in Server Components. Use cached helpers so revalidateTag works:
6. Blog index ({BLOG_PATH}/page.tsx)
Required behavior:
export const dynamic = "force-static"for public blog HTML.export const revalidate = 3600for first launch (see Caching); usefalseonly with a revalidation strategy.- Fetch
getArticlesList(rankBuddy, { limit: 24 })andgetClustersList(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
generateMetadatafor the index (title, description, canonical, Open Graph).
7. Article page ({BLOG_PATH}/[slug]/page.tsx)
Required behavior:
export const dynamic = "force-static"andexport const dynamicParams = true.generateStaticParams— paginategetArticlesList(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:
await rankBuddy.seo.generateMetadata(slug) returns a Next-compatible metadata object.
Page UI must include:
Article body — choose one:
<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):
/blog with the user’s {BLOG_PATH} (no trailing slash).
Shared helpers
Createsrc/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
Createsrc/app/api/rankbuddy/markdown/[slug]/route.ts:
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, nofollowon every.mdresponse — prevents duplicate indexing; AI crawlers can still fetch the URL. - Do not add
.mdURLs tositemap.xml. - Do not link to
.mdURLs 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: Accepton the.mdresponse — that header is only meaningful when the same URL negotiates representations byAccept. Here.mdis a distinct URL, so it doesn’t apply.
Caching
The Route Handler callsgetArticleBySlug — 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"andexport const dynamicParams = true.generateStaticParamsfromgetClusterSlugs(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
First launch (recommended)
Production (on-demand)
{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)
Verify caching after deploy
Run two requests against the same article: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(Tailwindslate-900) - Index: card layout with 16:9 cover thumbnails, title, 2-line excerpt
- Article: max-width ~
48remprose column, optional sidebar - Dark mode: support if the host app uses
dark:classes - Images:
altfromtitle, lazy loading,object-coveron 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.
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.