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

@growth-labs/cms

v0.1.2

Published

The Fronts-proven publishing/admin engine, packaged for reuse. `@growth-labs/cms` is a versioned library each site installs and parameterizes with its own `D1Database`, R2 media bucket, authz, and theme tokens — **not** a shared runtime engine. See [`cms-

Readme

@growth-labs/cms

The Fronts-proven publishing/admin engine, packaged for reuse. @growth-labs/cms is a versioned library each site installs and parameterizes with its own D1Database, R2 media bucket, authz, and theme tokens — not a shared runtime engine. See cms-reuse-decision.md for the package-not-engine ruling.

Status: schema contract + publishing engine + API routes. This package ships the D1 schema contract and its TypeScript types (the WS7-04/06 foundation), the publishing engine — the content data-access core (WS7-05) — and the API route handlers (@growth-labs/cms/routes, WS7-07): the /api/v1/publisher/* content/authors/media/cron surface as framework-agnostic mountable factories with auth/authz injected. The admin integration mounts multipart podcast uploads at /admin/api/media/podcast/{init,part,complete,abort}.

What's here

  • The D1 contract — the 11-table content/revision/media/authors/foundry-callback schema, verified against prod fronts-data 2026-05-29: content_items, article_content, video_content, podcast_content, content_revisions, content_tags, content_tag_links, content_relations, media_assets, authors, foundry_callback_events. content_items.type accepts article, video, podcast, newsletter, and page; article, newsletter, and page body rows are stored in article_content.
  • Row types — a TypeScript interface per table, with the CHECK domains encoded as union types.
  • Migration helpers — the SQL as a wrangler-applicable file plus an inlined constant for tests/tooling.
  • The publishing engine (WS7-05) — the content data-access core, exported from the package root and the @growth-labs/cms/engine subpath: content CRUD
    • lifecycle (createContent, updateContentItem, scheduleContent, publishContent, unpublishContent, duplicateContentItem), revisions (createRevision, getContentSnapshot), tags/relations, ensureUniqueSlug, countWords/estimateReadTime, the publish-validation guard, the scheduled-publish sweep (runScheduledPublish), the article-body validator, and the content-integrity sanitize passes. Every function takes an injected D1Database (the site supplies its own binding) — no Fronts coupling.

The contract excludes identity/entitlement tables (users, subscription*, *_entitlements, gl_identity_links, CRM, mailer) and the analytics-owned gl_content_progress. Those belong to other workstreams — pulling them in is the monolith-creep this boundary guards against.

Intentional schema divergences (do not normalize)

  • media_assets.created_at/updated_at are TEXT datetime('now'), while the content tables use INTEGER unixepoch(). This matches prod and the engine code; normalizing it breaks parity.
  • video_content uses video_id (renamed from stream_uid in migration 0018). Do not reintroduce stream_uid.
  • content_items.type CHECK includes newsletter (added in prod 0024) and page. The full domain is article, video, podcast, newsletter, and page.

Apply the schema

The supported adoption path is wrangler, against the package's migrations/:

wrangler d1 migrations apply <YOUR_D1_BINDING>

For tests and provisioning tooling without disk access, the same statements are available programmatically:

import { applyMigrations, CMS_TABLES } from '@growth-labs/cms/schema'

await applyMigrations(env.SITE_DB) // tests / provisioning only — never request-time

Use the types

import type { ContentItemRow, MediaAssetRow } from '@growth-labs/cms'

Use the engine

The engine functions take the site's own D1Database as the first argument:

import { createContent, publishContent, runScheduledPublish } from '@growth-labs/cms'

const { id } = await createContent(env.SITE_DB, {
  type: 'article',
  slug: 'hello-world',
  title: 'Hello, world',
  content: { bodyMarkdown: '# Hello\n\nFirst post.' },
})
await publishContent(env.SITE_DB, id, Math.floor(Date.now() / 1000))

// In a scheduled handler:
await runScheduledPublish(env.SITE_DB)

primaryCategory (stored in content_items.channel) is site-supplied taxonomy; it defaults to 'analysis' (the Fronts default) when omitted.

The duplicateContentItem column-alignment gotcha

duplicateContentItem does four INSERT…SELECT copies whose explicit column lists are hand-maintained and must stay positionally aligned to the schema. created_at/updated_at are deliberately omitted so the copy gets fresh DEFAULT (unixepoch()) timestamps; the duplicate is forced to status='draft', featured=0, null publish timestamps/revision, and a video copy is forced to processing_state='ready' (NOT re-queued to Foundry). Any future content_items column addition requires a matching edit in duplicateContentItem or the duplicate silently drops the new field.

Use the routes

The route handlers are framework-agnostic factories at the @growth-labs/cms/routes subpath. Each handler is a pure (ctx: RouteContext) => Promise<Response>; the consuming site supplies a thin adapter that maps its request + bindings + its own authz implementation onto a RouteContext. The package never decides who is an admin/publisher — it asks the injected authz guard, so no site's context.locals.user / ADMIN_EMAILS is baked in.

import { createCmsRoutes } from '@growth-labs/cms/routes'

const routes = createCmsRoutes({
  authz: {
    requireAdmin: (ctx) => (siteSaysAdmin(ctx) ? null : unauthorized()),
    requirePublisher: (ctx) => (siteSaysPublisher(ctx) ? null : unauthorized()),
  },
  // theme tokens default to the Fronts values when omitted:
  // mediaPublicDomain: 'media.fronts.co', mediaR2Binding: 'PUBLIC_MEDIA',
  // mediaSiteId: 'fronts', publishTimezone: 'Europe/Paris', …
})

// In an Astro route (the site's adapter builds `ctx` from its APIContext):
export const POST = (c) => routes.content.create(toRouteCtx(c))

The handler groups: routes.content (list/create/get/update/action — the action endpoint covers schedule/publish/regenerate-takeaways/unpublish/archive/ unschedule/duplicate), routes.authors (list/create/get/update), routes.media (uploadImage/listLibrary/libraryAction/uploadPodcast plus init/part/complete/abort multipart flows for both podcast audio and video source uploads), and routes.cron (publish — gated by requireAdmin).

The admin UI includes Social Share. Content selection fills destination URLs from the browser origin so URL inputs receive absolute links, and Author Social requires the selected author but does not require a social handle when the operator is minting a link for that author to post themselves.

The article-takeaways publish dispatch (Foundry/Hermes) is an OPTIONAL injected dispatchTakeaways hook on the content config — the package carries no Foundry coupling. Omit it and publish simply skips queuing takeaways.

Media transcription dispatch is also injected. When the host supplies the Foundry media hook, dispatch-to-foundry works for video and podcast content, and podcast create/update/publish automatically queues processing when podcast_content.audio_r2_key is present and the row has neither a processing_trigger_token nor a transcript. Foundry callbacks matched by processing_trigger_token update video processing state or, for podcasts, persist transcript, duration, description, and excerpt fields when the callback payload includes them.

Sites with publish prerequisites can inject publishReadiness on the content config. The hook receives { ctx, contentId, type, status } before the generic publish lifecycle runs. Returning { ready: false, blockers, message } blocks the manual publish with a 409 and leaves the content status unchanged; throwing from the hook fails closed with a 503. This keeps media-specific readiness checks in the host site while preventing a configured site from bypassing them.

Coming in later waves

  • Broader admin UI polish and per-site extension points beyond the shipped Masthead screens.