@betttercms/astro
v0.2.0
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
@betttercms/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 @betttercms/astroSetup
// astro.config.mjs
import { defineConfig } from "astro/config";
import bettercms from "@betttercms/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
});Server env (never bundled to the client):
BCMS_API_KEY=bcms_pk_... # content:read (or content:read:draft for previews)
BCMS_DRAFT_SECRET=<32+ chars> # signs the draft cookieAdd the virtual-module + Astro.locals types to src/env.d.ts:
/// <reference types="@betttercms/astro/env" />Reading content
---
import { loadPage, loadForms } from "bettercms:client";
import BcmsBlocks from "@betttercms/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 "@betttercms/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 |
| --- | --- |
| @betttercms/astro/components/BcmsBlocks.astro | Render a page's blockJson (all 8 block types). |
| @betttercms/astro/components/BcmsForm.astro | Render + submit a form (conditional fields, honeypot, Turnstile). |
| @betttercms/astro/components/BcmsImage.astro | Optimized image with a 1x/2x srcset via the media transform endpoint. |
Markup is class-driven and unstyled — you own the CSS.
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.
