Skip to content

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.ymlcard, 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.

Previous
Queries