## Documentation index
This index lists every available documentation page and its Markdown source.
- [Documentation](https://veta.varavel.com/docs/index.md)
- [Getting Started](https://veta.varavel.com/docs/getting-started/index.md)
- [Installation](https://veta.varavel.com/docs/installation/index.md)
- [Guides](https://veta.varavel.com/docs/guides/index.md)
- [Project Structure](https://veta.varavel.com/docs/guides/project-structure/index.md)
- [Configuration](https://veta.varavel.com/docs/guides/configuration/index.md)
- [Pages](https://veta.varavel.com/docs/guides/pages/index.md)
- [Data](https://veta.varavel.com/docs/guides/data/index.md)
- [Markdown](https://veta.varavel.com/docs/guides/markdown/index.md)
- [Templates](https://veta.varavel.com/docs/guides/templates/index.md)
- [Components](https://veta.varavel.com/docs/guides/components/index.md)
- [Filters](https://veta.varavel.com/docs/guides/filters/index.md)
- [Assets And Tailwind CSS](https://veta.varavel.com/docs/guides/assets-and-tailwind/index.md)
- [Themes](https://veta.varavel.com/docs/guides/themes/index.md)
- [Development Server](https://veta.varavel.com/docs/guides/development-server/index.md)
- [Build And Output](https://veta.varavel.com/docs/guides/build-and-output/index.md)
- [Deployment](https://veta.varavel.com/docs/guides/deployment/index.md)
- [Reference](https://veta.varavel.com/docs/reference/index.md)
- [CLI Reference](https://veta.varavel.com/docs/reference/cli/index.md)
- [Config Reference](https://veta.varavel.com/docs/reference/config/index.md)
- [Page Generators Reference](https://veta.varavel.com/docs/reference/page-generators/index.md)
- [Template Context Reference](https://veta.varavel.com/docs/reference/template-context/index.md)
- [Troubleshooting](https://veta.varavel.com/docs/reference/troubleshooting/index.md)
- [API](https://veta.varavel.com/docs/api/index.md)
- [JavaScript API](https://veta.varavel.com/docs/api/javascript/index.md)
- [File API](https://veta.varavel.com/docs/api/files/index.md)
- [HTTP Client](https://veta.varavel.com/docs/api/http-client/index.md)
- [Parse API](https://veta.varavel.com/docs/api/parse/index.md)
- [Template Functions](https://veta.varavel.com/docs/api/template-functions/index.md)
- [Environment And Console](https://veta.varavel.com/docs/api/environment-and-console/index.md)
- [Markdown Frontmatter](https://veta.varavel.com/docs/api/frontmatter/index.md)
## Documentation content
The complete documentation for this website follows, reproduced verbatim from every page.
---
# Veta Documentation
Veta is a static site generator built around JavaScript page generators, templates, structured data, components, filters, Markdown, themes and Tailwind CSS. It is distributed as a single CLI binary and is designed to turn project files into a static output directory that can be deployed anywhere.
Start with the step-by-step tutorial: [Getting Started](./getting-started/).
---
# 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](../installation/).
Verify the CLI is available:
```sh
veta --version
```
## 2. Create A Project
Create a starter project:
```sh
veta init my-site
cd my-site
```
The starter contains these files:
```txt
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:
```sh
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:
```yaml
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.output` is the directory written by `veta build`.
- `build.clean` removes the output directory before writing a new build.
- `dev.host` configures the local development server host.
- `dev.port` configures the local development server port.
- `dev.watch` is an array of additional directories for the development server to watch, beyond Veta's own files and directories.
- `html.minify` minifies generated `.html` files.
- `tailwindcss.stylesheets` points to Tailwind CSS entrypoints under `public/`.
- `tailwindcss.minify` minifies the generated stylesheet.
## 4. Edit Site Data
Open `data/site.json` and change the name or description:
```json
{
"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`:
```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`:
```js
{
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`:
```html
{{ page.title }} - {{ data.site.name }}
{{ page.title }}
{{ page.content }}
```
Templates receive four root values:
- `data`: global data loaded from `data/`.
- `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`:
```html
```
Use it inside page content:
```js
export default function({ parse }) {
const { html } = parse.markdown(
"Welcome to **Veta**.\n\nComponents are explicit.",
);
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:
```txt
content/posts/hello.md
content/posts/second.md
```
Example Markdown file with YAML frontmatter:
```md
---
title: Hello World
date: "2026-06-30"
tags:
- intro
---
# Hello World
This post is stored as Markdown.
```
Generate pages from those files:
```js
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:
```js
{
frontmatter: {
title: "Hello World",
date: "2026-06-30",
tags: ["intro"]
},
content: "# Hello World\n\nThis post is stored as Markdown.\n",
html: "
Hello World
\n
This post is stored as Markdown.
\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:
```css
@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:
```html
```
## 10. Build For Production
Stop the dev server and run:
```sh
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:
- [Project Structure](../guides/project-structure/)
- [Pages](../guides/pages/)
- [Templates](../guides/templates/)
- [JavaScript API](../api/javascript/)
- [Build And Output](../guides/build-and-output/)
---
# Installation
Veta is distributed as prebuilt binaries through GitHub Releases. The installers and package integrations download those release assets instead of rebuilding Veta locally.
## Linux And macOS
Use the shell installer:
```sh
curl -fsSL https://get.varavel.com/veta | sh
```
Or install with Homebrew:
```sh
brew install varavelio/tap/veta
```
## Windows
Use the PowerShell installer:
```powershell
irm https://get.varavel.com/veta.ps1 | iex
```
## npm
Install globally:
```sh
npm install --global @varavel/veta
```
Or install as a project development dependency:
```sh
npm install --save-dev @varavel/veta
```
The npm package installs the matching Veta binary for your platform during `postinstall`.
## Docker
Run Veta from Docker:
```sh
docker run --rm varavel/veta --help
```
Mount your project when you want to build it from a container:
```sh
docker run --rm -v "$PWD:/site" -w /site varavel/veta build
```
Images are also published to GitHub Container Registry as `ghcr.io/varavelio/veta`.
## Manual Download
Download archives from GitHub Releases:
```txt
https://github.com/varavelio/veta/releases
```
Release archives include Linux, macOS, and Windows binaries for supported architectures. Releases also publish `checksums.txt` and `manifest.json`.
## Verify Installation
Run:
```sh
veta --version
veta --help
```
You should see version information and the available commands.
---
# Guides
These guides explain how to build with Veta in practice.
- [Project Structure](/docs/guides/project-structure/)
- [Configuration](/docs/guides/configuration/)
- [Pages](/docs/guides/pages/)
- [Data](/docs/guides/data/)
- [Markdown](/docs/guides/markdown/)
- [Templates](/docs/guides/templates/)
- [Components](/docs/guides/components/)
- [Filters](/docs/guides/filters/)
- [Assets And Tailwind CSS](/docs/guides/assets-and-tailwind/)
- [Themes](/docs/guides/themes/)
- [Development Server](/docs/guides/development-server/)
- [Build And Output](/docs/guides/build-and-output/)
- [Deployment](/docs/guides/deployment/)
---
# Project Structure
A Veta project is a folder with a `veta.yaml` configuration file and optional feature directories. The starter project created by `veta init` shows the common layout:
```txt
.
veta.yaml
components/
data/
filters/
functions/
pages/
public/
templates/
```
Only `veta.yaml` and `pages/` are necessary for most useful sites. The other directories are optional and can be introduced as the project grows.
## `veta.yaml`
`veta.yaml` configures Veta itself. It controls build output, clean mode, generated HTML minification, Tailwind CSS, and themes.
Site content does not belong in `veta.yaml`. Put content, navigation, SEO metadata, and theme data in `data/` or content files read through the JavaScript file API.
## `pages/`
`pages/` contains flat JavaScript page generator files. Each file must end in `.js` and export a default function that returns an array of page objects.
The directory is intentionally flat. Do not put nested folders under `pages/`.
## `templates/`
`templates/` contains Pongo page templates and any supporting template files. A page object references templates relative to this directory:
```js
{
permalink: "/",
template: "base",
}
```
That can resolve `templates/base.html`, `templates/base.j2`, or another non-ignored file with the same stem.
Templates can include other files or import exported macros through normal Pongo tags. Veta does not prescribe subdirectories inside `templates/`; projects can organize supporting files however they prefer.
## `components/`
`components/` contains reusable component templates. Component tags are derived from file paths:
```txt
components/note.j2 ->
components/ui/card.j2 ->
```
Components can be placed inside page content and explicitly resolved with `parse.renderComponents(text)`. They receive tag attributes and slot content through `props`. Component resolution does not render Markdown.
## `data/`
`data/` contains global data files. Veta supports JSON, YAML, TOML, and JavaScript:
```txt
data/site.json -> data.site
data/navigation.yaml -> data.navigation
data/theme/colors.toml -> data.theme.colors
```
Nested directories become nested keys.
## `filters/`
`filters/` contains custom JavaScript template filters. The directory is flat and every filter file must end in `.js`.
```txt
filters/titlecase.js -> {{ page.title|titlecase }}
```
## `functions/`
`functions/` contains custom JavaScript template functions. The directory is flat and every function file must end in `.js`.
```txt
functions/excerpt.js -> {{ excerpt(page.content, 120) }}
```
## `public/`
`public/` contains static files copied to the output root. For example:
```txt
public/robots.txt -> dist/robots.txt
public/images/logo.svg -> dist/images/logo.svg
public/styles.css -> Tailwind entrypoint when configured
```
Public assets are copied as-is. Generated HTML minification applies only to generated page output, not to copied public files.
---
# Configuration
Veta configuration lives in YAML. The supported file names are checked in this order:
```txt
veta.yaml
veta.yml
.veta.yaml
.veta.yml
```
When you run `veta build` or `veta dev`, Veta searches from the current directory upward through parent directories until it finds one of those files. You can also pass an explicit config file:
```sh
veta build --config path/to/veta.yaml
veta dev --config path/to/veta.yaml
```
The project root is the directory that contains the resolved config file.
## Minimal Config
```yaml
build:
output: dist
```
If `build.output` is omitted or blank, Veta uses `dist`.
## Full Common Config
```yaml
build:
output: dist
clean: true
html:
minify: true
dev:
host: 127.0.0.1
port: 3000
watch:
- content
tailwindcss:
stylesheets:
- styles.css
minify: true
theme:
source: "./theme"
```
## `build`
`build` contains build workflow settings.
```yaml
build:
output: dist
clean: true
```
`output` is the production output directory used by `veta build`. It must be a relative project path.
`clean` removes the output directory before writing a new build.
## `html`
`html` contains generated HTML settings.
```yaml
html:
minify: true
```
`html.minify` minifies generated `.html` files only. It does not minify XML, Markdown, JSON, text, JavaScript, CSS, or files copied from `public/`.
## `dev`
`dev` configures the local development server.
```yaml
dev:
host: 127.0.0.1
port: 3000
watch:
- content
```
`host` is the network interface used by `veta dev`.
`port` is the local development server port.
`watch` adds project-relative files or directories to the watcher. Directories are watched recursively. Veta always watches its own project files and directories in addition to these paths.
Use `watch` for content directories that Veta cannot infer, such as `content/`, `posts/`, `docs/`, or files consumed through `files.readFile`.
## `tailwindcss`
`tailwindcss` enables Veta's embedded Tailwind CSS standalone integration.
```yaml
tailwindcss:
stylesheets:
- styles.css
minify: true
```
`stylesheets` lists Tailwind CSS entrypoints relative to `public/`. With the config above, Veta reads `public/styles.css` and writes the generated CSS to `dist/styles.css`.
`minify` passes Tailwind's minification flag to the standalone CLI.
If `tailwindcss.stylesheets` is omitted or empty, Tailwind CSS does not run.
## `theme`
`theme.source` points to a local theme directory or a GitHub theme source.
```yaml
theme:
source: "./themes/clean"
```
Themes can provide `templates/`, `components/`, `filters/`, `functions/`, `data/`, and `public/`. Project files override theme files.
## Unknown Fields
Veta rejects unknown config fields. This catches typos early and keeps configuration predictable.
---
# Pages
Pages are generated by JavaScript files in `pages/`. Each file must export a default function that returns an array of page objects.
```js
export default function({ data, parse }) {
const { html } = parse.markdown(`# ${data.site.name}`);
return [
{
permalink: "/",
template: "base",
title: "Home",
content: html,
},
];
}
```
## Directory Rules
`pages/` is flat. This is valid:
```txt
pages/site.js
pages/posts.js
```
This is not valid:
```txt
pages/blog/posts.js
```
Use multiple files when it helps organize generators, but keep them directly inside `pages/`.
## Page Object Contract
Every page object must have `permalink`.
```js
{
permalink: "/docs/intro/",
template: "base",
title: "Intro",
content: "
Intro
"
}
```
Fields:
- `permalink` is required and must be a string.
- `template` is optional and must be relative to `templates/`.
- `content` is optional and defaults to an empty string.
- Any extra fields are preserved and exposed to templates through `page`.
`layout` is not supported. Use `template`.
## Permalinks And Output Paths
Veta normalizes permalinks into output paths:
```txt
/ -> index.html
/about/ -> about/index.html
/feed.xml -> feed.xml
/llms.txt -> llms.txt
```
If the last permalink segment has an extension, Veta writes that exact file path. Otherwise, it writes an `index.html` file under the permalink path.
## Templated Pages
When `template` is present, Veta passes `content` unchanged and trusted to the named template. It does not automatically render Markdown or resolve components. The generator must return the final format that the template expects, usually HTML.
```js
export default function({ parse }) {
const source = "This supports **Markdown** and components.";
const { html } = parse.markdown(source);
const content = parse.renderComponents(html);
return [
{
permalink: "/about/",
template: "base",
title: "About",
content,
},
];
}
```
The template receives that final string as `page.content`. Use `parse.markdown(text)` and `parse.renderComponents(text)` explicitly, in the order required by the content.
## Raw Pages
When `template` is omitted, Veta writes `content` directly.
```js
{
permalink: "/feed.xml",
content: `${data.site.name}`,
}
```
Raw pages are useful for feeds, sitemaps, JSON, text files, Markdown files, and any other generated asset.
Because raw output is not transformed either, a generator can intentionally return Markdown, JSON, XML, or plain text without a template.
## Output Collisions
Two page objects cannot write the same output path. Veta reports an error if generators produce conflicting permalinks.
## Generating Pages From Files
Use the JavaScript file API to generate content-driven pages:
```js
export default function({ files, parse }) {
return files.listFiles("content/posts/**/*.md").map((path) => {
const { frontmatter, html } = parse.markdown(files.readFile(path));
const content = parse.renderComponents(html);
return {
permalink: files.toPermalink(path, { stripPrefix: "content" }),
template: "post",
title: frontmatter.title,
content,
};
});
}
```
---
# Data
Global data lives in `data/`. Veta loads data before page generation and exposes it as `data` in JavaScript generators, templates, components, and filters.
## Supported Formats
Veta supports:
```txt
.json
.yaml
.yml
.toml
.js
```
Examples:
```txt
data/site.json
data/navigation.yaml
data/theme.toml
data/github.js
```
## Data Keys
Data keys come from file paths without extensions:
```txt
data/site.json -> data.site
data/navigation.yaml -> data.navigation
data/theme/colors.toml -> data.theme.colors
```
Data file stems must be valid JavaScript-style identifiers. Prefer names like `site.json`, `navigation.yaml`, and `theme/colors.toml`. Avoid names like `site-name.json` because hyphens do not produce ergonomic template keys.
## Site Data Convention
Use `data/site.yaml` for project-level values such as site name, description, brand settings, and other values shared across templates:
```yaml
name: "My Site"
description: "A site built with Veta."
brand:
color: "purple"
```
This convention is optional, but it gives projects and themes a predictable place for site-wide settings.
Reusable themes should put configurable defaults in `data/site_defaults.yaml` instead of `data/site.yaml`. Projects can then provide `data/site.yaml` with only the values they want to customize. See [Themes](/docs/guides/themes/) for the recommended theme defaults pattern.
## JSON Data
```json
{
"name": "Veta Docs",
"description": "Documentation built with Veta."
}
```
Use it in a template:
```html
{{ data.site.name }}
```
## YAML Data
```yaml
main:
- label: Home
href: /
- label: Docs
href: /docs/
```
Use it in a template:
```html
{% for item in data.navigation.main %}
{{ item.label }}
{% endfor %}
```
YAML data files support one YAML document. Multiple YAML documents in one file are rejected.
## TOML Data
```toml
name = "Clean"
[colors]
primary = "blue"
```
Use it in a template:
```html
{{ data.theme.colors.primary }}
```
## JavaScript Data
JavaScript data files export a default function and return a value:
```js
export default function({ env, httpClient, parse }) {
if (env.VETA_MODE === "development") {
return { stars: 0, repo: "local/mock" };
}
const response = httpClient.get(
"https://api.github.com/repos/varavelio/veta",
);
const repo = parse.json(response.body);
return {
repo: repo.full_name,
stars: repo.stargazers_count,
};
}
```
Data JavaScript is synchronous. Return plain JSON-compatible data. Promises are not supported.
## Duplicate Keys
These files conflict because both try to define `data.site`:
```txt
data/site.json
data/site.yaml
```
These also conflict because one file tries to define `data.shop` while another tries to define `data.shop.products`:
```txt
data/shop.json
data/shop/products.json
```
Veta fails the build instead of guessing which value should win.
## Data Versus File API
Use `data/` for global data that should be loaded once and shared everywhere.
Use the JavaScript file API for content collections and project files you want to enumerate manually:
```js
const posts = files.listFiles("content/posts/**/*.md");
```
Templates can also load local or remote data on demand with `load_data`:
```html
{% set navigation = load_data("data/navigation.yaml") | parse_yaml %}
```
Use `load_data` for data that is only needed by a specific template, include, or component.
---
# Markdown
Veta provides explicit Markdown parsing through JavaScript and Pongo filters. It uses GitHub Flavored Markdown features and allows inline HTML. Page content is never rendered as Markdown automatically.
## Markdown In Page Content
Call `parse.markdown(text)` in a page generator and pass its `html` result to the page:
```js
export default function({ parse }) {
const { html } = parse.markdown("# About\n\nThis is **Markdown**.");
return [
{
permalink: "/about/",
template: "base",
title: "About",
content: html,
},
];
}
```
The template outputs that HTML:
```html
{{ page.content }}
```
Templated `page.content` is passed unchanged and trusted to the selected template, so Pongo does not escape it. The generator is responsible for producing the expected final format. Template-less content is also unchanged and is written as raw output, which makes raw Markdown pages possible.
## Markdown And Components
Markdown rendering and component resolution are independent operations. Call them explicitly in the order required by the source. The recommended generator flow is:
```js
const { frontmatter, html } = parse.markdown(files.readFile(path));
const content = parse.renderComponents(html);
return content;
```
`parse.renderComponents(text)` resolves registered component tags but does not render Markdown. For example:
```js
export default function({ files, parse }) {
const path = "content/about.md";
const { frontmatter, html } = parse.markdown(files.readFile(path));
const content = parse.renderComponents(html);
return [
{
permalink: "/about/",
template: "base",
title: frontmatter.title,
content,
},
];
}
```
Component slot content is not given an additional Markdown pass. See [Components](/docs/guides/components/) for component behavior and context.
Raw HTML and HTML-like component tags are preserved, including opening tags whose quoted attributes span multiple lines. Both paired tags and self-closing tags therefore remain available to the following component pass. Markdown and HTML files are trusted author input in Veta; this rendering step does not sanitize scripts, event attributes, or other raw HTML.
## Markdown Files
Veta does not automatically discover Markdown pages. Use JavaScript generators to read files and create pages:
```js
export default function({ files, parse }) {
return files.listFiles("content/posts/**/*.md").map((path) => {
const { frontmatter, html } = parse.markdown(files.readFile(path));
const content = parse.renderComponents(html);
return {
permalink: files.toPermalink(path, { stripPrefix: "content" }),
template: "post",
title: frontmatter.title,
content,
};
});
}
```
This keeps routing explicit and lets you decide how collections are sorted, filtered, paginated, or grouped.
## YAML Frontmatter
YAML frontmatter uses `---` delimiters:
```md
---
title: Hello World
draft: false
tags:
- guide
- intro
---
# Hello World
Post body.
```
## TOML Frontmatter
TOML frontmatter uses `+++` delimiters:
```md
+++
title = "Release Notes"
draft = false
tags = ["release", "notes"]
[meta]
author = "Veta"
+++
# Release Notes
Post body.
```
## `parse.markdown` Return Value
```js
const post = parse.markdown(files.readFile("content/posts/hello.md"));
```
Returns:
```js
{
frontmatter: {
title: "Hello World",
draft: false,
tags: ["guide", "intro"]
},
content: "# Hello World\n\nPost body.\n",
html: "
Hello World
\n
Post body.
\n"
}
```
`content` is the raw Markdown body and `html` is that body rendered as Markdown. If a Markdown file has no frontmatter, `frontmatter` is an empty object and `content` is the full input.
Frontmatter is detected only at the first line of the file. A `---` or `+++` line later in the document is treated as normal Markdown content.
## Pongo Markdown Filters
The Pongo `parse_markdown` filter keeps its template-specific return shape, `{ content, frontmatter }`, and does not render Markdown. Pipe `content` through the separate `markdown` filter when HTML is required:
```html
{% set post = load_data("content/post.md")|parse_markdown %}
{{ post.content|markdown }}
```
This differs from JavaScript `parse.markdown(text)`, which also returns `html`.
---
# Templates
Pongo page templates and supporting files live in `templates/`. Veta does not prescribe subdirectories inside it, so each project can organize layouts, fragments, and macro libraries as needed. A page object uses a template by setting `template`:
```js
export default function({ parse }) {
const { html } = parse.markdown("# Welcome");
return [
{
permalink: "/",
template: "base",
title: "Home",
content: html,
},
];
}
```
Veta resolves the name relative to `templates/`. It passes the generator's `content` string unchanged and trusted to the template; Markdown rendering and component resolution are explicit generator operations.
## Template Names
Veta supports any template extension, but `.j2` is the recommended convention for Pongo templates and components. Pongo uses Jinja-style syntax, and many editors and formatters already recognize `.j2` files well.
```txt
templates/base.j2
templates/navigation.j2
components/card.j2
```
You can include the file extension:
```js
template: "base.j2";
```
Or omit it:
```js
template: "base";
```
When the extension is omitted, Veta scans for a non-ignored file with the same stem. For example, `base` can resolve to `templates/base.j2`.
If more than one file matches the same extensionless name, Veta reports an ambiguous template error.
## Template Context
Templates receive exactly these root keys:
```txt
data
pages
page
props
```
Example:
```html
{{ page.title }} - {{ data.site.name }}
{% for item in pages %}
{{ item.title }}
{% endfor %}
{{ page.content }}
```
`props` is usually empty in page templates. It is populated when rendering components.
## Inheritance
Pongo inheritance works inside `templates/`:
```html
{# templates/base.j2 #}
{% block title %}{{ data.site.name }}{% endblock %}
{% block main %}{% endblock %}
```
```html
{# templates/pages/article.j2 #}
{% extends "../base.j2" %}
{% block title %}
{{ page.title }} | {{ block.Super }}
{% endblock %}
{% block main %}
{{ page.content }}
{% endblock %}
```
Use `./` or `../` for relative paths in `extends` and `include` statements.
## Includes
Pongo templates can include other files by project-relative path:
```html
{% include "templates/brand.html" %}
```
Includes receive the current template context, including `data`, `pages`, `page`, and `props`.
Use `with` to provide values explicitly and `only` to isolate an included file from the current context:
```html
{% include "templates/user-card.j2" with user=page.author only %}
```
Page templates and components use the same loader, so both can reuse files under `templates/`.
## Macros And Imports
Macros define callable template fragments. Add `export` when a macro must be imported from another file:
```html
{# templates/ui.j2 #}
{% macro button(text, href, tone="primary") export %}
{{ text }}
{% endmacro %}
```
Import the exported names that the caller needs. Imports can use aliases:
```html
{% import "templates/ui.j2" button as action %}
{{ action("Read the guide", "/guides/") }}
```
Macros can also be defined and called in the same file without `export`. Macro files use the normal template loader, including extensionless names and project-over-theme overrides.
## Loading Data
Pongo templates and components can load local or remote data with `load_data`:
```html
{% set navigation = load_data("data/navigation.yaml")|parse_yaml %}
{% set site = load_data("data/site.json")|parse_json %}
```
Use `load_data` for template-specific data. Use global `data/` files for data shared across the whole site. See [Template Functions](/docs/api/template-functions/) for the full API.
## Functions
Pongo templates and components can call built-in functions such as `url`, `regex_replace`, and `load_data`. Projects can add custom JavaScript functions in `functions/`:
```js
// functions/excerpt.js
export default function({ page }, value, length) {
return String(value || page.title).slice(0, Number(length));
}
```
```html
{{ excerpt(page.content, 120) }}
```
See [Template Functions](/docs/api/template-functions/) for details.
## Filters
Veta registers built-in filters and custom filters:
```html
{{ page.summary|markdown }}
```
Custom JavaScript filters live in `filters/` and are documented in [Filters](/docs/guides/filters/).
## Ignored Template Files
Veta ignores template files or path segments that:
- start with `.`
- end with `~`
- end with `.tmp`
This lets editors keep temporary files in the project without affecting builds.
---
# Components
Components are reusable templates stored in `components/`. Veta discovers their custom tags, and JavaScript can resolve those tags explicitly with `parse.renderComponents(text)`. Page content is not scanned for components automatically.
## Basic Component
Create `components/note.html`:
```html
```
Resolve it in a page generator:
```js
export default function({ parse }) {
const { html } = parse.markdown(
"Welcome to **Veta**.\n\nComponents are explicit.",
);
const content = parse.renderComponents(html);
return [
{
permalink: "/",
template: "base",
content,
},
];
}
```
The component receives its slot as `props.content`. `parse.renderComponents` does not render Markdown; the example renders Markdown first and then resolves components. Calling it directly with `Use **bold**.` leaves the Markdown markers in the slot unchanged.
## Props
Attributes become string props:
```js
const content = parse.renderComponents(
"Be careful.",
);
```
Component template:
```html
```
All attribute values are strings.
Component invocations use HTML-like syntax. Opening tags and quoted attributes
can span lines, and components without slot content can be self-closing:
```html
This component has **Markdown slot content** when Markdown is rendered first.
```
Keep attribute values quoted. A `>` inside a quoted value does not close the
tag.
## Component Names
Component tags are derived from file paths:
```txt
components/note.j2 ->
components/ui/card.j2 ->
```
Valid component tags start with a lowercase letter and can contain lowercase letters, numbers, and hyphens. Double hyphens are rejected.
## Nested Components
Components can be nested in content:
```js
const content = parse.renderComponents(`
Nested component content.
`);
```
The resolver handles registered nested tags present in the supplied source and leaves unregistered tags unchanged. It preserves component props and slots while recursively rendering nested components. Rendered component output is final and is not scanned again for more component tags, preventing templates from accidentally creating recursive expansion loops.
Component examples inside Markdown code spans or fences remain unchanged. The same applies to component-like text inside HTML attributes, comments, scripts, styles, code blocks, preformatted blocks, text areas, and titles.
## Explicit Ordering
The caller controls the transformation order. For Markdown files that may contain components, use:
```js
const { frontmatter, html } = parse.markdown(files.readFile(path));
const content = parse.renderComponents(html);
```
Markdown preserves multiline HTML-like component tags so the component pass can resolve both paired and self-closing forms. There is no implicit Markdown pass before or after component rendering. Do not pass component template output back through Markdown; the returned string can be assigned to a templated page as final trusted `content` or returned by a template-less page as raw output.
## Component Context
Component templates use the same root keys as Pongo templates when those values exist:
```txt
data
pages
page
props
```
`props` contains string attributes from the tag plus slot content in `props.content`.
When a page generator calls `parse.renderComponents`, global `data` is available to component templates. `page` and `pages` are not available yet because the generator is still creating the page list. If a context-bound JavaScript template function calls `parse.renderComponents`, its available runtime `page` and `pages` values flow into component rendering. Each resolved tag still supplies its own `props` and slot content.
## Component Inheritance
Components are Pongo templates, so they can use inheritance too:
```html
{# components/shell.j2 #}
{% block body %}{{ props.content }}{% endblock %}
```
```html
{# components/panel.j2 #}
{% extends "./shell.j2" %}
{% block class %}
panel
{% endblock %}
```
Use relative paths with `./` or `../` inside component templates.
## Pongo Reuse
Component templates can include supporting templates or import macros through normal Pongo tags:
```html
{# components/note.html #}
```
This is useful when the same markup or callable macro is needed from both page templates and content components. Supporting Pongo files can live anywhere under `templates/`; Veta does not prescribe their internal organization.
Explicit component resolution does not change Pongo behavior: component inheritance, includes, macro imports, relative paths, and the component template context continue to work normally.
## Ignored Component Files
Veta ignores component files or path segments that:
- start with `.`
- end with `~`
- end with `.tmp`
## Component Conflicts
If two files create the same tag, Veta picks the most specific deterministic winner and records the conflict internally. Avoid relying on conflicts. Use unique names.
---
# Filters
Filters transform values inside templates. Veta includes built-in filters and can load custom JavaScript filters from `filters/`.
## Built-In Filters
### `json`
Serializes a value as JSON:
```html
```
### `base64_encode`
Encodes a string as Base64:
```html
{{ "hello" | base64_encode }}
```
### `base64_decode`
Decodes a Base64 string:
```html
{{ "aGVsbG8=" | base64_decode }}
```
Invalid Base64 input fails the build.
### `markdown`
Renders a string as Markdown:
```html
{{ page.summary | markdown }}
```
The output is trusted HTML.
### Parse Filters
Parse filters convert strings into structured template values:
```html
{% set site = load_data("data/site.json") | parse_json %}
{% set navigation = load_data("data/navigation.yaml") | parse_yaml %}
{% set theme = load_data("data/theme.toml") | parse_toml %}
{% set post = load_data("content/post.md") | parse_markdown %}
```
`parse_markdown` parses YAML or TOML frontmatter and returns `{ content, frontmatter }`. It does not render Markdown to HTML; use `markdown` for rendering. This Pongo API is distinct from JavaScript `parse.markdown(text)`, which returns `{ frontmatter, content, html }` with the rendered body in `html`.
## Custom JavaScript Filters
Create `filters/titlecase.js`:
```js
export default function({ data }, input) {
return String(input)
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
```
Use it in a template:
```html
{{ page.title | titlecase }}
```
The filter file name becomes the filter name. `filters/titlecase.js` becomes `titlecase`.
## Filter Parameters
Filters can receive one parameter:
```html
{{ page.title | prefix:"Post: " }}
```
```js
export default function(runtime, input, parameter) {
return `${parameter}${input}`;
}
```
When a filter is called without a parameter, the third argument is `null`-like from the JavaScript side.
## Runtime Context
Custom filters receive the JavaScript runtime context as the first argument:
```js
export default function({ data, env }, input, parameter) {
return `${data.site.name}: ${input}`;
}
```
Filters are synchronous. Promises are not supported.
## Directory Rules
`filters/` is flat. Nested filter directories are not supported.
Every filter file must end in `.js`.
---
# Assets And Tailwind CSS
Static assets live in `public/`. Tailwind CSS is configured through `veta.yaml` and uses one or more stylesheets inside `public/` as entrypoints.
## Public Assets
Files in `public/` are copied to the output root:
```txt
public/robots.txt -> dist/robots.txt
public/images/logo.svg -> dist/images/logo.svg
```
Public files are copied as-is. Veta does not minify or transform copied public assets.
Use the `url` template function when linking copied assets from templates. It returns a path relative to the current page, so the generated site can be served from a domain root, a subdirectory, or static storage without a configured base URL:
```html
```
## Tailwind CSS Entrypoints
The starter uses `public/styles.css`:
```css
@import "tailwindcss";
```
Configure it with:
```yaml
tailwindcss:
stylesheets:
- styles.css
minify: true
```
`stylesheets` entries are relative to `public/`, so `styles.css` means `public/styles.css`.
You can configure multiple entrypoints:
```yaml
tailwindcss:
stylesheets:
- styles.css
- admin.css
minify: true
```
## Generated CSS Output
Veta writes each compiled stylesheet to the build output using the same path:
```txt
public/styles.css -> dist/styles.css
public/admin.css -> dist/admin.css
```
Tailwind scans the materialized output directory, so classes used in generated HTML are included.
If an entrypoint should scan a narrower set of files, configure that in the CSS file with Tailwind's `@source` directive.
## Minification
`tailwindcss.minify: true` minifies the generated stylesheet through Tailwind CSS.
This setting is separate from `html.minify`, which only affects generated `.html` files.
## Disabling Tailwind CSS
Remove `tailwindcss.stylesheets` or leave it empty:
```yaml
tailwindcss:
stylesheets: []
minify: true
```
Without `stylesheets`, Veta does not run Tailwind CSS.
## Practical Pattern
Use `public/styles.css` for your Tailwind entrypoint and keep images, fonts, and static files under `public/`:
```txt
public/
styles.css
fonts/inter.woff2
images/logo.svg
robots.txt
```
---
# Themes
Themes let you share templates, components, filters, functions, data, and public assets across projects.
Configure a theme with `theme.source`:
```yaml
theme:
source: "./themes/clean"
```
## What Themes Can Provide
A theme can contain these top-level directories:
```txt
templates/
components/
filters/
functions/
data/
public/
```
Other top-level directories are ignored by the theme overlay.
JavaScript files under `filters/`, `functions/`, and `data/` are trusted build code. Review remote themes before using them and prefer immutable tags or commit references for reproducible builds.
## Project Files Override Theme Files
Veta composes the theme and project into one filesystem. Project files win over theme files.
Example:
```txt
theme/templates/base.j2
templates/base.j2
```
The project's `templates/base.j2` overrides the theme template.
This lets a project use most of a theme while customizing selected files.
## Local Themes
Use a relative path:
```yaml
theme:
source: "./themes/blog"
```
The path is resolved from the project root.
## GitHub Themes
Remote theme sources use a GitHub-style reference:
```yaml
theme:
source: "owner/repository@ref"
```
Use tags or commit references when you want reproducible builds.
Veta caches remote themes under its runtime cache directory.
## Pages Stay In The Project
Themes provide building blocks. Projects still declare the pages they want to output through `pages/*.js`.
This keeps site structure explicit and prevents a theme from unexpectedly creating routes.
## Theme Data
Themes can provide data files, but project data can override them. A common pattern is:
```txt
theme/data/theme.json
data/theme.json
```
The project file can customize names, colors, navigation, or other theme-facing values.
Data overrides use the relative path without its extension. The project may
therefore replace a theme data file while choosing a different supported format:
```txt
theme: data/site.json
project: data/site.yaml
```
Both files represent `data.site`, so only the project YAML file is loaded. Veta
still rejects multiple files for the same data key within the project or within
the theme, such as `data/site.json` and `data/site.yaml` side by side.
## Theme Configuration Defaults
When building reusable themes, prefer exposing user-configurable defaults through `data/site_defaults.yaml` in the theme. Projects can then override only the values they care about with `data/site.yaml`:
```txt
theme/data/site_defaults.yaml
data/site.yaml
```
This keeps the theme defaults and project overrides available as separate template values:
```txt
data.site_defaults
data.site
```
Example theme defaults:
```yaml
# theme/data/site_defaults.yaml
name: "Clean Theme"
description: "A clean Veta site."
brand:
color: "blue"
logo: "/images/logo.svg"
```
Example project overrides:
```yaml
# data/site.yaml
name: "My Site"
brand:
color: "purple"
```
Then theme templates can prefer project values and fall back to theme defaults:
```html
{% if data.site and data.site.name %}
{{ data.site.name }}
{% else %}
{{ data.site_defaults.name }}
{% endif %}
```
Use this pattern when you want partial project customization. If a theme and the project both provide the same logical data path, the project file replaces the theme file completely, even when their extensions differ.
Veta does not deep-merge data files automatically. Keep fallbacks explicit in templates so theme behavior stays easy to understand.
---
# Development Server
`veta dev` starts a local development server with live reload.
```sh
veta dev
```
By default, it serves at:
```txt
http://127.0.0.1:3000/
```
## Config
Configure the server in `veta.yaml`:
```yaml
dev:
host: 127.0.0.1
port: 3000
watch:
- content
```
`host` changes the address Veta binds to.
`port` changes the port.
`watch` adds project-relative files or directories to the watcher. Directories are watched recursively.
The only `veta dev` CLI flag is `--config`, which works like `veta build --config`:
```sh
veta dev --config path/to/veta.yaml
```
## Temporary Output
The dev server does not write to `build.output`. Instead, it creates an OS temporary directory, builds the site there, serves that directory, and removes it on shutdown.
This means running `veta dev` will not create or modify `dist/`.
## Rebuilds
On startup, Veta performs a full build. When relevant project files change, it performs another full clean rebuild into the temporary directory.
Veta watches:
```txt
veta.yaml
veta.yml
.veta.yaml
.veta.yml
pages/
data/
templates/
components/
filters/
functions/
public/
```
It also watches every path configured in `dev.watch`.
The watcher is intentionally simple and predictable. It rebuilds the whole site rather than trying to cache partial work.
Changes to `dev.host`, `dev.port`, or `dev.watch` require restarting `veta dev`, because those values define the running server and watcher.
## Live Reload
Veta uses Server-Sent Events at:
```txt
/_veta/live
```
When serving generated `.html` files, the dev server injects a small live reload script into the HTTP response. The script is not written to disk and never appears in production builds.
## Not A Production Server
`veta dev` is for local development only. For production, run:
```sh
veta build
```
Then deploy the output directory to a static hosting provider.
---
# Build And Output
Use `veta build` to create a production static site:
```sh
veta build
```
Veta discovers the config file, derives the project root, loads data, and runs page generators. Generators explicitly parse Markdown or resolve components when needed. Veta then passes templated `content` unchanged to its selected template, keeps template-less `content` as raw output, writes generated files, copies public assets, and optionally runs Tailwind CSS.
## Output Directory
Configure the output directory in `veta.yaml`:
```yaml
build:
output: dist
```
The path must be relative to the project.
## Clean Builds
```yaml
build:
clean: true
```
When enabled, Veta removes the output directory before writing new files. This avoids stale files from previous builds.
## Generated Files
Page permalinks determine generated file paths:
```txt
/ -> dist/index.html
/about/ -> dist/about/index.html
/feed.xml -> dist/feed.xml
/llms.txt -> dist/llms.txt
```
Veta validates output paths and rejects duplicate output files.
## Public Assets
Files under `public/` are copied to the output root after generated files are prepared:
```txt
public/robots.txt -> dist/robots.txt
public/logo.svg -> dist/logo.svg
```
If a generated file and a public file claim the same output path, Veta fails the build.
## HTML Minification
```yaml
html:
minify: true
```
This minifies generated files with a `.html` extension. It does not minify:
- `.xml`
- `.md`
- `.json`
- `.txt`
- `.js`
- `.css`
- copied files from `public/`
## Tailwind CSS
When configured, Tailwind CSS runs after Veta writes the generated site and public files:
```yaml
tailwindcss:
stylesheets:
- styles.css
minify: true
```
This allows Tailwind to scan the final generated output and include classes used by templates, raw or explicitly parsed page content, and components explicitly resolved with `parse.renderComponents(text)`.
## Explicit Config File
Build with an explicit config:
```sh
veta build --config ./config/veta.yaml
```
The project root becomes the directory containing that config file.
---
# Deployment
Veta produces static files. There is no production server requirement.
Build the site:
```sh
veta build
```
Deploy the configured output directory, usually `dist/`.
## Generic Static Hosting
Any host that can serve static files can serve a Veta site:
```txt
dist/
index.html
about/index.html
styles.css
robots.txt
```
Upload the contents of `dist/` to your host.
## CI Builds
A typical CI job only needs to install Veta and run:
```sh
veta build
```
Then publish `dist/` as the static artifact.
## npm Projects
If Veta is installed as a development dependency, add scripts:
```json
{
"scripts": {
"dev": "veta dev",
"build": "veta build"
},
"devDependencies": {
"@varavel/veta": "latest"
}
}
```
## Docker Builds
You can build with Docker by mounting the project:
```sh
docker run --rm -v "$PWD:/site" -w /site varavel/veta build
```
## Production Reminder
Do not run `veta dev` in production. It is a local development workflow that serves a temporary output directory and injects live reload scripts into served HTML responses.
---
# Reference
Use these pages when you need exact syntax or behavior.
- [CLI Reference](/docs/reference/cli/)
- [Config Reference](/docs/reference/config/)
- [Page Generators Reference](/docs/reference/page-generators/)
- [Template Context Reference](/docs/reference/template-context/)
- [Troubleshooting](/docs/reference/troubleshooting/)
---
# CLI Reference
## `veta`
Shows help when called without arguments:
```sh
veta
```
## `veta init`
Creates a starter project.
```sh
veta init [PATH]
```
Examples:
```sh
veta init
veta init my-site
```
Flags:
```txt
--force overwrite starter files that already exist
```
## `veta dev`
Starts the local development server with live reload.
```sh
veta dev [--config FILE]
```
Examples:
```sh
veta dev
veta dev --config ./veta.yaml
```
Host, port, and additional watched paths are configured in `veta.yaml` under `dev`.
## `veta build`
Builds the site for production.
```sh
veta build [--config FILE]
```
Examples:
```sh
veta build
veta build --config ./config/veta.yaml
```
## `veta version`
Prints version information.
```sh
veta version
veta --version
veta -v
```
## Config Discovery
`veta build` and `veta dev` search from the current directory upward for:
```txt
veta.yaml
veta.yml
.veta.yaml
.veta.yml
```
Use `--config` to bypass discovery.
---
# Config Reference
Veta config is YAML. Unknown fields are rejected.
## Example
```yaml
build:
output: dist
clean: true
html:
minify: true
dev:
host: 127.0.0.1
port: 3000
watch:
- content
tailwindcss:
stylesheets:
- styles.css
minify: true
theme:
source: "./theme"
```
## `build.output`
Type: string
Default: `dist`
The production output directory used by `veta build`.
Must be a relative project path.
## `build.clean`
Type: boolean
Default: `false`
When true, Veta removes the output directory before writing the build.
## `html.minify`
Type: boolean
Default: `false`
When true, minifies generated `.html` files only.
## `dev.host`
Type: string
Default: `127.0.0.1`
The local interface used by `veta dev`.
## `dev.port`
Type: number
Default: `3000`
The TCP port used by `veta dev`.
## `dev.watch`
Type: array of strings
Default: `[]`
Additional project-relative files or directories watched by `veta dev`. Directories are watched recursively. These paths are added to Veta's built-in watch set.
Example:
```yaml
dev:
watch:
- content
- docs
```
## `tailwindcss.stylesheets`
Type: array of strings
Default: `[]`
When set, enables Tailwind CSS. Each path is relative to `public/`.
Example:
```yaml
tailwindcss:
stylesheets:
- styles.css
- admin.css
```
This reads `public/styles.css` and `public/admin.css`, then writes generated CSS to `dist/styles.css` and `dist/admin.css`.
## `tailwindcss.minify`
Type: boolean
Default: `false`
When true, passes Tailwind's minification flag to the embedded Tailwind CSS standalone CLI.
## `theme.source`
Type: string
Default: empty
When set, resolves and composes a theme with the project.
Examples:
```yaml
theme:
source: "./themes/blog"
```
```yaml
theme:
source: "owner/veta-theme-name@v1.0.0"
```
> Note: The theme should match the owner/veta-theme-{name}@{ref} pattern.
## Supported File Names
Veta checks these names in order:
```txt
veta.yaml
veta.yml
.veta.yaml
.veta.yml
```
---
# Page Generators Reference
Page generators return arrays of page objects:
```js
export default function({ parse }) {
const { html } = parse.markdown("# Home");
return [
{
permalink: "/",
template: "base",
title: "Home",
content: html,
},
];
}
```
## `permalink`
Required: yes
Type: string
The public URL path for the generated page.
Examples:
```txt
/
/about/
/feed.xml
/llms.txt
```
## `template`
Required: no
Type: string
Template name relative to `templates/`.
Examples:
```js
template: "base";
template: "pages/article.j2";
```
Do not prefix with `templates/`.
If omitted, the page is written as raw content.
## `content`
Required: no
Type: string
Defaults to an empty string.
For templated pages, content is passed unchanged and trusted to the selected template. Veta does not automatically render Markdown or resolve component tags. The generator must return the final format expected by the template, usually HTML. Use `parse.markdown(text)` and `parse.renderComponents(text)` explicitly when needed.
For template-less pages, content is written unchanged as raw output. It can contain HTML, Markdown, JSON, XML, text, or any other generated format.
A common content-file flow is:
```js
const { frontmatter, html } = parse.markdown(files.readFile(path));
const content = parse.renderComponents(html);
return {
permalink: files.toPermalink(path, { stripPrefix: "content" }),
template: "post",
title: frontmatter.title,
content,
};
```
## Extra Fields
Any extra fields are preserved and exposed as `page` in templates:
```js
{
permalink: "/posts/hello/",
template: "post",
title: "Hello",
date: "2026-06-30",
tags: ["guide"],
content: "
Hello
",
}
```
Template:
```html
```
## Normalized Fields
Veta also exposes normalized fields on `page`:
```txt
content
generator
index
outputPath
permalink
template
```
`generator` is the page generator file path.
`index` is the page's index in that generator's returned array.
`outputPath` is the generated file path inside the output directory.
## Removed Field: `layout`
`layout` is rejected. Use `template` instead.
---
# Template Context Reference
Veta templates receive a small root context:
```txt
data
pages
page
props
```
## `data`
Global data loaded from `data/`.
```html
{{ data.site.name }} {{ data.navigation.main }}
```
## `pages`
Array of all normalized pages.
```html
{% for item in pages %}
{{ item.title }}
{% endfor %}
```
Each item includes the original page fields plus normalized fields such as `permalink`, `outputPath`, `template`, `generator`, and `index`.
The complete `pages` list exists after page generators have returned. It is not available while a generator is still creating that list.
## `page`
The current normalized page.
```html
{{ page.title }}
{{ page.content }}
```
For templated pages, `page.content` is the generator's unchanged, trusted string. Veta does not automatically render Markdown or resolve components before template rendering.
## `props`
Component props.
In page templates, `props` is usually empty.
In component templates, `props` contains tag attributes and `props.content`:
```html
```
Component context depends on where `parse.renderComponents(text)` is called. Page generators provide global `data`, but not `page` or `pages` because those pages are still being created. A context-bound JavaScript template function can pass its available runtime `page` and `pages` values into component rendering. Tag attributes and slot content always supply the rendered component's `props`.
## Template Helpers
Pongo templates and components can call built-in and custom template functions. `load_data` reads local or remote data:
```html
{% set navigation = load_data("data/navigation.yaml")|parse_yaml %}
{% set site = load_data("data/site.json")|parse_json %}
```
See [Template Functions](/docs/api/template-functions/) for details.
They can also call `url` to generate current-page-relative links:
```html
{{ page.title }}
```
Custom functions from `functions/*.js` are available by file stem:
```html
{{ excerpt(page.content, 120) }}
```
---
# Troubleshooting
This page explains common errors and the shortest path to fixing them.
## Config File Not Found
Veta looks for:
```txt
veta.yaml
veta.yml
.veta.yaml
.veta.yml
```
It searches from the current directory upward.
Fixes:
```sh
veta init
veta build --config ./veta.yaml
veta dev --config ./veta.yaml
```
## Invalid Page Object
Every page must be an object with a string `permalink`.
Valid:
```js
{
permalink: "/about/",
template: "base",
content: "About"
}
```
If you see an error about `layout`, rename it to `template`.
## Template Not Found
Page templates are relative to `templates/`:
```js
template: "base";
```
Do not write:
```js
template: "templates/base.html";
```
If extensionless lookup is ambiguous, include the extension:
```js
template: "base.j2";
```
## Duplicate Output Path
Two pages cannot generate the same output file.
These conflict:
```js
{
permalink: "/about/";
}
{
permalink: "/about/index.html";
}
```
Change one permalink.
## Tailwind Input Missing
If `tailwindcss.stylesheets` includes `styles.css`, Veta expects:
```txt
public/styles.css
```
Create the file or remove it from `tailwindcss.stylesheets` to disable that Tailwind CSS entrypoint.
## Public Asset Collision
Generated files and public files share the same output namespace.
These conflict:
```txt
page permalink: /robots.txt
public/robots.txt
```
Move one of them.
## JavaScript Promise Error
Veta JavaScript is synchronous. Do not use `async` default exports or return promises.
Use synchronous `httpClient` calls instead:
```js
export default function({ httpClient, parse }) {
const response = httpClient.get("https://example.com/data.json");
return parse.json(response.body);
}
```
## Frontmatter Error
Frontmatter must start on the first line and close with the same delimiter:
```md
---
title: Hello
---
# Hello
```
Use `---` for YAML and `+++` for TOML.
---
# API
Veta exposes a small synchronous JavaScript API to data files, page generators, and filters. It also registers template helpers for Pongo templates and components.
- [JavaScript API](/docs/api/javascript/)
- [File API](/docs/api/files/)
- [HTTP Client](/docs/api/http-client/)
- [Parse API](/docs/api/parse/)
- [Template Functions](/docs/api/template-functions/)
- [Environment And Console](/docs/api/environment-and-console/)
- [Markdown Frontmatter](/docs/api/frontmatter/)
---
# JavaScript API
Veta uses JavaScript for three kinds of project files:
```txt
data/*.js -> global data producers
pages/*.js -> page generators
filters/*.js -> template filters
functions/*.js -> template functions
```
JavaScript files are self-contained and synchronous. They do not use imports, module loading, or asynchronous promises. Each file must export one default function.
## Runtime Context
The default export receives a context object as its first argument.
Common context keys:
```txt
files
httpClient
parse
env
```
Additional context keys depend on where the file runs.
## `data/*.js`
Data files run while global data is being loaded, so they do not receive `data`.
```js
export default function({ env, files, httpClient }) {
return {
mode: env.VETA_MODE || "production",
};
}
```
Return any JSON-compatible value. The value becomes part of `data` using the file path as its key.
Example:
```txt
data/github.js -> data.github
```
## `pages/*.js`
Page generators receive loaded global data:
```js
export default function({ data, parse }) {
const { html } = parse.markdown("# Home");
return [
{
permalink: "/",
template: "base",
title: data.site.name,
content: html,
},
];
}
```
Return an array of page objects. Templated content is passed unchanged and trusted to its template, while template-less content is written unchanged as raw output. Use `parse.markdown(text)` and `parse.renderComponents(text)` explicitly when a generator needs those transformations.
## `filters/*.js`
Filters receive the runtime context, the input value, and one optional parameter:
```js
export default function({ data }, input, parameter) {
const prefix = parameter || data.site.name;
return `${prefix}: ${input}`;
}
```
## `functions/*.js`
Template functions receive the runtime context followed by explicit template arguments:
```js
export default function({ page, data, files, parse }, value, length) {
return String(value || page.title).slice(0, Number(length));
}
```
Use the file stem as a function in Pongo templates and components:
```html
{{ excerpt(page.content, 120) }}
```
Use it in a template:
```html
{{ page.title|prefix:"Post" }}
```
## No Global `Veta`
Veta does not expose runtime APIs through a global `Veta` object. Always use the context argument:
```js
export default function({ files }) {
return files.listFiles("content/**/*.md");
}
```
## Console
The `console` object is available as a JavaScript global:
```js
export default function() {
console.log("Generating pages");
return [];
}
```
Supported methods are `debug`, `error`, `info`, `log`, and `warn`.
## Execution Model
Veta executes JavaScript synchronously. Promise-like return values are rejected.
Use synchronous calls only:
```js
export default function({ httpClient, parse }) {
const response = httpClient.get("https://example.com/data.json");
return parse.json(response.body);
}
```
Do not return a Promise:
```js
export default async function() {
return [];
}
```
## API Pages
- [File API](/docs/api/files/)
- [HTTP Client](/docs/api/http-client/)
- [Parse API](/docs/api/parse/)
- [Environment And Console](/docs/api/environment-and-console/)
- [Frontmatter](/docs/api/frontmatter/)
---
# File API
The file API is available as `files` in JavaScript context objects.
```js
export default function({ files }) {
return files.listFiles("content/**/*.md");
}
```
All paths are relative to the project root. Absolute paths and `..` path traversal are rejected.
## `files.listFiles(pattern)`
Returns sorted project-relative file paths matching a glob pattern.
```js
const posts = files.listFiles("content/posts/**/*.md");
```
Use `files.listFiles(".")` to list every file in the project. Empty patterns are rejected.
## `files.readFile(path)`
Reads a file as a UTF-8 string.
```js
const robots = files.readFile("public/robots.txt");
const site = parse.json(files.readFile("data/site.json"));
const { frontmatter, html } = parse.markdown(
files.readFile("content/posts/hello.md"),
);
const content = parse.renderComponents(html);
```
`files.readFile` performs no parsing or rendering. In this example, `frontmatter` contains metadata, `html` is the rendered Markdown body, and `content` is the result of explicitly resolving registered components. See [Parse API](/docs/api/parse/) and [Frontmatter](/docs/api/frontmatter/) for details.
## `files.toPermalink(path, options)`
Converts a project-relative path into a pretty permalink.
```js
files.toPermalink("content/posts/hello.md", { stripPrefix: "content" });
// "/posts/hello/"
```
If the source file is an `index` file, the last segment is removed:
```js
files.toPermalink("content/docs/index.md", { stripPrefix: "content" });
// "/docs/"
```
Options:
```js
{
stripPrefix: "content";
}
```
`stripPrefix` is optional. When present, Veta removes it as a complete path segment before generating the permalink. The source path must have that prefix.
## Security Rules
The file API rejects:
- empty file paths
- absolute paths
- Windows drive paths
- paths containing `..`
- symlink escapes outside the configured root
This keeps JavaScript file access confined to the project root.
---
# HTTP Client
The HTTP client is available as `httpClient` in JavaScript context objects.
It is synchronous and supports HTTP and HTTPS URLs only.
## Shortcut Methods
```js
httpClient.get(url, options);
httpClient.post(url, options);
httpClient.put(url, options);
httpClient.patch(url, options);
httpClient.delete(url, options);
httpClient.head(url, options);
```
Example:
```js
export default function({ httpClient, parse }) {
const response = httpClient.get(
"https://api.github.com/repos/varavelio/veta",
{
headers: {
Accept: "application/vnd.github+json",
},
},
);
if (!response.ok) {
throw new Error(`GitHub returned ${response.status}`);
}
return parse.json(response.body);
}
```
## Explicit Request Method
```js
httpClient.request("GET", "https://example.com/data.json");
```
The method is trimmed and uppercased. Empty methods or methods containing whitespace are rejected.
## Options
```js
{
headers: {
"Accept": "application/json",
"X-Trace": ["one", "two"]
},
body: "raw body",
timeoutMs: 5000
}
```
`body` must be a string.
For JSON request bodies, use `JSON.stringify` and set the content type yourself:
```js
httpClient.post("https://example.com/api", {
body: JSON.stringify({ message: "hello" }),
headers: { "Content-Type": "application/json" },
});
```
`timeoutMs` must be a positive number. The default timeout is 30 seconds.
## Response Shape
```js
{
body: "...",
headers: {
"Content-Type": ["application/json"]
},
ok: true,
status: 200,
statusText: "OK",
url: "https://example.com/data.json"
}
```
`ok` is `true` for status codes from 200 through 299.
`body` is always a string. Use `parse.json`, `parse.yaml`, or another parser when you need structured data.
## Development Advice
`veta dev` performs a full rebuild on file changes. If a data script fetches slow remote APIs, it will fetch them again on rebuild. For development, consider using `env` to return local mock data.
---
# Parse API
The parse API is available as `parse` in JavaScript context objects. It parses structured text, renders Markdown bodies, and explicitly resolves component tags. File and HTTP APIs return text; call the required operations in the order your output needs.
```js
export default function({ files, parse }) {
const { frontmatter, html } = parse.markdown(
files.readFile("content/posts/hello.md"),
);
const content = parse.renderComponents(html);
return [
{
permalink: "/posts/hello/",
template: "post",
title: frontmatter.title,
content,
},
];
}
```
## `parse.json(text)`
Parses one JSON value. Multiple JSON values are rejected.
```js
const site = parse.json("{\"title\":\"Veta\"}");
```
## `parse.yaml(text)`
Parses one YAML document. Multiple YAML documents are rejected.
```js
const navigation = parse.yaml("items:\n - label: Docs\n");
```
## `parse.toml(text)`
Parses one TOML document.
```js
const theme = parse.toml("name = \"Clean\"\n");
```
## `parse.markdown(text)`
Parses optional YAML or TOML frontmatter and renders the Markdown body to HTML.
```js
const post = parse.markdown(files.readFile("content/posts/hello.md"));
```
Return shape:
```js
{
frontmatter: { title: "Hello" },
content: "# Hello\n\nPost body.\n",
html: "
Hello
\n
Post body.
\n"
}
```
- `frontmatter` is the parsed object.
- `content` is the raw body after frontmatter is removed.
- `html` is the Markdown-rendered body.
Without frontmatter, `frontmatter` is `{}`, `content` is the full input, and `html` is the full input rendered as Markdown.
Raw HTML is preserved as trusted author content. HTML-like opening tags may span lines and can end with either `>` or `/>`; quoted `>` characters inside attributes do not end the tag. This allows paired and self-closing component invocations to survive until an explicit `parse.renderComponents` call.
## `parse.renderComponents(text)`
Resolves registered component tags in any supplied string and returns the transformed string:
```js
const content = parse.renderComponents(
"Check the configuration.",
);
```
Only registered tags are resolved; other tags remain unchanged. Props, slot content, nested components, Pongo component context, includes, and inheritance work as they do elsewhere. This operation does not render Markdown.
Component-like text remains unchanged inside HTML attributes, comments, raw-text and code elements such as `script`, `style`, `code`, `pre`, `textarea`, and `title`, as well as Markdown inline code and fenced code blocks. Component template output is not scanned again, which keeps rendering a bounded, one-pass transformation. Excessively deep input or recursive calls from component template functions fail with a controlled render-limit error.
The caller controls ordering. For a Markdown file that may contain component tags, the recommended page-generator flow is:
```js
const { frontmatter, html } = parse.markdown(files.readFile(path));
const content = parse.renderComponents(html);
return {
permalink: files.toPermalink(path, { stripPrefix: "content" }),
template: "post",
title: frontmatter.title,
content,
};
```
When called by a page generator, component templates receive global `data`; `page` and `pages` do not exist yet because the generator is creating the page list. When a context-bound JavaScript template function calls it, available runtime `page` and `pages` values can flow into component rendering. Props come from each tag's attributes and slot content.
## Pongo Filter Distinction
The Pongo `parse_markdown` filter is unchanged: it returns `{ content, frontmatter }` and does not render Markdown. Use Pongo's separate `markdown` filter to render that `content`. JavaScript `parse.markdown(text)` returns the additional `html` field described above.
Parsed values are normalized into JavaScript-compatible values. Dates are exposed as strings.
---
# Template Functions
Veta registers template functions for Pongo templates and components.
Built-in functions are always available:
- `url`
- `regex_replace`
- `load_data`
Custom functions live in `functions/*.js` and use the file stem as the template function name.
## `url`
`url` returns a portable URL for an internal root-relative path from the current page.
```html
Current page
```
From `/docs/intro/`, `url("/styles.css")` returns `../../styles.css`. From `/`, it returns `styles.css`.
External URLs, fragment-only URLs, and already-relative URLs are returned unchanged.
## `regex_replace`
`regex_replace` replaces text with a Go regular expression:
```html
{{ regex_replace("World Hello", "(\\w+) (\\w+)", "$2 $1") }}
```
The result can be rendered directly or assigned with `set`:
```html
{% set slug = regex_replace(page.title, "[^a-zA-Z0-9]+", "-") %}
{{ page.title }}
```
Invalid regular expressions fail the build.
## `load_data`
`load_data` reads a local project file or a remote URL as text from a template, include, or component.
Use `load_data` inside native Pongo expressions. Assign values with Pongo's built-in `set` tag:
```html
{% set navigation = load_data("data/navigation.yaml")|parse_yaml %}
{% for item in navigation.items %}
{{ item.label }}
{% endfor %}
```
Without a parse filter, `load_data` returns a string:
```html
{% set readme = load_data("content/readme.md") %}
{{ readme|markdown }}
```
### Local Files
Local paths are project-relative and can read files from the composed project and theme filesystem:
```html
{% set badge = load_data("data/badge.toml")|parse_toml %}
{{ badge.label }}
```
Local paths must be relative. Absolute paths, Windows drive paths, empty paths, and paths containing `..` are rejected.
### Remote URLs
Remote URLs use HTTP `GET`:
```html
{% set repo = load_data("https://api.github.com/repos/varavelio/veta")|parse_json %}
{{ repo.stargazers_count }}
```
Only `http` and `https` URLs are allowed. Non-2xx responses fail the build.
### Parse Filters
Use parse filters to convert loaded text into structured values:
- `parse_json`
- `parse_yaml`
- `parse_toml`
- `parse_markdown`
```html
{% set message = load_data("content/message.txt") %}
{% set site = load_data("data/site.json")|parse_json %}
{% set navigation = load_data("data/navigation.yaml")|parse_yaml %}
{% set theme = load_data("data/theme.toml")|parse_toml %}
```
Parsed values return normal template values:
```html
{{ site.title }} {{ navigation.items.0.label }} {{ theme.colors.primary }}
```
## Custom Functions
Custom functions are synchronous JavaScript files in `functions/`. Each file must export one default function:
```js
// functions/excerpt.js
export default function({ page }, value, length) {
console.log("excerpt", page.permalink);
return String(value).slice(0, Number(length));
}
```
Use the file stem as the function name:
```html
{{ excerpt(page.content, 120) }}
```
The first argument is the JavaScript runtime context. Template functions receive `data`, `pages`, `page`, `props`, `files`, `httpClient`, `parse`, and `env`. `console` is available as a JavaScript global, not as `context.console`.
If a template function calls `parse.renderComponents(text)`, its context-bound `data`, `page`, and `pages` values can flow into component templates. Each component tag still supplies its own attributes and slot content through `props`. This differs from page-generator calls, where `page` and `pages` are not available because the generator is still creating the page list.
Function files are flat. Nested directories under `functions/` are not supported. A custom function can override a built-in function by using the same file stem.
---
# Environment And Console
Veta exposes environment variables and console methods to JavaScript files.
## `env`
`env` is an object containing string environment variables captured from the process running Veta.
```js
export default function({ env }) {
return {
mode: env.VETA_MODE || "production",
};
}
```
Use environment variables for secrets, deployment settings, branch names, or development toggles.
Do not commit secrets into `data/` or `pages/`.
## `console`
The console API is available as a JavaScript global. It is not part of the default export context object.
Supported methods:
```txt
console.debug
console.error
console.info
console.log
console.warn
```
Example:
```js
export default function() {
console.info("Generating pages");
return [];
}
```
CLI output is prefixed with the log level:
```txt
[js info] Generating pages
```
Objects and arrays are rendered as JSON-like output.
---
# Markdown Frontmatter
`parse.markdown(text)` in JavaScript and `parse_markdown` in Pongo templates support optional frontmatter at the start of a Markdown string. Their return shapes differ: JavaScript also renders the body into an `html` field, while the Pongo filter keeps `{ content, frontmatter }`.
Supported delimiters:
```txt
--- YAML
+++ TOML
```
Frontmatter is detected only when the first line is exactly `---` or `+++`.
## YAML Frontmatter
```md
---
title: Hello
draft: false
tags:
- guide
- intro
---
# Hello
Body.
```
## TOML Frontmatter
```md
+++
title = "Hello"
draft = false
tags = ["guide", "intro"]
[meta]
author = "Veta"
+++
# Hello
Body.
```
## Return Shape
```js
const post = parse.markdown(files.readFile("content/posts/hello.md"));
```
```js
{
frontmatter: { title: "Hello", draft: false, tags: ["guide", "intro"] },
content: "# Hello\n\nBody.\n",
html: "
Hello
\n
Body.
\n"
}
```
`content` is the raw body, and `html` is the Markdown-rendered body. One blank line immediately after the closing delimiter is removed from `content` before `html` is rendered.
## Files Without Frontmatter
```md
# Plain Markdown
No frontmatter.
```
Returns:
```js
{
frontmatter: {},
content: "# Plain Markdown\n\nNo frontmatter.\n",
html: "
Plain Markdown
\n
No frontmatter.
\n"
}
```
Without frontmatter, `content` is the full input.
## Pongo `parse_markdown`
The Pongo filter remains a frontmatter parser only:
```html
{% set post = load_data("content/posts/hello.md")|parse_markdown %}
{{ post.content|markdown }}
```
It returns `{ content, frontmatter }`; use the separate `markdown` filter to produce HTML.
## Validation
Veta rejects:
- missing closing delimiters
- malformed YAML
- malformed TOML
- frontmatter that does not parse to an object
- multiple YAML documents
- non-finite numbers such as `NaN` or `Inf`
- maps with non-string keys
Parsed values are normalized into JavaScript-compatible values. Dates are exposed as strings.