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

chaingrow-blog

v0.2.2

Published

Drop-in Next.js SDK for rendering blogs published from ChainGrow.

Downloads

351

Readme

chaingrow-blog

Drop-in Next.js SDK for rendering blogs published from ChainGrow.

  • Zero database. Your site stores nothing — ChainGrow is the source of truth.
  • Instant publishing. When you press Publish in ChainGrow, the SDK's webhook handler invalidates the relevant page via revalidateTag. Next request serves fresh HTML.
  • Headless-first. The core SDK is unstyled semantic HTML. Supply your own components for any block type you want to customize.

Install

pnpm add chaingrow-blog
# or
npm install chaingrow-blog

Requires Next.js ≥ 14 (App Router) and React ≥ 18.

Setup (3 files)

1. Environment variable

CHAINGROW_API_KEY=ck_live_xxx

That's the only required variable. Generate the key in your ChainGrow dashboard under Integrations → API Keys. It authenticates your API calls AND verifies incoming webhooks, so there's only one secret to manage. The webhook delivery URL is derived automatically from the website you set on your business profile, so you never paste a URL anywhere.

(Optional: set CHAINGROW_API_URL if you're pointing at staging or a self-hosted deploy. Defaults to https://api.chaingrow.de.)

2. Webhook receiver

Mount the SDK's handler at the convention path /api/chaingrow/webhook — ChainGrow always POSTs to ${your-website}/api/chaingrow/webhook:

// app/api/chaingrow/webhook/route.ts
export { POST } from 'chaingrow-blog/next/webhook';

3. Blog page

Option A — Custom rendering (bring your own JSX):

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'; // Next.js built-in — serves a real HTTP 404 and renders app/not-found.tsx
import { getBlog } from 'chaingrow-blog/next';

export default async function BlogPage({
  params,
}: {
  params: { slug: string };
}) {
  const blog = await getBlog(params.slug);
  if (!blog) notFound();

  return (
    <article>
      <h1>{blog.title}</h1>
      {blog.blocks.map((b, i) => (
        /* render however you want */
        <YourCustomBlock key={i} block={b} />
      ))}
    </article>
  );
}

Option B — Default renderer with per-block overrides:

import { notFound } from 'next/navigation';
import { getBlog } from 'chaingrow-blog/next';
import { BlogRenderer } from 'chaingrow-blog/react';

export default async function BlogPage({
  params,
}: {
  params: { slug: string };
}) {
  const blog = await getBlog(params.slug);
  if (!blog) notFound();

  return (
    <article className="prose">
      <h1>{blog.title}</h1>
      <BlogRenderer
        blog={blog}
        components={{
          heading: ({ level, children }) =>
            level === 2 ? (
              <h2 className="text-3xl font-bold">{children}</h2>
            ) : (
              <h3 className="text-2xl font-semibold">{children}</h3>
            ),
          quote: ({ children }) => (
            <blockquote className="border-l-4 border-blue-500 pl-4 italic">
              {children}
            </blockquote>
          ),
        }}
      />
    </article>
  );
}

API

getBlog(slug, language?, opts?)

Fetches a blog by slug (server-side, ISR-cached under tag chaingrow:blog:<slug>). Returns null if not found.

listBlogs(opts?, clientOpts?)

Lists blogs (optionally filtered by status or language).

createClient({ apiUrl, apiKey })

Framework-agnostic client. Use from edge functions, scripts, or non-Next.js environments.

<BlogRenderer blog={blog} components={...} />

Renders all blog.blocks using unstyled default components. Every block type is overridable via the components prop — supply only the ones you want to customize.

Block types: heading, paragraph, list, quote, code, image_placeholder.

createWebhookHandler({ apiKey?, onEvent? })

Returns a { POST } route handler. Verifies the X-ChainGrow-Signature HMAC (signed server-side with sha256(apiKey)) and calls revalidateTag. Pass onEvent to run custom logic after revalidation (e.g. cache warming, analytics). By default reads CHAINGROW_API_KEY from the environment.

Rate-limit handling

Calls are transparently retried once on HTTP 429 Too Many Requests, honoring the Retry-After header. No configuration needed. Other errors bubble up unchanged.

How publishing works

ChainGrow dashboard                      Your Next.js site
───────────────────                      ─────────────────
Press Publish
     │
     ▼
blog.status = PUBLISHED
     │
     ▼
HMAC-signed webhook  ──────────────►  /api/chaingrow/webhook
                                            │
                                            ▼
                                      verify signature
                                            │
                                            ▼
                                      revalidateTag('chaingrow:blog:<slug>')
                                            │
                                            ▼
Next user request                     /blog/<slug> cache miss
     │                                      │
     ▼                                      ▼
Fresh HTML                            getBlog() → GET /api/v1/blogs/by-slug/<slug>
                                            │
                                            ▼
                                      cached for next visitors

Releasing new versions (maintainers only)

Consumers installing chaingrow-blog can ignore this section — it's internal notes for ChainGrow maintainers cutting a new release.

The SDK is published to npm by a GitHub Actions workflow (.github/workflows/publish-blog-sdk.yml). The workflow only runs when you push a git tag matching blog-sdk-v*. Regular commits and branch pushes do nothing — npm is never touched unless you explicitly create and push a release tag.

Triggers

on:
  push:
    tags:
      - 'blog-sdk-v*'   # only fires on matching tags
  workflow_dispatch:     # manual "Run workflow" button in GitHub Actions UI

That's it. Two ways to trigger a publish:

  1. Push a tag named blog-sdk-v<version> (the normal flow)
  2. Click "Run workflow" on the Actions page in GitHub (manual escape hatch)

No other event publishes. You can push commits to main all day without releasing.

Cutting a release

# 1. Bump the version in packages/blog-sdk/package.json
#    "version": "0.1.0"  →  "version": "0.1.1"

# 2. Commit the version bump
git add packages/blog-sdk/package.json
git commit -m "chore(blog-sdk): release v0.1.1"
git push                                   # normal push, nothing publishes yet

# 3. Create the release tag on that commit
git tag blog-sdk-v0.1.1

# 4. Push the tag (separate command — git push alone doesn't push tags)
git push origin blog-sdk-v0.1.1            # this is what fires the workflow

About a minute later the new version lands on npmjs.com/package/chaingrow-blog.

Why tags and not branches?

Tags are immutable bookmarks pointing at a specific commit, and git treats them as a separate namespace from branches. Splitting the release trigger onto tags means:

  • Normal development pushes never accidentally publish
  • Every published version has a matching tag you can git checkout to see exactly what was shipped
  • Cutting a release is an explicit, deliberate action (git tag + git push origin <tag>), not a side effect of merging to main

Versioning rules

Follow semver:

| Change | Bump | |---|---| | Adding a new field to Blog (backwards-compatible) | patch (0.1.00.1.1) | | Adding a new exported function or component | minor (0.1.00.2.0) | | Changing the Block type union | major (0.1.01.0.0) — customer override maps will break | | Removing or renaming any exported symbol | major |

Fixing a mistake

If you tagged the wrong commit and it hasn't published yet:

git tag -d blog-sdk-v0.1.1                      # delete local tag
git push origin :refs/tags/blog-sdk-v0.1.1      # delete remote tag
# then re-tag correctly

If it already published: npm blocks unpublishing after 72 hours. The recipe is to deprecate the bad version and bump:

npm deprecate [email protected] "published in error — use 0.1.2"
# then cut a fresh release with the fix

License

MIT