npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

next 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. getPostOr404 calls Next's notFound() 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-tags

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

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