---
title: Writing a site in vzk
description: 'Read this guide before the first write: it holds how a site is laid
  out, the loop to work in, and how to read a refused write.'
sidebar:
  order: 8
---

Read this guide before the first write: it holds how a site is laid out, the
loop to work in, and how to read a refused write.

vzk is one language for a whole site: HTML with values in braces, control
written as attributes of an element, and a small script that runs before the
markup. The compiler checks every file of the site together on every write.
`describe_vocabulary topic=language` is the whole language on two pages, with
its examples. Read it before the first write, and write only what it shows.

| Task | Guide or vocabulary |
| --- | --- |
| The whole language, with its examples | `language` |
| Pages, layouts, components, fields, links, pictures | `vzk/pages` |
| Rows in a page, a row's own page, search, forms | `vzk/tables` |
| Languages and translated words | `vzk/languages` |
| Classes, the stylesheet, a file's own style | `vzk/styling` |
| A file's client script, behaviours, packages | `vzk/javascript` |
| The paths a site's files may have | `layout` |
| Field types and roles of a table | `fields` |
| Head fields a site sets for every page | `meta` |
| Complete starting stylesheets | `looks` |
| A new site: whose it is, which pages, what words | `building` |
| A list that stays up to date, visitors who sign in, a chat | `apps` |

Call `describe_vocabulary` with `topic=guides` and a guide's name to read
one.

## A site is a set of files

| File | What it is |
| --- | --- |
| `site.json` | the edition, the fields every file reads as `site.fields.<name>`, and how the site's languages are addressed |
| `layouts/site.vzk` | the layout, which wraps every page |
| `pages/index.vzk` | the page at `/` |
| `pages/prices.vzk` | the page at `/prices` |
| `pages/journal/[slug].vzk` | the page each row of a table has, under `/journal` |
| `components/PostCard.vzk` | the component written `<PostCard />` |
| `locales/uk/pages/prices.json` | the words of `pages/prices.vzk` in Ukrainian (`vzk/languages`) |
| `tests/<name>.test.ts` | scenarios the site's pages are played by (`vzk/tests`) |
| `scripts/<name>.js` | the site's own JavaScript (`vzk/javascript`) |
| `stylesheet.css` | the site's stylesheet (`vzk/styling`) |
| `importmap.json` | packages pinned from the web |

`describe_vocabulary topic=layout` lists the accepted paths, and a write to
any other path is refused. Nothing is imported: a component is used by its
name, and a layout is applied by the site.

A table is a file too, `tables/<handle>.json`. Make and change one with the
table tools (`vzk/tables`), which write it on a branch; merging the branch
changes the table.

Files live on branches. `main` is what the public site serves. Nothing is
written to `main` directly: work happens on a branch of your own, and merging
that branch is the publish.

## A file

A `.vzk` file holds up to four parts, in this order, each optional: the
server script, the markup, one `<style>` and one client `<script>`.

```html
<script server>
  const notes = [
    "I'm not VAT registered, so the price is the price.",
    "Parts are charged at what I paid for them, and you get the receipt.",
  ]
</script>

<head>
  <title>Prices | Dan Okafor Plumbing, South Manchester</title>
</head>

<div id="ask" class="px-gutter py-xl max-w-page mx-auto">
  <h2 class="font-heading font-strong text-title text-ink">Not sure which one it is?</h2>
  <ul class="flex flex-col gap-2xs max-w-lead">
    <li :for={note in notes} class="text-sm leading-6 text-muted">{note}</li>
  </ul>
  <a href="tel:{site.fields.dial}" class="btn-primary">Call {site.fields.phone}</a>
</div>
```

The markup is HTML read strictly:

- Every element is closed. Void elements such as `<img>`, `<br>` and
  `<input>` stand alone, and `<x />` closes an element with no content.
- A file holds what goes inside the body, and a `<head>`. It writes no
  doctype, `<html>` or `<body>`.
- `{expr}` writes a value, escaped. A missing value, `true` and `false` write
  nothing.
