@rando-ge/blog-sdk
v0.3.4
Published
Typed client and SEO utilities for the rando-blog Hub. Makes it impossible to ship a blog page missing critical SEO fields.
Maintainers
Readme
@rando-ge/blog-sdk
Typed client and SEO utilities for the rando-blog Hub. The SDK's job is to make it impossible to accidentally ship a blog page that's missing critical SEO fields — not through components, but through TypeScript types and one-line helpers that do the metadata heavy lifting.
Integrating with the help of an AI coding agent (Claude Code, Cursor, etc.)? Point it at
AGENTS.md— a step-by-step integration guide with strict rules and anti-patterns, optimized for agent consumption.
Install
npm install @rando-ge/blog-sdknext and react are optional peer dependencies — only required if you import from @rando-ge/blog-sdk/next.
Quick start (Next.js App Router)
1. One-time setup file
// lib/blog.ts
import { createBlogSdk } from '@rando-ge/blog-sdk/next';
export const blog = createBlogSdk({
apiKey: process.env.BLOG_HUB_API_KEY!,
baseUrl: process.env.BLOG_HUB_URL!,
siteDomain: 'client-site.com',
});2. Post page
// app/blog/[slug]/page.tsx
import { blog } from '@/lib/blog';
export const generateMetadata = blog.metadataFor('slug');
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await blog.getPostOr404(params.slug);
return (
<article className="prose lg:prose-xl mx-auto py-12">
<h1>{post.title}</h1>
<p className="text-sm text-gray-500">
By {post.author} · {post.readingTime} min read
</p>
{/* drop in whatever markdown component your project already uses */}
<YourMarkdownComponent source={post.content} />
<blog.JsonLd post={post} />
</article>
);
}That's it. SSR happens because the page is a Server Component by default. <title> and <meta> tags land in the initial HTML via generateMetadata. The <script type="application/ld+json"> lands in the initial HTML via <blog.JsonLd>. Google sees a complete page on first byte.
3. List page
// app/blog/page.tsx
import Link from 'next/link';
import { blog } from '@/lib/blog';
export default async function BlogIndexPage() {
const { posts } = await blog.getPosts({ limit: 20 });
return (
<>
<blog.BlogIndexJsonLd
posts={posts}
name="Acme Blog"
description="Stories and guides about composting and gardening."
/>
<ul>
{posts.map((p) => (
<li key={p.id}>
<Link href={`/blog/${p.slug}`}>{p.title}</Link>
<p>{p.excerpt}</p>
</li>
))}
</ul>
</>
);
}<blog.BlogIndexJsonLd> emits the Blog (collection) JSON-LD for the index page — Google needs this to recognize the URL as a blog listing. Publisher, logo, language, and per-post URLs are derived automatically from the posts and your SDK config.
What the SDK does for you
- Throws on missing SEO fields. If a post is missing
seoTitle/seoDescription/author/publishedAt, the page build fails. You cannot ship an empty<title>. - Automatic 404s.
getPostOr404calls Next'snotFound()on a Hub 404 — no try/catch in your page. - ISR by default. Every fetch is wired with
next: { revalidate: 300, tags: ['blog-posts'] }. Override via constructor or per-call. - Zero styling. Layout, fonts, colors, spacing — 100% yours. The SDK ships zero CSS.
Need lower-level access?
blog.client is the raw BlogHubClient. Use it for sitemap generation, custom 404 logic, or anything outside the page-render path.
// app/sitemap.ts
import { blog } from '@/lib/blog';
export default async function sitemap() {
const { posts } = await blog.client.getPosts({ limit: 100 });
return posts.map((p) => ({
url: `https://client-site.com/blog/${p.slug}`,
lastModified: p.updatedAt ?? p.publishedAt,
}));
}What the types enforce
Every SEO-critical field on BlogPost carries an inline JSDoc explaining why it matters — so AI agents and developers see the guidance the moment they hover the field. generateMetadata and generateStructuredData throw if any required field (slug, title, seoTitle, seoDescription, author, publishedAt) is empty. Better a failed build than a page with an empty <title>.
API surface
Core (@rando-ge/blog-sdk)
| Export | Purpose |
| --- | --- |
| BlogHubClient | Typed fetch client. Unwraps the Hub's { success, data } envelope. Throws BlogHubError for non-2xx. |
| BlogHubError | Custom error with code, message, status. |
| generateMetadata(post, { siteDomain, pathPrefix? }) | Returns a Next.js-compatible Metadata object. |
| generateStructuredData(post, { siteDomain, pathPrefix? }) | Returns a BlogPosting JSON-LD payload. |
| generateBlogIndexStructuredData(posts, { siteDomain, name, description, ... }) | Returns a Blog (collection) JSON-LD payload for the index page. |
| formatDate(iso, locale?, options?) | Intl-based date formatter. |
| formatReadingTime(minutes, locale?) | Returns "5 min read". |
| BlogPost, PostsQuery, PostsListResponse, PostStatus, ImageStatus | Types. |
Next.js (@rando-ge/blog-sdk/next)
| Export | Purpose |
| --- | --- |
| createBlogSdk(config) | One-line setup. Returns { client, getPost, getPostOr404, getPosts, metadataFor, structuredDataFor, JsonLd }. Server-only — never import from a Client Component. |
| createNextBlogClient(config) | Lower-level: just the typed client wrapped with Next's fetch caching. Use this if you don't want the metadataFor / JsonLd ergonomics. |
| ISR.ONE_MINUTE / FIVE_MINUTES / ONE_HOUR / ONE_DAY | Revalidation presets in seconds. |
BlogSdk methods (returned by createBlogSdk)
| Method | Purpose |
| --- | --- |
| client | Raw BlogHubClient. Escape hatch for sitemap, custom error handling, etc. |
| getPost(slug) | Fetch a post. Throws BlogHubError on any failure including 404. |
| getPostOr404(slug) | Fetch a post or call Next's notFound() on 404. App Router only. |
| getPosts(query?) | List published posts for this site. |
| metadataFor('slug') | Returns a ready-to-export generateMetadata function. |
| structuredDataFor(post) | Build a JSON-LD payload manually. Usually you want <JsonLd> instead. |
| JsonLd | Server-safe component that emits BlogPosting JSON-LD for a single post. |
| BlogIndexJsonLd | Server-safe component that emits Blog (collection) JSON-LD for the index page. Props: posts, name, description. |
Markdown rendering
The SDK intentionally does not ship a markdown renderer. post.content is Markdown; pass it to whatever your site already uses (react-markdown, marked, MDX, etc.). Bundling a renderer would force a choice on every consumer and add weight.
Distribution — public npm
Published to the public npm registry as @rando-ge/blog-sdk. No authentication needed to install.
Publishing (maintainers)
Push a vX.Y.Z tag. The .github/workflows/publish.yml workflow runs typecheck, tests, build, and npm publish using the repo's NPM_TOKEN secret.
npm version patch # bumps version + creates the tag
git push --follow-tagsLocal development against an unpublished build
You can install the built tarball directly without going through the registry:
# in this repo:
npm run build && npm pack
# in the consuming project:
npm install /path/to/rando-ge-blog-sdk-0.3.1.tgzUse this while iterating on the SDK; switch to the published version once the API stabilises.
Server fields not exposed by this SDK
The Hub also stores draft / archived posts, image-pipeline state (imageStatus, imageError, imageAttempts), and admin metadata. Public reads via x-api-key only return published posts and SEO-relevant fields, which is exactly what this SDK exposes. Use the admin REST API directly for editorial workflows.
