---
title: Pages, layouts, components and fields in vzk
description: 'Use this guide for the structure of a vzk site: what a page, a layout
  and a component are, the fields an owner edits apart from the markup, links, and
  pictures.'
sidebar:
  order: 11
---

Use this guide for the structure of a vzk site: what a page, a layout and a
component are, the fields an owner edits apart from the markup, links, and
pictures. Read `vzk/authoring` first for the editing loop.

## A page

A page is a file under `pages/`, and its path is its address:
`pages/prices.vzk` is `/prices`, and `pages/index.vzk` is `/`. Each part of
the path is lower-case letters, digits, `-` and `_`. One address is one
page, so a site holds `pages/prices.vzk` or `pages/prices/index.vzk`, never
both.

What a page says about itself is written in its server script and its
`<head>`:

| Written | Means |
| --- | --- |
| `<head>` with a `<title>` and a description | the page's own head (`vzk/authoring`, "The head") |
| `export const name = "Prices"` | the page's name, which `site.pages` lists it under |
| `export const fields = { ... }` | the page's named fields (below) |
| `export const layout = "plain"` | wraps the page in `layouts/plain.vzk`; `null` wraps it in none |

A page whose file is `pages/<path>/[slug].vzk` is drawn once for each row of
a table (`vzk/tables`).

### Blocks

Every top-level element of a page or of a layout is a block, and has an
`id`:

```html
<section id="hero" note="The first thing a visitor sees.">
  <h1>Bread, every morning</h1>
</section>
```

`outline` lists a file's blocks, and `read_block`, `write_block` and
`edit_block` find one by its address, `pages/index.vzk#hero`. A top-level
element with no `id` is a warning, and has no address. `note` says in the
author's words what a block is for: it is shown in the outline and never
sent to a visitor. An element deeper in a file that carries `note` is a
block of its own. An `id` on an element under `:for` holds a part of the
item, as in `id="faq-{item.slug}"`.

## A layout

`layouts/site.vzk` wraps every page. The page is drawn where the layout says
`<slot />`:

```html
<script server>
  const links = page.fields?.links ?? []
</script>

<head>
  <meta name="description" content={page.description ?? "Dan Okafor, plumber in South Manchester: leaks, boilers, bathrooms and blocked drains."}>
</head>

<div id="navbar" class="sticky top-0 z-30 bg-surface border-b border-ink/8">
  <p class="font-heading font-strong text-brand text-ink">{site.fields.brand}</p>
  <a :for={link in links} href={link.href} class="link-quiet">{link.label}</a>
  <a href="tel:{site.fields.dial}" class="btn-compact">{site.fields.phone}</a>
</div>

<slot />

<div id="footer" class="border-t border-ink/8">
  <p class="text-[13px] text-muted">© {today().year} Dan Okafor Plumbing, Manchester</p>
</div>
```

- A layout reads the page it wraps: `page.title` and `page.description` from
  the page's head, `page.path`, and the page's fields as `page.fields.<name>`.
  A field the page does not declare is a missing value there, so read one
  with `?.` and `??` as above.
- A top-level element of a page that says `slot="aside"` fills the layout's
  `<slot name="aside" />`. What is written inside a `<slot>` is drawn when
  nothing was given for it.
- A change to a layout changes every page it wraps once the branch is
  merged, so look at a few of them in the preview before asking the owner.

## A component

A component is a file directly in `components/`, used by its name, which is
a capital letter and then letters and digits. Its inputs are the names it
takes out of `props`:

```html
<script server>
  const { alt, picture = null } = props
</script>

<div class="frame">
  <img :if={picture} src={picture} alt={alt} class="size-full object-cover">
  <div :else class="frame-empty"><span class="text-xs text-muted px-4 text-center">{alt}</span></div>
</div>
```

```html
<Frame class="aspect-[4/3] rounded-surface" alt="Dan at work under a kitchen sink" />
```

- An attribute on a use is an input when the component declares it:
  `alt="A text"` is that text, `picture={value}` is the value, and a bare
  name is `true`. An input with a default (`picture = null`) may be left out.
- `class`, `id`, `style`, `hidden`, `data-*` and `aria-*` that the component
  does not declare land on its root element, so a component given one has
  exactly one root element. Any other attribute is an error that names the
  nearest input.
- `<slot />` in a component draws what was written between its tags, and
  `<slot name="title" />` draws the children that say `slot="title"`.
- `:if` and `:for` work on a component's tag as on an element.
- A component reads its own names and what it was given, never its
  caller's, and a component that uses itself is an error.

## Named fields

A named field is a value an owner edits apart from the markup: the words on
a button, a price, a list of links. A file names its own in its server
script, and what a field is comes from how its value is written:

