Skip to content

Field names and building blocks

Edward Baitsewe edited this page Nov 10, 2026 · 5 revisions

Field names and building blocks

Status: draft. Settled while the first client site moves to Slim Killer. Part of the Astro site rules (rule 6).

Every client site names its fields the same way and builds its pages from the same building blocks. Then the control panel, the editor hints and hammer site:check work the same on every site, and a developer who knows one site knows them all.

A building block is a shape, not a look. @monatemedia/slim-killer-astro gives the schema (the fields, their labels and editor hints). Each site's own components decide how the block looks.

Naming rules

N1. camelCase English nouns. heroImage, imageSide, faqs. No abbreviations except seo, cta, faq and href.

N2. One name for one idea. Use the names in the table below. If a field means the same as a name in the table, it takes that name. A new idea gets a new name here first, the same way the Glossary works.

N3. Fields that belong together are a group, not a prefix. cta.heading, not ctaHeading. card.image, not cardImage. One group is one box in the editor, and one block in code.

N4. Every image is an image(). An image is always { src, alt }, with an optional focus. There are no separate …Alt or …Position fields.

N5. Lists are plural. sections, faqs, images, people. One item of a list is named by the singular in its editor label ("Section 2").

N6. Choices are named, never raw CSS. A background is background: 'sand', from a list the site defines, never bgColor: '#f4ead5'. The client picks from the site's design. She cannot break it.

N7. Links are { label, href }. Not externalUrl and externalLabel.

The names

Name Meaning Editor
title The page's heading. The browser tab and search results show it as "title — shortName". A line break starts a new line in the heading; the tab shows a space text
description The page's description in search results textarea
eyebrow The small heading above a heading text
heading A block's heading text
subheading The line under a heading text
lead The page's opening paragraph, under the title textarea
body A block's text, in Markdown markdown
heroImage The large image at the top of the page, and the image shown when the page is shared image
image / images An image, or a list of images, in a block image
src Inside an image: the file name, in content/images/ (a photo) or content/graphics/ (a graphic) image
alt Inside an image: what it shows, for people who cannot see it text
focus Inside an image: the part to keep in view when it is cropped (optional) text
label, href A link's text and address text
background A named background from the site's design select
imageSide Which side a section's image is on: left or right; empty lets the page decide (optional) select
heroButton The button in the page's opening: a link() text
grid The heading and the line under it, above a list of cards text
order An entry's place in lists: 1 first, then 2 number
businessName In the settings: the full, official business name text
shortName In the settings: the name people use day to day; it ends every tab title text
phone, email, address In the settings: contact details; the tel: link is worked out from phone text
social In the settings: links to the business's pages, for example social.facebook text
mainMenu, infoMenu In the settings: lists of link(), for the header and the footer list
headerButton In the settings: the button in the header, a link() text

The page's opening (title, eyebrow, lead, heroImage) stays at the top level and is not a group: every page has one, and the page template reads it directly.

Building blocks

Import them from @monatemedia/slim-killer-astro/schema. Each one returns a Zod schema with its labels and editor hints already set.

Block Shape Used for
image() { src, alt, focus? } Every image
link() { label, href } Buttons and links
seo(shortName) title, description Spread into every collection that makes pages
card() { image?, text } How an entry shows in a list of cards
quote() { text, cite } A quote and who said it
cta() { heading, subheading?, label, href } The "book now" banner
section() { eyebrow?, heading, body?, image?, imageSide?, background?, quote? } Text with an image beside it
gallery() { heading?, background?, images } A set of images

section() and gallery() take the site's background names: section({ backgrounds: ['light', 'sand', 'dark'] }).

A block shows only what the page draws. If a page leaves out part of a block, the schema leaves it out too, so the client never fills in a field that changes nothing: section({ omit: ['eyebrow', 'imageSide'] }).

Search limits are build rules. seo(shortName) limits title to what is left of 60 characters after " — shortName", and description to 160, with .max(). A text over the limit stops the build, and the control panel stops typing at the limit. Each field explains in its help text what search engines do with it.

Site-wide names live in the settings, content/settings/index.md: businessName and shortName. The layout builds every tab title as "title — shortName"; pages pass only their own title. There is no separate search title.

Pages that exist once are collections with one entry, content/<page>/index.md: home, about, not-found, and so on. A page that lists a collection keeps its own text in content/<collection>-list/index.md (articles-list), because the collection's own name is taken by its entries.

Text marked markdown is rendered as Markdown by the site (Astro's own processor, straight quotes kept as typed). Templates never split text into paragraphs themselves.

A shared page shows its main image (heroImage), or the site's default image when the page has none. There is no separate share image.

A site can add fields to a block (section().extend({ people: … })) and can have fields of its own, if the names follow N1 to N7.

Example

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
import { seo, card, image, section } from '@monatemedia/slim-killer-astro/schema';

const destinations = defineCollection({
  loader: glob({ pattern: '**/[^_]*.md', base: '../content/destinations' }),
  schema: z.object({
    ...seo(shortName),
    card: card(),
    eyebrow: z.string().optional().meta({ title: 'Small heading above the title' }),
    heroImage: image({ title: 'Main image' }),
    lead: z.string().meta({ title: 'Opening paragraph', input: 'textarea' }),
    sections: z.array(section()).default([]).meta({ title: 'Sections' }),
  }),
});

And in an entry:

title: "The old harbour"
description: "A walk around the harbour and its lighthouse."
card:
  image:
    src: "harbour.jpg"
    alt: "Fishing boats in the harbour at sunset"
  text: "Two hours on foot, from the market to the lighthouse."
heroImage:
  src: "lighthouse.jpg"
  alt: "The lighthouse seen from the harbour wall"
  focus: "center 30%"

To settle

  • focus in the editor. A text field for now ("center 30%"). Later, a point the client clicks on the image.

Clone this wiki locally