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

@bettercms-ai/astro

v0.14.4

Published

The BetterCMS adapter for Astro — a `bettercms()` integration, a `bettercms:client` virtual module, typed content loaders, draft preview, and native .astro rendering components.

Readme

@bettercms-ai/astro

The BetterCMS adapter for Astro — a bettercms() integration, a bettercms:client virtual module, typed content loaders, draft preview, and native .astro rendering components. The Astro equivalent of @sanity/astro.

Install

npm install @bettercms-ai/astro

Setup

// astro.config.mjs
import { defineConfig } from "astro/config";
import bettercms from "@bettercms-ai/astro";

export default defineConfig({
  integrations: [
    bettercms({
      apiUrl: "https://api.bettercms.ai", // or PUBLIC_BCMS_API_URL
      workspace: "my-workspace",          // or PUBLIC_BCMS_WORKSPACE
      // projectId, studioUrl, mediaUrl optional
    }),
  ],
  output: "server", // required for draft mode + live reads
});

Environment variables

Every public value resolves in this order: bettercms({ ... }) option, then env, then the bcms-content.json snapshot the deploy workflow writes before the build.

| Variable | Option | Read | Default / notes | | --- | --- | --- | --- | | PUBLIC_BCMS_API_URL | apiUrl | config time | Required (option, env or snapshot) | | PUBLIC_BCMS_WORKSPACE | workspace | config time | Required (option, env or snapshot) | | PUBLIC_BCMS_PROJECT_ID | projectId | config time | Optional project scope for forms | | PUBLIC_BCMS_MEDIA_URL | mediaUrl | config time | https://cdn.bettercms.ai | | PUBLIC_BCMS_STUDIO_URL | studioUrl | config time | Unset. <BcmsVisualEditing> falls back to https://bettercms.ai | | BCMS_API_KEY | none | server runtime | content:read, or content:read:draft for draft previews. Never bundled | | BCMS_DRAFT_SECRET | none | server runtime | Signs the draft cookie. Runtime process.env wins over a build-time value. Without it /api/bcms/draft/enable returns 500 |

The BetterCMS-generated deploy workflow sets PUBLIC_BCMS_API_URL, PUBLIC_BCMS_WORKSPACE and PUBLIC_BCMS_PROJECT_ID for the build. Its repo secret BCMS_API_KEY reaches only the content-fetch step, never the build. On your own host, set the two server variables yourself.

This package ships no revalidation route. Point the project's revalidation webhook (Project → Settings) at your rebuild hook. Its secret (the dashboard calls it BETTERCMS_WEBHOOK_SECRET) signs the body as x-bettercms-signature: sha256=<hmac>.

Add the virtual-module + Astro.locals types to src/env.d.ts:

/// <reference types="@bettercms-ai/astro/env" />

Reading content

---
import { loadPage, loadForms } from "bettercms:client";
import BcmsBlocks from "@bettercms-ai/astro/components/BcmsBlocks.astro";

const page = await loadPage(Astro, "home");
const { items: forms, turnstileSiteKey } = await loadForms(Astro);
---
{page && <BcmsBlocks blocks={page.blocks} forms={forms} turnstileSiteKey={turnstileSiteKey} />}

Loaders take Astro so they automatically read drafts when draft mode is on: loadPage, loadEntry, loadEntries, loadForms. The raw client and getClient(Astro) are exported too.

SEO — per-page <head>

loadPage() returns the page's metaTitle, metaDescription, and the rich metaJson (OG / Twitter / canonical / JSON-LD) edited in the dashboard's SEO panel. Resolve them page-over-site with resolveSeo and map the result into your <head> (same precedence as the live *.bettercms.site renderer):

---
import { loadPage } from "bettercms:client";
import { resolveSeo, type SiteSeoDefaults } from "@bettercms-ai/astro";

const { slug } = Astro.params;
const page = await loadPage(Astro, slug ?? "home");

const siteDefaults: SiteSeoDefaults = {
  metaDescription: "Selected work, experience, and contact information.",
  ogImage: "https://example.com/og-default.png",
  twitterHandle: "@acme",
};

const seo = page ? resolveSeo(page, siteDefaults) : null;
---
{seo && (
  <Fragment slot="head">
    <title>{seo.title}</title>
    {seo.description && <meta name="description" content={seo.description} />}
    {seo.canonical && <link rel="canonical" href={seo.canonical} />}
    {seo.og.title && <meta property="og:title" content={seo.og.title} />}
    {seo.og.description && <meta property="og:description" content={seo.og.description} />}
    {seo.og.image && <meta property="og:image" content={seo.og.image} />}
    <meta property="og:type" content={seo.og.type} />
    <meta name="twitter:card" content={seo.twitter.card} />
    {seo.twitter.image && <meta name="twitter:image" content={seo.twitter.image} />}
    {seo.twitter.site && <meta name="twitter:site" content={seo.twitter.site} />}
    {seo.jsonLd.map((node) => (
      <script type="application/ld+json" set:html={JSON.stringify(node)} />
    ))}
  </Fragment>
)}

