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

@highjumpdigitalsoftware/blog-kit

v0.8.1

Published

Canonical, framework-agnostic blog component kit. One shared core (CSS tokens + .bk-* styles + manifest + behaviours) consumed by thin React (Next.js) and Astro adapters. The blog-publisher MCP reads core/manifest as its single source of truth, and sites

Downloads

163

Readme

blog-kit — shared, multi-framework blog component kit

One canonical component library that every blog (Next.js or Astro) is built from. A site supplies only its content (the MDX in its content repo) and its paint (theme.json); everything structural — component markup, CSS, behaviours, and the component manifest — lives here and is shared.

Canonical copy lives at /home/dan/blog-kit (its own standalone project — destined for its own highjumpdigital/blog-kit repo). It is vendored into each consumer at build time (a per-site sync-blog-kit.mjs copies core/ + the right adapter into the site). Edit the canonical copy; never hand-edit a vendored copy. Pin each site to a kit version.


Two ways to consume the kit

The kit serves both an in-house vendored path and an external installable-package path from one source of truth.

  1. Vendored (our own sites). sync-blog-kit.mjs copies core/ + the right adapter (react/ or astro/) into the site's src/_blogkit/, committed to the site repo. This is unchanged — see INTEGRATION.md. The blog-publisher MCP reads core/manifest/ as its single source of truth. On this path the site owns the MDX render options, so pass the kit's: options={ARTICLE_MDX_OPTIONS} (exported from @/_blogkit/react). It carries blockJS: false, without which next-mdx-remote strips every JSX-expression attribute and ~17 components silently render as nothing.

  2. Installable package @highjumpdigitalsoftware/blog-kit (external clients). The same core/ + react/ + astro/ ship as a published npm package with three extra abilities the vendored path doesn't need. The managed path is dual-framework: a first-class Next.js path AND a first-class Astro path — same content API, same swappable source, same eject.

    • A swappable content-source adapter — local (read .mdx from the repo) or hjd-api (fetch MDX from our per-tenant content API over Bearer auth). The two satisfy the same ContentSource interface (framework-neutral), so eject is a one-line source.mode flip on either framework.
    • Route surfaces + a connect/init CLI — <Article> (/blog/[slug]) and <BlogIndex> (/blog), source-agnostic, on both frameworks. npx @highjumpdigitalsoftware/blog-kit connect <pairing-code> (or init) scaffolds the routes + an optional revalidate route + writes config/env. The framework is taken from the exchange, else --framework next|astro, else sniffed from the project.
      • Next.js: App-Router ISR routes (export const revalidate), MDX rendered via next-mdx-remote/rsc. New posts appear with no redeploy via the ISR timer; instant via /api/blogkit/revalidate (revalidatePath).
      • Astro: SSR routes (output: 'server', prerender:false). Astro has no ISR and its MDX is build-time-only, so the remote MDX string is compiled at request time with @mdx-js/mdx + the kit's React component map → static HTML injected via set:html (identical markup to the .astro components, eject-safe). Freshness = SSR always-fresh + an in-process TTL cache (the ISR analogue), busted instantly by the scaffolded /api/blogkit/revalidate Astro endpoint. Works on a host like Replit with zero cooperation.
    • npm packaging — npm run build (tsc -p tsconfig.package.json + scripts/pkg-copy-assets.mjs) emits dist/; npm run selfcheck is a fast Node-only sanity check. The package source lives under package/ (package/astro/ = the Astro managed layer); the .astro route surfaces ship raw under astro/blogkit/. The existing core/ / react/ components / astro/ components / demo/ / manifest are untouched (the component maps were factored into react/components-map.ts, re-exported by react/index.ts — same public surface — so the Astro render path can import the map without pulling Next).
    # Next.js
    npm i @highjumpdigitalsoftware/blog-kit
    npm i next-mdx-remote remark-gfm rehype-slug rehype-autolink-headings rehype-pretty-code
    npx @highjumpdigitalsoftware/blog-kit connect <pairing-code>   # scaffolds Next ISR routes + writes config/env
    
    # Astro
    npm i @highjumpdigitalsoftware/blog-kit
    npm i @mdx-js/mdx react react-dom remark-gfm rehype-slug rehype-autolink-headings
    npx astro add node                              # an SSR adapter; set output:"server"
    npx @highjumpdigitalsoftware/blog-kit connect <pairing-code>   # scaffolds Astro SSR routes (framework from the exchange)
    // Next: app/blogkit.config.ts   ·   Astro: src/blogkit.config.ts
    import { defineConfig } from "@highjumpdigitalsoftware/blog-kit";        // Next
    // import { defineConfig } from "@highjumpdigitalsoftware/blog-kit/astro"; // Astro
    export default defineConfig({
      source: { mode: "hjd-api" },          // URL+token from BLOGKIT_CONTENT_URL / BLOGKIT_SITE_TOKEN
      brand:  { name: "Acme Co" },          // or { mode: "local", postsDir: "content/posts" } (ejected)
    });

    See the design doc hjd-app/docs/managed-blog-plan.md (§7 adapter, §8 connection/API/freshness, §9 eject). The Astro path's request-time MDX compile is the key difference from Next's ISR — both meet "new posts appear with no redeploy".


