SDK & Export API

Guide: Build a Full Blog

Last updated June 4, 2026

Guide: Build a Full Blog

This guide ties the pieces together into a complete blog. The examples use Next.js App Router with Server Components, which keeps the API key private and gives you caching for free, but the data calls are the same in any framework.

1. Shared client

Create one client and reuse it everywhere. Set ISR caching here once.

ts
// lib/rankhiker.ts
import { createClient } from '@rankhiker/sdk';

export const rh = createClient({ apiKey: process.env.RANKHIKER_API_KEY!, baseUrl: 'https://rankhiker.com', requestInit: { next: { revalidate: 300 } }, });

2. List page

tsx
// app/blog/page.tsx
import Link from 'next/link';
import { rh } from '@/lib/rankhiker';

export default async function BlogIndex() { const posts = await rh.listArticles({ limit: 20 });

return ( <main data-rankhiker="blog-list"> {posts.map((post) => ( <article key={post.id} data-rankhiker="blog-card"> <Link href={/blog/${post.slug}}> {post.featuredImage && <img src={post.featuredImage} alt={post.title} />} <h2>{post.title}</h2> <p>{post.excerpt}</p> <div data-rankhiker="meta"> <span>{post.author.name}</span> <span>{post.readingTime} min read</span> </div> </Link> </article> ))} </main> ); }

Reusing the data-rankhiker attribute names means the Styling Guide stylesheet styles this hand-written markup too.

3. Detail page

tsx
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { rh } from '@/lib/rankhiker';

export async function generateStaticParams() { const posts = await rh.listArticles({ limit: 1000 }); return posts.map((post) => ({ slug: post.slug })); }

export default async function ArticlePage({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const post = await rh.getArticleBySlug(slug); if (!post) notFound();

return ( <article data-rankhiker="blog-post"> <h1>{post.title}</h1> {post.keyTakeaways.length > 0 && ( <aside data-rankhiker="takeaways"> <h2>Key Takeaways</h2> <ul> {post.keyTakeaways.map((point, i) => ( <li key={i}>{point}</li> ))} </ul> </aside> )} <div data-rankhiker="content" dangerouslySetInnerHTML={{ __html: post.content }} /> {post.faq.length > 0 && ( <section data-rankhiker="faq"> <h2>FAQ</h2> {post.faq.map((item, i) => ( <details key={i}> <summary>{item.question}</summary> <p>{item.answer}</p> </details> ))} </section> )} </article> ); }

getArticleBySlug returns the full article (toc, faq, keyTakeaways, views, wordCount), so the detail page can show everything the list page omits.

4. Scope to one workspace

If your account has several workspaces and you only want one blog's articles, pass workspaceId. Omitting it returns articles from all your workspaces.

ts
const posts = await rh.listArticles({
  workspaceId: '665abc123...',
  limit: 20,
});

Apply the same workspaceId on both the list page and generateStaticParams so the two stay consistent.

5. The Markdown caveat

content is delivered as HTML from the RankHiker editor, which is why the detail page injects it with dangerouslySetInnerHTML (React) or v-html / innerHTML (Vue). If your articles are authored in Markdown, convert before rendering:

ts
import { marked } from 'marked';

const html = marked.parse(post.content);

6. Deploy and caching

  • Keep RANKHIKER_API_KEY in a server-only env var (no NEXT_PUBLIC_ prefix) when fetching server-side.
  • requestInit: { next: { revalidate: 300 } } revalidates content every 5 minutes; lower it for fresher data or raise it to hit the API less.
  • For always-fresh data use requestInit: { cache: 'no-store' }.
  • generateStaticParams pre-builds every article at deploy time, so pages load instantly and only revalidate in the background.
That is a complete blog: one shared client, a cached list page, a statically generated detail page, optional workspace scoping, and a clear path for Markdown content.

Was this article helpful?