- `name={expr}` is an attribute, left out when the value is missing, and
  `name="text {expr}"` is text with values in it.
- `class={["card", lead && "card-lead"]}` is a list, and `false` entries
  drop out. A class name is one whole literal (`vzk/styling`).
- An address (`href`, `src`, `action`) starts with `/`, `#`, `?`,
  `https://`, `http://`, `mailto:`, `tel:` or `sms:`.
- `<!-- a comment -->` and `note="..."` on an element are the author's and
  never reach a visitor.
- Refused: `onclick` and any other attribute that holds code, a `<script>`
  other than the file's own two, `<object>` and `<embed>`.

Control is written on the element it governs:

| Written | Means |
| --- | --- |
| `:if={cond}`, `:else-if={cond}`, `:else` | siblings in a row |
| `:for={item in list}`, `:for={(item, i) in list}` | once for each item |
| `<template :if={cond}>`, `<template :for={item in list}>` | groups several elements and adds none |

A condition is a boolean, a text or a value that may be missing. A number or
a list is never one: write `count > 0` and `list.length > 0`.

The server script is a small, pure part of TypeScript. It has `const` and
`let`, `function` declarations at the top, `if` and `else` with braces, and
`for (const x of list)`. It has no `while`, no `i++`, no `push` or `sort`,
no `await`, no `import` and no `===`: write `[...list, x]`, `toSorted` and
`==`. The `language` topic lists the methods of a list and of a text and the
functions a file may call (`formatDate`, `markdown`, `plural` and the rest).

## Pages, layouts and components

A page is a file under `pages/`, and its path is its address. A layout wraps
every page and draws the page where it says `<slot />`. A component is a
file under `components/` that takes its inputs out of `props`. `vzk/pages`
covers all three, and the named fields an owner edits apart from the markup.

## Links between pages

Write a link to a page as its plain address:

```html
<a href="/prices" class="link-underline">Every price</a>
<a href="#contact" class="link-underline">Or ask me to ring you back</a>
```

The check warns about a literal link to a page the site does not have, and
about `href="#name"` that no element of the page answers. On a site in
several languages a link follows the language being read without anything
written for it (`vzk/languages`). `site.pages` lists the site's named pages
for a navbar (`vzk/pages`).

## The head

A file says what it wants in the page's head in HTML's own words, in a
`<head>` at the top level of its markup:

```html
<head>
  <title>Prices | Dan Okafor Plumbing, South Manchester</title>
  <meta name="description" content="What a plumber costs in South Manchester: call-out, hourly rate and the price of common jobs, written down before any work starts.">
</head>
```

The set is closed: `<title>`, `<meta name>` with `description`, `keywords`,
`author`, `theme-color` or `robots`, `<meta property="og:image">` and
`<link rel="canonical">`. Anything else there is refused, and a `<meta>` or
a `<link>` outside `<head>` is too. When a layout and a page declare the
same tag the layout's is used, and it reads the page's through `page.title`
and `page.description`. What no file says comes from the site's meta
(`update_site_meta`). The platform writes the rest of the head, the language
alternates and the sharing tags among them.

## The editing loop

1. Call `get_site`. It says the site's pages, its tables, the branches
   not yet merged and the preview URL.
2. Start a branch for the task with `create_branch`, named for it
   (`agent/new-hours`). One branch is one change the owner approves or turns
   down whole.
3. Call `outline` with the branch as `ref`. It lists the site's files, and
   with `path` the blocks of one `.vzk` file: each block's address, lines,
   size and note. A block is a top-level element of a page or a layout, or
   any element that carries `note` or `region`, and its address is its file
   and its `id`: `pages/index.vzk#hero`.
4. Read what you will change: one block with `read_block`, or whole files
   with `read_files`. Never write from memory. To learn what blocks do
   before reading their text, call `describe`: see "What a block does, and
   what its owner noted" below.