The article hero (managed path)

<Article> renders <ArticleHero> above the body: the post's featuredImage, category, reading time, title, description, author and date — one designed header instead of a bare <h1>.

It owns the page's <h1>. A host route must not print the title as well; the scaffolded route no longer does.

Three shapes, one markup (core/css/article-hero.css, all --blog-* tokens, so a tenant theme recolours it for free):

| shape | when | |---|---| | overlay (default) | post has a featured image — image band, scrim, copy over it | | stacked | article.heroStyle: "stacked" — image on top, copy below on paper | | plain | post has no featured image — copy on a tinted panel (automatic; heroStyle is ignored) |

// blogkit.config.ts
export default defineConfig({
  source: { mode: "hjd-api" },
  brand: { name: "Acme Co" },
  article: { heroStyle: "stacked", showBackLink: false },  // all optional
});

Two escape hatches, both above the config: pass hero to <Article> (Next) or fill its hero slot (Astro) to draw your own frame — yours then owns the <h1> — or set article.showHero: false for no hero at all.

Why it lives here: every managed post already carries featuredImage, and the kit already drew the listing card from it. Leaving the article header to each site meant a site that took the scaffold as-is shipped its posts with no header at all (0.6.x and earlier). npm run validate-hero proves the render, the three shapes, and React/Astro markup parity.

Upgrading an existing install to 0.7

The hero arrives with the package, but the route file in the site still has the old bare <h1> above <Article> — which would now render the title twice. Bump the kit, then re-stamp the routes from this version's templates:

npm i @highjumpdigitalsoftware/blog-kit@latest
npx @highjumpdigitalsoftware/blog-kit init --force   # rewrites the scaffolded routes; review the diff

The same command also adds the category + pagination routes (see below) if the install predates them. Check the output: it must say OVERWROTE … (and wrote … for the new routes). If it only says skip (exists), the package did not actually upgrade and you are running a pre-0.7 CLI, which had no --force — confirm with npm ls @highjumpdigitalsoftware/blog-kit. (Pre-0.7 CLIs ignore an unknown flag silently; 0.7.1+ rejects it and names its own version.) --force never touches blogkit.config.ts — your brand, paths and knobs survive — and leaves any file that already matches the template alone. A site that hand-edited its route should instead delete its own <h1> (and can copy the OG image block from templates/blogkit/app/blog/[slug]/page.tsx.tmpl).


The listing routes (managed path)

blog-kit init scaffolds the whole listing surface, not just /blog:

| route | what it is | |---|---| | /blog | page 1 — featured hero + card grid + category nav | | /blog/[slug] | the article (hero + body + tail) | | /blog/page/[page] | listing pages 2+ | | /blog/category/[category] | a category landing page | | /blog/category/[category]/page/[page] | category pages 2+ |

<BlogIndex> renders category pills and a pager, so the last three are not optional: without them every pill and every "Older →" link is a 404, which is what two live client sites shipped with before 0.7.

Each route meets templates/blog-index/STANDARD.md: a self-referencing canonical (never back to page 1), a unique <title>, /page/1 → the listing base, and 404 for an unknown category or an out-of-range page. Canonicals are absolute once you set brand.siteUrl in blogkit.config.ts, relative until then — the kit never invents an origin.

The routes are scaffolded under /blog. If listingBasePath is something else (Knightsbridge uses /news), move the scaffolded folder to match — the config drives every link the kit renders.

npm run validate-listing proves the selection logic, npm run validate-scaffold proves what the CLI writes, and scripts/validate-blog-index.mjs --root <site> [--base <url>] proves a real install (it now understands the managed layout as well as the vendored one).


The shape

core/                         framework-AGNOSTIC — the real "build once"
  css/
    tokens.css                --blog-* default theme, declared on .blog-root
    base.css                  scoped reset + prose typography (isolation layer)
    components.css            every component's .bk-* BEM styling
    index.css                 entrypoint: @imports the three above (in order)
  icons.ts                    shared inline SVG strings
  lib.ts                      shared prop logic (tone resolution, cx, …)
  behaviors/                  shared vanilla JS for interactive bits (data-bk-* hooks)
  manifest/
    components.json           SINGLE source of truth — the MCP reads this
    templates.json
    frontmatter.json
react/                        thin .tsx wrappers + blogComponents map (Next.js)
astro/                        thin .astro wrappers (Astro)
demo/index.html               static preview of components across themes

