Building a theme
Theme helpers
The components and composables every theme needs, so no theme writes them twice.
@nibble is the theme runtime: a small set of components and composables for the things every theme needs.
<script setup lang="ts">
import { RichText, Image, Blocks, Pagination, SeoHead, useSite, useGlobals, useNavigation } from '@nibble'
</script>
| Import | What it does |
|---|---|
RichText |
renders rich text, already turned into safe HTML on the server |
Image |
renders an asset at a named preset, with srcset and dimensions |
Blocks |
renders a replicator field, each set through views/sets/<name>.vue |
Pagination |
the links for a paginated query |
SeoHead |
title, description, canonical, Open Graph and structured data |
PreviewBar |
the bar shown when someone is previewing a draft |
useSite, useGlobals, useNavigation, useLocale |
shared props, without passing them down |
useNibbleForm |
posting a form and handling its errors |
Rich text is HTML, and already safe
Rich text is stored as structured data, not HTML, and rendered on the server through an allowlist that strips
script, on* attributes and javascript: URLs. Uploaded SVGs are sanitised on upload.
<RichText :value="page.body" />
Do not render content through v-html yourself. RichText exists so that the sanitising happens in one
place, where it is tested, rather than in every theme.
Images
<Image :asset="page.featured_image" preset="hero" />
A preset is a named size from config/nibble.yml — card, hero, content and og out of the box, plus any
you add. The image is served through Nibble’s own transform endpoint and cached by content, so a URL never goes
stale when someone re-crops the picture.
Alt text comes from the asset, so it is right everywhere without being retyped. See Assets.
Blocks
<Blocks :blocks="page.blocks" />
Each set in the field is rendered by views/sets/<name>.vue, receiving that set’s fields as props. This is how a
page built from blocks is drawn, and why adding a new kind of block is a new file rather than a change to an
existing view.
Shared props
<script setup lang="ts">
const site = useSite()
const globals = useGlobals()
const navigation = useNavigation()
</script>
<template>
<nav>
<a v-for="item in navigation.main" :key="item.id" :href="item.url">{{ item.title }}</a>
</nav>
</template>
These are available on every page without being threaded through props, which keeps a deeply nested component from needing its parents’ help to know the site’s name.
Forms
<script setup lang="ts">
const { values, errors, submitting, submit } = useNibbleForm('enquiry')
</script>
It posts to the form’s endpoint, handles validation errors coming back from the server, and exposes a submitting flag for disabling the button. The form’s fields and rules are in the schema; see Forms for what an editor sees.