Skip to content

Writing a site in vzk

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.

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 .vzk file holds up to four parts, in this order, each optional: the server script, the markup, one <style> and one client <script>.

<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).

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.

Write a link to a page as its plain address:

<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).

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:

<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.

  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.

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:

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

Copy a card and name the copy:

{ "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.

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.

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:

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

Section titled “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:

## #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.

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

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.

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.

  • 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="...">.

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.