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

@timholtweb/cms-core

v2.9.0

Published

Shared Google Sheets + Google Docs CMS platform for Astro content collections -- reconciled from astro-composer-starter-v2.0 and timholtmusic.com.

Readme

@timholtweb/cms-core

A reusable Google Sheets/Docs → Markdown content-management engine for static site projects. It syncs structured rows from a Google Sheet (and, optionally, long-form body content from linked Google Docs) into real, git-tracked Markdown files, tracks identity so repeated syncs update rather than duplicate content, and refuses to publish a change that looks like it would silently destroy existing content.

What cms-core is responsible for

  • Reading rows from a Google Sheet and normalizing them via a project's own adapters
  • Generating Markdown frontmatter (and, optionally, resolving a linked Google Doc as the entry's body) from each normalized row
  • Tracking content identity across syncs (lib/identity-map.mjs), so a second sync of the same row updates the same file instead of creating a duplicate
  • Verifying a proposed change against the current, real content before writing it, and refusing to publish automatically if the change looks like it would remove existing content (lib/verify.mjs, the information-loss safeguard in lib/sync.mjs)
  • Validating a project's real readiness to publish -- cms doctor -- including Sheet/adapter column agreement, a project-identity guardrail, and a deterministic, fail-safe scan of whether an adapter's declared outputFields actually cover what a real Astro content.config.ts schema requires (lib/schema-scanner.mjs)
  • Optionally writing a real, plain-language sync result directly into the same Google Sheet row that was processed (lib/row-status.mjs), so a nontechnical editor can see the outcome without checking a terminal or GitHub Actions
  • A CLI (cms) tying all of the above together: sync, verify, check, doctor, publish, audit, adopt

Drive-managed images (imageSources)

An adapter can optionally declare one or more imageSources entries to let a client associate a Google Drive image with a row through the Sheet, instead of (or alongside) manually-authored image entries. This section documents the actual, current implementation -- not an aspirational future API.

Current scope: array fields only. imageSources resolves into a list-shaped frontmatter field (e.g. images: [...], or any other array field an adapter's schema defines). It does not currently support a single-image field (a scalar object like cover or headshot) -- that would be a materially different capability, not yet built.

Declaring an image source

// A generic adapter, not tied to any specific collection name:
export const someAdapter = {
  // ...
  imageSources: [
    {
      fieldKey: "images",              // the frontmatter array field this populates
      sheetLinksColumn: "Image Uploads",
      sheetAltColumn: "Image Uploads Alt",
      sheetCaptionColumn: "Image Uploads Caption", // optional
    },
  ],
};

fieldKey can be any name the adapter's own schema uses -- images, photos, or anything else. Nothing in cms-core hardcodes a field name; the adapter's declaration is the sole source of truth, including for the managed-removal safety exemption described below.

Identity and provenance

  • Drive file ID is the canonical external identity -- stable across a Drive-side rename, never reused for a different file.
  • Drive filename is editorial/human metadata -- validated (descriptive, lowercase, hyphen-separated, letters/numbers/hyphens, a real image extension), but never invented or auto-sequenced by the CMS. An invalid filename fails the row with an actionable message; the CMS never silently renames or suffixes it.
  • Local asset path is the stable production representation, derived once, at first import, directly from the validated Drive filename. It never changes on a later Drive-side rename.
  • Provenance (.cms/identity-map.json's media map, keyed by Drive file ID) records the relationship between the Drive identity and the local asset -- collection, field, filename, local path, and a content hash of the current bytes.
  • Duplicate-path safety: two distinct Drive files may never claim the same local path. A real collision fails the row rather than silently suffixing a filename.

Sync behavior

  • First import: downloads the file, validates the filename, writes it to src/assets/images/<collection>/<filename>, and records provenance.
  • Subsequent sync, unchanged file: reuses the existing local path and skips re-downloading.
  • Drive-side rename: does not move, rename, or duplicate the local asset. The local path is permanent once established; only the informational driveFilename metadata updates.
  • Drive-side byte replacement (same file, same name): detected via a content-hash fingerprint independent of any visible frontmatter field, so a pure content swap still registers as a real change and triggers a real re-download -- not a false no-op.
  • Reorder / add / remove within a list: existing entries keep their identity; reordering never renames or re-downloads anything.

Removal semantics (a platform invariant)

The Sheet controls whether an image is currently referenced and published. Drive remains the source asset. Clearing a Sheet association removes the image's reference from the published content, but never automatically deletes the corresponding local asset from the repository.

Re-adding the same Drive file afterward reuses its existing provenance and local asset -- it is not treated as a new import.

The information-loss safety net and the managed-removal exemption

cms-core's existing, generic information-loss protection (lib/verify.mjs) blocks any sync where a field would go from populated to empty, regardless of collection or field name -- proven elsewhere on this platform to catch real accidental data loss (e.g. a cleared date field). Deliberately clearing a Drive-managed image association produces exactly that shape of diff, so a second, narrowly-scoped mechanism (lib/managed-removal.mjs) tells the two cases apart:

  • A wipeout is exempted from information-loss only when every single vanished entry is provably Drive-managed, for that exact field (as declared in the adapter's own imageSources), for that exact collection.
  • If even one vanished entry is a manually-authored image the CMS has no provenance record for, the removal stays blocked -- a manual-only removal, or a mixed manual-and-Drive removal, is still treated as a real risk requiring human review.
  • Collections with no imageSources declared receive no exemption at all -- the original, strict behavior is completely unchanged for them.

Transactionality

Image resolution happens before markdown generation, inside the same staged, all-or-nothing sync sync.mjs already performs for content: images are written to a staging mirror, promoted alongside markdown only after validation/build succeeds, and rolled back entirely on failure or --dry-run. A failed sync never leaves a partial mix of promoted images and un-promoted content, or vice versa.

Manual and Drive-managed images coexist

An adapter's existing, manually-authored image entries (however that adapter already parses them from its own Sheet column) are never replaced -- Drive-resolved entries are appended alongside them. Both kinds of entries share the exact same frontmatter shape; nothing downstream needs to know which kind an entry is.

What this capability deliberately does not do

To avoid the wrong assumption that these are missing bugs: as of this writing, imageSources does not provide inline body-image authoring, a generalized gallery UI beyond a plain list, automatic filename generation, Drive itself as a production asset host (Drive is only ever a one-time source; the committed local file is what the site actually serves), automatic local-asset deletion, or a universal caption/credit content model (caption/credit are optional, adapter-declared fields, not guaranteed across every collection). A single-image field (a scalar, not a list) is also not yet supported -- see the scope note above.

What cms-core is NOT responsible for

cms-core has no knowledge of Astro, page routing, JSON-LD, taxonomy presentation, theme tokens, or site templates. That's the Astro Portfolio Starter Kit's job. A project's real Astro schema (content.config.ts) and cms-core's adapters must agree with each other (the schema scanner above helps catch when they don't), but cms-core never generates or owns that schema itself.

Relationship to the Astro Portfolio Starter Kit

The Starter Kit is a real, working Astro site template that consumes cms-core as a dependency. It owns everything cms-core deliberately doesn't: page routing, site presentation, JSON-LD builders, taxonomy/ browse pages, theme configuration, and the actual rendered site. A project built on the Starter Kit declares its own adapters (one per content collection) and registers them in a real cms.config.mjs at the project root -- cms-core reads that file and the adapters it points to; it has no built-in knowledge of any specific project's collections.

Installation

Consuming projects currently install this as a local, packed dependency:

{
  "dependencies": {
    "@timholtweb/cms-core": "file:vendor/timholtweb-cms-core-2.6.0.tgz"
  }
}

Basic CLI usage

Run via npx cms <command> from a project that has a real cms.config.mjs at its root:

npx cms doctor                          # is this project ready to publish?
npx cms doctor --key <path> --sheet-id <id>   # same, including live Sheet checks
npx cms publish <collection> --live --key <path> --sheet-id <id> --dry-run
npx cms publish <collection> --live --key <path> --sheet-id <id>
npx cms check --generate                # (re)generate the integrity manifest
npx cms adopt <collection> --live --key <path> --sheet-id <id>
npx cms audit <collection>

See a real project's own PLATFORM-ARCHITECTURE.md (in the Starter Kit) for the complete adapter contract and setup walkthrough -- cms-core itself has no project-specific setup docs, since it has no opinion about what a project's collections should be.

Testing

CI workflow template

.github-workflow-cms-publish.yml is a template, meant to be copied into a consuming project as .github/workflows/cms-publish.yml -- it is not this package's own CI configuration. It defines the real GitHub Action a project uses to run cms publish against a real Sheet on demand (e.g. triggered from an Apps Script menu). Set the real GDRIVE_SERVICE_ACCOUNT_KEY secret in the consuming repository before using it; the git identity it commits as is a neutral placeholder ([email protected]) meant to be changed per project, not a real, shared account.

npm test

Runs the real, permanent regression suite (node --test test/*.test.mjs) -- currently 22 tests covering row-level status feedback, the schema scanner, and the manual-promotion identity round trip.

Versioning and release philosophy

Every real version bump in this package corresponds to a real, evidenced need found while building an actual project on top of it -- not speculative hardening. See CHANGELOG.md for the full, honest history of what each version fixed and why, including several entries that describe a real bug found on a real project before being fixed here.

Current version

2.6.0