Get started
Three steps, about five minutes.
Create an API key
In the Blogkit app, open API keys and create a key. Copy it straight away — it is only shown once.
Install the client and add your key
The key goes in an environment variable on your server. Never put it in browser code.
npm install @blogkitco/clientBLOGKIT_API_KEY=paste-your-key-hereShow your posts
Pick your framework. You get a
/blogpage listing your posts, and a page for each one.// lib/blogkit.ts import { createClient } from '@blogkitco/client' export const blogkit = createClient({ apiKey: process.env.BLOGKIT_API_KEY!, }) // app/blog/page.tsx import Link from 'next/link' import { blogkit } from '@/lib/blogkit' export default async function Blog() { const { data: posts } = await blogkit.blogs.list() return ( <ul> {posts.map((post) => ( <li key={post.id}> <Link href={`/blog/${post.slug}`}>{post.title}</Link> </li> ))} </ul> ) } // app/blog/[slug]/page.tsx import { BlogContent } from '@blogkitco/client/react' import '@blogkitco/client/styles.css' import { blogkit } from '@/lib/blogkit' export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const post = await blogkit.blogs.get((await params).slug) return ( <article> <h1>{post.title}</h1> <BlogContent blocks={post.blocks} /> </article> ) }// lib/blogkit.ts import { createClient } from '@blogkitco/client' export const blogkit = createClient({ apiKey: import.meta.env.BLOGKIT_API_KEY!, }) <!-- src/pages/blog/index.astro --> --- import { blogkit } from '../../lib/blogkit' const { data: posts } = await blogkit.blogs.list() --- <ul> {posts.map((post) => <li><a href={`/blog/${post.slug}/`}>{post.title}</a></li>)} </ul> <!-- src/pages/blog/[slug].astro --> --- import { renderBlocks } from '@blogkitco/client' import '@blogkitco/client/styles.css' import { blogkit } from '../../lib/blogkit' export async function getStaticPaths() { const { data } = await blogkit.blogs.list({ limit: 100 }) return data.map((post) => ({ params: { slug: post.slug } })) } const post = await blogkit.blogs.get(Astro.params.slug!) --- <article> <h1>{post.title}</h1> <Fragment set:html={renderBlocks(post.blocks)} /> </article>import { createClient, renderBlocks, escapeHtml } from '@blogkitco/client' const apiKey = process.env.BLOGKIT_API_KEY if (!apiKey) throw new Error('Set BLOGKIT_API_KEY') const blogkit = createClient({ apiKey, cache: true, }) // Express shown; any server works the same way. app.get('/blog/:slug', async (req, res) => { const post = await blogkit.blogs.get(req.params.slug) res.send(`<h1>${escapeHtml(post.title)}</h1>${renderBlocks(post.blocks)}`) })# Every published post curl https://api.blogkit.co/v1/blogs \ -H "Authorization: Bearer $BLOGKIT_API_KEY" # One post, with its content as blocks curl https://api.blogkit.co/v1/blog/your-post-slug \ -H "Authorization: Bearer $BLOGKIT_API_KEY"
Build with AI
Using Claude, Cursor or Copilot? Open your site’s project, paste this prompt, and the assistant adds the blog — styled to match your site.
Add a blog to this website using Blogkit, a headless blog CMS. Posts are written in Blogkit and read with its official client, @blogkitco/client.
Setup
- Install: npm install @blogkitco/client
- The API key is in the environment variable BLOGKIT_API_KEY. I will add it to the env file myself. Never hard-code a key and never ask me to paste one here.
- Create one client in a server-only module and use it only in server code (server components, loaders, getStaticPaths, route handlers). The key must never reach the browser.
import { createClient } from '@blogkitco/client'
export const blogkit = createClient({ apiKey: process.env.BLOGKIT_API_KEY! })
Build
1. A blog index at /blog: `const { data, meta } = await blogkit.blogs.list({ page, limit: 12 })`. Each post has slug, title, excerpt, publishedAt, readingTime, featuredImage (url, alt, width, height, placeholder, variants.card / .hero / .thumb / .og), author (name, avatar) and categories. Paginate with meta.hasNext.
2. A post page at /blog/[slug]: `const post = await blogkit.blogs.get(slug)`.
- React or Next.js: `import { BlogContent } from '@blogkitco/client/react'` then `<BlogContent blocks={post.blocks} />`. It works in Server Components.
- Any other framework: `import { renderBlocks } from '@blogkitco/client'`. `renderBlocks(post.blocks)` returns sanitised HTML to insert as raw HTML (set:html, v-html, innerHTML).
- Import `@blogkitco/client/styles.css` once. It only styles `.bk-content`. Match the site by setting --bk-accent, --bk-ink, --bk-font and --bk-measure rather than overriding selectors.
- A missing slug throws `BlogkitNotFound` (import it from '@blogkitco/client'). Catch it and show the site's normal 404.
- Metadata: title from post.seo.metaTitle ?? post.title, description from post.seo.metaDescription ?? post.excerpt, image from post.seo.ogImage, canonical from post.seo.canonicalUrl.
3. Optional, if it fits the site: category and tag pages (`blogkit.blogs.list({ category })`, `blogkit.blogs.list({ tag })`, `blogkit.categories.list()`, `blogkit.tags.list()`), related posts (`blogkit.blogs.related(slug, 3)` — title, excerpt and date only), search (`blogkit.search(q)`; each result's highlight is safe HTML with <mark>), and a sitemap (`for await (const post of blogkit.blogs.all())`).
Freshness
- Next.js: pass `fetchOptions: { next: { revalidate: 60 } }` to createClient, and use generateStaticParams with blogkit.blogs.all().
- Static sites (Astro, Eleventy and the like): fetch at build time; the site rebuilds to show new posts.
- A long-running Node server: pass `cache: true` to createClient.
Rules
- Make the blog look like part of this site: reuse its layout, header, fonts, colours and components.
- Follow the project's conventions: its router, TypeScript if it uses it, its styling approach. The package exports types: Post, PostSummary, Block.
- Use the client for every request, not fetch by hand. Add no other packages for rendering posts.
Start by reading the project to find its framework, router and main layout. Then tell me your plan in a few lines before writing any code.
Docs: https://blogkit.co/docsAdd your key to the env file yourself — the prompt tells the assistant to read it from BLOGKIT_API_KEYand never to ask for it. The same text is at blogkit.co/llms.txt for tools that read a URL.
List posts
blogs.list() returns your published posts, newest first, 20 at a time.
const { data: posts, meta } = await blogkit.blogs.list({ page: 1, limit: 10 })
posts[0].title // "Hello world"
posts[0].slug // "hello-world" — use it in the link
posts[0].excerpt // a short summary
posts[0].featuredImage?.variants.card // a ready-sized image URL
meta.hasNext // is there another page?
// Only one category, or one tag:
await blogkit.blogs.list({ category: 'news' })
await blogkit.blogs.list({ tag: 'guides' })Show a post
blogs.get(slug) returns one post. The client turns its content into HTML for you.
import { BlogContent } from '@blogkitco/client/react'
import '@blogkitco/client/styles.css'
const post = await blogkit.blogs.get(slug)
<article>
<h1>{post.title}</h1>
<BlogContent blocks={post.blocks} />
</article>import { renderBlocks } from '@blogkitco/client'
import '@blogkitco/client/styles.css'
const post = await blogkit.blogs.get(slug)
const html = renderBlocks(post.blocks) // safe HTML — insert it as-isA slug that does not exist throws BlogkitNotFound. Catch it and show your own 404:
import { BlogkitNotFound } from '@blogkitco/client'
try {
post = await blogkit.blogs.get(slug)
} catch (error) {
if (error instanceof BlogkitNotFound) return notFound() // your 404
throw error
}Match your design
The stylesheet gives posts good defaults. Set a few variables to match your site — it only ever styles.bk-content.
.bk-content {
--bk-font: 'Inter', sans-serif;
--bk-ink: #111827;
--bk-accent: #e11d48;
--bk-measure: 70ch; /* line length */
}// Your own component for any block type; the rest render as usual.
<BlogContent blocks={post.blocks} renderers={{ code: MyCodeBlock }} />Show new posts
How quickly a new post appears depends on how your site is built.
- Next.js
- Pass fetchOptions: { next: { revalidate: 60 } } to createClient. New posts appear within a minute.
- Static sites
- Astro, Eleventy and friends fetch at build time. Rebuild after publishing — a deploy hook works well.
- Node servers
- Pass cache: true to createClient. Posts are cached for a minute and refreshed in the background.
API keys
Every request needs a key. Keep it on your server, in an environment variable.
For your real site. Reads published posts only.
For local development. Also sees drafts. 30 requests a minute, never counted against your plan.
A key is shown once, when you create it. Lost one, or think it leaked? Revoke it and create another — revoking takes effect within a minute.
The client
Everything @blogkitco/client does. It works in Node, Deno, Bun, Cloudflare Workers and every framework, with TypeScript types included.
blogs.list({ page, limit, category, tag })- A page of published posts, newest first
blogs.get(slug)- One post, with its content
blogs.all()- Every post, a page at a time — for sitemaps and static builds
blogs.related(slug, 3)- A few related posts: title, excerpt and date
search(query)- Posts matching a search, with highlighted snippets
categories.list() · tags.list()- Every category or tag, with post counts
renderBlocks(post.blocks)- A post’s content as safe HTML
<BlogContent blocks={post.blocks} />- The same, as a React component
HTTP API
Not using JavaScript? Call the API directly, with your key as a bearer token. Every endpoint is a GET and returns JSON.
curl https://api.blogkit.co/v1/blog/hello-world \
-H "Authorization: Bearer $BLOGKIT_API_KEY"GET /v1/blogs- published posts, newest first
GET /v1/blog/:slug- one post, with its content
GET /v1/blog/:slug/related- related posts
GET /v1/search?q=- full-text search
GET /v1/categories- categories, with counts
GET /v1/tags- tags, with counts
GET /v1/media/:id- one image, every size
Listing options
page, limit- which page, and how many (1–100, default 20)
category, author- filter by slug
tag- filter by tag slug; repeat for several
include=blocks- add each post’s content to the list
Blocks
You only need this if you render posts yourself. A post’s content is a list of blocks, each with atype and its data:
heading- A section heading, with an anchor for linking
paragraph- Text, with bold, italic, links and inline code
list- A bulleted or numbered list
image- An image with alt text, a caption and ready-sized versions
code- A code sample, with its language and an optional filename
quote- A quotation, with an optional source
embed- A YouTube video, a post on X, a CodePen, or a link
divider- A break between sections
Skip block types you do not recognise rather than failing — new ones may be added. The client’s types list every field.
Errors
Errors come back as { "error": { "code", "message", "requestId" } }. The client turns them into typed errors, like BlogkitNotFound.
401 invalid_api_key- The key is missing or wrong.
401 key_revoked- The key was revoked. Create a new one.
404 post_not_found- No published post has that slug.
422 invalid_query- A query parameter is not valid.
429 rate_limited- Too many requests. Wait and retry.
429 plan_limit_reached- This month’s requests are used up.
500 internal_error- Our fault. Quote the requestId if you write in.
Limits
Counted per project. Cached responses count as a fifth of a request, so most sites never come close.
Free
60 per minute
5k / mo · Enough to wire up a site and see it render.
Team
600 per minute
500k / mo · Per project, not per key. Raise it by asking.
Too many requests in a minute returns a 429 with Retry-After; the client waits and retries once for you.
Updated 27 Sep 2026.