5. Write. `write_block` replaces one block whole, `edit_block` moves, copies,
   renames, removes, wraps, extracts or adds one without its text being sent, `edit_file` changes
   one passage of one file (use it for a block that shares a line with other
   markup), and `write_files` commits any number of whole files at once. Pass
   the commit you read at as `base`, and a `message` saying what changed in
   the owner's terms. A `.vzk` file is fixed and put in canonical form as it
   is written, so the branch may hold other text than you sent. The answer
   lists each fix under `applied` and each file whose text changed under
   `formatted`, with the changed lines as they now stand: copy a later `find`
   from those. A file marked `read_again` gained or lost lines, or changed
   in too many to list, so read it again before you edit it. A write the
   checks refuse says the same two, since their sentences name lines of the
   text as it stood fixed and formatted.
   `edit_block` alone writes the file as the edit leaves it, neither fixed
   nor formatted, and its answer has no `applied` and no `formatted`.
6. Try a change before writing it with `check`: give it the branch as `ref`
   and the same `files` list `write_files` takes. It writes nothing, and
   answers `ok`, `problems` and `warnings`, and under `would_apply` and
   `would_format` what a write would fix and format.
7. Give the owner the branch's `preview_url`. `diff` with the branch as `to`
   lists what it changes against `main`. A preview runs the branch's own
   scripts, so a menu or a gallery responds there as it will once merged.
8. Merge only after approval, with `merge_branch`. The site goes live with
   everything on the branch in one step.

Merge without asking only when the owner asked for the change to go live at
once. `list_branches` shows every branch not yet merged and who started it;
leave a branch you did not start alone unless the owner asks you to carry it
on.

### Moving, copying and removing a block

`edit_block` changes where a block is and leaves what it says, so the file
is neither read nor sent. Use it in place of `write_block` when a block's
text stays as it is, and in place of `edit_file` when the change is a whole
block. Its `operation` is `delete`, `duplicate`, `rename`, `move`, `wrap`,
`extract` or `inline` for the block at `address`, or `insert` for new
`markup`. `wrap` takes as `markup` one element with nothing in it, such as
`<div class="band"></div>`. A `move` and an `insert` say where with one of
`before`, `after` or `into` and a block's address, or `end` and a file's
path. A block moves within its own file or to another page or layout.

`rename` gives the block the new `id`, and what points at the old one in
its file follows. `extract` makes the block a component of the `name` you
give, written to `components/<name>.vzk`, with what it reads of its page
passed at the tag. `inline` takes the address of a component's tag and puts
the component's markup back in its place, the way back from `extract`.

Move the hours above the hero:

```json
{ "branch": "agent/new-hours", "operation": "move",
  "address": "pages/index.vzk#hours", "before": "pages/index.vzk#hero" }
```

Copy a card and name the copy:

```json
{ "branch": "agent/new-hours", "operation": "duplicate",
  "address": "pages/index.vzk#rye", "id": "wheat" }
```

The answer's `blocks` are the addresses the edit left, so a copy is there to
hand to `read_block` and `write_block`. Each edit is one commit, and the
answer's `parent` and `paths` undo it: call `revert` with `to` the `parent`
and `paths` the `paths`. Undo several edits newest first.

## What a write checks

Every write is checked before its commit is made, and the site is judged
whole: the compiler reads every file of the branch together, against the
tables the site has. A write that leaves an error anywhere is refused and
the branch stays as it was. So a refusal may name a file the write did not
touch, when the change broke what that file reads, such as an input of a
component or a field of a table.

Make a table before the page that reads it: the check holds each `filter`,
`sortBy` and form to the tables the site has, their fields and their marks.

### Reading a refusal

A finding has a file, a line, a column, a code and one sentence that ends
with what to do. A refused write, and each of `check`'s `problems`, names
the file and says its findings in the order the compiler gave them:

```text
pages/index.vzk has 1 error. Line 1, column 4 (VZK0012): `className` is `class` here.
```

All of a file's errors come back in one pass, so fix them together in one
write.

Warnings refuse nothing, and are worth reading before a merge. The common
ones: a top-level element of a page or a layout with no `id` (`VZK0243`,
give each one an `id`, since that is the address `read_block` and
`write_block` find it by), a link to a page the site does not have, a
`class` whose names cannot be read before a page is drawn (`VZK0304`), and a
picture with no `alt` (`VZK0600`).

