@csirt-cms/widget-vue
v0.1.1
Published
Vue 3 and Nuxt runtime renderers for the CSIRT CMS widget system.
Maintainers
Readme
@csirt-cms/widget-vue
Vue 3 and Nuxt runtime renderers for the CSIRT CMS widget system. Takes the
{ component, props } sections the Public API returns and renders them through a
registry your Page Builder never has to know about.
Every widget produces the same markup and class names as
@csirt-cms/widget-react,
so the two share one stylesheet and one set of schemas. A parity check in the
repo server-renders all ten widgets in both frameworks and fails the build if
their class vocabularies diverge.
npm install @csirt-cms/widget-vuePeers: vue ^3.4, zod ^4.
Quick start
// main.ts
import { createApp } from "vue";
import { CsirtCmsWidgets } from "@csirt-cms/widget-vue";
import "@csirt-cms/widget-vue/styles.css";
import App from "./App.vue";
createApp(App).use(CsirtCmsWidgets).mount("#app");The plugin registers every built-in widget and two global components,
CmsPageRenderer and CmsWidgetPreview:
<script setup lang="ts">
import { createCmsClient, useCmsPage } from "@csirt-cms/widget-vue";
const client = createCmsClient({ baseUrl: import.meta.env.VITE_CMS_URL, site: "csirt" });
const { data: page, loading } = useCmsPage(client, "beranda");
</script>
<template>
<p v-if="loading">Loading…</p>
<CmsPageRenderer v-else-if="page" :sections="page.sections" />
</template>Plugin options:
app.use(CsirtCmsWidgets, {
widgets: [MyWidget], // additional widgets
includeBuiltIns: true, // register the ten built-ins too (default)
registerComponents: true, // register the global components (default)
});Or import the component directly and skip the plugin:
<script setup lang="ts">
import { PageRenderer, builtInWidgets, registerWidgets } from "@csirt-cms/widget-vue";
registerWidgets(builtInWidgets);
</script>
<template>
<PageRenderer :sections="sections" />
</template>PageRenderer
<PageRenderer
:sections="page.sections"
device="mobile"
:preview="false"
:now="new Date()"
>
<template #fallback="{ widgetKey }">
<p>Unknown widget: {{ widgetKey }}</p>
</template>
</PageRenderer>It accepts the Public API's { component, props } pairs directly, or the fuller
SectionState the Page Builder holds.
For each section it migrates stored props to the widget's current version, fills
gaps from defaults, and validates. A section that still fails validation renders
its empty state rather than taking the page down. An unregistered key warns in
development and renders the #fallback slot if you supply one.
Nuxt
Register in a plugin so the server and client registries agree:
// plugins/cms-widgets.ts
import { CsirtCmsWidgets } from "@csirt-cms/widget-vue";
import "@csirt-cms/widget-vue/styles.css";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(CsirtCmsWidgets);
});Fetch with useAsyncData so the payload is server-rendered and hydrated:
<script setup lang="ts">
import { createCmsClient } from "@csirt-cms/widget-vue";
const client = createCmsClient({ baseUrl: useRuntimeConfig().cmsUrl, site: "csirt" });
const { data: page } = await useAsyncData("page", () => client.getPage("beranda"));
</script>
<template>
<CmsPageRenderer v-if="page" :sections="page.sections" />
</template>Markdown and HTML are rendered and sanitized during the server render, so no Markdown parser or sanitizer reaches the browser.
Adding a widget
Nothing in the Page Builder changes — it renders whatever is registered.
npm run generate:widget -- testimonial --name "Testimonial" --category MarketingOr by hand:
<!-- Renderer.vue -->
<script setup lang="ts">
import type { TestimonialProps } from "./types";
defineProps<TestimonialProps>();
</script>
<template>
<blockquote class="cms-w cms-w-testimonial">{{ quote }} — {{ author }}</blockquote>
</template>import { defineWidget, registerWidget } from "@csirt-cms/widget-vue";
import { testimonialDefinition } from "./definition";
import Renderer from "./Renderer.vue";
registerWidget(defineWidget(testimonialDefinition, { renderer: Renderer }));Props types must be plain interfaces, not
z.inferaliases. Vue's SFC compiler resolves types itself, separately from TypeScript, and cannot follow Zod's inferred conditional types —defineProps<SomeZodInfer>()fails with "Unresolvable type reference". Every definition in@csirt-cms/widget-coreexports a hand-written interface for exactly this reason.
Registry and preview
import {
getWidgets, getWidgetsByCategory, getWidgetCategories, searchWidgets,
} from "@csirt-cms/widget-vue";<CmsWidgetPreview widget-key="hero-carousel" :props="draftProps" device="mobile" />Composables
const { data, loading, error, reload } = useCmsPage(client, "beranda");Also useCmsNavigation, useCmsAnnouncements, useCmsTheme. Under Nuxt prefer
useAsyncData.
Styling
One stylesheet, shared with the React package. Every value is a CSS custom property with a fallback, so a site's theme overrides any of it:
import { themeToCssText } from "@csirt-cms/widget-vue";
useHead({ style: [{ children: themeToCssText(theme) }] });Dark mode follows the OS, or is forced with data-cms-theme="dark" on <html>.
Security
The html and markdown widgets sanitize before rendering. The built-in
sanitizer suits trusted CMS editors — for untrusted input, pass your own via the
sanitizer prop. See the @csirt-cms/widget-core README for the full note.
License
MIT
