static_

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>&copy; {{ 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>

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)