Getting Started
This guide builds a small Veta site from scratch. By the end, you will understand the project structure, the page generator model, templates, data files, components, Markdown content, Tailwind CSS, the development server, and the production build.
1. Install Veta
Install Veta with any of the methods documented in Installation.
Verify the CLI is available:
veta --version
2. Create A Project
Create a starter project:
veta init my-site
cd my-site
The starter contains these files:
my-site/
veta.yaml
components/
note.html
data/
site.json
pages/
site.js
public/
robots.txt
styles.css
templates/
base.html
Start the development server:
veta dev
Open the printed local URL. Veta builds the site into a temporary directory, serves it locally, watches your project files, and reloads the browser when a rebuild finishes. The development server does not write to dist/.
3. Understand The Config
The starter veta.yaml looks like this:
build:
output: dist
clean: true
dev:
host: 127.0.0.1
port: 3000
watch: []
html:
minify: true
tailwindcss:
stylesheets:
- styles.css
minify: true
The important defaults are:
build.outputis the directory written byveta build.build.cleanremoves the output directory before writing a new build.dev.hostconfigures the local development server host.dev.portconfigures the local development server port.dev.watchis an array of additional directories for the development server to watch, beyond Veta's own files and directories.html.minifyminifies generated.htmlfiles.tailwindcss.stylesheetspoints to Tailwind CSS entrypoints underpublic/.tailwindcss.minifyminifies the generated stylesheet.
4. Edit Site Data
Open data/site.json and change the name or description:
{
"name": "My Veta Site",
"description": "A small site built with Veta."
}
Data files become available in templates and page generators through the data object. The file data/site.json becomes data.site.
5. Generate Pages With JavaScript
Open pages/site.js:
export default function({ data, parse }) {
const home = parse.markdown(`Welcome to **${data.site.name}**.`).html;
const about = parse.markdown(
"This page was generated from `pages/site.js`.",
).html;
return [
{
permalink: "/",
template: "base",
title: "Home",
content: home,
},
{
permalink: "/about/",
template: "base",
title: "About",
content: about,
},
];
}
Every file in pages/ must be a JavaScript file. It must export a default function that returns an array of page objects.
Each page object needs a permalink:
{
permalink: "/about/",
template: "base",
title: "About",
content: "About this site."
}
If template is present, Veta passes content unchanged and trusted to the selected file in templates/. It does not automatically process Markdown or components, so the generator must produce the final expected format, usually HTML. If template is omitted, Veta writes content unchanged as raw output.
6. Use A Template
Open templates/base.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ data.site.description }}">
<title>{{ page.title }} - {{ data.site.name }}</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<nav>
<strong>{{ data.site.name }}</strong>
{% for item in pages %}
<a href="{{ item.permalink }}">{{ item.title }}</a>
{% endfor %}
</nav>
<main>
<h1>{{ page.title }}</h1>
{{ page.content }}
</main>
</body>
</html>
Templates receive four root values:
data: global data loaded fromdata/.pages: every normalized page returned by your page generators.page: the current page.props: component props when rendering a component.
7. Add A Component
Components are templates stored in components/. The starter includes components/note.html:
<aside class="rounded border border-zinc-200 bg-zinc-50 p-4">
{{ props.content }}
</aside>
Use it inside page content:
export default function({ parse }) {
const { html } = parse.markdown(
"Welcome to **Veta**.\n\n<note>Components are explicit.</note>",
);
const content = parse.renderComponents(html);
return [
{
permalink: "/",
template: "base",
title: "Home",
content,
},
];
}
parse.renderComponents resolves registered component tags and passes tag attributes and slot content through props. It does not render Markdown, which is why this example calls parse.markdown first. Components are not resolved unless the generator explicitly calls it.
8. Read Markdown Files
Create content files:
content/posts/hello.md
content/posts/second.md
Example Markdown file with YAML frontmatter:
---
title: Hello World
date: "2026-06-30"
tags:
- intro
---
# Hello World
This post is stored as Markdown.
Generate pages from those files:
export default function({ files, parse }) {
const posts = files.listFiles("content/posts/**/*.md");
return posts.map((path) => {
const { frontmatter, html } = parse.markdown(files.readFile(path));
const content = parse.renderComponents(html);
return {
permalink: files.toPermalink(path, { stripPrefix: "content" }),
template: "base",
title: frontmatter.title,
content,
};
});
}
parse.markdown returns:
{
frontmatter: {
title: "Hello World",
date: "2026-06-30",
tags: ["intro"]
},
content: "# Hello World\n\nThis post is stored as Markdown.\n",
html: "<h1>Hello World</h1>\n<p>This post is stored as Markdown.</p>\n"
}
content is the raw body, while html is the Markdown-rendered body. Without frontmatter, frontmatter is {} and content is the full input.
9. Add Styles With Tailwind CSS
The starter uses public/styles.css as the Tailwind entrypoint:
@import "tailwindcss";
When tailwindcss.stylesheets includes styles.css, Veta reads public/styles.css, runs the embedded Tailwind CSS standalone CLI against the generated output, and writes dist/styles.css.
Use classes directly in templates and components:
<body class="mx-auto max-w-2xl px-6 py-10 text-zinc-950">
10. Build For Production
Stop the dev server and run:
veta build
Veta writes the production site to dist/ by default. Generated .html files are minified when html.minify: true is set. Public assets are copied from public/ to the output root.
You can deploy dist/ to any static host.
Next Steps
Read these next: