As of
Est. 

Static Site Generation with VitePress

 Vite-
 Press
This static site has been built with Vite­Press and is hosted on Cloudflare Pages.

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:

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:

json
{
  "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.

js
// ./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/.vitepress/dist. 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:

js
// ./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.

js
// ./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.

js
// ./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 ComponentWhat is it for?
AsOfOutputs the creation date and the last modification date of a page
MainPageTeleports the Table of Contents into the right sidebar
GlossaryPageTeleports 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.

vue
<!-- ./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.

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" }],
],
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" }],
],

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.

js
export default defineConfig({
  ...
  cleanUrls: true,
  ...
export default defineConfig({
  ...
  cleanUrls: true,
  ...

  1. VitePress also offers useRouter() and useData() composables to access router and site data from any component. ↩︎