Building a theme
Views
The Vue components that render your pages, and how Nibble picks one.
A view is a Vue single-file component under themes/<yours>/views/. It is rendered on the server and then
hydrated in the browser, so visitors get HTML immediately and search engines see a complete page.
themes/almanac/
├── layouts/default.vue the frame around every page
├── views/
│ ├── home.vue one view
│ ├── home.yml what that view needs — see Queries
│ ├── guides/index.vue
│ ├── guides/show.vue
│ ├── sets/quote.vue one block of a replicator field
│ └── errors/404.vue
├── components/PostCard.vue your own components
└── styles/theme.css
Which view renders a request
Nibble resolves the URL to an entry, a taxonomy term or a taxonomy index, then picks the first template that is set:
- the entry’s own
templatefield, if it has one - its blueprint’s
template - its collection’s
template default
Terms use their taxonomy’s template, falling back to taxonomies/show.
That order means an unusual page can have its own view without needing its own collection.
Generate one already wired up:
bin/rails nibble:generate:view guides/index --collection=guides
It writes the view, its query sidecar, and prints the line to add to the collection.
What a view receives
Every view gets page — the record being rendered — plus whatever its sidecar asked for, plus shared props for
the site, its globals and its navigation.
<script setup lang="ts">
import type { ViewProps, GuidesGuide } from '../../.nibble/types'
import { RichText, Image } from '@nibble'
defineProps<ViewProps['guides/show'] & { page: GuidesGuide }>()
</script>
<template>
<article>
<h1>{{ page.title }}</h1>
<Image v-if="page.featured_image" :asset="page.featured_image" preset="hero" />
<RichText :value="page.body" />
</article>
</template>
Those types are generated from your blueprints, so page.body is known to exist and a typo is caught before the
page is.
A view never fetches anything itself. If it needs more content, that goes in its query sidecar — which is what lets Nibble know exactly what a page depended on, and clear the right caches when any of it changes.
Layouts
layouts/default.vue wraps every view. A page can ask for another by setting a layout, which is how a landing
page drops the site’s usual header without a second theme.
Blocks
views/sets/<name>.vue renders one set of a replicator field. An editor builds the page from blocks, and each
block is drawn by the file of that name — a quote, a gallery, a call to action. See
Theme helpers for the Blocks component that runs them.
Error pages
views/errors/404.vue and views/errors/500.vue render those responses, with the same layout and styling as
everything else.
Highlighting code
Nibble renders a fenced code block as <pre><code class="language-ruby"> — the shape Prism, highlight.js and
Shiki all look for — and stops there. Highlighting is the theme’s decision, because it is appearance, and a
site that publishes no code should carry no highlighter.
The crumbs theme highlights with highlight.js, loading only the grammars a page actually uses:
npm install highlight.js -w @nibble-theme/<yours>
// after the HTML is in the DOM
const { default: hljs } = await import('highlight.js/lib/core')
hljs.registerLanguage('ruby', (await import('highlight.js/lib/languages/ruby')).default)
document.querySelectorAll('pre code[class*="language-"]').forEach((block) => hljs.highlightElement(block))
Nibble deliberately does not highlight on the server: the built-in option writes inline colours in a theme of its own choosing, which no stylesheet of yours could override.
Styles
styles/theme.css is the theme’s stylesheet, built by Vite. Themes are npm workspaces, so a theme can have
dependencies of its own:
npm install <package> -w @nibble-theme/<yours>
Commit the lockfile afterwards — it is shared with Nibble, and an upgrade rebuilds it rather than installing from it, so a conflict there is harmless.