The siv manual on one page

Every chapter of the manual on one page, to read from start to end, search with Ctrl-F or print.

All of this is also available in siv itself, without a network connection: run siv help. Download siv and read more on the siv home page.

1. Getting started

From downloading siv to your own article in the browser, in five minutes. You need a terminal and a text editor.

Download siv # ↑

siv is one file. Download siv for Linux, let everyone (ugo: user, group and others) run it, and try it:

$ chmod ugo+x siv
$ ./siv
siv – Sivutin static site generator. Help: siv help

Next: Create a website:
  siv init my-website

Every siv command ends with a hint like this "Next:". When you don't know what to do, run siv alone.

To use it as siv in any folder, copy it to a folder on your PATH:

$ sudo cp siv /usr/local/bin/

Create a website # ↑

$ siv init my-website
Created my-website/ from the 'minimal' starter.

Go into your project:
  cd my-website
$ cd my-website

The new website already works: it has a look, a front page and a few sample pages to read and delete.

Write an article # ↑

$ siv new "My first page"
Created content/my-first-page/index.md

Next: Write something on your page:
  micro content/my-first-page/index.md

Open the file in any text editor. The lines between the +++ lines are the front matter: the title and the date. Your text goes below them:

+++
title = "My first page"
published = "2026-10-15 w42 Thu"
+++

Today I made a website with siv.
It took five minutes, and most of
it was writing this text.

## What I did

1. Downloaded siv
2. Ran *siv init* and *siv new*
3. Wrote this page in an editor

The page is a file in a folder:

	$ ls content/my-first-page/
	index.md

Next, an image of my cat.

The text is Markdown: a blank line starts a new paragraph, ## makes a heading, and lines starting with a number make a list. The next chapter shows the rest.

See it in the browser # ↑

$ siv preview
siv 0.1.31 preview 2026-10-15 w42 Thu 04:13
04:13:56 Built in 0.31s: 15 written, 0 removed
Preview: http://localhost:8888/my-first-page/
  You saved this page in the last hour. Front page: http://localhost:8888/
Stop with Ctrl-C. Sandbox: landlock (files and network)

Open the address in a browser. siv opens straight at the page you saved last:

Your Markdown file on the left, the page siv made from it on the right
Your Markdown file on the left, the page siv made from it on the right

Keep the preview running while you write. Every time you save the file, the browser reloads the page. The front page lists your article, newest first, with its first paragraph as the summary:

The front page lists the new article above the sample pages
The front page lists the new article above the sample pages

Stop the preview with Ctrl-C.

Make the website # ↑

$ siv build
siv 0.1.31 build 2026-10-15 w42 Thu 04:16
Converting images: 7 new files...
Built in 2.22s  (templates 0.00s, images 2.17s, other 0.05s)
  Pages       6
  Images      1 → 7 files, 7 encoded, 0 cached
  Output      15 written, 0 unchanged, 0 removed
  Links       38 internal OK
  Checks      OK
  Sandbox     landlock (files and network)
  Skipped     feed.xml: the feed needs the site's address. Set it: siv config base-url https://example.com/

Next: Tip: put an image in your page's folder and add it with ![Description](image.jpg)

The website is now in the public/ folder: plain HTML files, images and nothing else. Open public/index.html in a browser, or copy the folder to any web server, see Publishing.

Every build checks the links between your pages. A broken link stops the build, so visitors never see it.

What's where # ↑

siv.toml      settings: siv config
content/      your pages and their images
templates/    the HTML and CSS around every page
skel/         the starting text of new pages
public/       the finished website, made by siv build
Makefile      make builds, make publish publishes

Each page is a folder with an index.md. The folder's name is the page's address: content/my-first-page/ is shown at /my-first-page/. Images go into the same folder as the page.

When you're lost # ↑

  • siv alone tells you what to do next.
  • siv help lists every help topic. This manual is made from them.
  • siv help siv2201 explains one error code.

Next: Writing with Markdown

2. Writing with Markdown

Pages are plain text in Markdown: a few marks, like * and #, that read well as text and turn into a web page. Each example below shows the Markdown, then what siv makes of it.

siv reads CommonMark, plus tables, footnotes, strikethrough, task lists, heading anchors and definition lists.

Paragraphs # ↑

A blank line starts a new paragraph. Lines next to each other join into one paragraph, so you can break lines wherever you like.

Write as you would write an email. This line joins the one above.

A blank line starts a new paragraph.

Write as you would write an email. This line joins the one above.

A blank line starts a new paragraph.

To break a line inside a paragraph, end it with a backslash:

Roses are red,\
violets are blue.

Roses are red,
violets are blue.

Bold, italic and code # ↑

*Italic*, **bold**, ~~strikethrough~~ and `code`.

Italic, bold, strikethrough and code.

Headings # ↑

A line starting with # is a heading. The page title is the biggest heading, so on a page start from ##:

## A heading

### A smaller heading

#### The smallest