Reads are cached; a SEO edit appears on the next fetch. For instant refresh on publish, configure the project's revalidation webhook (Project → Settings) against your rebuild or on-demand-revalidation hook.

Components

| Component | Purpose | | --- | --- | | @bettercms-ai/astro/components/BcmsBlocks.astro | Render a page's blockJson — every block type the builder can author (heading, text/richtext, image, button, spacer, video, columns, form, section, navbar, footer, slider, tabs, component), with block style tokens applied. | | @bettercms-ai/astro/components/BcmsForm.astro | Render + submit a form (conditional fields, honeypot, Turnstile). | | @bettercms-ai/astro/components/BcmsImage.astro | Optimized image with a 1x/2x srcset via the media transform endpoint. | | @bettercms-ai/astro/components/BcmsLiveBlocks.astro | BcmsBlocks as a hydrated island that lets the Visual Editor re-render your DRAFTS with your own components and CSS. See below. |

Markup is class-driven and unstyled — you own the CSS.

Phone fields with a country code picker

A phone field whose author enabled Country code picker in the builder renders a country control beside the number. Whatever the visitor picks, the field still submits one value, under its own key, in E.164 (+14155550123) — there is no second key to handle.

Two classes are yours to style:

| Class | Element | |---|---| | bcms-phone | the wrapper around the country control and the number | | bcms-phone-country | the country control itself |

The country control is a native <select> built at build/SSR time, so no phone library reaches the visitor's browser. The trade is that the number is not reformatted as it is typed — the value is still composed correctly on submit. @bettercms-ai/next makes the other choice and ships a formatter in a lazy chunk.

Live editing on a static site — <BcmsLiveBlocks>

When the Visual Editor frames a static build it can only show what was built, so editing a draft falls back to the platform's approximate renderer and the canvas stops looking like your site. <BcmsLiveBlocks> fixes that for pages whose body is a block tree: it renders the same markup <BcmsBlocks> does, but as a client:load island that accepts draft documents from the editor over postMessage (same-origin, parent frame only, and only after the editor's own handshake — a third-party page that iframes your site is on a foreign origin and gets nothing). Nothing is fetched by the page and no credentials enter it.

It needs a JSX renderer, which a component cannot add for you:

npm i @astrojs/preact preact
// astro.config.mjs
import preact from "@astrojs/preact";
export default defineConfig({ integrations: [bettercms(), preact({ compat: true })] });
---
import BcmsLiveBlocks from "@bettercms-ai/astro/components/BcmsLiveBlocks.astro";
const page = await getPage(Astro.params.slug);
const { forms } = await readForms();
---
<BcmsLiveBlocks slug={page.slug} blocks={page.blocks} forms={forms} />

compat: true is required — the island reuses the React <BcmsDraftBridge> from @bettercms-ai/next so both SDKs speak one protocol. Roughly 10 kB of Preact plus the block renderer; @astrojs/react works too if your project already uses it.

⚠️ It emits the React class names, not this package's. <BcmsBlocks> from @bettercms-ai/next classes its output bcms-block bcms-block--text, bcms-button--primary, bcms-column; BcmsBlocks.astro above classes the same tree bcms-text, bcms-btn-primary, bcms-cols. They are NOT interchangeable: a stylesheet written for the .astro grammar styles nothing on this island, so swapping a page over unstyles it. Cover both spellings — the starters wrap them in one :is() selector for exactly this reason — or take the class list from @bettercms-ai/next's src/blocks.tsx.

It only helps pages you render through BcmsBlocks. A hand-written .astro layout, or content pulled into your own components, is invisible to the bridge — for those, structural draft preview needs a server-rendered draft route instead (output: 'server' + the draft-mode routes below).

Draft mode

The integration injects /api/bcms/draft/enable?token=<jwt>&redirect=/path and /api/bcms/draft/disable. Generate the preview-token link from the dashboard; visiting enable validates the token against the backend, sets a signed cookie, and subsequent loads return draft content. Disable with the disable route.

Two draft lanes

| Lane | Build | How the canvas shows a draft | | --- | --- | --- | | SSR | output: "server" + the draft routes above + BCMS_DRAFT_SECRET + a content:read:draft BCMS_API_KEY | The loaders read the draft on every request, so hand-written layouts preview too | | Static | Any static build, with the page body rendered through <BcmsLiveBlocks> | The editor pushes the draft block tree to the island over postMessage. No fetch, no key in the page. Only the BcmsLiveBlocks region updates |

Headless projects (your own host)

BetterCMS never builds, releases or deploys a headless project. Any build, artifact or deploy request for one returns 409 HEADLESS_NO_BUILD.

  • Deploy: push to your repository. Your provider (Vercel, Netlify or Cloudflare Pages) builds it. For fresh content on publish, point the revalidation webhook at the provider's rebuild hook.
  • Draft: use your framework's draft route on the provider's preview deployment URL. For Astro that is the SSR lane above: output: "server", the injected /api/bcms/draft/* routes, BCMS_DRAFT_SECRET, and a BCMS_API_KEY carrying content:read:draft. Keep the draft key off production.