Est.
Static Site Generation with VitePress
VitePress is a static site generator that supports Markdown and Vue components. It is ideal for documentation sites, but should also work well for my jottings. I had no problems using VitePress out of the box. You can just start to write markdown files and have them rendered on a website. The model is simple: there is a one-to-one mapping between file paths and URLs.
I developed this website using v0.22.4
. Meanwhile, I stepped up to 1.0.0-beta.5.
Theme and Config had breaking changes. That was no surprise and is well documented.
VitePress is Fun
There are two things that are real fun about VitePress:
- It comes with all the benefits of Vite:
- You can watch your site grow while you type due to fast HMR.
- You can automate things with Vite's plugins.
- It is build on top of Vue:
Starter Configuration
Most projects use VitePress as a tool for generating documentation in the same way as they use testing or linting tools inside another project. In this case, VitePress is usually located in the docs
directory.
If your project is just VitePress only, it may feel more natural to start in the root directory of your project. In what follows, you might replace docs
or ./docs
with .
for the root directory.
A minimal package.json
file for VitePress looks like this:
{
"name": "vitepress-demo",
"version": "1.0.0",
"scripts": {
"dev": "vitepress dev docs",
"build": "vitepress build docs",
"serve": "vitepress serve docs"
},
"devDependencies": {
"vitepress": "^1.0.0-beta.5"
}
}
{
"name": "vitepress-demo",
"version": "1.0.0",
"scripts": {
"dev": "vitepress dev docs",
"build": "vitepress build docs",
"serve": "vitepress serve docs"
},
"devDependencies": {
"vitepress": "^1.0.0-beta.5"
}
}
That will allow you to run VitePress in development mode by invoking npm run dev
.
You also need some minimal configuration in ./docs/.vitepress/config.js
to get navigation and sidebar entries.
// ./docs/.vitepress/config.ts
export default {
title: "Title",
description: "Description ...",
themeConfig: {
nav: [
{
text: "ergberg.tk",
link: "https://ergberg.tk",
},
],
sidebar: {
"/": [
{text: "A Single Topic", link: "topic1"},
{
text: "A Topic Group",
items: [
{
text: "First Topic of Group",
link: "topic2",
},],},],},},};
// ./docs/.vitepress/config.ts
export default {
title: "Title",
description: "Description ...",
themeConfig: {
nav: [
{
text: "ergberg.tk",
link: "https://ergberg.tk",
},
],
sidebar: {
"/": [
{text: "A Single Topic", link: "topic1"},
{
text: "A Topic Group",
items: [
{
text: "First Topic of Group",
link: "topic2",
},],},],},},};
As you can see, there can be different sidebars for different contexts. If you define multiple contexts, the "/"
context should be defined last in sequence as it is the default for all routes.
With the above configuration you just need some Markdown in ./docs/index.md
, ./docs/topic1.md
and docs/topic2.md
. Then you are done with the minimal configuration.
Over time, my config grew. I used JavaScript functions to structure the config and to generate the glossary part of the sidebar. Some extensions are covered below.
Develop, Build & Deploy
The static website is now created using npm run build
. VitePress will then generate the static site in ./docs/.vite
. You can deploy this directory directly to a static web hosting site such as Vercel, Netlify or Amazon CloudFront. This jotter runs on Cloudflare Pages.
Of course, you first want to check it locally before you deploy it elsewhere: VitePress offers npm run serve
to serve your static site on localhost:5000
.
But usually you will run the dev server with npm run dev
and watch how your site evolves on localhost:3000
.
Advanced Topics
A great source of joy comes from the fact that VitePress is simple at its core, but highly customizable with easy-to-build or ready-to-use components.
Configure Markdown-it & Vite
VitePress has a load of plugins for Markdown-it already built in. You will enjoy
- markdown-it-anchor
- markdown-it-attrs
- markdown-it-emoji
- markdown-it-table-of-contents
but also several VitePress specific plugins that mainly deal with formatting of (fenced) code, provide containers for tip, info, warning, danger & details, enable Vue components in Markdown and deal with headings & links.
Still, I missed some nice to haves
in Markdown. And so I extended ./docs/.vitepress/config.js
with additional Markdown-it plugins:
// ./docs/.vitepress/config.ts
...
markdown: {
lineNumbers: false,
theme: { light: "vitesse-light", dark: "vitesse-dark" },
config: (md: any) => {
md.use(footnote);
md.use(kbd);
md.use(mark);
md.use(span);
},
}
// ./docs/.vitepress/config.ts
...
markdown: {
lineNumbers: false,
theme: { light: "vitesse-light", dark: "vitesse-dark" },
config: (md: any) => {
md.use(footnote);
md.use(kbd);
md.use(mark);
md.use(span);
},
}
For more information on the theme
entry, read about dark mode in VitePress.
There is also a slot for Vite config in the VitePress configuration file. For the jottings, I mainly use it to add the site specific Vite plugins.
// ./docs/.vitepress/config.ts
...
vite: {
resolve: {
preserveSymlinks: true,
},
plugins: [
imagetools(),
...
]
}
// ./docs/.vitepress/config.ts
...
vite: {
resolve: {
preserveSymlinks: true,
},
plugins: [
imagetools(),
...
]
}
Theming
To make changes to VitePress's default theme, create ./docs/.vitepress/theme/index.js
. I use this file to define my customized layout and to register some Vue components globally in the VitePress app. This way I can use them on any page without any further declaration. As you can see, the enhanceApp
function also gets access to the VitePress router and the site data.[1] Besides Layout
and enhanceApp
, the theme can also define a NotFound
component for 404 errors.
// ./docs/.vitepress/theme/index.js
import MyLayout from './Layout.vue'
import AsOf from "./components/AsOf.vue";
...
import "./styles/tailwind.css";
import "./styles/vars.css";
...
export default {
Layout: MyLayout,
enhanceApp: ({ app, router, siteData }) => {
app.component("AsOf", AsOf);
app.component("MainPage", MainPage);
app.component("GlossaryPage", GlossaryPage);
...
}
}
// ./docs/.vitepress/theme/index.js
import MyLayout from './Layout.vue'
import AsOf from "./components/AsOf.vue";
...
import "./styles/tailwind.css";
import "./styles/vars.css";
...
export default {
Layout: MyLayout,
enhanceApp: ({ app, router, siteData }) => {
app.component("AsOf", AsOf);
app.component("MainPage", MainPage);
app.component("GlossaryPage", GlossaryPage);
...
}
}
Calling app.component
with a Vue component registers that component for the use in any markdown file without the necessity of any further import statements there. The three components given above are
Vue Component | What is it for? |
---|---|
AsOf | Outputs the creation date and the last modification date of a page |
MainPage | Teleports the Table of Contents into the right sidebar |
GlossaryPage | Teleports search results into the right side bar and inserts topic links below the main content. |
The example above also shows, how CSS files are included into the theme. There are also jottings with details on styling with Tailwind.
The Layout
component can define a whole new, independent structure. Just insert the <Content/>
component in your template to give VitePress a place to render the Markdown content.
Since the original default layout of VitePress 0.22 works quite well for my site, I am using it in combination with the more modern VitePress 1.0.0 home page and page-footer. I am happy with the slots the VitePress developers have provided in their template. Below you see how the 1.0.0 components VPHome
and VPDocFooter
are plugged into the #home
and #page-bottom
slots. The UI controls for the right sidebar, feedback button, dark mode, and the topic map are put into the slot that normally contains the Algolia search box. The search and topic map are disabled on the home screen.
<!-- ./docs/.vitepress/theme/Layout.vue-->
...
<template>
<Layout>
<template #home><VPHome /></template>
<template #navbar-search
><SearchButton
v-if="frontmatter.layout !== 'home'"
/><TopAside /><MailMe /><DarkMode /><TopicMapToggle
v-if="frontmatter.layout !== 'home'"
/></template>
<template #page-bottom><VPDocFooter /></template>
</Layout>
</template>
<!-- ./docs/.vitepress/theme/Layout.vue-->
...
<template>
<Layout>
<template #home><VPHome /></template>
<template #navbar-search
><SearchButton
v-if="frontmatter.layout !== 'home'"
/><TopAside /><MailMe /><DarkMode /><TopicMapToggle
v-if="frontmatter.layout !== 'home'"
/></template>
<template #page-bottom><VPDocFooter /></template>
</Layout>
</template>
Configuring PWA Headers
To automatically generate the header fields necessary for a PWA for every page, I added the following elements to the head slot of the VitePress configuration.
head: [
["link", {rel: "manifest", href: "/site.webmanifest", crossorigin: "use-credentials"}],
["meta", {name: "theme-color", content: "grey"}],
["link", {rel: "apple-touch-icon", href: "/apple-touch-icon.png", sizes: "180x180"}],
["link", {rel: "mask-icon", href: "/e3.svg", color: "grey"}],
["script", { src: "/registerSW.js" }],
],
head: [
["link", {rel: "manifest", href: "/site.webmanifest", crossorigin: "use-credentials"}],
["meta", {name: "theme-color", content: "grey"}],
["link", {rel: "apple-touch-icon", href: "/apple-touch-icon.png", sizes: "180x180"}],
["link", {rel: "mask-icon", href: "/e3.svg", color: "grey"}],
["script", { src: "/registerSW.js" }],
],
Link Normalization / cleanUrls
Before 1.0.0-alpha.6, VitePress added .html
to routes like /basics/vitepress
when generating links. Now the behavior can be configured in the VitePress config. I opt out of getting .html
appended, as Cloudflare Pages redirects .html
URLs to the extension-less path.
export default defineConfig({
...
cleanUrls: true,
...
export default defineConfig({
...
cleanUrls: true,
...