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

@writbee/next

v0.2.0

Published

Writbee for Next.js: cached server reads, a browser client, and a build-time guard around the secret key.

Readme

@writbee/next

Writbee for Next.js. Two entry points, drawn along the line that matters: the server reads with your secret key through Next's cache, the browser reads with a publishable key. Mixing them up fails the build instead of leaking the key.

npm install @writbee/next

Server

# .env.local
WRITBEE_API_URL=https://api.writbee.com/api
WRITBEE_SECRET_KEY=sk_...
// app/blog/page.tsx
import { createServerClient } from "@writbee/next"

export default async function BlogPage() {
  const posts = await createServerClient().getPosts()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>{post.title}</li>
      ))}
    </ul>
  )
}
// app/blog/[slug]/page.tsx
import { createServerClient } from "@writbee/next"
import { notFound } from "next/navigation"

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await createServerClient().findPost(slug)

  if (!post) notFound()

  return <article dangerouslySetInnerHTML={{ __html: post.content ?? "" }} />
}

getPosts() returns the first page — 20 posts, or up to 100 with { limit }. For a sitemap or anything that needs the whole archive, walk the cursor with allPosts(), which holds one page at a time:

// app/sitemap.ts
import { createServerClient } from "@writbee/next"
import type { MetadataRoute } from "next"

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const entries: MetadataRoute.Sitemap = []

  for await (const post of createServerClient().allPosts({ limit: 100 })) {
    entries.push({
      url: `https://example.com/blog/${post.slug}`,
      lastModified: post.published_at,
    })
  }

  return entries
}

listPosts({ limit, cursor }) is the same thing one page at a time, returning { posts, nextCursor } — see the @writbee/core README for how cursors behave.

Responses are cached for 60 seconds by default and tagged writbee:posts. Pass revalidate and extra tags to change that, or baseUrl / apiKey to bypass the environment:

createServerClient({ revalidate: 3600, tags: ["blog"] })
createServerClient({ revalidate: false }) // cached until the tag is revalidated

Revalidating on publish

revalidate: false plus a webhook is the setup worth having: pages stay static until a post actually changes.

// app/api/writbee/revalidate/route.ts
import { revalidateWritbee } from "@writbee/next"

export async function POST(request: Request) {
  if (request.headers.get("x-writbee-secret") !== process.env.WRITBEE_WEBHOOK_SECRET) {
    return new Response("Unauthorized", { status: 401 })
  }

  revalidateWritbee()

  return Response.json({ revalidated: true })
}

Browser

The key and URL are passed in rather than read from the environment, on purpose: Next inlines NEXT_PUBLIC_* variables into your app's own code at build time, and that does not extend to a package inside node_modules. Read them in your app, where it works, and hand them over.

"use client"

import { createBrowserClient, type Post } from "@writbee/next/client"
import { useEffect, useState } from "react"

export function LatestPosts() {
  const [posts, setPosts] = useState<Post[]>([])

  useEffect(() => {
    const writbee = createBrowserClient({
      baseUrl: process.env.NEXT_PUBLIC_WRITBEE_API_URL!,
      apiKey: process.env.NEXT_PUBLIC_WRITBEE_PUBLISHABLE_KEY!,
    })

    const controller = new AbortController()

    writbee
      .getPosts({ signal: controller.signal })
      .then(setPosts)
      .catch(() => {})

    return () => controller.abort()
  }, [])

  return <ul>{posts.map((p) => <li key={p.slug}>{p.title}</li>)}</ul>
}

Create the client inside the component rather than at module scope. A client component's module is also evaluated while Next prerenders on the server, so a missing NEXT_PUBLIC_ variable would fail the build there — far from where you would look for it.

Only a publishable (pk_) key belongs here, and it must list your domain in the dashboard — the API enforces that per key, through CORS. There is no caching on this side, so prefer the server entry whenever the data can be read on the server.

This package exports functions, not components, so it carries no "use client" directive: call it from your own client component and it works.

The guards

@writbee/next imports server-only and @writbee/next/client imports client-only. Import the wrong one and the build fails with a clear message, instead of your sk_ key quietly reaching the browser. That is the main thing this package adds over calling @writbee/core yourself.

Errors, methods and the Post type are re-exported from @writbee/core — see its README for the error codes.

Notes

  • Requires Next 14 or newer (App Router), and is ESM only.
  • sideEffects: false is deliberately absent: it would let a bundler drop import "server-only", removing the guard exactly where it is needed.