@bettercms-ai/nuxt
v0.4.0
Published
BetterCMS for Nuxt: settings from NUXT_PUBLIC_BCMS_*, the read client per request with draft mode, the editor's draft routes, and the page sections server-rendered by BetterCMS.
Readme
@bettercms-ai/nuxt
BetterCMS for Nuxt. Your own components stay your own: they bind a few attributes onto their elements, and every element becomes editable in the BetterCMS visual editor. Pages stay current when content is published.
npm i @bettercms-ai/nuxt// server/utils/bcms.ts
export const bcms = createBcms({ env: process.env });
// server/plugins/bcms.ts: <html data-bcms-draft> in drafts, <meta name="generator">
export default defineNitroPlugin(bcmsNitroPlugin(bcms));
// plugins/bcms.client.ts: a publish, and the editor's re-render request, refetch the page's data
export default defineNuxtPlugin(() => {
startBcmsLive({ refresh: () => refreshNuxtData(), src: useRuntimeConfig().public.bcmsLiveUrl });
});
// server/routes/api/draft.get.ts: the path the editor's "Preview on site" opens
export default defineEventHandler((e) => bcms.draftEnable(toWebRequest(e)));
// server/api/post/[slug].get.ts: drafts while the editor previews
export default defineEventHandler(async (e) => (await bcms.client(toWebRequest(e))).getEntry(getRouterParam(e, "slug")!));<!-- your own markup, bound to its content -->
<script setup>
import { entryAttrs, entryField, imageAttrs, rich } from "@bettercms-ai/nuxt";
const { data: post } = await useFetch("/api/post/hello-world");
</script>
<template>
<article v-bind="entryAttrs(post)">
<h1 v-bind="entryField(post, 'title')">{{ post.data.title }}</h1>
<img v-bind="{ ...imageAttrs(post.data.cover, { sizes: '100vw' }), ...entryField(post, 'cover') }" class="cover" />
<div v-bind="entryField(post, 'body')" v-html="rich(post.data.body)" />
</article>
</template>| Helper | What it binds |
|---|---|
| fieldAttrs(path, page.fieldStyles) | A page field. It also applies the field's stored style, which turns on the editor's Align, Size and Weight controls. |
| entryAttrs(entry) | A collection entry's card. |
| entryField(entry, path) | A field of that entry. |
| sectionRootAttrs(block) | A section root (its spacing and design). |
| sectionColumnAttrs(block) | The section's content column. |
| imageAttrs(value, { path, sizes }) | An optimized <img> with a srcset, alt text, the focal point and the field binding. |
| rich(value), lines(value), plain(value) | Rich text as HTML, a one-line value, or plain text for attributes. |
Sections
A page built from sections in BetterCMS renders as the hosted renderer draws it, so forms, sliders and tabs work exactly as they do on a BetterCMS-hosted site. Pass the request and the editor's session gets the draft, with every section and value clickable:
// server/api/sections/[slug].get.ts
export default defineEventHandler((e) => bcms.sections(getRouterParam(e, "slug")!, undefined, toWebRequest(e)));<div v-bcms-sections v-html="sectionsMarkup(sections)" />Your own sections in the CMS order
A page that draws its own sections can let BetterCMS order them, and draw sections added in the editor between them: name the page's sections (slots, in source order) and loop over sectionItems. npx @bettercms-ai/convert --sections writes this for you.
// server/api/bcms-sections/[slug].get.ts
export default defineEventHandler((e) =>
bcms.sections(getRouterParam(e, "slug")!, undefined, toWebRequest(e), { slots: String(getQuery(e).slots ?? "").split(",").filter(Boolean) }));<script setup>
import { sectionItems, vBcmsSections } from "@bettercms-ai/nuxt";
const bcmsSlots = ["hero", "code:features"];
const { data } = await useFetch("/api/bcms-sections/home", { query: { slots: bcmsSlots.join(",") } });
const bcmsSections = computed(() => sectionItems(data.value, bcmsSlots, "home"));
</script>
<template>
<template v-for="item in bcmsSections" :key="item.key">
<template v-if="item.slot === 'hero'"><section class="hero" v-bind="item.root">…</section></template>
<template v-else-if="item.slot === 'code:features'"><div v-bind="item.box"><Features /></div></template>
<div v-else-if="item.slot === null" data-bcms-rendered="platform" v-bcms-sections v-html="item.html" />
</template>
</template>root goes on a section's own root element (its placement's data-bcms-block and style); box on a wrapper when the section is a component. With no answer, every slot renders in source order.
Settings
Options are read first, then NUXT_PUBLIC_BCMS_*, then BCMS_*, the same precedence as every adapter:
| Variable | Purpose |
|---|---|
| NUXT_PUBLIC_BCMS_API_URL | The API to read from. |
| NUXT_PUBLIC_BCMS_WORKSPACE | Your workspace. |
| NUXT_PUBLIC_BCMS_PROJECT_ID | Your project. |
| BCMS_DRAFT_SECRET | Signs the draft cookie. |
Don't put a page that a draft session can render behind a shared routeRules cache (swr, isr). The draft marker would be cached and served to visitors.
The sections run small inline scripts (forms, sliders and tabs), so a Content-Security-Policy must allow inline scripts.
