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

@vulcantech/blog

v0.1.1

Published

Server-side blog fetch layer for VulcanTech consumer sites — ISR-tagged reads from the CMS, with signed draft-preview support. Shared so each site doesn't duplicate the fetch code.

Readme

@vulcantech/blog

Server-side blog fetch layer for VulcanTech consumer sites. Reads published posts from the CMS with Next.js ISR tags, and supports signed draft-preview tokens so unpublished posts can be reviewed on the live site. Shared so each site stops duplicating lib/cms/blog.ts.

Exports: fetchBlogPosts, fetchBlogPostBySlug, resolvePreviewToken, GET (the draft-preview route handler), tagsForBlogList / tagsForBlogPost, and the CMSBlogPost / CMSBlogAuthor types.

Server-only — uses VULCANTECH_API_KEY; import from server components / route handlers only.

Integration

Wire blog into a consumer site (Next.js App Router):

1. Add the dependency:

npm install @vulcantech/blog

(Inside the VulcanTech monorepo, use the workspace wiring in MONOREPO.md instead.)

2. Set the env — see Required env below.

3. (Optional) thin re-export src/lib/cms/blog.ts to preserve @/lib/cms/blog import paths:

export * from "@vulcantech/blog";

4. Blog pages — list + single post (server components):

import { fetchBlogPosts, fetchBlogPostBySlug, resolvePreviewToken } from "@vulcantech/blog";

// app/blog/page.tsx
const posts = await fetchBlogPosts();

// app/blog/[slug]/page.tsx — resolve the preview token (Draft Mode) and pass it through
const previewToken = await resolvePreviewToken();
const post = await fetchBlogPostBySlug(slug, { previewToken });

In generateMetadata, prefer the post's derived keywords string (the CMS builds it from the post's dedicated SEO keyword fields — target keyword first) and fall back to tags:

keywords: post.keywords || (post.tags?.length ? post.tags : [post.title, "Brand"]),

5. Draft-preview route src/app/api/preview/route.ts — one line, shared implementation:

export { GET } from "@vulcantech/blog";

The CMS mints a signed token and links to /api/preview?token=…&slug=…; the handler validates it, enables Next Draft Mode, and redirects to the post. Reading the token opts the render dynamic — keep the public [slug] route static/ISR and let Draft Mode switch it.

6. ISR invalidation — ensure src/app/api/revalidate/route.ts exists; the CMS purges blog:{projectId} (list) and blog:{projectId}:{slug} (single) on edit.

Scoped routes (routePrefix)

Posts carry an optional routePrefix that decides which URL space they live in on the consumer site: "blog"/blog/<slug> (the default; legacy posts with no prefix count as blog), "legal"/legal/<slug>, "documentation"/documentation/<slug>, etc. Editors set it per-post in the CMS dashboard. One Firestore collection powers all of them — the split happens at fetch/render time.

Wiring a scoped route (e.g. documentation) takes three pieces:

1. A scope predicate — keep /blog clean. fetchBlogPosts() returns every published post regardless of prefix, so the blog list/related-posts must filter, or scoped posts leak onto /blog. Centralize the predicate in one module (e.g. src/lib/cms/postScope.ts) instead of scattering !== "legal" checks:

export const BLOG_ROUTE_PREFIX = "blog";
/** Legacy posts predate the field, so no prefix also means "blog". */
export function isBlogPost(post: { routePrefix?: string }): boolean {
  return !post.routePrefix || post.routePrefix === BLOG_ROUTE_PREFIX;
}

New prefixes are then excluded from /blog automatically, with no further edits.

2. The scoped pages. List pages fetch with the prefix; the [slug] page must ALSO guard the prefix so a blog slug can never render on the scoped route:

// app/documentation/page.tsx
const docs = await fetchBlogPosts({ routePrefix: "documentation" });

// app/documentation/[slug]/page.tsx
const post = await fetchBlogPostBySlug(slug);
if (!post || post.routePrefix !== "documentation") notFound();

generateStaticParams should use the same fetchBlogPosts({ routePrefix }) call.

3. Revalidation. Nothing extra — the CMS purges the same blog:{projectId} tags on edit, so scoped pages revalidate with the blog as long as they use this package's fetchers (which attach those tags).

Required env

VULCANTECH_PROJECT_ID, VULCANTECH_API_KEY (required) · VULCANTECH_CMS_URL (optional, default https://cms.vulcantech.io) · VULCANTECH_BLOG_NO_CACHE (optional, 1/true to bypass ISR while debugging).

Per-app values

None beyond env — all blog logic is shared.