A component = look + wrapper. The look (HTML structure + CSS) is framework-agnostic and lives in core/. The wrapper is a ~10-line shim per framework that emits the agreed markup. Restyle = edit core/css once → every framework + every site updates. Add a component = CSS once + a tiny wrapper in react/ and astro/ + a manifest entry.


The build contract (every component MUST follow)

  1. Styling lives only in core/css/components.css as global .bk-<name> BEM classes. No Astro <style> blocks, no Tailwind, no inline style= with literal colours. This is what lets React and Astro render byte-identically.
  2. Every value keys off a --blog-* token (see tokens.css). Never hardcode a brand colour, font, radius, or shadow. A tenant re-skins by overriding tokens only.
  3. Everything renders under .blog-root. The base layer re-asserts inherited properties there, so the host page's CSS can't bleed in and the blog can't leak out. The only sanctioned inbound channel is the --blog-* token layer.
  4. Shared logic goes in core/lib.ts; shared markup (icons) in core/icons.ts; shared interactivity in core/behaviors/*.js (attached via data-bk-* attributes). Wrappers stay thin and identical across frameworks.
  5. Both adapters emit the same tag, classes, and structure. The React wrapper uses className + dangerouslySetInnerHTML; the Astro wrapper uses class + set:html + <slot/>. Nothing else should differ.
  6. Renamed components keep an alias. If a canonical name differs from what a live site already authored, add the old name to aliasComponents (React) / the Astro barrel and to the manifest entry's aliases, so existing published posts never break.
  7. Add a manifest/components.json entry (description, props, example, role, recommendedFor, pack, frameworks, aliases). The MCP surfaces this verbatim to authoring agents.
  8. If the component's content arrives as a non-string prop, declare it in react/prop-guard.tsx. Anything taking items / rows / posts / a score renders nothing when that prop is absent — which is the right behaviour on a client's live page and the reason a whole class of integration bug used to be invisible (see Behaviours, and blockJS: false in INTEGRATION.md). Listing it there turns "rendered nothing" into a console error naming the cause. List only the props that ARE the content: decorative extras like AuthorCard.links are deliberately excluded, because warning about those trains everyone to ignore the warning.
  9. If the component needs a behaviour, it mounts its own activator — <BehaviorMount /> in the React wrapper, the hoisted <script> in the Astro twin. Never leave activation to the site.

CalloutBox is the reference implementation of rules 1–7; ProseList (guarded) and TableOfContents (self-activating) cover 8 and 9.

Packs

  • core — universal components every site gets.
  • capture / hjd — domain packs (review/SaaS vs SEO-agency) enabled per site via the MCP's components allowlist.
  • per-site custom — code that lives in the site repo's overlay, merged into the render map; not in this kit.

Behaviours — automatic

The interactive components (TableOfContents, FAQList, ComparisonTable, CodeSnippet, KeywordTable, DeliveryComparison) render markup that framework-neutral vanilla JS in core/behaviors/*.js completes on the client. They activate themselves — there is no per-site wiring. Each component carries its own activator (react/BehaviorMount.tsx on the React side, a hoisted module <script> on the Astro side), so a component works wherever it is rendered: the kit's <Article>, a hand-rolled route, a one-off page.

This used to be a manual step and it was the kit's sharpest edge, because forgetting it failed silently. <TableOfContents/> is the extreme case — its <ul> is empty in markup and the behaviour is its content — so a route that never mounted the activator published a permanently empty "In this article" box. It looked correct in every preview (previews mount the activator), so it could sit live for weeks. Self-activation removes the step instead of documenting it harder.

<BlogBehaviors /> and BlogBehaviors.astro remain exported and working; existing sites need no change. Behaviours are idempotent, so activating twice costs one extra querySelectorAll.

Activation is always explicit — a behaviour module never starts itself. It used to, and inside a bundled React app that was a bug: the client chunk evaluates before React hydrates, so the TOC appended <li>s to a <ul> React believed was empty and the FAQ rewrote attributes, failing hydration (React #418) and forcing that subtree to be thrown away and re-rendered on the client. Fixed in 0.8.1. For an unbundled page with no framework, import core/behaviors/auto.js, which is the one file that starts them implicitly.

Two things the TableOfContents behaviour handles on its own, so no pipeline config is required:

  • Scope — it scans the prose region containing it (.bk-prose → .blog-root, falling back to body), so a site needn't wrap the body in any particular tag.
  • Anchors — it mints ids for headings that have none, matching what rehype-slug would have produced. Server-rendered ids are still nicer (linkable from the first paint) but no longer a precondition for the entries to work.

Theming

A tenant's theme.json (in its content repo) is compiled to a stylesheet that overrides --blog-* tokens on .blog-root, loaded after core/css/index.css. Only brand colours + font families change; structure stays shared. See demo/index.html for the same components under the Capture (orange) and HJD (cyan) themes.