```html
<script server>
  export const fields = {
    /** The words on the button. */
    cta: "Order a loaf",
    price: 320,
    open: true,
    more: "/bread",
    tone: "calm" as "calm" | "bold",
    loaves: [
      { name: "Rye", price: 4 },
      { name: "Wheat", price: 3 },
    ],
  }
</script>

<h1 class={{ calm: "soft", bold: "loud" }[fields.tone]}>{fields.cta}</h1>
<p :if={fields.open}>From {fields.price / 100} today</p>
<ul><li :for={loaf in fields.loaves}>{loaf.name}: {loaf.price}</li></ul>
<a href={fields.more}>More bread</a>
```

- A text, a number and `true` or `false` are fields of those kinds. A text
  that is one address (`"/bread"`, `"https://example.com/"`) is a link.
  `assets["oven"]` is a picture. `"calm" as "calm" | "bold"` is a choice
  among its texts. A list of records that all hold the same names is a list.
- An empty list says what its items hold after `as`: `loaves: [] as { name:
  text, price: number }[]`, or `kinds: [] as text[]`. A list emptied by an
  edit is written that way, so it keeps its columns and the owner's panel
  still adds to it. A bare `[]` says nothing, and takes records of texts,
  numbers and booleans.
- The doc comment above a field (`/** ... */`) is its label, what a person
  calls it.
- A file reads its own fields as `fields.cta`. A layout and a component read
  the fields of the page they are drawn for as `page.fields.cta`.
- `site.json` holds the fields every file reads, as `site.fields.<name>`:

```json
{
  "edition": 1,
  "language": "visitor",
  "fields": {
    "brand": "Dan Okafor Plumbing",
    "phone": "0161 496 0732",
    "dial": "+44 161 496 0732"
  }
}
```

Name a field for what it holds (`cta`, `hours`, `links`), never with a
counter. Keep markup in the file and plain words in fields: a field's value
is escaped where it is written.

## What a file reads

| Name | What |
| --- | --- |
| `props` | in a component, the inputs it was given |
| `fields.<name>`, `page.fields.<name>` | the file's own fields, and those of the page being drawn |
| `site.fields.<name>` | the fields of `site.json` |
| `site.name` | the site's name |
| `site.pages` | the site's named pages, each as `{ name, path, url, current }` |
| `page.path`, `page.title`, `page.description` | the address asked for, and what the page's head declares |
| `page.locale`, `page.languages` | the language of the page, and each language it is served in (`vzk/languages`) |
| `params.slug` | on a `[slug]` page, that part of the address |
| `query.<name>` | what the address asks for after `?`, a visitor's text |
| `tables.<handle>` | a table the public may read (`vzk/tables`) |
| `assets["<key>"]`, `assets.has("<key>")` | a picture among the site's files |

A visitor's text (`query`, `params`, a row of a table visitors write to) is
shown as text and is refused in an address, a class, a style or an id.

## A navbar

There are two ways. A page may hand the layout its own links as a field,
which suits a site whose links differ from page to page:

```html
<script server>
  export const fields = {
    links: [
      { label: "Prices", href: "/prices" },
      { label: "Areas", href: "#areas" },
      { label: "Call me back", href: "#contact" },
    ],
  }
</script>
```

Or the layout draws `site.pages`, which holds every page that has a fixed
address and says `export const name`, in the order of its address:

```html
<nav aria-label="Main">
  <a :for={p in site.pages} href={p.url} class={[p.current && "on"]}>{p.name}</a>
</nav>
```

Write `p.url` into `href` and compare `p.path`, never `p.name`. A `[slug]`
page and a page with no name are not in the list.

## Pictures

`list_files` and `upload_file` answer each file's `key`. A file reads the
picture as `assets["<key>"]`, and asks first where the site may not have it
yet:

```html
<img :if={assets.has("hero")} src={assets["hero"]} alt="">
```

- A key is written out, or is the author's own value. A visitor's text never
  chooses a file.
- `assets["<key>"]` for a key the site has no picture under is a missing
  value, which is why a place for a picture the site may not have yet asks
  with `assets.has` first.
- Every `<img>` says an `alt`: what the picture shows, or `alt=""` for
  decoration.

The `reference` that `list_files` answers for a file is that expression,
ready to write in a page.

## Dates and long text

`today()` is the day the page is asked for, so a year that must move is
`{today().year}`. `formatDate(date, "d MMMM yyyy")` writes a date, with the
names of months in the language of the page.

`{markdown(text)}` draws a Markdown text as markup. It is written in one
place only, as the content of an element, and it is the one value that is
not escaped. `excerpt(text, 30)` gives the first words of such a text as
plain text.