The headings of this manual are made like this. Each heading gets an anchor, made of its text: "Bold, italic and code" gets #bold-italic-and-code. An anchor is the part of an address after #, so a link can open the page at that heading. Move the mouse over a heading to see its # link, and copy the address from it.

When a page has two or more headings, the starter shows a table of contents at the top, like the one on this page.

To choose the anchor yourself:

## Fix it now {#fix}
Read the [siv home page]https://terokarvinen.com/siv. Back to the [contents]../. Up to [Paragraphs]#paragraphs on this page.

Read the siv home page. Back to the contents. Up to Paragraphs on this page.

Link to your own pages with a path from this page: ../other-page/. Or from the site's root: /other-page/. siv checks every link to your own pages when it builds, and a broken one stops the build.

Bare addresses become links by themselves:

See https://example.com/ or www.example.com, or write to tero@example.com.

See https://example.com/ or www.example.com, or write to tero@example.com.

Lists # ↑

- Lists start with a dash
- Each item on its own line
    - Indent four spaces for an inner list
- Back to the outer list

1. Numbered lists
2. Work the same way
  • Lists start with a dash
  • Each item on its own line
    • Indent four spaces for an inner list
  • Back to the outer list
  1. Numbered lists
  2. Work the same way

A task list has a box in each item:

- [x] Buy coffee
- [ ] Write the article

A definition list explains words:

Static website : A website made of plain files, without code running on the server.

Static website : A website made of plain files, without code running on the server.

Code # ↑

Indent with a tab # ↑

The quickest way to show code is to indent it with a tab, or four spaces. siv guesses the language and colors it. Lines starting with $ are commands; the rest is their output:

List the files:

	$ ls -a
	.  ..  content  siv.toml  templates

List the files:

$ ls -a
.  ..  content  siv.toml  templates

The same works for code in many languages:

	def hello(name):
	    print(f"Hello, {name}")
def hello(name):
    print(f"Hello, {name}")

siv guesses from clear signs only: a first line starting with $ , a #! line, and the typical marks of C, HTML, CSS, Python, shell and JavaScript. When it's not sure, the code stays plain text, without colors:

	Tugboat   1970
	Ferry     1956
Tugboat   1970
Ferry     1956

Name the language # ↑

For a longer example, or to name the language yourself, put the code between lines of three backticks, with the language after the first ones:

```c
#include <stdio.h>

int main(void) {
    printf("Hello\n");
    return 0;
}
```
#include <stdio.h>

int main(void) {
    printf("Hello\n");
    return 0;
}

Known names: python, bash, sh, c, go, rust, java, js, html, css and many more. text turns the colors off. linuxcli (or console) colors $ lines as commands and the rest as output, even when the first line isn't a command:

```linuxcli
Install the web server:
$ sudo apt-get install apache2
Reading package lists... Done
```
Install the web server:
$ sudo apt-get install apache2
Reading package lists... Done

When you copy commands from the page, the $ isn't copied along.

Code in a sentence # ↑

Backticks inside a line make code in a sentence: `siv build` shows as siv build.

Turn guessing off # ↑

Guessing is on by default. To turn it off for the whole site:

$ siv config autodetect-code-lang false

Or for one page, in its front matter: autodetect-code-lang = false.

Quotes # ↑

> A quote starts with a greater-than sign.
> Quotes stay "as you typed them".

A quote starts with a greater-than sign. Quotes stay "as you typed them".

siv leaves quotation marks as you typed them: curly quotes differ between languages.

Tables # ↑

| Boat    | Year |
|---------|-----:|
| Tugboat | 1970 |
| Ferry   | 1956 |
BoatYear
Tugboat1970
Ferry1956

The colon in -----: aligns a column to the right. The columns don't need to line up in the text.

