Repository navigation
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.
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.
| 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.
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.
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%"-
focusin the editor. A text field for now ("center 30%"). Later, a point the client clicks on the image.