Skip to content

Building a theme

Views and layouts

Which view renders which page, what a view receives, layouts, blocks, error pages and code highlighting.

A view is a Vue single-file component that renders one kind of page. This guide covers how Nibble chooses one, what it receives, and the layouts, blocks and error pages around it.

After reading this guide, you will know:

  • How Nibble decides which view renders a request.
  • What props every view receives, and how to type them.
  • How to generate a view, and wire it to a collection.
  • How layouts, blocks and error pages work.
  • How to highlight code blocks.

1. Which view renders a request

Once a URL has been resolved, the view is the first of these that is set:

For View
an entry its own template, then its blueprint’s, then its collection’s, then default
a page written as a file its frontmatter’s template, then the same chain
a taxonomy term its taxonomy’s template, then taxonomies/show
a collection’s index route its index_template, then collections/index
a taxonomy’s index route its index_template, then taxonomies/index
nothing errors/404

A view name is a path under views/ without .vue: posts/show is site/themes/tidewater/views/posts/show.vue.

Important

Every theme must have pages/show, posts/show, posts/index, taxonomies/show, taxonomies/index, search, errors/404 and errors/500. nibble:check names any that are missing. A theme generated from crumbs has them all.

That order is why an unusual page does not need an unusual collection. Tidewater’s “Book a demo” page is an ordinary page whose own Template is set to demo.

2. Generating a view

bin/rails nibble:generate:view customers/show --collection=customers

writes views/customers/show.vue, typed for the collection’s first blueprint, and views/customers/show.yml, a query sidecar listing the collection’s entries, then tells you the line to add to the collection:

wrote site/themes/tidewater/views/customers/show.vue
  add template: customers/show to schema/collections/customers.yml to use it

Delete the sidecar if the view needs nothing but its own page. Then restart bin/dev, since the view is a new file.

3. What a view receives

Prop What it is
page the entry, term or file page being rendered, with its fields
one prop per sidecar query whatever the query sidecar asked for
site the locale, the URL, every global and every navigation — usually read through composables
seo the title, description, canonical URL and social tags SeoHead renders
layout, preview used by Nibble’s own components

Types for all of it are generated from your schema:

<!-- site/themes/tidewater/views/posts/show.vue -->
<script setup lang="ts">
import { Image, RichText } from '@nibble'
import type { PostsPost, ViewProps } from '@site/types'

const props = defineProps<ViewProps['posts/show'] & { page: PostsPost }>()
</script>

<template>
  <article>
    <h1>{{ page.title }}</h1>
    <p v-if="page.excerpt">{{ page.excerpt }}</p>
    <Image v-if="page.featured_image" :image="page.featured_image" sizes="(min-width: 60rem) 60rem, 100vw" />
    <RichText :value="page.body" />
  </article>
</template>

ViewProps['posts/show'] types what the sidecar returns; PostsPost types the page. A field that does not exist is a type error, caught by npm run check before it is a broken page.

Important

A view never fetches its own content — no fetch, no API calls. Put what it needs in its sidecar, so Nibble knows what the page depended on and can clear its cache precisely.

4. Layouts

A layout is the frame around a page: header, footer, the <head> tags. layouts/default.vue wraps everything unless a collection or taxonomy names another:

# site/schema/collections/help.yml
layout: help

Tidewater’s help centre uses layouts/help.vue — a slimmer header with a search box and no marketing navigation. A layout name that does not exist falls back to default.

<!-- site/themes/tidewater/layouts/default.vue -->
<script setup lang="ts">
import { PreviewBar, SeoHead } from '@nibble'
import SiteHeader from '../components/SiteHeader.vue'
import SiteFooter from '../components/SiteFooter.vue'
</script>

<template>
  <SeoHead />
  <PreviewBar />
  <SiteHeader />
  <main><slot /></main>
  <SiteFooter />
</template>

Warning

Keep <SeoHead /> in every layout. Without it a page has no title, description or canonical link, and search engines see an unlabelled page.

5. Blocks

Each set of a replicator field is drawn by views/sets/<set handle>.vue, which receives the set’s fields as props. Blocks loops over a field’s blocks and picks the right component for each:

<Blocks :blocks="page.blocks" />

A block whose set has no view renders nothing, so adding a set to a blueprint before its view exists is safe. Tidewater’s pricing block is built end to end in Getting Started.

6. Error pages

views/errors/404.vue renders every address that resolves to nothing, and views/errors/500.vue renders a page that failed — both inside the default layout, so they look like the rest of the site.

Tip

Every 404 is counted, most-requested first, on the control panel’s Missing pages screen at /admin/404s, and in the dashboard’s Top missing pages widget. It is the quickest way to find a link that broke. See Redirects and SEO.

7. 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 — a site that publishes no code carries no highlighter.

Tidewater’s help centre shows code for its API, so its theme highlights in the browser, loading only the languages a page uses. The theme copied from crumbs already does this in lib/highlight.ts, with highlight.js as the theme’s own dependency.

Note

Nibble does not highlight on the server on purpose: server-side highlighting writes inline colours in a theme of its own choosing, which no stylesheet of yours could override.