mirror of https://github.com/vuejs/vitepress
commit
80c588e24a
@ -1,3 +1,3 @@
|
||||
github: [yyx990803, kiaking, posva, pikax]
|
||||
github: [yyx990803, kiaking, brc-dd, posva]
|
||||
open_collective: vuejs
|
||||
patreon: evanyou
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
## Basic Usage
|
||||
|
||||
```js
|
||||
export default {
|
||||
load() {
|
||||
return {
|
||||
data: 'hello'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
export default {
|
||||
async load() {
|
||||
return (await fetch('...')).json()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Generating Data Based On Local Files
|
||||
|
||||
```js
|
||||
import { readDirSync } from 'node:fs'
|
||||
|
||||
export default {
|
||||
watch: ['*.md'],
|
||||
async load() {
|
||||
//
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Typed Data
|
||||
|
||||
```ts
|
||||
export interface Data {
|
||||
// data type
|
||||
}
|
||||
|
||||
declare const data: Data
|
||||
export { data }
|
||||
|
||||
export default {
|
||||
async load(): Promise<Data> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -1,29 +1,43 @@
|
||||
# Prev Next Link
|
||||
|
||||
You can customize the text of previous and next links. This is helpful if you want to show different text on previous/next links than what you have on your sidebar.
|
||||
You can customize the text and link for the previous and next pages (shown at doc footer). This is helpful if you want a different text there than what you have on your sidebar. Additionally, you may find it useful to disable the footer or link to a page that is not included in your sidebar.
|
||||
|
||||
## prev
|
||||
|
||||
- Type: `string`
|
||||
- Type: `string | false | { text?: string; link?: string }`
|
||||
|
||||
- Details:
|
||||
|
||||
Specify the text to show on the link to the previous page.
|
||||
Specifies the text/link to show on the link to the previous page. If you don't set this in frontmatter, the text/link will be inferred from the sidebar config.
|
||||
|
||||
If you don't set this in frontmatter, the text will be inferred from the sidebar config.
|
||||
- Examples:
|
||||
|
||||
- Example:
|
||||
- To customize only the text:
|
||||
|
||||
```yaml
|
||||
---
|
||||
prev: 'Get Started | Markdown'
|
||||
---
|
||||
```
|
||||
```yaml
|
||||
---
|
||||
prev: 'Get Started | Markdown'
|
||||
---
|
||||
```
|
||||
|
||||
## next
|
||||
- To customize both text and link:
|
||||
|
||||
- Type: `string`
|
||||
```yaml
|
||||
---
|
||||
prev:
|
||||
text: 'Markdown'
|
||||
link: '/guide/markdown'
|
||||
---
|
||||
```
|
||||
|
||||
- Details:
|
||||
- To hide previous page:
|
||||
|
||||
```yaml
|
||||
---
|
||||
prev: false
|
||||
---
|
||||
```
|
||||
|
||||
## next
|
||||
|
||||
Same as `prev` but for the next page.
|
||||
Same as `prev` but for the next page.
|
||||
|
||||
@ -1,53 +1,45 @@
|
||||
# What is VitePress?
|
||||
|
||||
VitePress is [VuePress](https://vuepress.vuejs.org/)' little brother, built on top of [Vite](https://vitejs.dev/).
|
||||
VitePress is a [Static Site Generator](https://en.wikipedia.org/wiki/Static_site_generator) (SSG). It is designed for building performant content-centric websites, such as this documentation you are reading right now. It also powers the documentation for [Vue.js](https://vuejs.org/), [Vite](https://vitejs.dev/), and many more<!-- TODO: showcase page? -->. In a nutshell, VitePress takes your source content written in [Markdown](https://en.wikipedia.org/wiki/Markdown), applies a theme to it, and generates a directory of static HTML pages (and necessary asset files) that can be easily deployed anywhere.
|
||||
|
||||
::: warning
|
||||
VitePress is currently in `alpha` status. It is already suitable for out-of-the-box documentation use, but the config and theming API may still change between minor releases.
|
||||
:::
|
||||
## Developer Experience
|
||||
|
||||
## Motivation
|
||||
VitePress aims to provide a great Developer Experience (DX) when working with Markdown content.
|
||||
|
||||
We love VuePress v1, but being built on top of Webpack, the time it takes to spin up the dev server for a simple doc site with a few pages is just becoming unbearable. Even HMR updates can take up to seconds to reflect in the browser!
|
||||
- **[Vite-Powered:](https://vitejs.dev/)** instant server start, with edits always instantly reflected (<100ms) without page reload.
|
||||
|
||||
Fundamentally, this is because VuePress v1 is a Webpack app under the hood. Even with just two pages, it's a full on Webpack project (including all the theme source files) being compiled. It gets even worse when the project has many pages - every page must first be fully compiled before the server can even display anything!
|
||||
- **[Built-in Markdown Extensions:](/guide/markdown)** Frontmatter, tables, syntax highlighting... you name it. Specifically, VitePress provides many advanced features for working with code blocks, making it ideal for highly technical documentation.
|
||||
|
||||
Incidentally, Vite solves these problems really well: nearly instant server start, an on-demand compilation that only compiles the page being served, and lightning-fast HMR. Plus, there are a few additional design issues I have noted in VuePress v1 over time but never had the time to fix due to the amount of refactoring it would require.
|
||||
- **[Vue-Enhanced Markdown](/guide/using-vue):** each Markdown page is also a Vue [Single-File Component](https://vuejs.org/guide/scaling-up/sfc.html), thanks to Vue template's 100% syntax compatibility with HTML. You can embed interactivity in your static content using Vue templating features or imported Vue components.
|
||||
|
||||
Now, with Vite and Vue 3, it is time to rethink what a "Vue-powered static site generator" can really be.
|
||||
## Performance
|
||||
|
||||
## Improvements over VuePress v1
|
||||
Unlike many traditional SSGs, a website generated by VitePress is in fact a [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) (SPA).
|
||||
|
||||
There're couple of things that are improved from VuePress v1....
|
||||
- **Fast Initial Load**
|
||||
|
||||
### It uses Vue 3
|
||||
The initial visit to any page will be served the static, pre-rendered HTML for maximum loading speed, together with a JavaScript bundle that turns the page into a Vue SPA ("hydration"). The hydration process is extremely fast: on [PageSpeed Insights](https://pagespeed.web.dev/), typical VitePress sites achieve near-perfect performance scores even on low-end mobile devices with a slow network.
|
||||
|
||||
Leverages Vue 3's improved template static analysis to stringify static content as much as possible. Static content is sent as string literals instead of JavaScript render function code - the JS payload is therefore much cheaper to parse, and hydration also becomes faster.
|
||||
- **Fast Post-load Navigation**
|
||||
|
||||
Note the optimization is applied while still allowing the user to freely mix Vue components inside markdown content - the compiler does the static/dynamic separation for you automatically and you never need to think about it.
|
||||
More importantly, the SPA model leads to better user experience **after** the initial load. Subsequent navigation within the site will no longer cause a full page reload. Instead, the incoming page's content will be fetched and dynamically updated. VitePress also automatically pre-fetches page chunks for links that are within viewport. In most cases, post-load navigation will feel instant.
|
||||
|
||||
### It uses Vite under the hood
|
||||
- **Interactivity Without Penalty**
|
||||
|
||||
- Faster dev server start
|
||||
- Faster hot updates
|
||||
- Faster build (uses Rollup internally)
|
||||
To be able to hydrate the dynamic Vue parts embedded inside static Markdown, each Markdown page is processed as a Vue component and compiled into JavaScript. This may sound inefficient, but the Vue compiler is smart enough to separate the static and dynamic parts, minimizing both the hydration cost and payload size. For the initial page load, the static parts are automatically eliminated from the JavaScript payload and skipped during hydration.
|
||||
|
||||
### Lighter page weight
|
||||
## Theming & Extensibility
|
||||
|
||||
Vue 3 tree-shaking + Rollup code splitting
|
||||
- Does not ship metadata for every page on every request. This decouples page weight from total number of pages. Only the current page's metadata is sent. Client side navigation fetches the new page's component and metadata together.
|
||||
- Does not use vue-router because the need of VitePress is very simple and specific - a simple custom router (under 200 LOC) is used instead.
|
||||
VitePress ships with a feature-rich default theme designed for documentation purposes. It allows you to spin up a beautiful documentation site like this one with minimal effort, and doesn't require any Vue-specific knowledge.
|
||||
|
||||
### Other differences
|
||||
VitePress also supports fully customized themes with the developer experience of a standard Vite + Vue application. Being built on Vite also means you can directly leverage Vite plugins from its rich ecosystem. This makes VitePress an ideal choice for building sites that is content-centric but also requires non-trivial interactivity. The [Vue.js documentation](https://github.com/vuejs/docs) is a good example of such customization.
|
||||
|
||||
VitePress is more opinionated and less configurable: VitePress aims to scale back the complexity in the current VuePress and restart from its minimalist roots.
|
||||
And of course, you can use it to build a blog! The [official Vue.js blog](https://github.com/vuejs/blog) is also built with VitePress.
|
||||
|
||||
VitePress is future oriented: VitePress only targets browsers that support native ES module imports. It encourages the use of native JavaScript without transpilation, and CSS variables for theming.
|
||||
## What About VuePress?
|
||||
|
||||
## Will this become the next vuepress in the future?
|
||||
VitePress is the spiritual successor of VuePress. The original VuePress was based on Vue 2 and webpack. With Vue 3 and Vite under the hood, VitePress provides significantly better DX, better production performance, a more polished default theme, and a more flexible customization API.
|
||||
|
||||
We already have [vuepress-next](https://github.com/vuepress/vuepress-next), which would be the next major version of VuePress. It also makes lots of improvements over VuePress v1, and also supports Vite now.
|
||||
The API difference between VitePress and VuePress mostly lies in theming and customization. If you are using VuePress 1 with the default theme, it should be relatively straightforward to migrate to VitePress.
|
||||
|
||||
VitePress is not compatible with the current VuePress ecosystem (mostly themes and plugins). The overall idea is that VitePress will have a drastically more minimal theming API (preferring JavaScript APIs instead of file layout conventions) and likely no plugins (all customization is done in themes).
|
||||
|
||||
There is an [ongoing discussion](https://github.com/vuejs/vitepress/discussions/548) about this topic. If you're curious, please leave your thoughts!
|
||||
There has also been effort invested into VuePress 2, which also supports Vue 3 and Vite with more compatibility with VuePress 1. However, maintaining two SSGs in parallel isn't sustainable, so the Vue team has decided to focus on VitePress as the main recommended SSG in the long run.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,178 @@
|
||||
import {
|
||||
loadConfigFromFile,
|
||||
normalizePath,
|
||||
type Plugin,
|
||||
type ViteDevServer
|
||||
} from 'vite'
|
||||
import fs from 'fs-extra'
|
||||
import c from 'picocolors'
|
||||
import path from 'path'
|
||||
import { resolvePages, type SiteConfig } from '../config'
|
||||
|
||||
export const dynamicRouteRE = /\[(\w+?)\]/g
|
||||
|
||||
interface UserRouteConfig {
|
||||
params: Record<string, string>
|
||||
content?: string
|
||||
}
|
||||
|
||||
interface RouteModule {
|
||||
path: string
|
||||
config: {
|
||||
paths:
|
||||
| UserRouteConfig[]
|
||||
| (() => UserRouteConfig[] | Promise<UserRouteConfig[]>)
|
||||
}
|
||||
dependencies: string[]
|
||||
}
|
||||
|
||||
export type ResolvedRouteConfig = UserRouteConfig & {
|
||||
/**
|
||||
* the raw route, e.g. foo/[bar].md
|
||||
*/
|
||||
route: string
|
||||
/**
|
||||
* the actual path with params resolved, e.g. foo/1.md
|
||||
*/
|
||||
path: string
|
||||
}
|
||||
|
||||
export const dynamicRoutesPlugin = async (
|
||||
config: SiteConfig
|
||||
): Promise<Plugin> => {
|
||||
let server: ViteDevServer
|
||||
|
||||
return {
|
||||
name: 'vitepress:dynamic-routes',
|
||||
|
||||
configureServer(_server) {
|
||||
server = _server
|
||||
},
|
||||
|
||||
resolveId(id) {
|
||||
if (!id.endsWith('.md')) return
|
||||
const normalizedId = id.startsWith(config.root)
|
||||
? normalizePath(path.relative(config.root, id))
|
||||
: id.replace(/^\//, '')
|
||||
const matched = config.dynamicRoutes.routes.find(
|
||||
(r) => r.path === normalizedId
|
||||
)
|
||||
if (matched) {
|
||||
return normalizedId
|
||||
}
|
||||
},
|
||||
|
||||
load(id) {
|
||||
const matched = config.dynamicRoutes.routes.find((r) => r.path === id)
|
||||
if (matched) {
|
||||
const { route, params, content } = matched
|
||||
const routeFile = normalizePath(path.resolve(config.root, route))
|
||||
config.dynamicRoutes.fileToModulesMap[routeFile].add(id)
|
||||
|
||||
let baseContent = fs.readFileSync(routeFile, 'utf-8')
|
||||
|
||||
// inject raw content
|
||||
// this is intended for integration with CMS
|
||||
// we use a speical injection syntax so the content is rendered as
|
||||
// static local content instead of included as runtime data.
|
||||
if (content) {
|
||||
baseContent = baseContent.replace(/<!--\s*@content\s*-->/, content)
|
||||
}
|
||||
|
||||
// params are injected with special markers and extracted as part of
|
||||
// __pageData in ../markdownTovue.ts
|
||||
return `__VP_PARAMS_START${JSON.stringify(
|
||||
params
|
||||
)}__VP_PARAMS_END__${baseContent}`
|
||||
}
|
||||
},
|
||||
|
||||
async handleHotUpdate(ctx) {
|
||||
const mods = config.dynamicRoutes.fileToModulesMap[ctx.file]
|
||||
if (mods) {
|
||||
// path loader module or deps updated, reset loaded routes
|
||||
if (!/\.md$/.test(ctx.file)) {
|
||||
Object.assign(
|
||||
config,
|
||||
await resolvePages(config.srcDir, config.userConfig)
|
||||
)
|
||||
}
|
||||
for (const id of mods) {
|
||||
ctx.modules.push(server.moduleGraph.getModuleById(id)!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveDynamicRoutes(
|
||||
routes: string[]
|
||||
): Promise<SiteConfig['dynamicRoutes']> {
|
||||
const pendingResolveRoutes: Promise<ResolvedRouteConfig[]>[] = []
|
||||
const routeFileToModulesMap: Record<string, Set<string>> = {}
|
||||
|
||||
for (const route of routes) {
|
||||
// locate corresponding route paths file
|
||||
const jsPathsFile = route.replace(/\.md$/, '.paths.js')
|
||||
let pathsFile = jsPathsFile
|
||||
if (!fs.existsSync(jsPathsFile)) {
|
||||
pathsFile = route.replace(/\.md$/, '.paths.ts')
|
||||
if (!fs.existsSync(pathsFile)) {
|
||||
console.warn(
|
||||
c.yellow(
|
||||
`missing paths file for dynamic route ${route}: ` +
|
||||
`a corresponding ${jsPathsFile} or ${pathsFile} is needed.`
|
||||
)
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// load the paths loader module
|
||||
let mod: RouteModule
|
||||
try {
|
||||
mod = (await loadConfigFromFile(
|
||||
{} as any,
|
||||
path.resolve(pathsFile)
|
||||
)) as RouteModule
|
||||
} catch (e) {
|
||||
console.warn(`invalid paths file export in ${pathsFile}.`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (mod) {
|
||||
// this array represents the virtual modules affected by this route
|
||||
const matchedModuleIds = (routeFileToModulesMap[
|
||||
normalizePath(path.resolve(route))
|
||||
] = new Set())
|
||||
|
||||
// each dependency (including the loader module itself) also point to the
|
||||
// same array
|
||||
for (const dep of mod.dependencies) {
|
||||
routeFileToModulesMap[normalizePath(path.resolve(dep))] =
|
||||
matchedModuleIds
|
||||
}
|
||||
|
||||
const resolveRoute = async (): Promise<ResolvedRouteConfig[]> => {
|
||||
const loader = mod.config.paths
|
||||
const paths = await (typeof loader === 'function' ? loader() : loader)
|
||||
return paths.map((userConfig) => {
|
||||
return {
|
||||
path: route.replace(
|
||||
dynamicRouteRE,
|
||||
(_, key) => userConfig.params[key]
|
||||
),
|
||||
route,
|
||||
...userConfig
|
||||
}
|
||||
})
|
||||
}
|
||||
pendingResolveRoutes.push(resolveRoute())
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
routes: (await Promise.all(pendingResolveRoutes)).flat(),
|
||||
fileToModulesMap: routeFileToModulesMap
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import { compile, match } from 'path-to-regexp'
|
||||
import type { SiteConfig, UserConfig } from '../config'
|
||||
|
||||
export function resolveRewrites(
|
||||
pages: string[],
|
||||
userRewrites: UserConfig['rewrites']
|
||||
) {
|
||||
const rewriteEntries = Object.entries(userRewrites || {})
|
||||
const rewrites = rewriteEntries.length
|
||||
? Object.fromEntries(
|
||||
pages
|
||||
.map((src) => {
|
||||
for (const [from, to] of rewriteEntries) {
|
||||
const dest = rewrite(src, from, to)
|
||||
if (dest) return [src, dest]
|
||||
}
|
||||
})
|
||||
.filter((e) => e != null) as [string, string][]
|
||||
)
|
||||
: {}
|
||||
return {
|
||||
map: rewrites,
|
||||
inv: Object.fromEntries(Object.entries(rewrites).map((a) => a.reverse()))
|
||||
}
|
||||
}
|
||||
|
||||
function rewrite(src: string, from: string, to: string) {
|
||||
const urlMatch = match(from)
|
||||
const res = urlMatch(src)
|
||||
if (!res) return false
|
||||
const toPath = compile(to)
|
||||
return toPath(res.params)
|
||||
}
|
||||
|
||||
export const rewritesPlugin = (config: SiteConfig): Plugin => {
|
||||
return {
|
||||
name: 'vitepress:rewrites',
|
||||
configureServer(server) {
|
||||
// dev rewrite
|
||||
server.middlewares.use((req, _res, next) => {
|
||||
if (req.url) {
|
||||
const page = req.url
|
||||
.replace(/[?#].*$/, '')
|
||||
.slice(config.site.base.length)
|
||||
if (config.rewrites.inv[page]) {
|
||||
req.url = req.url.replace(page, config.rewrites.inv[page]!)
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue