Join the waitlist
Documentation

Add a blog to your site.

Write posts in Blogkit. Show them on your site with a few lines of code — or let your AI assistant do it.

Get started

Three steps, about five minutes.

  1. Create an API key

    In the Blogkit app, open API keys and create a key. Copy it straight away — it is only shown once.

  2. Install the client and add your key

    The key goes in an environment variable on your server. Never put it in browser code.

    terminal
    npm install @blogkitco/client
    .env
    BLOGKIT_API_KEY=paste-your-key-here
  3. Show your posts

    Pick your framework. You get a /blog page 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>
      )
    }

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.

Prompt for your AI assistant
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/docs

Add 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.

blogs.list()
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>

A slug that does not exist throws BlogkitNotFound. Catch it and show your own 404:

Missing posts
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 */
}

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.

Live · bk_live_…

For your real site. Reads published posts only.

Test · bk_test_…

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
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.

Too many requests in a minute returns a 429 with Retry-After; the client waits and retries once for you.

Updated 27 Sep 2026.