A table copied from a spreadsheet is tab-separated. Put it in a ```tsv block as it is:

```tsv
Boat	Engine	Power
Tugboat	1.2	62
Canoe	0.9	27
```
BoatEnginePower
Tugboat1.262
Canoe0.927

Columns of numbers are aligned to the right by themselves. A tsv block can also become a chart: Charts.

Footnotes # ↑

siv is one file.[^single]

[^single]: A static binary, without dependencies.

siv is one file.1

1

A static binary, without dependencies.

The footnotes are collected at the end of the page, with links back.

Images # ↑

An image alone on its line becomes a figure, with its description as the caption:

![Sunrise over a lake]lake.jpg

Put the image in the page's folder. siv makes every size a screen needs. More in Images.

The summary # ↑

The front page and other lists show each page's first paragraph as its summary. To choose a longer summary, put a <!--more--> line after it:

The first paragraph.

The second paragraph, also in the summary.

<!--more-->

The rest of the page.

Notes to yourself # ↑

HTML comments are not published:

Visible text.

<!-- Check the version numbers before the next course. -->

Visible text.

Other HTML is published as it is, for the rare thing Markdown can't do: HTML.

Next: Images

3. Images

Put a photo in the page's folder and add it with one line. siv makes every size a screen needs, removes the camera's location data and adds a caption.

An image with a caption # ↑

Copy the image into the page's folder, next to index.md:

content/my-first-page/index.md
content/my-first-page/lake.jpg
A page with a photo: the Markdown on the left, the page on the right
A page with a photo: the Markdown on the left, the page on the right

An image alone on its line becomes a figure. Its description is shown as the caption, and screen readers read it aloud:

![Sunrise over a lake]lake.jpg
Sunrise over a lake
Sunrise over a lake

Click the image to see it bigger. Press Esc, or click again, to close it:

The image shown bigger, over the page
The image shown bigger, over the page

Without a description, ![](lake.jpg), the image is decorative: no caption, and it doesn't open bigger. Describe every image that matters to the text. A missing description gets a warning.

Images beside the text # ↑

Add a layout after the image, in braces. {.right} floats it to the right, half the text column wide, and the text flows around it:

![The lake]lake.jpg{.right}

The text after the image flows on its left, and continues below it when the image ends. Keep writing, and the text fills the space beside the image, line by line, as long as there is room for it.
The lake
The lake

The text after the image flows on its left, and continues below it when the image ends. Keep writing, and the text fills the space beside the image, line by line, as long as there is room for it.

{.left} works the same way on the left. On a phone, both are as wide as the text.

Wide and full-width images # ↑

{.wide} makes an image wider than the text. {.full} makes it as wide as the browser window:

![A wide image]lake.jpg{.wide}
A wide image
A wide image

Set the widths with siv config text-width 42 and siv config wide-width 64, in em: one em is the width of the letter m.

Small images: logos and icons # ↑

max= shows an image at most this many pixels wide, or fits it into a box:

![siv's sun]sun.svg{max=60}

The sun above is at most 60 pixels wide.
siv's sun
siv's sun

The sun above is at most 60 pixels wide.

{.right max=100x100} fits an image in a 100 × 100 box on the right. Phones still get a sharp version.

An image in a sentence # ↑

An image inside a line of text stays there, as big as it is:

A tiny sun ![sun]sun.svg{max=20} in a sentence.

A tiny sun sun in a sentence.

Put the image inside a link. It opens the address instead of the bigger image:

[![Back to the contents]sun.svg{max=80}]../
Back to the contents
Back to the contents

What siv does to each image # ↑

When it builds, siv turns lake.jpg into several sizes, in two formats: AVIF for current browsers and JPEG for the rest (PNG for PNG images). The browser picks the smallest file that looks sharp on its screen, so a phone doesn't load an image made for a big screen.

siv also

  • turns the image upright, following the camera
  • converts the colors to the ones browsers show
  • removes the metadata, including GPS location
  • never makes an image bigger than the original

The original file isn't published. The converted images are kept in .siv/cache/img/, so only new or changed images are converted. The first build of a site with many photos takes a while:

$ siv build
Converting images: 7 new files...
Built in 2.22s  (templates 0.00s, images 2.17s, other 0.05s)
  Images      1 → 7 files, 7 encoded, 0 cached

SVG and GIF images, and images from other sites, are used as they are.

Choosing the quality # ↑

The default quality suits photos. To compare qualities by eye, with the file sizes:

$ siv util imgtest content/my-first-page/lake.jpg

It makes a page of the same image at several qualities. Choose the smallest file that looks right, and set it:

$ siv config avif-quality 72
$ siv config jpeg-quality 82

Other settings # ↑

After the image, in braces:

.right .left .wide .full   layout
#lake                      an anchor, to link to the image
focus=30%,60%              the point to keep when the image is cropped
max=200                    at most 200 pixels wide
max=200x150                fits in a 200 × 150 box

In siv.toml (siv config):

image-widths = [480, 800, 1200, 1600, 2400]   sizes of AVIF images
fallback-widths = [800, 1600]                 sizes of JPEG or PNG images
avif-quality = 72
jpeg-quality = 82

The details, like how a browser picks the size: siv help images.

Next: Charts and diagrams

4. Charts and diagrams

Write the numbers as text, and siv draws the chart when it builds the site. Charts are images inside the page: they need no JavaScript, they print, and they follow the visitor's dark mode.

A bar chart # ↑

Write the data in a block named bar. Separate the columns with one tab: the first row names the series, the first column labels the points.

```bar title="Boats in the harbor"
Year	Sail	Motor
2023	15	28
2024	21	25
2025	26	24
```
Boats in the harbor Sail Motor 0 10 20 30 2023 2024 2025 2023, Sail: 15 2024, Sail: 21 2025, Sail: 26 2023, Motor: 28 2024, Motor: 25 2025, Motor: 24
Data
YearSailMotor
20231528
20242125
20252624
Boats in the harbor

A table copied from a spreadsheet is already tab-separated: paste it as it is. Under each chart, "Data" opens the numbers as a table, for screen readers and for anyone who wants the exact values.

A line chart # ↑

line draws one line for each series. An empty cell is a missing value, a gap in the line:

```line title="Water temperature" unit=" °C"
Month	2024	2025
May	8,5	9,1
Jun	14,2	15,0
Jul	19,8	
Aug	18,1	20,3
```
Water temperature 2024 2025 7,5 °C 10,0 °C 12,5 °C 15,0 °C 17,5 °C 20,0 °C 22,5 °C May Jun Jul Aug May, 2024: 8,5 °C Jun, 2024: 14,2 °C Jul, 2024: 19,8 °C Aug, 2024: 18,1 °C May, 2025: 9,1 °C Jun, 2025: 15,0 °C Aug, 2025: 20,3 °C
Data
Month20242025
May8,59,1
Jun14,215,0
Jul19,8
Aug18,120,3
Water temperature

Numbers can have a decimal comma, 8,5, and spaces between thousands, 12 000.

A stacked bar chart # ↑

stack puts the series on top of each other, to show the total:

```stack title="Visitors by season"
Season	Day trips	Overnight
Spring	120	40
Summer	480	210
Autumn	160	50
```
Visitors by season Day trips Overnight 0 200 400 600 800 Spring Summer Autumn Spring, Day trips: 120 Summer, Day trips: 480 Autumn, Day trips: 160 Spring, Overnight: 40 Summer, Overnight: 210 Autumn, Overnight: 50
Data
SeasonDay tripsOvernight
Spring12040
Summer480210
Autumn16050
Visitors by season

Options # ↑

After the name of the chart:

title="Text"    the caption, also read by screen readers
unit=%          added to the numbers on the axis: 50%
unit=" °C"      quotes keep the space: 20 °C

The series get the colors of a palette that works for color-blind visitors and on dark backgrounds. To change a color, add to templates/layout.css:

.chart .s1 { fill: #c00; stroke: #c00; }

A mistake in the data, like a word where a number should be, stops the build. The error tells the line to fix.

Sequence diagrams # ↑

A sequence block shows messages going back and forth, top to bottom:

```sequence title="Loading a page"
Browser -> Server: GET /charts/
Server -> Server: find the file
Server --> Browser: 200 OK
```
Loading a page Browser Server GET /charts/ find the file 200 OK
Steps
  1. Browser → Server: GET /charts/
  2. Server → Server: find the file
  3. Server → Browser: 200 OK
Loading a page

-> is a message and --> a reply, drawn dashed. The participants are drawn in the order they first appear. Lines starting with # are comments. "Steps" opens the messages as a numbered list.

A table instead # ↑

A tsv block becomes a table instead of a chart: Tables.

Next: Pages, folders and addresses

5. Pages, folders and addresses

A siv website is a folder of folders. Each page is a folder with an index.md file, and the folders become the addresses of the pages.

A page is a folder # ↑

Everything in content/ is published at the same path:

content/index.md                  /                the front page
content/hello/index.md            /hello/
content/hello/cat.jpg             /hello/cat.jpg
content/boats/tugboat/index.md    /boats/tugboat/

Keep each page's images and other files in its folder, and use them by name: ![Cat](cat.jpg). When you move the folder, everything moves along.

A folder without index.md is not a page, but its files are still published. Folders inside it that have an index.md are pages again.

Create a page # ↑

$ siv new "Trams in Helsinki"
Created content/trams-in-helsinki/index.md

The title goes into the page as you wrote it. The folder name is made of it with the address rule below. A page can go inside another folder:

$ siv new boats/tugboat
Created content/boats/tugboat/index.md

A short name like boats/tugboat gets the title "Tugboat". Use quotes around a title with characters the shell treats specially, like ', ? or !: siv new "Tero's trip: what is DNS?".

New pages start from skel/default.md, a skeleton you can edit:

+++
title = "{{ title }}"
published = "{{ now }}"
+++

Write the first paragraph here. It's the summary shown on the front page.

Write the rest of the page here.

The front matter # ↑

The lines between +++ at the top of a page are its front matter, in TOML: a name, = and a value.

+++
title = "Boat repairs"
published = "2026-10-12 w42 Mon"
tags = ["boats", "repair"]
+++

The text of the page starts here.

The most used keys:

title          the page title
published      the date, optionally with a time
updated        the date of the last real update
tags           a list of tags: siv help tags
draft = true   shown in preview only, never published
listed = false published, but left out of lists
aliases        old addresses that lead here, see below
cover          an image for lists of pages

Every key: siv help front-matter. A misspelled key gets a warning that suggests the right one.

Dates # ↑

published = "2026-10-12 w42 Mon"
published = "2026-10-12 w42 Mon 14:30"
published = 2026-10-12

The first form, from calendar.txt, has the week number and weekday, so you see them while you write. If they don't match the date, siv tells the right ones:

content/foo/index.md:3:25: error[siv2201 date]: 2026-12-01 is w49 Tue, not w01 Mon

Lists show dated pages first, newest first, then pages without a date.

Drafts # ↑

A page with draft = true is shown by siv preview, but never built into the website. Remove the line to publish it. A link from a published page to a draft gets a warning, as the link would go nowhere.

Addresses # ↑

The address of a page comes from its folder name. Folder names, new pages and heading anchors follow one rule:

  • only a-z, 0-9 and -
  • lowercase
  • ä, ö and å become a, o and a; other accents are removed
  • every other character becomes -

"Catalyzing change in Öljytön enterprises" becomes catalyzing-change-in-oljyton-enterprises.

Write a link to your own page as a path, from this page or from the root:

[Tugboat]../boats/tugboat/
[Tugboat]/boats/tugboat/

siv checks every link to your pages, and every anchor, on every build. A broken link stops the build, with the file and line to fix.

In the finished website, siv writes the links in the style set in siv.toml. The default, links = "relative", works on any server, in any folder.

Moving a page # ↑

Addresses are forever: other sites and bookmarks link to them. When you move or rename a page, keep the old address working with an alias:

+++
title = "Tugboat"
aliases = ["/old-address/"]
+++

The old address becomes a small page that sends the visitor to the new one.

The 404 page # ↑

A page at content/404/index.md is the "page not found" page. Tell your web server to show /404/ for missing pages: Publishing.

Site-wide files # ↑

There is no separate folder for static files. Files next to the front page are published at the root of the site:

content/favicon.ico     /favicon.ico
content/robots.txt      /robots.txt
content/fonts/a.woff2   /fonts/a.woff2

Files and folders whose name starts with a dot, like .git, are not published, with a warning. .well-known/ is the exception, as certificates need it.

A text file starting with +++ is a template that siv fills in, like the Atom feed content/feed.xml. Other files are copied as they are.

Dated copies of a page # ↑

A course page changes every year, but last year's version should stay readable. siv util archive makes a dated copy, an edition, at an address of its own:

$ siv util archive karaoke-101
Created the 2026 edition in content/2026/karaoke-101-2026/:
  content/2026/karaoke-101-2026/index.md

Details: siv help editions.

Reusable text # ↑

A snippet is text you use on many pages, kept in skel/snippets/:

$ siv util insert old-version karaoke-101
Inserted skel/snippets/old-version.md at the top of content/karaoke-101/index.md

The text becomes part of the page, as plain Markdown. Details: siv help snippets.

Next: Lists, tags and the feed

6. Lists, tags and the feed

siv lists your pages for you: on the front page, on the list of all pages, on tag pages and in the feed. When you add a page, every list gets it.

The front page # ↑

The starter's front page lists the pages, newest first, each with its title, date and summary. The summary is the first paragraph, or the text above a <!--more--> line.

The front page is content/index.md. The text in it is shown above the list:

+++
title = "My website"
template = "list.html"
+++

Welcome. I write here about what I'm working on and learning.

template = "list.html" makes the page a list. Any folder's page can be one: content/blog/index.md with template = "list.html" lists the pages in content/blog/.

To leave a page out of the lists, but still publish it:

listed = false

All pages # ↑

/all/ lists every page of the site, by date. The starter links to it from the footer of every page.

Tags # ↑

Give a page tags in its front matter:

tags = ["boats", "repair"]

Tag pages are made by one page with for-each. The kitchen-sink starter has one: siv init my-test --starter kitchen-sink.

+++
title = "Tags"
template = "tags.html"
for-each = "tags"
listed = false
+++

This page in content/tags/index.md makes /tags/, listing every tag, and a page for each tag, like /tags/boats/, listing its pages.

The tag page of the kitchen-sink starter: every tag, and how many pages have it
The tag page of the kitchen-sink starter: every tag, and how many pages have it

The template tags.html decides what they show. Pages with many tags in common can be listed with page.related(), and long lists split over several pages with paginate(): siv help tags.

The feed # ↑

Feed readers check a site's feed to see new pages. siv makes an Atom feed at /feed.xml, with the 20 newest pages. The feed needs the address of your site, as feeds use full addresses:

$ siv config base-url https://example.com/
$ siv config timezone Europe/Helsinki

Without the address, the build skips the feed and tells you so. The time zone makes the times right in feed readers.

Drafts, unlisted pages and pages without a date are left out of the feed. To leave out one page, add in-feed = false.

The feed is made by the template content/feed.xml. To send only the summaries, change p.content to p.summary in it. Details: siv help feed.

With the address set, siv also writes /sitemap.xml for search engines.

Pages in other languages # ↑

Set the site's language, and a page's own language in its front matter:

$ siv config lang en
+++
title = "Sauna"
lang = "fi"
+++

Browsers, screen readers and translators use it: a screen reader reads a Finnish page with a Finnish voice.

A page in two languages has a file for each, in the same folder:

content/sauna/index.md        /sauna/       the main version
content/sauna/index.fi.md     /sauna/fi/    in Finnish

siv can also detect the language of each page from its text: siv help languages.

Next: Changing the look

7. Changing the look

A new site already has a look. Change it when you want to, from a color to the whole page. Keep siv preview running: the browser reloads every time you save.

Where the look is # ↑

templates/style.css    colors, fonts, spacing: your own CSS
templates/layout.css   images, charts and the image viewer
templates/code.css     the colors of code
templates/base.html    the HTML around every page
siv.toml               the width of the text and other settings

style.css is short and yours to change. The other CSS files you seldom need to touch. siv puts the CSS inside every page, so a page shows at once, without waiting for another file. The code colors are only added to pages that have code.

Colors and fonts # ↑

The colors are at the top of templates/style.css. Change one, and the whole site follows:

:root {
  --text: #1d1d1f;
  --background: #fdfdfc;
  --muted: #6b6b70;
  --link: #1a5fb4;
  --code-background: #f2f2f0;
}

The starter uses the visitor's own system font. For a bookish look, without any font files, change font-family under body:

font-family: ui-serif, Georgia, serif;
A new background color, link color and font, and the front page with them
A new background color, link color and font, and the front page with them

Dark mode # ↑

Visitors whose computer or phone is in dark mode get the second set of colors, inside @media (prefers-color-scheme: dark) in style.css. Charts and code follow it too:

The starter in dark mode
The starter in dark mode

Change both sets of colors, or delete the dark one to have one look only.

Width of the text # ↑

$ siv config text-width 40
$ siv config wide-width 60

The text column is 40 em wide, and {.wide} images 60 em. One em is the width of the letter m, so the text stays easy to read at any font size. siv uses the widths to choose the image sizes too.

Your own font files # ↑

Put the font files on your own site, rather than loading them from a font service: the site keeps working, and visitors aren't tracked.

content/fonts/myfont.woff2

Declare the font in templates/style.css, and use it:

@font-face {
  font-family: MyFont;
  src: url(/fonts/myfont.woff2) format("woff2");
  font-display: swap;
}
body { font-family: MyFont, ui-serif, serif; }

Write the address from the root of the site, /fonts/.... siv turns it into the right address on every page. More: siv help fonts.

A menu # ↑

The header of every page is in templates/base.html. Add links to it:

<header>
  <nav>
    <a href="/" class="site-title">{{ site.title }}</a>
    <a href="/about/">About</a>
    <a href="/blog/">Blog</a>
  </nav>
</header>

Make the pages first, siv new About. A link in a menu is checked like any other: if the page doesn't exist, the build stops and says so.

Templates # ↑

Templates turn each page into HTML. They are HTML files with places for the page's values, written in the Jinja2 template language:

base.html     the frame of every page: head, header, footer
page.html     a normal page, inside base.html
list.html     a page that lists other pages, like the front page
all.html      the list of all pages
macros.html   pieces used by the others, like the table of contents

To show the date and tags under each title, edit page.html:

<h1>{{ page.title }}</h1>
{% if page.published %}<p class="date">{{ page.published }}</p>{% endif %}
{% if page.tags %}<p class="tags">{{ page.tags | join(", ") }}</p>{% endif %}

A template of your own # ↑

Copy page.html to templates/recipe.html, change it, and use it in a page's front matter:

template = "recipe.html"

Your own front matter keys # ↑

Declare a key in siv.toml, with the value pages get when they don't set it:

[keys]
subtitle = ""

Then a page can set subtitle = "and other stories", and a template show it:

{% if page.subtitle %}<p class="subtitle">{{ page.subtitle }}</p>{% endif %}

What a template can use # ↑

Put {% debug %} in a template, and siv preview shows every value the page has. Every value, function and filter: siv help template-reference.

The kitchen-sink starter is a fuller example to copy from: siv init my-test --starter kitchen-sink. Templates and CSS never touch your pages in content/, so you can try things safely.

HTML # ↑

HTML in Markdown # ↑

Write HTML where Markdown has no mark of its own:

Press <kbd>Ctrl</kbd>+<kbd>C</kbd> to stop.

<details>
<summary>Show the long output</summary>

Markdown works here, between blank lines.

</details>

Press Ctrl+C to stop.

Show the long output

Markdown works here, between blank lines.

An HTML file in content/ is published as it is, for a page you made elsewhere.

What siv checks # ↑

Every build checks the pages siv publishes: an element not closed, the same id twice, an image without a description or a size, a link without text, and every link inside the site. For a full check against the HTML standard, use the W3C checker.

JavaScript # ↑

A siv site needs no JavaScript. Use it to add something on top, so the page still works without it. The image viewer works like this: without the script, clicking an image simply opens it.

content/lightbox.js     a script, published as it is
templates/base.html     loads it: <script src="/lightbox.js" defer></script>

More: siv help js.

Next: Publishing

8. Publishing

The finished website is the public/ folder. Publishing means copying it to a web server. Any server that can serve files will do: no database, no PHP, nothing to update.

Build # ↑

$ siv build

Only changed files are written. Files siv didn't make are removed from public/, so it always matches your project. If there is an error, nothing is written, and public/ keeps the last working version.

--strict    stop with an error code if there are warnings
--drafts    include pages with draft = true

Copy it to the server # ↑

The project's Makefile publishes with rsync:

$ make publish

The first time, edit the Makefile: delete the @echo line, remove the # in front of the rsync line, and change the server's address:

rsync -a --chmod=D755,F644 --exclude=/.well-known/acme-challenge/ public/ tero@example.com:/home/tero/publicsite/

Try it first with --dry-run added: rsync lists what it would send, without sending anything.

  • -a keeps the times of the files. siv gives each file the time of its newest source, so rsync sends only the files that really changed.
  • --chmod=D755,F644 lets the web server read the files. Without it, a server may answer "403 Forbidden".

make publish never deletes anything on the server: addresses are forever. make cleanup deletes only old copies of converted images.

A safer server # ↑

Make a separate user on the server for publishing, and let it only copy files to the web folder. In that user's ~/.ssh/authorized_keys:

command="rrsync /home/tero/publicsite/",restrict ssh-ed25519 AAAA...

Server settings # ↑

The 404 page, content/404/index.md, is published as /404/index.html. Tell the server to show it for missing pages. In nginx:

error_page 404 /404/index.html;

In Apache: ErrorDocument 404 /404/index.html.

A page you moved has an alias: Moving a page. A server redirect (301) is faster, and set in the server's configuration.

Keeping an old site # ↑

Moving from an old site? Put its files in oldpublic/. siv copies them into the new site, so every old address keeps working. A new page with the same address as an old file replaces it, with a warning.

Moving from Hugo: siv import hugo DIR makes a siv project of a Hugo site, with the old addresses kept as aliases. Details: siv help import and siv help oldpublic.

Next: Checks and explanations

9. Checks and explanations

siv checks your site on every build, so visitors don't find the mistakes first. Every message says the file, the line and what to do.

$ siv build
siv 0.1.31 build 2026-10-15 w42 Thu 04:24
content/my-first-page/index.md:22: error[siv5001 broken-link]: link to /my-cat/ goes nowhere
Build failed: 1 error. Nothing was written to public/.

The message has the file and line, error or warning, a code and a name, and what is wrong. An error stops the build, and the website in public/ stays as it was. A warning doesn't stop the build.

Each code has a help page with an example and the fix:

$ siv help siv5001
# siv5001 broken-link

An internal link points to an address where nothing is published.
...

What siv checks # ↑

  • links between your pages, and links to anchors, like #tables
  • links to drafts, which won't be published
  • dates, front matter and settings
  • HTML elements left open or closed twice, the same id twice
  • images without a description or a size
  • links without text, which screen readers can't name
  • CSS with a missing { or }
  • invisible characters, like zero-width spaces
  • JPEG files published with the camera's GPS location

A wrong weekday in a date is an error too, with the right one:

content/my-first-page/index.md:3:25: error[siv2201 date]: 2026-10-15 is w42 Thu, not w42 Fri

Every code: siv help codes.

Check without building # ↑

$ siv check
Checks OK.

With a file, siv check content/my-first-page/index.md shows only the messages about that file. Editors can run it every time you save, and jump to the line: Editors.

Turn a check off # ↑

Most checks can be turned off, or made a warning or an error, by name. In siv.toml:

[checks]
dotfile = "off"
url-name = "error"

Or for one folder and everything below it, in the folder's index.md:

[recursive.checks]
dotfile = "off"

What is this file? # ↑

siv explain tells what a file is, what uses it and where it goes:

$ siv explain content/my-first-page/index.md
content/my-first-page/index.md — page source
  Published at /my-first-page/
  Output       public/my-first-page/index.html
  Template     templates/page.html
  Language     en

Front matter:
  title              "My first page"                            page title
  published          "2026-10-15 w42 Thu 04:13"                 publication date, optionally with time

Code blocks without a language name:
  line 17: guessed linuxcli

It also explains folders, templates, settings and addresses:

siv explain                     the current folder
siv explain templates/page.html which pages use the template
siv explain siv.toml            every setting and its value
siv explain /my-first-page/     an address: its source and template

Other tools # ↑

siv stats         a report of the site: pages, images, words
siv fmt           tidies the front matter of every page
siv util vitag    edit the tags of every page at once, in an editor
siv config        lists every setting, or changes one

Every command: Reference.

Next: Editors

10. Editors

Any text editor works for siv pages. A few settings make the editor show siv's messages next to your text, insert images and complete commands.

micro # ↑

micro is a simple terminal editor, and siv suggests it in its hints. siv prints the settings that make micro check a page every time you save it:

$ siv util micro
-- siv checks for the micro editor's linter plugin.
-- Put this in ~/.config/micro/init.lua (create the file if it's missing).
...

Put the output in ~/.config/micro/init.lua. Then a broken link or a wrong date shows up next to the line, as soon as you save.

vim and Emacs # ↑

siv check FILE prints its messages as file:line:column, which most editors understand:

vim     :set makeprg=siv\ check\ %   then :make
Emacs   M-x compile RET siv check FILE

Inserting an image # ↑

$ siv util chooseimg content/my-first-page

lists the images in the folder, newest first, and prints the Markdown for the one you choose: ![Sunrise](sunrise.jpg). An editor can run it to insert an image at the cursor.

Tab completion # ↑

Press Tab to complete siv's commands, options and help topics:

$ siv util completion bash --install

For zsh: siv util completion zsh --install.

Next: Reference

11. Reference

Every command, setting and front matter key, in one place. The full details are inside siv: siv help TOPIC.

Commands # ↑

siv                         What to do next
siv init NAME               Create a project from a starter
    --starter minimal       Which starter: minimal or kitchen-sink
siv new "Title"             Create a page: content/title/index.md
siv new boats/tugboat       Create a page in a folder
siv preview                 Show the site on localhost while you write
    --port 8888             First port to try
    --bind 0.0.0.0          Listen on all interfaces (default localhost)
siv build                   Build the website into public/
    --strict                Exit with code 2 if there are warnings
    --drafts                Include pages marked draft = true
siv check [FILE]            Run the checks, write nothing
siv explain [-r] [PATH]     Explain files, folders and addresses
siv explain --vars PATH     Values a template sees
siv clean                   Delete the cache, .siv/cache/
siv config                  List every setting and where it comes from
siv config KEY VALUE        Change a setting in siv.toml
siv fmt [--check]           Tidy front matter
siv stats                   A report about the site
siv import hugo DIR [NAME]  Create a project from a Hugo site
siv help [TOPIC|CODE]       Documentation
siv help --dump-all         Every help page as one text, for grep

Tools:

siv util archive PAGE [YEAR] [LABEL]   a dated edition of a page
siv util insert NAME [PAGE]            insert a snippet from skel/snippets/
siv util imgtest FILE                  compare image qualities by eye
siv util vitag                         edit the tags of all pages at once
siv util lang [PAGE...]                write detected languages into pages
siv util chooseimg [DIR]               print Markdown for an image
siv util completion bash|zsh           Tab completion for the shell
siv util micro                         settings for the micro editor

Every command works anywhere inside the project: siv finds siv.toml in the folder or the folders above it. Exit codes: 0 success, 1 errors, 2 warnings with --strict.

Settings # ↑

Settings are in siv.toml. siv config lists every one, with its value and where the value comes from. Change one with siv config KEY VALUE, or edit the file.

title                  the site's title
base-url               the site's address: needed for the feed and sitemap
lang                   the default language of pages, like "en"
autodetect-lang        languages siv may detect, like ["en", "fi"]
links                  how links are written: relative, root, absolute or file
hints                  next-step hints: all, pro or off
timezone               for the feed, like "Europe/Helsinki"
text-width             width of the text column, in em
wide-width             width of {.wide} images, in em
image-widths           sizes of AVIF images, in pixels
fallback-widths        sizes of JPEG and PNG images
avif-quality           1-100
jpeg-quality           1-100
autodetect-code-lang   guess the language of code blocks
verbatim               files copied byte for byte
dotfiles               dotfiles that are published, like [".well-known"]
[keys]                 your own front matter keys and their defaults
[checks]               turn checks off, or make them errors

Front matter # ↑

title               the page title
published           the date, optionally with a time
updated             the date of the last real update
tags                a list of tags
lang                the page's language, like "fi"
summary-lang        the language of the summary, if different
cover               an image for lists of pages
template            the template, like "list.html"
allow-templating    true lets the text use template code
skel                the skeleton for new pages below (in [recursive])
aliases             old addresses that lead here
draft               true: shown in preview only
listed              false: left out of lists
in-feed             false: left out of the feed
archived, edition   set on dated copies by siv util archive
for-each            make a page for each value, like each tag
verbatim            files copied byte for byte
[recursive]         settings for this folder and everything below
[checks]            turn checks off for this page

[recursive] settings apply to the folder's page and everything below it. What is written closer to the page wins: the command line, then the page, then the nearest folder, then folders further up, then siv.toml.

Safety # ↑

siv never runs as root. While it runs, it limits itself with Linux's Landlock: each command can only touch the files it needs, and only the preview may use the network, for its own port. Even a bug, or a malicious template, can't read your other files or send them anywhere.

  Sandbox     landlock (files and network)

For a second sandbox, any command runs inside bubblewrap with --bwrap. Details: siv help security.

Your website is yours # ↑

siv is free software, licensed GPL-3.0-or-later. That license does not extend to your website. The files siv init makes, the pages you write and everything siv build writes are yours to use, change, publish and license however you like. Details: siv help license.

All help topics # ↑

$ siv help topics

lists every topic. To read or search all of it at once:

$ siv help --dump-all > siv-help.txt