Contents13
- The short answer — VuePress 1.x splits into _posts and .vuepress
- Glossary: Static Site Generator (SSG)
- config.js — the site-wide configuration file
- locales — the default language
- title — the site title
- plugins — registering VuePress plugins
- plugins/directories — URL directory structure
- plugins/frontmatters — tag feature
- enhanceApp.js — where Vue plugins plug in
- theme — where layouts and CSS live
- Comparison table — VuePress 1.x and the main 2026 SSGs
- FAQ
- Closing
2026 update: A post ported over from the old VuePress blog. VuePress 1.x is no longer a viable choice for new development, so the body here is kept as an archive of “how the build looked in 2021”. The idea of splitting directories by concern still applies to current SSGs like Astro, so read it as comparison material.
Back in 2021, the personal blog that became the precursor to Aulvem was built on VuePress 1.x with @vuepress/plugin-blog. There wasn’t much information out there beyond the official docs, and I got stuck on directory layout and plugin configuration more than once — so once the site was actually running, I wrote this down as a working note.
As of 2026 I’ve moved over to Astro 5, and VuePress 1.x is for all practical purposes unmaintained (citation needed — confirm the official EOL announcement for the 1.x line). So read what follows as “a record of how the call was made at the time”.
The short answer — VuePress 1.x splits into _posts and .vuepress
Short answer: put article Markdown under _posts/, and configuration plus theme under .vuepress/. That was the standard layout when using plugin-blog.
The reason is separation of concerns. Content (written often) and framework configuration (written rarely) go in different directories, which keeps deploy logs and git diffs easier to read.
Concretely, the layout looked like this.
src
├─ _posts Markdown for each article
│ └─ category
│ └─ page.md
└─ .vuepress
├─ components Article-side components (Vue)
│ └─ Component.vue
├─ public
│ └─ img.jpg
├─ theme
│ ├─ components Components (Vue)
│ │ └─ Component.vue
│ ├─ layouts Page layouts (Vue)
│ │ └─ Layout.vue
│ └─ styles
│ └─ style.styl
├─ config.js VuePress config (JS)
└─ enhanceApp.js Vue plugin entry (JS)
During the build phase the three things you touch are config.js, enhanceApp.js, and theme/. During operations the files you touch are _posts/ and public/. Because the roles are different, once the build is done, the set of files you actually edit shifts on its own.
A caveat: this layout assumes VuePress 1.x + plugin-blog 1.9. VuePress 2.x changed both the config location and the theme model, so a 1.x configuration won’t run on 2.x as-is.
Glossary: Static Site Generator (SSG)
SSG (Static Site Generator): A generic name for tools that convert sources like Markdown into static HTML at build time. Unlike a CMS like WordPress that runs on the server for every request, the resulting HTML can simply be served from a CDN. VuePress, Astro, Next.js (output:static), Hugo, and Jekyll all fall into this category.
VuePress is one such SSG, built on Vue.js and optimized for “Markdown-centric documentation sites”. plugin-blog is the plugin that bolts on “blog-style routing (tags, categories, post lists)” on top — that mental model is enough.
config.js — the site-wide configuration file
Short answer: config.js is where both VuePress core and plugin-blog settings live together. The site’s language, title, and routing are decided here.
The reason is consolidation. You can split plugins and locales into separate files, but keeping it all in one file means the file reads as “the site’s spec”.
A concrete example:
module.exports = {
locales: {
'/': { lang: 'ja' }
},
title: 'Title(config.js)',
plugins: [[
'@vuepress/blog',
{directories: [
{
id: 'home',
dirname: 'home',
path: '/',
},
{
id: 'tech',
dirname: '_posts/tech',
path: '/tech/',
itemPermalink: '/tech/:slug'
}
],
frontmatters: [
{
id: "tag",
keys: ["tag"],
path: "/tag/",
layout: "Tag",
scopeLayout: "Tag"
},
],},
]]
}
locales — the default language
locales sets the site’s default language. For a Japanese site, setting / to lang: 'ja' is enough. For multilingual setups, you add another key like /en/.
title — the site title
title is used as the site name. From Vue files you can reference it as $siteTitle. It becomes the reference point for things like OG image generation and header-logo swaps, so not hard-coding the title elsewhere makes operations easier.
plugins — registering VuePress plugins
You add VuePress plugins to the plugins array. For any plugin including @vuepress/blog, if you want to pass options, the rule at the time was to use a [name, options] tuple rather than just the string name.
You’ll need to dig into each plugin’s docs for detailed options. Writing it all out in config.js makes the file balloon, so back then I kept reference URLs in comments.
plugins/directories — URL directory structure
directories decides “which Markdown folder maps to which URL”. For the proto-Aulvem blog I wanted the category structure to map straight to URLs, so I added one array entry per category.
{
id: 'tech',
dirname: '_posts/tech', File path holding articles for this category
path: '/tech/', /tech/{{article path}}
itemPermalink: '/tech/:slug' Format for {{article path}}. :slug is the filename minus the date
}
A caveat: dirname: '_posts/tech' only reaches one level deep and does not pick up grandchildren (this is plugin-blog 1.9 behavior). So in practice categories could only nest one level. Sub-categories meant adding more directories entries.
itemPermalink lets you toggle whether the date appears in the URL. I didn’t want dates in URLs, so I ran with just :slug. That followed the conventional wisdom at the time that “dates in URLs make a page feel stale” (citation needed — whether this actually affects rankings is case by case).
plugins/frontmatters — tag feature
frontmatters generates tag pages. Writing keys: ["tag"] makes the tag field in a Markdown frontmatter become a tag page.
{
id: "tag",
keys: ["tag"], Key set in the Markdown frontmatter
path: "/tag/",
layout: "Tag",
scopeLayout: "Tag"
}
Once this is in place, two globals become available: $tag and $currentTag.
$tagis the list of all tags and the articles bound to each. Used when building a tag index page.$currentTagis the “current tag” info when an individual tag page is open.
$currentTag looked like this:
inside the current page's `$currentTag` object
key: "Vue"
pageKeys: ["xxxxxx"]
pages: [{...}]
path: "/tag/Vue/"
scope: "tag"
A caveat: this style of “API that generates globals” plays badly with TypeScript — there’s no way to type it. Schema-enforced typing, like Astro’s content collections, feels more like the current mainstream approach.
enhanceApp.js — where Vue plugins plug in
Short answer: it’s the entry point for installing ordinary Vue plugins into VuePress. Think of it as “the place where you put Vue.use”.
The reason is that VuePress creates the Vue instance for you behind the scenes, so you can’t call Vue.use() from a regular main.js like in an SPA. Instead, enhanceApp.js hands you the Vue instance via its arguments.
import VScrollLock from 'v-scroll-lock'
export default ({
Vue,
options,
router,
siteData
}) => {
Vue.use(VScrollLock)
}
A caveat: putting heavy work here slows the initialization of every page. Better to keep this file to Vue.use only and push real logic down into components.
theme — where layouts and CSS live
Short answer: theme/ is the directory for non-article Vue files and CSS. The workflow was to copy the default theme via npm run eject and override on top of it.
The reason is that VuePress’s default theme isn’t directly editable. Running eject expands the default theme set under .vuepress/theme/, where you can edit anything.
Right after eject, the directory looked like this:
theme
├── `global-components`
│ └── xxx.vue
├── `components`
│ └── xxx.vue
├── `layouts`
│ ├── Layout.vue _(**Mandatory**)_
│ └── 404.vue
├── `styles`
│ ├── index.styl
│ └── palette.styl
├── `templates`
│ ├── dev.html
│ └── ssr.html
├── `index.js`
├── `enhanceApp.js`
└── package.json
UI parts get edited or added under components/, and CSS lives either inside Vue files or under styles/. layouts/Layout.vue is required — delete it or rename it and the build dies immediately.
A caveat: once you eject, you can’t auto-follow upstream theme updates anymore. I knew that going in at the time, but if I were doing this today I’d lean toward “minimal overrides only”, staying on the upstream theme — that’s the easier maintenance path (citation needed — VuePress 2.x may have a different mechanism here).
Comparison table — VuePress 1.x and the main 2026 SSGs
Lining up VuePress at the time against the SSGs most often used for the same role in 2026:
| Lens | VuePress 1.x (2021) | Astro 5 (2026) | Next.js (App Router) | Hugo |
|---|---|---|---|---|
| Primary language | Vue 2 | framework-agnostic | React | Go templates |
| Target | docs / small blogs | content sites broadly | apps and beyond | static sites with many pages |
| Config file | .vuepress/config.js | astro.config.mjs | next.config.js | config.toml |
| Article location | _posts/** | src/content/** | app/** or freeform MDX | content/** |
| Type safety | none (globals) | content collections with zod | TS and RSC | none |
| Partial hydration | whole site is an SPA | islands (static by default) | RSC + Client Components | static only |
| 2026 maintenance | 1.x effectively halted (citation needed) | active | active | active |
The point here isn’t superiority but a difference in how concerns get sliced. For the narrow use case of “output a Markdown-centric blog statically”, VuePress 1.x worked fine. Aulvem itself moved to Astro because the article count grew and I wanted type safety.
FAQ
Q. Is VuePress 1.x still usable in 2026?
A. You can still get it running. But maintenance — plugin-blog included — has all but stopped, and you can’t expect timely vulnerability patches in dependencies. Not the safe pick for new builds. For keeping an existing site alive, pinning Node versions and locking your package-lock.json properly is the minimum bar (citation needed — confirm whether an official EOL announcement exists).
Q. How is VuePress different from Astro / Next.js? A. VuePress is Vue 2-based, starting from “Markdown-centric documentation sites”. Astro is framework-agnostic and uses partial hydration (islands), which makes mixing Markdown and dynamic parts easier. Next.js is React-based and covers anything from static sites to dynamic apps. Same “static site” label, different scope and assumptions.
Q. Why split _posts and .vuepress?
A. Separation of concerns between “article content” and “framework configuration”. The idea of grouping things that change at different rates into different directories carries over to Astro’s src/content/ vs. src/layouts/ split. The opposite school exists too — Next.js App Router puts everything under app/ — so this is partly a matter of taste in design.
Q. What would I pick today for a similar setup? A. For a Markdown-focused blog, Astro + content collections is the closest in feel. If staying on Vue is required, VitePress and Nuxt Content are the successor candidates, but they each cover slightly different ground — check the official docs to see if they fit your use case (citation needed).
Closing
VuePress 1.x’s layout rested on a simple separation: “write in _posts, configure in .vuepress”. At the time, that directness was what carried me through to getting the site running.
As of 2026 Aulvem itself runs on Astro 5. The same idea of splitting concerns across directories survives in a different form — so I’d be glad if this post gets used as a past snapshot, useful for comparing against today’s SSGs.