Pages & layouts
A page is a .vue or .md file in pages/. Folders become nested URLs: pages/about.vue is /about/ (or /about, see cleanUrls), and pages/index.vue is /.
A Vue page
Optional YAML frontmatter sits on top and feeds the <head> (title, description, canonical, Open Graph, Twitter, JSON-LD). Everything in <template> is the body.
---
title: About
description: Shown in search results (120-160 characters).
image: /images/og-about.jpg # social image, optional
imageAlt: Describe it
seoTitle: A fully custom <title> # optional
noindex: true # optional
layout: landing # optional, uses layouts/landing.vue
faq: # optional, emits FAQPage structured data
- q: Question?
a: Answer.
---
<template>
<h1>About</h1>
<Hero title="Hi" :items="data.things" />
<p>Built {{ currentYear }} · {{ site.siteName }}</p>
</template>
Every page needs a title, a description and exactly one <h1>, or static check warns. The frontmatter is parsed by Static and stripped before Vue sees it, so error line numbers still match your file.
You never write <html>, <head>, charset, viewport, SEO tags, the stylesheet link or the dark mode script. They are injected for you.
Markdown pages
pages/about.md takes the same frontmatter and renders inside the layout with typography styles (this site is written that way). Headings get ids, external links get rel="noopener noreferrer", and images are lazy-loaded.
Layouts
layouts/default.vue wraps every page. Put <slot /> where the page goes. If it is missing, a bare <slot /> is used. A page can choose another layout with layout: landing.
<template>
<header><a href="/">{{ site.siteName }}</a><ThemeToggle /></header>
<main><slot /></main>
<footer>© {{ currentYear }}</footer>
</template>
To control <body> classes either set "bodyClass" in site.json, or return a full document from the layout:
<template>
<html>
<body class="bg-white dark:bg-zinc-950">
<slot />
</body>
</html>
</template>
Navigation
data/site.json → navigation is a list of { slug, title } (add href to override the link). In templates it is the global navigation, with url and isActive filled in for the current page:
<a v-for="item in navigation" :key="item.slug" :href="item.url" :class="{ active: item.isActive }">{{ item.title }}</a>
Data-driven pages
A page named with brackets generates one page per item of a JSON array or a collection:
---
each: apps # data/apps.json, or a collection name
as: app # variable name inside the page template
param: id # item field used in the URL (default: the bracket name)
where: enabled # optional: only items where this field is truthy
title: "{{ app.name }}"
description: "{{ app.seoDescription || app.description }}"
noindex: "{{ app.noindex }}"
---
<!-- pages/app/[id].vue -->
<template>
<AppDetail :app="app" />
</template>
Frontmatter strings support {{ path }} and a || b fallbacks. Add an item to the JSON and it gets a page, a sitemap entry and SEO tags.
URLs
site.cleanUrls:
| value | pages/about.vue becomes |
|---|---|
false (default) | about.html |
true | /about/ (about/index.html) |
"extensionless" | /about (file about.html; works on Netlify and Cloudflare Pages) |