Pages, layouts, components and fields in vzk
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
Section titled “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
Section titled “Blocks”Every top-level element of a page or of a layout is a block, and has an
id:
<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
Section titled “A layout”layouts/site.vzk wraps every page. The page is drawn where the layout says
<slot />:
<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.titleandpage.descriptionfrom the page’s head,page.path, and the page’s fields aspage.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
Section titled “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:
<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><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 istrue. An input with a default (picture = null) may be left out. class,id,style,hidden,data-*andaria-*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 sayslot="title".:ifand:forwork 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
Section titled “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:
<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
trueorfalseare 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 }[], orkinds: [] 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 aspage.fields.cta. site.jsonholds the fields every file reads, assite.fields.<name>:
{ "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
Section titled “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
Section titled “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:
<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:
<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
Section titled “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:
<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 withassets.hasfirst.- Every
<img>says analt: what the picture shows, oralt=""for decoration.
The reference that list_files answers for a file is that expression,
ready to write in a page.
Dates and long text
Section titled “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.