Optimize Your VitePress Blog's Architecture Design
In the preceding article, we initialized our VitePress project and examined its structure. We also began incorporating our custom theme. Today, we will further progress by defining the blog's architecture and design to ensure it remains manageable and scalable over time.
The src
Directory
To begin, we will relocate our project's source files to a designated src
directory. This approach prevents cluttering the root directory with source files and avoids including unwanted files like README.md
in our project's pages.
What constitutes our source files? Specifically, is the theme a source file? The answer is no. As we're developing a content-centric website, only markdown and public files are considered source files. Everything related to the theme is part of the VitePress configuration and will reside in the .vitepress
directory.
To modify the source directory, we need to update the srcDir
option in the config.mts
file located in the .vitepress
directory. Here is the revised content of the config.mts
file:
import { defineConfig } from 'vitepress'
export default defineConfig({
srcDir: 'src',
// ...
})
This option informs VitePress where our markdown pages are stored relative to the project root. In our instance, the project root is the repository's root.
The src
directory will also encompass a public
directory, where static assets like images, fonts, and other unaltered files will be stored.
Note
The public
directory associates with the site's root and is not processed by VitePress. Therefore, files in the public
directory will be copied to the root of the generated site without any transformation.
.
├── .vitepress
│ └── config.mts
├── src
│ ├── public
│ └── index.md
└── package.json
This structure is significantly cleaner and more organized. If we aim to write a blog post, we simply open the src
directory, and if we desire to update our theme, we access the .vitepress
directory.
The .vitepress
Directory
Regarding the .vitepress
directory, we can create subdirectories to aid in structuring our theme, maintaining simplicity akin to a standard Vite + Vue project.
Under .vitepress/theme
, we will establish the following directories:
components
: Contains all the Vue components used in the theme.composables
: Contains all the Vue composables utilized in the theme.pages
: Contains all the Vue components employed as pages in theLayout.vue
component.styles
: Contains all the CSS files applied in the theme.types
: Contains all the TypeScript types utilized in the theme.utils
: Contains all the utility functions used in the theme.
.
├── .vitepress
│ ├── theme
│ │ ├── components
│ │ ├── composables
│ │ ├── pages
│ │ ├── styles
│ │ ├── types
│ │ ├── utils
│ │ ├── index.ts
│ │ └── Layout.vue
│ └── config.mts
├── src
│ ├── public
│ └── index.md
└── package.json
This structure closely mirrors what many Vue developers are accustomed to, which is advantageous! It eliminates the need to learn a new project structure while utilizing VitePress. If you're familiar with structuring a Vue project, you already know how to structure a VitePress project. 👌
The pages
Directory
I acknowledge this might seem confusing. I mentioned earlier that VitePress features a minimal router, and routes are generated from the src
directory's structure. So, what do these pages
imply?
VitePress follows the convention of using Layout.vue
as the entry point for the Vue application. Given this scenario, having a layouts
directory would imply that a layout utilizes a layout, which seems peculiar to me.
Renaming Layout.vue
to App.vue
might be an option, yet I find it less appealing. I prefer retaining the Layout.vue
name as it's a VitePress convention. Moreover, as VitePress doesn't have a router, we can't use a RouterView
component to render the layout and the page. This implies that Layout.vue
and App.vue
serve different purposes.
Ultimately, establishing a pages
directory seemed the most logical decision. Within this directory, we include Vue components such as NotFound.vue
, Blog/BlogIndex.vue
, Blog/BlogPost.vue
, etc., forming a file routing system.
Note
The pages
directory is my approach to organizing the project. It's not mandated by VitePress. Any .vue
files could reside in the components
or layouts
directory. Feel free to customize the structure according to your preferences.
In conclusion, we'll use Layout.vue
to route our Markdown files to the appropriate page component. To accomplish this, we'll use the layout
key in the frontmatter of our Markdown files. This key facilitates importing the correct page component in the Layout.vue
file.
Consider the following Markdown file:
---
layout: blog-post
---
# Hello, World!
In the Layout.vue
file, we can render the Blog/BlogPost.vue
component as follows:
<script lang="ts" setup>
import { useData } from 'vitepress'
const { frontmatter } = useData() // This is the frontmatter of the current Markdown file
</script>
<template>
<BlogPost v-if="frontmatter.layout === 'blog-post'" />
<Content v-else /> // Just render the content of the Markdown file
</template>
This approach enables a dynamic routing system without an actual router. Meanwhile, Layout.vue
remains the entry point of the Vue application.
Note
If anything isn't clear, don't worry. We'll apply this in the upcoming articles when introducing blog posts and the blog list.
Configuring Unplugin Vue Components
In the previous code snippet, we used the BlogPost
component without importing it. This magic becomes feasible with the help of Unplugin Vue Components.
Note
This step is entirely optional, and you can skip it if desired. For me, it's an excellent way to maintain clean code, and as a Nuxt user, I have a slight preference for this method. 😅
Behind the scenes, this plugin for Vite scans our files for Vue components as they pass through the Vite pipeline. When it detects a component, it adds the relevant import statement at the top of the file. This aligns with Vite's on-demand philosophy, ensuring small bundle sizes and allowing component chunking where they are utilized.
This plugin seamlessly integrates with TypeScript, ensuring that we do not compromise type checking or auto-completion in our IDE.
Installing Necessary Dependencies
First, we must install TypeScript and the Node types. This is essential as the plugin generates a declaration file containing our components' types.
pnpm add -D typescript @types/node vue
Note
The installation of vue
is required when utilizing pnpm due to its hoisting mechanism. Refer to the shamefully-hoist
option for further explanation.
Next, a tsconfig.json
file should be created at the project's root to guide TypeScript's behavior.
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true
},
"include": ["./.vitepress/**/*"]
}
The @types/node
is automatically incorporated by TypeScript. We include all files within the .vitepress
directory where our TypeScript and Vue components reside. This pattern also imports .d.ts
files, which is crucial for the plugin.
You should also add "type": "module"
in the package.json
file and modify the extensions of postcss.config.js
and tailwind.config.js
to .cjs
.
Configuration
Finalizing the configuration of the plugin is straightforward:
- Indicate to the plugin the directory where our Vue components reside.
- Define to the plugin the naming conventions for our Vue components.
- Specify to the plugin the location for creating the declaration file.
Given the config.mts
file resides within the .vitepress
directory, there is more configuration than in a standard Vite project where the configuration vite.config.ts
is situated at the project's root.
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vitepress'
const currentDir = dirname(fileURLToPath(import.meta.url))
const componentsDir = resolve(currentDir, 'theme', 'components')
const pagesDir = resolve(currentDir, 'theme', 'pages')
export default defineConfig({
// ...
vite: {
plugins: [
Components({
dirs: [
componentsDir,
pagesDir,
],
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
dts: resolve(
currentDir,
'components.d.ts'
),
}),
],
},
})
The dirs
option directs the plugin to where our components are located. In our context, we have two directories: theme/pages
and theme/components
, relative to the config.mts
file. This distinction matters because the process initiates in the project's root, but the configuration lies within the .vitepress
directory. It prevents the use of ../../
in paths.
The include
option specifies the file extensions that should be scanned to add import statements. We include .vue
and .md
files as we might have Vue components in Markdown files without the need for explicit imports, which is a fantastic feature.
The dts
option guides the plugin on where to create the declaration file. We desire to create the components.d.ts
file within the .vitepress
directory's root. This declaration file aids our IDE and TypeScript in providing auto-completion and type checking, even when components are not explicitly imported.
When we launch our development server, we should verify the plugin's functionality by examining the components.d.ts
file. Initially, this file might not feature any components as we don't have any. Creating a temporary component will allow observation of the changes and requires a development server restart. Use r to restart the server.
Note
The current setup will not function as described, and it's normal. Continue reading to understand why and see the final configuration.
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
}
}
This file is generated by the plugin, should remain unchanged manually, and should be added to the .gitignore
file.
node_modules
cache
components.d.ts
However, the present setup does not work due to limitations originating from Vite. Underneath, the plugin connects to the Vite files watcher, meaning it only responds to files and directories monitored by Vite. Vite, by default, watches only the root
directory specified by the srcDir
of the config.mts
file. Consequently, files and directories within the .vitepress
directory remain undetected by Vite and the plugin.
To address this, we must instruct Vite to monitor the directory where our Vue components and pages reside. This is achieved by devising a compact plugin to update Vite's watcher configuration:
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitepress'
const currentDir = dirname(fileURLToPath(import.meta.url))
const componentsDir = resolve(currentDir, 'theme', 'components')
const pagesDir = resolve(currentDir, 'theme', 'pages')
export default defineConfig({
srcDir: 'src',
vite: {
plugins: [
{
name: 'watcher',
configureServer(server) {
server.watcher.add([componentsDir, pagesDir])
},
},
// ...
],
},
})
With this adjustment, everything should function as intended. The plugin generates the declaration file, providing auto-completion and type checking within our IDE when a component is created without the needs to restart the development server.
On-Demand Everything
Can this on-demand philosophy extend further to include everything?
Absolutely, and we will achieve this using the unplugin-auto-import
plugin. This plugin functions similarly to the unplugin-vue-components
plugin but for JavaScript and TypeScript files.
First, we must install the plugin:
pnpm add -D unplugin-auto-import
Next, configure the plugin in the config.mts
file:
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import AutoImport from 'unplugin-auto-import/vite'
import { defineConfig } from 'vitepress'
const currentDir = dirname(fileURLToPath(import.meta.url))
// ...
const composablesDir = resolve(currentDir, 'theme', 'composables')
const utilsDir = resolve(currentDir, 'theme', 'utils')
export default defineConfig({
srcDir: 'src',
vite: {
plugins: [
{
name: 'watcher',
configureServer(server) {
// ...
server.watcher.add([composablesDir, utilsDir])
},
},
AutoImport({
imports: ['vue', 'vitepress'],
dirs: [composablesDir, utilsDir],
dts: resolve(currentDir, 'auto-imports.d.ts'),
}),
// ...
],
},
})
With this configuration, we accomplish three objectives:
- Automatically import all composable and utility functions from
vue
andvitepress
, allowingref
,computed
,useData
, etc., to be used without explicit imports. - Scan and register all composable and utility functions in the
theme/composables
andtheme/utils
directories. - Enable the watcher to monitor
theme/composables
andtheme/utils
directories, similar to theunplugin-vue-components
plugin.
This plugin generates an auto-imports.d.ts
file for the same reasons as the components.d.ts
file. It too should be added to the .gitignore
file.
node_modules
cache
components.d.ts
auto-imports.d.ts
Scripts
To conclude this article, let's remove the docs:
prefix from our scripts in the package.json
file. Since we're not creating documentation, and VitePress is the sole project in the repository, we can safely eliminate this prefix.
{
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview"
}
}
Excellent! We now have a clean and organized project structure, ready to commence our blogging endeavor. In the subsequent article, we will develop the blog list and leverage one of VitePress’s core features: the data loader.
Thanks for reading! My name is Estéban, and I love to write about web development.
I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.
If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.
I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!