nuxt-sdui-layer
v1.0.1
Published
A Nuxt module for the **Server-Driven UI (SDUI)** rendering engine. Drop it into any Nuxt 3+ app to get globally registered components (`SDUIRenderer`, `SlideshowV1`, `GalleryV1`, `HeaderV1`), auto-imported composables, and a ready-to-use Iconify plugin —
Downloads
11
Readme
nuxt-sdui-layer
A Nuxt module for the Server-Driven UI (SDUI) rendering engine. Drop it into any Nuxt 3+ app to get globally registered components (SDUIRenderer, SlideshowV1, GalleryV1, HeaderV1), auto-imported composables, and a ready-to-use Iconify plugin — all driven by a single Firestore document.
Requirements
| Dependency | Version | |---|---| | Node.js | >= 18 | | Nuxt | >= 3.0.0 | | Vue | >= 3.3.0 | | Tailwind CSS | >= 3.3.0 | | Firebase | >= 9 (web modular SDK) | | @nuxtjs/i18n | >= 8 |
1. Install
npm install nuxt-sdui-layerLocal development (monorepo / sibling folder):
// package.json
{
"dependencies": {
"nuxt-sdui-layer": "file:../nuxt-sdui-layer-export"
}
}2. Register the Module
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'nuxt-sdui-layer',
'@nuxtjs/i18n',
],
i18n: {
locales: ['en', 'th'],
defaultLocale: 'en',
},
})All components, composables, utils, and the Iconify plugin are available immediately — no extra imports needed.
3. Required Boilerplate
A. Firebase plugin — plugins/firebase.ts
useSiteSettings (included in the module) reads from Firestore via $db provided by a Nuxt plugin. You must create this plugin in your own app:
// plugins/firebase.ts
import { defineNuxtPlugin } from '#app'
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
export default defineNuxtPlugin((nuxtApp) => {
const app = initializeApp({
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
})
nuxtApp.provide('db', getFirestore(app))
})# .env
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_APP_ID=B. Firestore settings/website document
useSiteSettings subscribes to settings/website. Create this document in Firestore with at minimum:
{
"defaultLanguage": "en",
"supportedLanguages": ["en", "th"],
"siteName": "My Site",
"activeHeaderId": "",
"activeFooterId": "",
"faviconUrl": ""
}4. Rendering a Component
Fetch a document from the sdui_components Firestore collection and pass it to <SDUIRenderer>:
<!-- pages/index.vue -->
<template>
<SDUIRenderer
v-if="component"
:node="component.layout"
:contentDictionary="component.content"
:activeLocale="locale"
/>
</template>
<script setup>
import { doc, getDoc } from 'firebase/firestore'
const { $db } = useNuxtApp()
const { locale } = useI18n()
const { data: component } = await useAsyncData('home-hero', async () => {
const snap = await getDoc(doc($db, 'sdui_components', 'home_hero'))
return snap.exists() ? snap.data() : null
})
</script>Firestore document shape
{
"id": "home_hero",
"name": "Homepage Hero",
"layout": {
"id": "root",
"type": "Container",
"props": { "class": "w-full" },
"children": [
{
"id": "hero_slide_node",
"type": "Slideshow_v1",
"slotName": "hero_slider",
"props": { "class": "" },
"children": []
}
]
},
"content": {
"hero_slider": {
"settings": {
"heading_size": { "desktop": "xl", "tablet": "lg", "mobile": "md" },
"overlay_opacity": { "desktop": "40", "tablet": "60", "mobile": "60" }
},
"slides": [
{
"id": "slide_1",
"translations": {
"en": {
"title": "Hello World",
"subtitle": "Welcome to our site",
"cta1Text": "Shop Now",
"cta1Link": "/shop"
}
},
"commonMedia": {
"image": "https://example.com/hero.jpg",
"imageTablet": "https://example.com/hero-tablet.jpg",
"imageMobile": "https://example.com/hero-mobile.jpg"
}
}
]
}
}
}layout holds the node tree (structure + Tailwind classes).
content holds the editorial data, keyed by each node's slotName.
SDUIRenderer merges them at render time.
5. What the Module Provides
Components (globally registered — no import needed)
| Component | Description |
|---|---|
| <SDUIRenderer> | Core recursive renderer — walks the layout tree and mounts the correct component for each node type |
| <SlideshowV1> | Hero slideshow with autoplay, transitions, overlay text, responsive images |
| <GalleryV1> | Image gallery with nav arrows, dot indicators, responsive breakpoints |
| <HeaderV1> | Site header with logo, nav links, mobile hamburger menu, optional CTA |
All components are registered with global: true so resolveComponent('SlideshowV1') inside SDUIRenderer works correctly at runtime.
Composables (auto-imported)
| Composable | Description |
|---|---|
| useComponentTheme(props) | Returns getResponsiveClass(key), getLocalizedValue(content, field), currentBreakpoint |
| useSiteSettings() | Reactive settings object synced from Firestore settings/website |
Utils (auto-imported)
| Util | Description |
|---|---|
| processResponsiveClasses(classStr, breakpoint) | Strips Tailwind breakpoint prefixes for a given viewport |
| sduiSchemas | Schema registry — field definitions and defaults for every component type |
| sduiPresets | Tailwind class preset library (Containers, Typography, Grids, Buttons, Decorations) |
Plugin (auto-registered)
iconify-setup — registers custom social:whatsapp and social:line icons so <Icon icon="social:whatsapp" /> works out of the box.
6. Adding a New Module (e.g. TestimonialGrid_v1)
Three files, then the UI is fully wired — no other changes needed.
Step 1 — Schema entry in utils/sduiSchemas.js
"TestimonialGrid_v1": {
name: "Testimonial Grid",
description: "Responsive grid of customer testimonials.",
module: true, // required — makes the "Add" button appear automatically in admin
uiColor: 'amber', // button color: violet | teal | cyan | indigo | amber | rose | emerald | purple
settings: [
{ key: 'columns', label: 'Columns', type: 'select', responsive: true,
options: [{ v: '2', l: '2 Columns' }, { v: '3', l: '3 Columns' }] },
],
translatableSlideFields: [
{ key: 'name', label: 'Customer Name', type: 'text' },
{ key: 'quote', label: 'Quote', type: 'textarea' },
],
commonSlideFields: [
{ key: 'avatar', label: 'Avatar Image', type: 'image' },
{ key: 'rating', label: 'Rating', type: 'select',
options: [{ v: '5', l: '5 stars' }, { v: '4', l: '4 stars' }, { v: '3', l: '3 stars' }] },
],
defaultSettings: {
columns: { desktop: '3', tablet: '2', mobile: '1' }
}
}Step 2 — Vue component (components/TestimonialGridV1.vue)
<script setup>
import { useComponentTheme } from '../composables/useComponentTheme'
const props = defineProps({
node: { type: Object, default: () => ({}) },
content: { type: Object, default: () => ({}) },
settings: { type: Object, default: () => ({}) },
activeLocale: { type: String, default: 'en' },
})
const { getResponsiveClass, getLocalizedItemValue } = useComponentTheme(props)
</script>
<template>
<div :class="['grid gap-6', `grid-cols-${getResponsiveClass('columns', '3')}`]">
<div v-for="item in content.slides" :key="item.id" class="p-6 bg-white rounded-2xl shadow">
<img :src="item.commonMedia?.avatar" class="w-12 h-12 rounded-full" />
<p class="mt-3 text-slate-700">"{{ getLocalizedItemValue(item, 'quote') }}"</p>
<p class="mt-2 font-bold">{{ getLocalizedItemValue(item, 'name') }}</p>
</div>
</div>
</template>Step 3 — Register in components/sdui/SDUIRenderer.vue
// inside the resolvedComponent computed, add one case:
case 'testimonial_grid_v1': return resolveComponent('TestimonialGridV1')The "Add" button in the admin panel and the settings editor panel are generated automatically from the schema. No other files need to change.
7. Node Type Reference
Native HTML types (rendered directly)
| type | Renders as |
|---|---|
| container | <div> |
| grid | <div> |
| text | <span> |
| heading1–heading4 | <h1>–<h4> |
| paragraph | <p> |
| image | <img> |
| link | <a> |
| button | <button> |
| icon | <Icon> (Iconify) |
Dynamic Vue components
Any snake_case_v1 type → converted to PascalCase → resolveComponent():
| Firestore type | Resolved component |
|---|---|
| slideshow_v1 | SlideshowV1 |
| gallery_v1 | GalleryV1 |
| header_v1 | HeaderV1 |
| testimonial_grid_v1 | TestimonialGridV1 |
8. Publishing to npm
Before publishing, update package.json:
{
"name": "@yourscope/nuxt-sdui-layer",
"version": "1.0.0"
}Remove "private": true, run the schema lint, then publish:
npm run lint:schema # verify no schema drift
npm publish --access public