### What a block does, and what its owner noted

`describe` says what blocks do, in sentences made from their code alone:
what a block shows, what it repeats, where a form of it saves. Give it a
`path` for one file's blocks, an `address` for one block, or neither for
a line on every file of the site (what it is, and how many blocks it
holds), and a `ref` as `outline` takes one. `says` is a list of lines, and
a line indented under another belongs to it. With a `path` or an `address`
and `manifest: true`, the file and each block also carry `facts`, the same
knowledge as data. Files with an error are not described: `check` says
what the errors are.

A person keeps notes on a page's blocks in the notes file beside it,
`pages/index.notes.md` for `pages/index.vzk`, each block's under a heading
that names it:

```markdown
## #hero
Keep it short.
- [ ] Ask about the photo.
```

`read_block`, and `describe` with a `path` or an `address`, answer them
under `notes`: `text` as it was written, `open` with each check-box still
to do, and `done`, the number ticked. Read them before changing a block,
since they are what its owner asked of it. They are never sent to a
visitor. A block with no notes has no `notes` key.

Notes follow their block by its `id`. `edit_block` with `delete` takes
the block's notes with it, and those of the blocks inside it, in the
same commit, and its `paths` name the notes file too. A `rename` moves the
heading of the block's notes, a `move` to another file takes the notes to
the notes file beside that one, and an `extract` takes the notes of the
blocks inside the block to the component's. Any other write
that removes a block or changes its `id` leaves its notes on no block,
and comes back with the warning `VZK0244` under the notes file. Move the
notes under the heading of the block they are now about, or remove them,
in the same branch.

### Spellings from other languages

Each of these is refused with a sentence that says what to write:

| Written | In vzk |
| --- | --- |
| `{{ title }}` | `{title}` |
| `{% if a %}`, and a value with a filter after a bar | `:if={a}` on the element, and a method or a function in place of the filter |
| `className="lead"` | `class="lead"` |
| `:if="a"`, `v-if="a"` | `:if={a}` |
| `{shown && <p>Here</p>}`, `{posts.map(post => <li>...</li>)}` | `:if={shown}` or `:for={post in posts}` on the element |
| `a === b` | `a == b` |

## Stale writes, a moved main and undoing

These work as they do on any site. `base` protects a file somebody else
changed on the branch after you read it: a write that touches one is
refused and names it, so read it again and redo the change. When `main` has
moved under an open branch, `merge_branch` refuses and `update_branch`
replays the branch onto `main` as it is now, listing each conflict for you
to resolve. `log` lists the commits on `main`, and `revert` on a new branch
puts files back as an earlier commit held them.

## What is live at once

These have no branch and no preview, and are live when the call returns:
table definitions, a row, site meta, site languages, and the favicon and
social image. Say so to the owner before making one of those changes.

## Where content belongs

- Markup belongs in a page, and markup several pages share in the layout or
  a component.
- A value the owner changes apart from the markup (the words on a button, a
  price, a list of links) belongs in the file's `export const fields`.
- A value every file reads (a phone number, the opening hours) belongs in
  the `fields` of `site.json`.
- Repeating dated or structured content belongs in a table. An article is a
  `posts` row with its text in `body`.
- A date that has to move with the calendar is written from `today()`, as in
  `© {today().year}`, never typed into a field.
- Visual tokens and CSS rules that several files share belong in
  `stylesheet.css`, and a rule for one file's markup in that file's
  `<style>`.
- A visitor submission belongs in a publicly writable table, through a
  `<form table="...">`.

## Context discipline

Do not load every guide for a small edit. A copy change usually needs
`get_site`, a branch, `outline` for the page and `read_block` for the one
block, not the whole file and not the craft guide.

If the guides and the `language` topic confirm the platform cannot represent
a request, check `list_feature_requests` and file the gap with
`request_feature` right away, without asking the customer first, then build
the closest supported result.
