# Vitrine > A Hugo theme for documentation, with examples that run on the page next to their code. ## A live example The walk below runs on this page. Under it are the files it's made of, exactly as they run: no build step, and no second copy of the code to keep in sync. {{< example name="walk" title="A random walk" >}} ## What you get **Examples that run.** A folder with `example.html`, and optionally `style.css` and `script.js`, becomes a live demo with its files in tabs below it. Its CSS is scoped to the example, its script runs as a module, and an import map lets the script import a library by its npm name. See [Live examples](/docs/examples/). **Navigation.** Tabs across the top for the site's sections, a two-level sidebar for the section you're in, "On this page" on the right, previous and next at the bottom. See [Pages and navigation](/docs/writing/pages-and-navigation/). **Docs for AI assistants.** `llms.txt`, `llms-full.txt` and a Markdown copy of every page, with no setup. See [llms.txt](/docs/reference/llms-txt/). **Small.** Plain CSS and one small script, no framework and no Node build. Hugo does everything. --- # Docs > How to build documentation with Vitrine, from installing it to examples that run on the page. Vitrine is a theme for [Hugo](https://gohugo.io/), the static site generator. You write pages in Markdown, Hugo turns them into a website, and Vitrine decides how that website looks and works: the tabs, the sidebar, code blocks, live examples and the rest. These docs are built with Vitrine itself. If you're new, start with [Getting started](getting-started/). The [reference](reference/) lists every setting, front matter key and shortcode in one place. --- # Getting started > From an empty folder to a running docs site. Two steps: [install](installation/) Hugo and the theme, then [write your first pages](first-pages/). If you already have a Hugo site, skip to the part of the installation that adds the theme. --- # Installation > Hugo, the theme, and the three settings Vitrine needs from your site. ## Requirements - [Hugo](https://gohugo.io/installation/) 0.146 or newer. The standard edition is enough; Vitrine doesn't use Sass. - [Git](https://git-scm.com/). - [Go](https://go.dev/doc/install), only if you add the theme as a Hugo module (recommended, see below). Check what you have: ```sh hugo version git --version go version ``` ## Create a site Skip this if you have one already. ```sh hugo new project my-docs cd my-docs git init ``` ## Add the theme There are two ways. A **Hugo module** is the modern one: Hugo downloads the theme itself, in the version you name, and updating is one command. A **Git submodule** puts a copy of the theme's repository into your site's `themes/` folder and needs no Go. {{< tabs group="install" >}} {{< tab "Hugo module" >}} Turn your site into a module, then import the theme in `hugo.toml`: ```sh hugo mod init github.com/you/my-docs ``` ```toml {file="hugo.toml"} [module] [[module.imports]] path = "github.com/maxfettinger/vitrine" ``` ```sh hugo mod get github.com/maxfettinger/vitrine@latest ``` The module path after `init` is a name for your site, usually its repository's address; it doesn't have to exist. Update the theme later with `hugo mod get -u`. {{< /tab >}} {{< tab "Git submodule" >}} Add the theme's repository under `themes/` and name it in `hugo.toml`: ```sh git submodule add https://github.com/maxfettinger/vitrine.git themes/vitrine ``` ```toml {file="hugo.toml"} theme = "vitrine" ``` Someone cloning your site later needs `git clone --recurse-submodules`. Update the theme with `git submodule update --remote`. {{< /tab >}} {{< /tabs >}} ## The settings Vitrine needs Hugo lets a theme set only a few kinds of settings for a site (its params, menus, output formats and media types). Vitrine needs three more, so they go in your `hugo.toml`: ```toml {file="hugo.toml"} # Only Markdown files are pages: an example's example.html can sit in a folder next to its page [contentTypes] [contentTypes.'text/markdown'] # Code colours as CSS classes, so light and dark mode can colour them [markup.highlight] noClasses = false # "On this page" lists the h2 and h3 headings [markup.tableOfContents] startLevel = 2 endLevel = 3 ``` {{< callout >}} Without `contentTypes`, Hugo treats `example.html` in a page's folder as a page of its own and the [example shortcode](/docs/examples/example-shortcode/) can't find it. Examples in `assets/examples/` work either way. {{< /callout >}} And a title, a description for search engines and link previews, and the main menu, which becomes the tabs in the header: ```toml {file="hugo.toml"} baseURL = "https://docs.example.org/" title = "my-lib" [params] description = "my-lib does one thing well." [[menus.main]] name = "Docs" pageRef = "/docs" weight = 10 ``` ## Run it ```sh hugo server ``` Open the address it prints, usually `http://localhost:1313/`. The page reloads while you edit. Next: [your first pages](/docs/getting-started/first-pages/). --- # Your first pages > Sections become tabs, folders become the sidebar, and every page needs a title and a weight. ## The content folder A Vitrine site is a few folders of Markdown files. This one has a start page, a Docs tab with two chapters and an Examples tab: ```text content/ ├── _index.md the start page ├── docs/ │ ├── _index.md the Docs tab's own page ("Overview" in the sidebar) │ ├── installation.md │ └── guide/ │ ├── _index.md a chapter: a group in the sidebar │ ├── basics.md │ └── advanced.md └── examples/ ├── _index.md └── counter/ ├── index.md a page with files next to it (a page bundle) └── demo/ an example's files ``` Each folder at the top of `content/` is a **section**. A folder inside a section with an `_index.md` is a **subsection**: in the sidebar it's a group that opens. The sidebar shows two levels; put deeper folders' pages one level up. ## A page Every page starts with front matter between `---` lines: ```md {file="content/docs/installation.md"} --- title: Installation lead: Everything you need before the first build. weight: 1 --- ## Requirements Text in Markdown. ``` `title` is the heading and the name in the sidebar. `lead` is the line under the title, also used as the page's description for search engines unless you set `description`. `weight` sorts pages: lower comes first. All keys are in [Front matter](/docs/reference/front-matter/). Create pages by hand or with Hugo: ```sh hugo new content docs/installation.md ``` ## Tabs in the header The tabs are your site's main menu. Add one entry per section, with `pageRef` pointing at it: ```toml {file="hugo.toml"} [[menus.main]] name = "Docs" pageRef = "/docs" weight = 10 [[menus.main]] name = "Examples" pageRef = "/examples" weight = 20 [[menus.main]] name = "npm" url = "https://www.npmjs.com/package/my-lib" weight = 30 ``` An entry with a `url` to another site opens in a new tab and gets a small arrow. ## The start page `content/_index.md` is a hero: the title large, the lead, a row of facts, an install command and buttons. The page's Markdown follows below. ```md {file="content/_index.md"} --- title: my-lib lead: What it does, in one or two sentences. facts: ["4 kB gzip", "0 dependencies", "MIT"] install: npm install my-lib actions: - { label: Get started, url: docs/installation/ } - { label: GitHub, url: "https://github.com/you/my-lib" } --- ## Why my-lib Text in Markdown. ``` The first button is filled, the others outlined. Next: [Writing](/docs/writing/). --- # Writing > Pages, navigation, and what Markdown can do in Vitrine. Vitrine reads ordinary Markdown. These chapters cover how pages become navigation, what each Markdown element looks like, and the two shortcodes for writing: tabs and callouts. --- # Pages and navigation > The header's tabs, the sidebar, "On this page", and the links under every page. ## The page frame Every page has the same header: the site's name and version, the tabs, the repository's icon, the light and dark switch, and on phones a menu button. Below it, a page in a section has three columns: - the **sidebar** on the left: the pages of the section you're in; - the **text** in the middle, at most 46rem wide (about 80 characters a line); - **On this page** on the right: the page's `##` and `###` headings, the one you're reading marked as you scroll. Pages outside a section (the start page, a top-level page like `content/about.md`, and the 404 page) have no side columns. On medium screens "On this page" is left out. On phones the sidebar opens over the page from the menu button and closes with a link, Escape or the button. ## The sidebar The sidebar shows the **current tab's** pages, two levels deep: 1. the section's own page (`_index.md`), as "Overview"; 2. its pages and subsections, sorted by `weight`; 3. in each subsection, its "Overview" and its pages. The subsection with the current page is open. Pages without a `weight` come after those with one, sorted by date and then title, Hugo's default order. Use `linkTitle` for a shorter name in the sidebar and the tabs than the page's `title`. ## Breadcrumbs, previous and next Above the title, breadcrumbs name the sections the page is in. Under the text: - **Edit this page**, if you set `params.editPath` to your repository's edit address for the content folder: ```toml {file="hugo.toml"} [params] editPath = "https://github.com/you/my-lib/edit/main/docs/content/" ``` - **Previous** and **Next**: the pages before and after this one in the sidebar's order, across subsections. ## A section's own page A section's or subsection's `_index.md` shows its text, then a list of its pages with their leads. Keep the text short: an overview of what's in the chapter. ## Tabs in the header The header's tabs are your main menu ([Your first pages](/docs/getting-started/first-pages/#tabs-in-the-header) shows how). A tab is marked when you're on its page or any page below it. On phones the tabs scroll sideways between the name and the buttons. ## Light and dark The switch in the header toggles light and dark mode and remembers the choice. Until a reader uses it, the site follows `params.mode.default`: ```toml {file="hugo.toml"} [params.mode] default = "auto" # "light", "dark", or "auto" (the reader's system setting) ``` The choice is applied before the page is drawn, so it never flashes the other mode. --- # Markdown > Headings, links, code blocks, tables and images, and what Vitrine adds to each. Vitrine uses Hugo's Markdown (CommonMark with GitHub's extensions: tables, task lists, strikethrough). A few elements get extra behaviour through Hugo's render hooks. ## Headings Start a page's chapters at `##`; the page's title is its `#`. Every `##` starts at a thin rule. Headings get a `#` link to themselves, shown on hover, so readers can link to a chapter. `##` and `###` headings are listed in "On this page". ## Links ```md [Installation](/docs/getting-started/installation/) [a chapter on this page](#code-blocks) [the config file](hugo.toml) [Hugo](https://gohugo.io/) ``` - A link to a page, written as its path in `content/`, becomes that page's address. It works with or without the trailing slash, and keeps working if your site lives under a subpath like `example.org/docs/`. - A link to a file next to the page (in its page bundle) becomes the file's address. - A path from the site's root to a file in `static/` gets the site's base path. - A link to another site opens in a new tab, and screen readers hear "opens in a new tab". ## Code blocks Fenced code gets a head with its language and a copy button: ```js const answer = 42; ``` Name the file instead of the language with `file`: ````md ```js {file="script.js"} const answer = 42; ``` ```` ```js {file="script.js"} const answer = 42; ``` Hugo's highlighting options work as usual, for example highlighted lines and line numbers: ````md ```go {hl_lines=[2] linenos=true} func main() { fmt.Println("hello") } ``` ```` ```go {hl_lines=[2] linenos=true} func main() { fmt.Println("hello") } ``` Inline code like `npm install` gets a light box. ## Tables | Key | Type | Default | |---|---|---| | `title` | string | none | | `weight` | integer | 0 | | `lead` | string, Markdown | none | A table wider than the column (on a phone, say) scrolls sideways in its own box, so the page doesn't. Long code in a cell may break on narrow screens. Align a column with colons in the separator row (`|---:|` for right). ## Quotes, lists and images > A quote is set off by a rule on its left. Lists and task lists work as on GitHub: - [x] write the docs - [ ] publish them Images in Markdown are shown at most as wide as the column. Put them next to the page in its folder and link them by name: `![The settings dialog](settings.png)`. Always write the alt text. --- # Tabs and callouts > Two shortcodes for writing: the same thing in several variants, and a note or warning beside the text. ## Tabs Use tabs for the same content in variants: package managers, operating systems, languages. ```md {{}} {{}} ```sh npm install my-lib ``` {{}} {{}} ```sh pnpm add my-lib ``` {{}} {{}} ``` (Without the indentation: it's only there to show the code fences inside.) {{< tabs group="install" >}} {{< tab "npm" >}} ```sh npm install my-lib ``` {{< /tab >}} {{< tab "pnpm" >}} ```sh pnpm add my-lib ``` {{< /tab >}} {{< tab "yarn" >}} ```sh yarn add my-lib ``` {{< /tab >}} {{< /tabs >}} A tab holds any Markdown. Arrow keys move between the tabs. ### Tab groups With `group`, every tab set of the same group switches together: pick "pnpm" once, and every install command on every page shows pnpm. The choice is kept in the reader's browser. This tab set is in the group `install`, like the one on [Installation](/docs/getting-started/installation/), whose tabs are named differently and so keep their own choice. {{< tabs group="install" >}} {{< tab "npm" >}} ```sh npm update my-lib ``` {{< /tab >}} {{< tab "pnpm" >}} ```sh pnpm update my-lib ``` {{< /tab >}} {{< tab "yarn" >}} ```sh yarn upgrade my-lib ``` {{< /tab >}} {{< /tabs >}} ## Callouts A note or a warning beside the text, in Markdown: ```md {{}} The default. A note with **Markdown** in it. {{}} {{}} Something that can go wrong. {{}} ``` {{< callout >}} The default. A note with **Markdown** in it. {{< /callout >}} {{< callout "warning" >}} Something that can go wrong. {{< /callout >}} Use them sparingly: a page full of callouts has none that stand out. --- # Maths > Formulas with MathJax, on the pages that ask for it. ## Turning it on Markdown would read the backslashes in `\(` and `\[` as escapes and drop them before MathJax sees the formula. Hugo's passthrough extension leaves formulas untouched; add it to your `hugo.toml` once: ```toml {file="hugo.toml"} [markup.goldmark.extensions.passthrough] enable = true [markup.goldmark.extensions.passthrough.delimiters] block = [['\[', '\]'], ['$$', '$$']] inline = [['\(', '\)']] ``` Then add `math: true` to a page's front matter, or turn it on for every page: ```toml {file="hugo.toml"} [params] math = true ``` A page can then turn it off with `math: false`. MathJax 4 loads only on pages with maths turned on. ## Writing formulas In-line maths goes between `\(` and `\)`, display maths between `$$` lines (or `\[` and `\]`): ```md The identity \(e^{i\pi} + 1 = 0\) and the integral $$ \int_0^1 x^2 \, dx = \frac{1}{3} $$ ``` The identity \(e^{i\pi} + 1 = 0\) and the integral $$ \int_0^1 x^2 \, dx = \frac{1}{3} $$ ## Long formulas On a narrow screen, in-line maths breaks across lines like words. A display formula too wide for the column scrolls sideways in its own box, and so does an in-line formula that can't break, so the page itself never scrolls sideways: $$ \left\lVert u - u_h \right\rVert_{L^2(\Omega)} \le C h^{p+1} \left\lvert u \right\rvert_{H^{p+1}(\Omega)} \quad\text{for all}\quad u \in H^{p+1}(\Omega) \cap H^1_0(\Omega) $$ --- # Live examples > Examples that run on the page, with their files shown below them exactly as they run. The `example` shortcode is what Vitrine is named after: a glass case with the real thing inside. A folder of up to three files becomes a running demo, and the same files, unchanged, appear in tabs below it. What readers copy is what ran. Start with [the example shortcode](example-shortcode/), then read how [styles, scripts and imports](styles-and-scripts/) stay inside their example and load libraries. --- # The example shortcode > A folder next to your page, one line in its Markdown, and the example runs. ## A first example Make the page a page bundle (a folder with an `index.md`) and put the example's files in a folder next to it: ```text content/examples/counter/ ├── index.md └── demo/ ├── example.html required: the markup ├── style.css optional └── script.js optional ``` Then, in `index.md`: ```md {{}} ``` This is the [counter example](/examples/counter/), running (its files live in `assets/examples/counter/`, so this page and the Examples tab can share them; more below): {{< example name="counter" title="A counter" >}} The frame's label says "live" and the example's title. Under it, the files in tabs, with a copy button for the file shown. ## Options Pass the folder's name alone, as above, or as `name` with other options: | Option | Default | What it does | |---|---|---| | `name` | none | the folder with the example's files | | `title` | none | a title in the frame's label | | `run` | `true` | `false` shows only the files | | `code` | `true` | `false` runs the example without showing its files | | `frame` | `true` | `false` runs it bare: no frame, no files, its markup straight in the page | ```md {{}} {{}} {{}} ``` A shortcode call takes either one name or named options, not both: `{{}}` is an error. ## Where the files can live The shortcode looks for `/example.html` in two places, in this order: 1. next to the page, in its bundle: for examples that belong to one page; 2. in your site's `assets/examples//`: for examples several pages share, or examples on pages that aren't bundles (the start page's random walk lives in `assets/examples/walk/`). ## Shared parts Some examples need markup that isn't part of what they demonstrate. The [photo viewer](/examples/photo-viewer/) needs one `` per page. Put it in an example of its own and run it bare, with `frame="false"`: ```md {{}} {{}} ``` The bare example's markup, style and script go into the page without a frame or tabs. Write about it once, on a page of its own, and show its files there. ## Paths in the markup Write paths in `example.html` the way they'd look on your own site, from its root: ```html … ``` If your docs live under a subpath, say `example.org/docs/`, Vitrine adds it to every `src`, `href` and `poster` starting with `/` when the example runs. The files in the tabs keep the path as you wrote it. ## What else to know - **Ids are page-wide.** Every example's markup is part of the page, so two examples on one page can't use the same `id`. Give elements ids that name the example (`counter`, `walk-again`). - **Scripts run once,** as modules, after the page is parsed. They see the whole page, so select your elements by id, not by tag. - **No `` or `` in `example.html`**: it's a fragment that goes into the page. --- # Styles, scripts and imports > How an example's CSS stays inside it, how its script runs, and how it imports a library by its npm name. ## Styles stay inside the example Vitrine wraps an example's `style.css` in [`@scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/@scope) when it runs: ```css @scope ([data-example="demo"]) { /* your style.css */ } ``` So a plain `button { … }` styles that example's buttons and nothing else on the page, and two examples can use the same class names. The tab shows your file without the wrapper. An example's rules come after the theme's, so they win over it. The theme gives buttons inside a frame a look of their own (filled, in the accent colour), which your style can replace. The theme's colours are custom properties you can use, like `var(--accent)` and `var(--bg-soft)` (all are listed in [Customizing](/docs/guides/customizing/)): an example that uses them follows light and dark mode. `@scope` works in all current major browsers. An older browser ignores the whole style, and the example runs unstyled. ## Scripts run as modules `script.js` goes into the page as ` ``` ## Your own templates Copy a template from the theme to the same path in your site and change it there. The ones you're most likely to want: | Template | What it draws | |---|---| | `layouts/_partials/footer.html` | the footer | | `layouts/_partials/header.html` | the header: name, version, tabs, buttons | | `layouts/_partials/page-head.html` | breadcrumbs, title and lead | | `layouts/_partials/page-foot.html` | "Edit this page", previous and next | | `layouts/_partials/sidebar.html` | the section's navigation | | `layouts/home.html` | the start page's hero | | `layouts/_markup/render-codeblock.html` | code blocks | | `layouts/_markup/render-link.html` | links in Markdown | For example, a footer with your own links: ```go-html-template {file="layouts/_partials/footer.html"} ``` {{< callout "warning" >}} A copied template no longer gets the theme's updates. Copy as few as you can, and compare yours with the theme's when you update it. {{< /callout >}} ## Your own shortcodes Put them in `layouts/_shortcodes/` in your site. A shortcode with a theme shortcode's name replaces it. Hugo's [shortcode docs](https://gohugo.io/content-management/shortcodes/) explain how to write one. ## Page templates and archetypes A section can have its own page template: `layouts/
/page.html` for its pages, `list.html` for its own page. Start from the theme's `layouts/page.html`. [Archetypes](https://gohugo.io/content-management/archetypes/) are the templates `hugo new content` fills in. The theme's gives a page a title, a weight and an empty lead. Yours, in `archetypes/default.md` or per section (`archetypes/docs.md`), wins: ```md {file="archetypes/docs.md"} --- title: "{{ replace .File.ContentBaseName "-" " " | title }}" weight: 10 lead: "" draft: true --- ## Overview ``` --- # Deployment > Building the site and putting it online, with GitHub Pages as the example. ## Building ```sh hugo --minify ``` writes the site to `public/`: plain HTML, CSS and JavaScript that any web server can serve. Set `baseURL` in `hugo.toml` to the address the site will have, including a subpath if it has one (`https://you.github.io/my-lib/`). ## GitHub Pages In your repository's settings, under **Pages**, set the source to **GitHub Actions**. Then add this workflow. It installs Hugo and Go (for the theme as a Hugo module), builds the site and publishes it on every push to `main`: ```yaml {file=".github/workflows/pages.yml"} name: Pages on: push: branches: [main] workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: false jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive # if you added the theme as a Git submodule - uses: actions/setup-go@v5 with: go-version: stable - name: Install Hugo run: | curl -sSL -o hugo.deb https://github.com/gohugoio/hugo/releases/download/v0.167.0/hugo_0.167.0_linux-amd64.deb sudo dpkg -i hugo.deb - uses: actions/configure-pages@v5 id: pages - run: hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/" - uses: actions/upload-pages-artifact@v3 with: path: public deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment uses: actions/deploy-pages@v4 ``` `--baseURL` takes the address GitHub gives the site, so `baseURL` in `hugo.toml` doesn't matter for this build. With a custom domain, set it in the same settings page; GitHub then passes it to the build. ## Other hosts Netlify, Cloudflare Pages and Vercel build Hugo sites too: point them at your repository, with `hugo --minify` as the build command and `public` as the output folder. Set the Hugo version in their settings (an environment variable `HUGO_VERSION`), and Go if you use Hugo modules. --- # Roadmap > What Vitrine doesn't do yet, and might, if people need it. Vitrine 1.0 does what its own docs and a small library's docs need. These are left out on purpose for now; open an [issue](https://github.com/maxfettinger/vitrine/issues) if you need one, and say what for. | Feature | Notes | |---|---| | Search | most likely with [Pagefind](https://pagefind.app/), which indexes the built site and needs no server | | Translations | the theme's own words ("On this page", "Previous", "Edit this page") are English; Hugo's i18n files would make them translatable, and multilingual sites possible | | A third sidebar level | the sidebar shows two levels | | Versioned docs | one site per version works today, with a link between them; a version menu is not built in | | Last updated dates | from Git (`enableGitInfo`), under each page | | An announcement banner | today: the body-end hook | | Diagrams | Mermaid, as a code block render hook | | "Copy page" button | the page's Markdown copy to the clipboard, for pasting into an assistant | Already there but easy to miss: [llms.txt](/docs/reference/llms-txt/), [maths](/docs/writing/maths/), [hooks and your own CSS](/docs/guides/customizing/). --- # Examples > Small examples running on the page, each with exactly the files it's made of. Each page here runs one example made with the [example shortcode](/docs/examples/example-shortcode/). The files in the tabs are the files that run. --- # Counter > Markup, a scoped style and a module script: the three files an example can have. A button that counts its clicks. Its style uses a plain `button` selector, which reaches this example only: the theme wraps the file in `@scope` when it runs. {{< example name="counter" title="Counter" >}} The files live in `assets/examples/counter/`, so this page and [The example shortcode](/docs/examples/example-shortcode/) can both run them. --- # Random walk > A canvas drawn by a script, in the theme's colours. A walk on a grid, one step at a time. The script reads the theme's `--accent` colour from the canvas, so a new walk after switching to dark mode is drawn in the dark accent. With reduced motion turned on in the system, the whole walk appears at once. {{< example name="walk" title="Random walk" >}} --- # Photo viewer > A library from npm, imported by its name through the import map. The photos open in [dialog-lightbox](https://github.com/maxfettinger/dialog-lightbox), a small photo viewer on the native `` element. The script imports it as `"dialog-lightbox"`, the way it reads after `npm install`; the site's [import map](/docs/examples/styles-and-scripts/#importing-a-library-by-its-npm-name) sends that name to jsDelivr, and `params.stylesheets` adds the library's CSS. {{< example name="demo" title="Photo viewer" >}} Click a photo, then swipe or use the arrow keys; Escape or the back button closes it. The viewer is one `` for the whole page: [The dialog](/examples/dialog/) shows its markup and style. It runs bare here, without a frame: {{< example name="dialog" frame="false" >}} --- # The dialog > A part several examples share, shown once, running elsewhere. The [photo viewer](/examples/photo-viewer/) needs one `` on its page. It isn't part of what that example demonstrates, so it lives in `assets/examples/dialog/` and runs bare there, with `frame="false"`. This page only shows its files, with `run="false"`: {{< example name="dialog" run="false" >}}