Merge branch 'main' into fix-layout-shifts

pull/1844/head
Em Zhan 4 months ago committed by GitHub
commit d8cd3dc4e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -37,7 +37,7 @@ body:
attributes:
label: System Info
description: Output of `npx envinfo --system --npmPackages vitepress --binaries --browsers`
render: sh
render: Text
placeholder: System, Binaries, Browsers
validations:
required: true

@ -30,6 +30,8 @@ After cloning the repo, run:
```sh
# install the dependencies of the project
$ pnpm install
# setup git hooks
$ pnpm simple-git-hooks
```
### Setup VitePress Dev Environment

@ -0,0 +1,16 @@
### Description
<!-- Please insert your description here and provide info about the "what" this PR is solving. -->
### Linked Issues
<!-- e.g. fixes #123 -->
### Additional Context
<!-- Is there anything you would like the reviewers to focus on? -->
---
> [!TIP]
> The author of this PR can publish a _preview release_ by commenting `/publish` below.

@ -0,0 +1,18 @@
name: Add continuous release label
on:
issue_comment:
types: [created]
permissions:
pull-requests: write
jobs:
label:
if: ${{ github.event.issue.pull_request && (github.event.comment.user.id == github.event.issue.user.id || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') && startsWith(github.event.comment.body, '/publish') }}
runs-on: ubuntu-latest
steps:
- run: gh issue edit ${{ github.event.issue.number }} --add-label cr-tracked --repo ${{ github.repository }}
env:
GITHUB_TOKEN: ${{ secrets.CR_PAT }}

@ -0,0 +1,49 @@
name: CR
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
on:
pull_request:
branches: [main]
types: [opened, synchronize, labeled, ready_for_review]
paths-ignore:
- '.github/**'
- '!.github/workflows/cr.yml'
- '__tests__/**'
- 'art/**'
- 'docs/**'
- '*.md'
push:
branches: [main]
paths-ignore:
- '.github/**'
- '!.github/workflows/cr.yml'
- '__tests__/**'
- 'art/**'
- 'docs/**'
- '*.md'
tags-ignore:
- '*'
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.number }}
cancel-in-progress: true
jobs:
release:
if: ${{ ((github.event_name == 'pull_request' && !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'cr-tracked') && !contains(github.event.pull_request.labels.*.name, 'spam') && !contains(github.event.pull_request.labels.*.name, 'invalid')) || (github.event_name == 'push' && !startsWith(github.event.head_commit.message, 'release:'))) && github.repository == 'vuejs/vitepress' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm build
- run: npx pkg-pr-new publish --compact --no-template --pnpm

@ -2,22 +2,25 @@ name: Lock Threads
on:
schedule:
- cron: 0 0 * * *
- cron: 38 4 * * *
workflow_dispatch:
permissions:
issues: write
pull-requests: write
discussions: write
concurrency:
group: lock
jobs:
action:
if: github.repository == 'vuejs/vitepress'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: 7
pr-inactive-days: 7
exclude-any-issue-labels: 'keep-open'
exclude-any-pr-labels: 'keep-open'

@ -7,11 +7,12 @@ on:
jobs:
release:
if: github.repository == 'vuejs/vitepress'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Create Release for Tag
id: release_tag

@ -0,0 +1,18 @@
name: Close stale issues and PRs
on:
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 30
days-before-close: -1
stale-issue-label: stale
stale-pr-label: stale
# action has a bug that keeps removing the stale label from stale PRs
# it's thinking that the PR is updated when the stale label is added
remove-pr-stale-when-updated: false
operations-per-run: 1000

@ -1,45 +1,47 @@
name: Test
env:
NODE_OPTIONS: --max-old-space-size=6144
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
VITEST_SEGFAULT_RETRY: 3
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node_version: [16, 18, 20]
os: [ubuntu-latest]
node_version: [20, 22, latest]
include:
- os: windows-latest
node_version: 22
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
- name: Set node version to ${{ matrix.node_version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}
cache: pnpm
- name: Install deps
run: pnpm install
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- name: Install Playwright
run: pnpm playwright install chromium

1
.gitignore vendored

@ -15,3 +15,4 @@ examples-temp
node_modules
pnpm-global
TODOs.md
*.timestamp-*.mjs

@ -1,2 +1,2 @@
shell-emulator=true
resolution-mode=highest
auto-install-peers=false

@ -5,3 +5,5 @@ pnpm-lock.yaml
cache
template
temp
!CHANGELOG.md
.temp

File diff suppressed because it is too large Load Diff

@ -1,14 +1,13 @@
# VitePress (beta) 📝💨
# VitePress 📝💨
[![Test](https://github.com/vuejs/vitepress/workflows/Test/badge.svg)](https://github.com/vuejs/vitepress/actions)
[![test](https://github.com/vuejs/vitepress/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/vuejs/vitepress/actions/workflows/test.yml)
[![npm](https://img.shields.io/npm/v/vitepress)](https://www.npmjs.com/package/vitepress)
[![nightly releases](https://img.shields.io/badge/nightly-releases-orange)](https://nightly.akryum.dev/vuejs/vitepress)
[![chat](https://img.shields.io/badge/chat-discord-blue?logo=discord)](https://chat.vuejs.org)
---
VitePress is [VuePress](https://vuepress.vuejs.org)' spiritual successor, built on top of [vite](https://github.com/vitejs/vite).
Currently, it is in the `beta` stage. It is already suitable for out-of-the-box documentation use. There still might be issues, however, we do not plan to introduce any breaking changes from here on until the stable release.
VitePress is a Vue-powered static site generator and a spiritual successor to [VuePress](https://vuepress.vuejs.org), built on top of [Vite](https://github.com/vitejs/vite).
## Documentation

@ -1,5 +1,73 @@
import { defineConfig, type DefaultTheme } from 'vitepress'
const nav: DefaultTheme.Config['nav'] = [
{
text: 'Home',
link: '/'
},
{
text: 'API Reference',
items: [
{
text: 'Example',
link: '/home.html'
},
{
component: 'ApiPreference',
props: {
options: ['JavaScript', 'TypeScript', 'Flow'],
defaultOption: 'TypeScript'
}
},
{
component: 'ApiPreference',
props: {
options: ['Options', 'Composition'],
defaultOption: 'Composition'
}
}
]
},
{
component: 'NavVersion',
props: {
versions: [
{
text: 'v1.x',
link: '/'
},
{
text: 'v0.x',
link: '/v0.x/'
}
]
}
},
{
text: 'Nested',
items: [
{
text: 'Level 1 - 1',
items: [
{
text: 'Level 2 - 1',
link: '/nested/level1-1/level2-1'
}
]
},
{
text: 'Level 1 - 2',
items: [
{
text: 'Level 2 - 2',
link: '/nested/level1-2/level2-2'
}
]
}
]
}
]
const sidebar: DefaultTheme.Config['sidebar'] = {
'/': [
{
@ -86,7 +154,32 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
export default defineConfig({
title: 'Example',
description: 'An example app using VitePress.',
markdown: {
image: {
lazyLoading: true
}
},
themeConfig: {
sidebar
nav,
sidebar,
search: {
provider: 'local',
options: {
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.search === false) return ''
if (env.relativePath.startsWith('local-search/excluded')) return ''
return html
}
}
}
},
vite: {
server: {
watch: {
usePolling: true,
interval: 100
}
}
}
})

@ -0,0 +1,83 @@
<script setup lang="ts">
import { useLocalStorage } from '@vueuse/core'
const props = defineProps<{
options: string[]
defaultOption: string
screenMenu?: boolean
}>()
// reactivity isn't needed for props here
const key = removeSpaces(`api-preference-${props.options.join('-')}`)
const name = key + (props.screenMenu ? '-screen-menu' : '')
const selected = useLocalStorage(key, () => props.defaultOption)
const optionsWithKeys = props.options.map((option) => ({
key: name + '-' + removeSpaces(option),
value: option
}))
function removeSpaces(str: string) {
return str.replace(/\s/g, '_')
}
</script>
<template>
<div class="VPApiPreference" :class="{ 'screen-menu': screenMenu }">
<template v-for="option in optionsWithKeys" :key="option">
<input
type="radio"
:id="option.key"
:name
:value="option.value"
v-model="selected"
/>
<label :for="option.key">{{ option.value }}</label>
</template>
</div>
</template>
<style scoped>
.VPApiPreference {
display: flex;
margin: 12px 0;
border: 1px solid var(--vp-c-border);
border-radius: 6px;
font-size: 14px;
color: var(--vp-c-text-1);
}
.VPApiPreference:first-child {
margin-top: 0;
}
.VPApiPreference:last-child {
margin-bottom: 0;
}
.VPApiPreference.screen-menu {
margin: 12px 0 0 12px;
}
.VPApiPreference input[type='radio'] {
pointer-events: none;
position: fixed;
opacity: 0;
}
.VPApiPreference label {
flex: 1;
margin: 2px;
padding: 4px 12px;
cursor: pointer;
border-radius: 4px;
text-align: center;
}
.VPApiPreference input[type='radio']:checked + label {
background-color: var(--vp-c-default-soft);
color: var(--vp-c-brand-1);
}
</style>

@ -0,0 +1,50 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vitepress'
import VPNavBarMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavBarMenuGroup.vue'
import VPNavScreenMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavScreenMenuGroup.vue'
const props = defineProps<{
versions: { text: string; link: string }[]
screenMenu?: boolean
}>()
const route = useRoute()
const sortedVersions = computed(() => {
return [...props.versions].sort(
(a, b) => b.link.split('/').length - a.link.split('/').length
)
})
const currentVersion = computed(() => {
return (
sortedVersions.value.find((version) => route.path.startsWith(version.link))
?.text || 'Versions'
)
})
</script>
<template>
<VPNavBarMenuGroup
v-if="!screenMenu"
:item="{ text: currentVersion, items: versions }"
class="VPNavVersion"
/>
<VPNavScreenMenuGroup
v-else
:text="currentVersion"
:items="versions"
class="VPNavVersion"
/>
</template>
<style scoped>
.VPNavVersion :deep(button .text) {
color: var(--vp-c-text-1) !important;
}
.VPNavVersion:hover :deep(button .text) {
color: var(--vp-c-text-2) !important;
}
</style>

@ -0,0 +1,12 @@
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import ApiPreference from './components/ApiPreference.vue'
import NavVersion from './components/NavVersion.vue'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('ApiPreference', ApiPreference)
app.component('NavVersion', NavVersion)
}
} satisfies Theme

@ -4,8 +4,8 @@ exports[`render correct content > main content 1`] = `
[
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
"Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of \\"de Finibus Bonorum et Malorum\\" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, \\"Lorem ipsum dolor sit amet..\\", comes from a line in section 1.10.32.",
"The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from \\"de Finibus Bonorum et Malorum\\" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.",
"Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.",
"The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.",
"It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).",
"There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.",
]

@ -0,0 +1,16 @@
import fs from 'node:fs'
import { defineLoader } from 'vitepress'
type Data = Record<string, boolean>[]
export declare const data: Data
export default defineLoader({
watch: ['./data/*'],
async load(files: string[]): Promise<Data> {
const data: Data = []
for (const file of files.sort().filter((file) => file.endsWith('.json'))) {
data.push(JSON.parse(fs.readFileSync(file, 'utf-8')))
}
return data
}
})

@ -1,20 +0,0 @@
import fs from 'fs'
import { defineLoader } from 'vitepress'
type Data = Record<string, boolean>[]
export declare const data: Data
export default defineLoader({
watch: ['./data/*'],
async load(files: string[]): Promise<Data> {
const foo = fs.readFileSync(
files.find((f) => f.endsWith('foo.json'))!,
'utf-8'
)
const bar = fs.readFileSync(
files.find((f) => f.endsWith('bar.json'))!,
'utf-8'
)
return [JSON.parse(foo), JSON.parse(bar)]
}
})

@ -1,7 +1,7 @@
# Static Data
<script setup lang="ts">
import { data } from './basic.data.js'
import { data } from './basic.data.mjs'
import { data as contentData } from './contentLoader.data.js'
</script>

@ -1,3 +1,6 @@
import fs from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
describe('static data file support in vite 3', () => {
beforeAll(async () => {
await goto('/data-loading/data')
@ -7,36 +10,99 @@ describe('static data file support in vite 3', () => {
expect(await page.textContent('pre#basic')).toMatchInlineSnapshot(`
"[
{
\\"foo\\": true
"a": true
},
{
\\"bar\\": true
"b": true
}
]"
`)
expect(await page.textContent('pre#content')).toMatchInlineSnapshot(`
"[
{
\\"src\\": \\"---\\\\ntitle: bar\\\\n---\\\\n\\\\nHello\\\\n\\\\n---\\\\n\\\\nworld\\\\n\\",
\\"html\\": \\"<p>Hello</p>\\\\n<hr>\\\\n<p>world</p>\\\\n\\",
\\"frontmatter\\": {
\\"title\\": \\"bar\\"
"src": "---\\ntitle: bar\\n---\\n\\nHello\\n\\n---\\n\\nworld\\n",
"html": "<p>Hello</p>\\n<hr>\\n<p>world</p>\\n",
"frontmatter": {
"title": "bar"
},
\\"excerpt\\": \\"<p>Hello</p>\\\\n\\",
\\"url\\": \\"/data-loading/content/bar.html\\",
\\"transformed\\": true
"excerpt": "<p>Hello</p>\\n",
"url": "/data-loading/content/bar.html",
"transformed": true
},
{
\\"src\\": \\"---\\\\ntitle: foo\\\\n---\\\\n\\\\nHello\\\\n\\\\n---\\\\n\\\\nworld\\\\n\\",
\\"html\\": \\"<p>Hello</p>\\\\n<hr>\\\\n<p>world</p>\\\\n\\",
\\"frontmatter\\": {
\\"title\\": \\"foo\\"
"src": "---\\ntitle: foo\\n---\\n\\nHello\\n\\n---\\n\\nworld\\n",
"html": "<p>Hello</p>\\n<hr>\\n<p>world</p>\\n",
"frontmatter": {
"title": "foo"
},
\\"excerpt\\": \\"<p>Hello</p>\\\\n\\",
\\"url\\": \\"/data-loading/content/foo.html\\",
\\"transformed\\": true
"excerpt": "<p>Hello</p>\\n",
"url": "/data-loading/content/foo.html",
"transformed": true
}
]"
`)
})
test.runIf(!process.env.VITE_TEST_BUILD)('hmr works', async () => {
const a = fileURLToPath(new URL('./data/a.json', import.meta.url))
const b = fileURLToPath(new URL('./data/b.json', import.meta.url))
try {
await fs.writeFile(a, JSON.stringify({ a: false }, null, 2) + '\n')
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: false }, { b: true }], null, 2),
undefined,
{ timeout: 3000 }
)
} finally {
await fs.writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n')
}
let err = true
try {
await fs.unlink(b)
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: true }], null, 2),
undefined,
{ timeout: 3000 }
)
err = false
} finally {
if (err) {
await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
}
}
try {
await fs.writeFile(b, JSON.stringify({ b: false }, null, 2) + '\n')
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: true }, { b: false }], null, 2),
undefined,
{ timeout: 3000 }
)
} finally {
await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
}
})
/*
MODIFY a.json with { a: false }
this should trigger a hmr update and the content should be updated to [{ a: false }, { b: true }]
reset a.json
DELETE b.json
this should trigger a hmr update and the content should be updated to [{ a: true }]
reset b.json if failed
CREATE b.json with { b: false }
this should trigger a hmr update and the content should be updated to [{ a: true }, { b: false }]
reset b.json
*/
})

@ -1,8 +1,14 @@
export default {
async paths() {
return [
{ params: { id: 'foo' }, content: `# Foo` },
{ params: { id: 'bar' }, content: `# Bar` }
]
import { defineRoutes } from 'vitepress'
import paths from './paths'
export default defineRoutes({
async paths(watchedFiles: string[]) {
// console.log('watchedFiles', watchedFiles)
return paths
},
watch: ['**/data-loading/**/*.json'],
async transformPageData(pageData) {
// console.log('transformPageData', pageData.filePath)
pageData.title += ' - transformed'
}
}
})

@ -0,0 +1,4 @@
export default [
{ params: { id: 'foo' }, content: `# Foo` },
{ params: { id: 'bar' }, content: `# Bar` }
]

@ -1,7 +1,7 @@
---
title: Multiple Levels Outline
editLink: true
outline: 'deep'
outline: deep
---
# h1 - 1

@ -0,0 +1 @@
# Local search config excluded

@ -0,0 +1,5 @@
---
search: false
---
# Local search frontmatter excluded

@ -0,0 +1 @@
# Local search included

@ -0,0 +1,31 @@
describe('local search', () => {
beforeEach(async () => {
await goto('/')
})
test('exclude content from search results', async () => {
await page.locator('#local-search button').click()
const input = await page.waitForSelector('input#localsearch-input')
await input.type('local')
await page.waitForSelector('ul#localsearch-list', { state: 'visible' })
const searchResults = page.locator('#localsearch-list')
expect(await searchResults.locator('li[role=option]').count()).toBe(1)
expect(
await searchResults.filter({ hasText: 'Local search included' }).count()
).toBe(1)
expect(
await searchResults.filter({ hasText: 'Local search excluded' }).count()
).toBe(0)
expect(
await searchResults
.filter({ hasText: 'Local search frontmatter excluded' })
.count()
).toBe(0)
})
})

@ -1,7 +1,11 @@
# Foo
This is before region
<!-- #region snippet -->
## Region
this is region
This is a region
<!-- #endregion snippet -->
This is after region

@ -0,0 +1,27 @@
# header 1
header 1 content
## header 1.1
header 1.1 content
### header 1.1.1
header 1.1.1 content
### header 1.1.2
header 1.1.2 content
## header 1.2
header 1.2 content
### header 1.2.1
header 1.2.1 content
### header 1.2.2
header 1.2.2 content

@ -180,3 +180,43 @@ export default config
## Markdown At File Inclusion
<!--@include: @/markdown-extensions/bar.md-->
## Markdown Nested File Inclusion
<!--@include: ./nested-include.md-->
## Markdown File Inclusion with Range
<!--@include: ./foo.md{6,8}-->
## Markdown File Inclusion with Range without Start
<!--@include: ./foo.md{,8}-->
## Markdown File Inclusion with Range without End
<!--@include: ./foo.md{6,}-->
## Markdown At File Region Snippet
<!--@include: ./region-include.md#snippet-->
## Markdown At File Range Region Snippet
<!--@include: ./region-include.md#range-region{3,4}-->
## Markdown At File Range Region Snippet without start
<!--@include: ./region-include.md#range-region{,2}-->
## Markdown At File Range Region Snippet without end
<!--@include: ./region-include.md#range-region{5,}-->
## Markdown File Inclusion with Header
<!--@include: ./header-include.md#header-1-1-->
## Image Lazy Loading
![vitepress logo](/vitepress.png)

@ -5,6 +5,8 @@ const getClassList = async (locator: Locator) => {
return className?.split(' ').filter(Boolean) ?? []
}
const trim = (str?: string | null) => str?.replace(/\u200B/g, '').trim()
beforeEach(async () => {
await goto('/markdown-extensions/')
})
@ -62,8 +64,61 @@ describe('Emoji', () => {
describe('Table of Contents', () => {
test('render toc', async () => {
const items = page.locator('#table-of-contents + nav ul li')
const count = await items.count()
expect(count).toBe(24)
expect(
await items.evaluateAll((elements) =>
elements.map((el) => el.childNodes[0].textContent)
)
).toMatchInlineSnapshot(`
[
"Links",
"Internal Links",
"External Links",
"GitHub-Style Tables",
"Emoji",
"Table of Contents",
"Custom Containers",
"Default Title",
"Custom Title",
"Line Highlighting in Code Blocks",
"Single Line",
"Multiple single lines, ranges",
"Comment Highlight",
"Line Numbers",
"Import Code Snippets",
"Basic Code Snippet",
"Specify Region",
"With Other Features",
"Code Groups",
"Basic Code Group",
"With Other Features",
"Markdown File Inclusion",
"Region",
"Markdown At File Inclusion",
"Markdown Nested File Inclusion",
"Region",
"After Foo",
"Sub sub",
"Sub sub sub",
"Markdown File Inclusion with Range",
"Region",
"Markdown File Inclusion with Range without Start",
"Region",
"Markdown File Inclusion with Range without End",
"Region",
"Markdown At File Region Snippet",
"Region Snippet",
"Markdown At File Range Region Snippet",
"Range Region Line 2",
"Markdown At File Range Region Snippet without start",
"Range Region Line 1",
"Markdown At File Range Region Snippet without end",
"Range Region Line 3",
"Markdown File Inclusion with Header",
"header 1.1.1",
"header 1.1.2",
"Image Lazy Loading",
]
`)
})
})
@ -161,7 +216,7 @@ describe('Line Numbers', () => {
describe('Import Code Snippets', () => {
test('basic', async () => {
const lines = page.locator('#basic-code-snippet + div code > span')
expect(await lines.count()).toBe(7)
expect(await lines.count()).toBe(11)
})
test('specify region', async () => {
@ -214,7 +269,7 @@ describe('Code Groups', () => {
// blocks
const blocks = div.locator('.blocks > div')
expect(await blocks.nth(0).locator('code > span').count()).toBe(7)
expect(await blocks.nth(0).locator('code > span').count()).toBe(11)
expect(await getClassList(blocks.nth(1))).toContain('line-numbers-mode')
expect(await getClassList(blocks.nth(1))).toContain('language-ts')
expect(await blocks.nth(1).locator('code > span').count()).toBe(3)
@ -229,8 +284,79 @@ describe('Markdown File Inclusion', () => {
const h1 = page.locator('#markdown-file-inclusion + h1')
expect(await h1.getAttribute('id')).toBe('foo')
})
test('render markdown using @', async () => {
const h1 = page.locator('#markdown-at-file-inclusion + h1')
expect(await h1.getAttribute('id')).toBe('bar')
})
test('render markdown using nested inclusion', async () => {
const h1 = page.locator('#markdown-nested-file-inclusion + h1')
expect(await h1.getAttribute('id')).toBe('foo-1')
})
test('render markdown using nested inclusion inside sub folder', async () => {
const h1 = page.locator('#after-foo + h1')
expect(await h1.getAttribute('id')).toBe('inside-sub-folder')
const h2 = page.locator('#after-foo + h1 + h2')
expect(await h2.getAttribute('id')).toBe('sub-sub')
const h3 = page.locator('#after-foo + h1 + h2 + h3')
expect(await h3.getAttribute('id')).toBe('sub-sub-sub')
})
test('support selecting range', async () => {
const h2 = page.locator('#markdown-file-inclusion-with-range + h2')
expect(trim(await h2.textContent())).toBe('Region')
const p = page.locator('#markdown-file-inclusion-with-range + h2 + p')
expect(trim(await p.textContent())).toBe('This is a region')
})
test('support selecting range without specifying start', async () => {
const p = page.locator(
'#markdown-file-inclusion-with-range-without-start ~ p'
)
expect(trim(await p.nth(0).textContent())).toBe('This is before region')
expect(trim(await p.nth(1).textContent())).toBe('This is a region')
})
test('support selecting range without specifying end', async () => {
const p = page.locator(
'#markdown-file-inclusion-with-range-without-end ~ p'
)
expect(trim(await p.nth(0).textContent())).toBe('This is a region')
expect(trim(await p.nth(1).textContent())).toBe('This is after region')
})
test('support markdown region snippet', async () => {
const h2 = page.locator('#markdown-at-file-region-snippet + h2')
expect(await h2.getAttribute('id')).toBe('region-snippet')
const line = page.locator('#markdown-at-file-range-region-snippet + h2')
expect(await line.getAttribute('id')).toBe('range-region-line-2')
const lineWithoutStart = page.locator(
'#markdown-at-file-range-region-snippet-without-start + h2'
)
expect(await lineWithoutStart.getAttribute('id')).toBe(
'range-region-line-1'
)
const lineWithoutEnd = page.locator(
'#markdown-at-file-range-region-snippet-without-end + h2'
)
expect(await lineWithoutEnd.getAttribute('id')).toBe('range-region-line-3')
})
test('ignore frontmatter if range is not specified', async () => {
const p = page.locator('.vp-doc')
expect(await p.textContent()).not.toContain('title')
})
})
describe('Image Lazy Loading', () => {
test('render loading="lazy" in the <img> tag', async () => {
const img = page.locator('#image-lazy-loading + p img')
expect(await img.getAttribute('loading')).toBe('lazy')
})
})

@ -0,0 +1,9 @@
---
title: Nested Include
---
<!--@include: ./foo.md-->
### After Foo
<!--@include: ./subfolder/inside-subfolder.md-->

@ -0,0 +1,13 @@
<!-- #region range-region -->
## Range Region Line 1
## Range Region Line 2
## Range Region Line 3
<!-- #endregion range-region -->
<!-- #region snippet -->
## Region Snippet
<!-- #endregion snippet -->

@ -0,0 +1,3 @@
# Inside sub folder
<!--@include: ./subsub/subsub.md-->

@ -0,0 +1,3 @@
## Sub sub
<!--@include: ./subsubsub/subsubsub.md-->

@ -1,6 +1,14 @@
{
"name": "tests-e2e",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run",
"watch": "DEBUG=1 vitest",
"site:dev": "vitepress",
"site:build": "vitepress build",
"site:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "workspace:*"
}

@ -5,7 +5,7 @@ const timeout = 60_000
export default defineConfig({
test: {
setupFiles: ['vitestSetup.ts'],
globalSetup: ['__tests__/e2e/vitestGlobalSetup.ts'],
globalSetup: ['vitestGlobalSetup.ts'],
testTimeout: timeout,
hookTimeout: timeout,
teardownTimeout: timeout,

@ -1,13 +1,13 @@
import getPort from 'get-port'
import type { Server } from 'node:net'
import { chromium, type BrowserServer } from 'playwright-chromium'
import { build, createServer, serve } from 'vitepress'
import type { ViteDevServer } from 'vite'
import type { Server } from 'net'
import { build, createServer, serve } from 'vitepress'
let browserServer: BrowserServer
let server: ViteDevServer | Server
const root = '__tests__/e2e'
const root = '.'
export async function setup() {
browserServer = await chromium.launchServer({

@ -1,98 +1,72 @@
import { chromium, type Browser, type Page } from 'playwright-chromium'
import { fileURLToPath } from 'url'
import path from 'path'
import fs from 'fs-extra'
import {
scaffold,
build,
createServer,
serve,
ScaffoldThemeType,
type ScaffoldOptions
} from 'vitepress'
import type { ViteDevServer } from 'vite'
import type { Server } from 'net'
import getPort from 'get-port'
import { nanoid } from 'nanoid'
import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
import { chromium } from 'playwright-chromium'
import { createServer, scaffold, ScaffoldThemeType } from 'vitepress'
let browser: Browser
let page: Page
const tempDir = fileURLToPath(new URL('./.temp', import.meta.url))
const getTempRoot = () => path.join(tempDir, nanoid())
beforeAll(async () => {
browser = await chromium.connect(process.env['WS_ENDPOINT']!)
page = await browser.newPage()
const browser = await chromium.launch({
headless: !process.env.DEBUG,
args: process.env.CI
? ['--no-sandbox', '--disable-setuid-sandbox']
: undefined
})
const page = await browser.newPage()
const themes = [
ScaffoldThemeType.Default,
ScaffoldThemeType.DefaultCustom,
ScaffoldThemeType.Custom
]
const usingTs = [false, true]
const variations = themes.flatMap((theme) =>
usingTs.map(
(useTs) => [`${theme}${useTs ? ' + ts' : ''}`, { theme, useTs }] as const
)
)
afterAll(async () => {
await page.close()
await browser.close()
await fs.remove(tempDir)
})
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'temp')
async function testVariation(options: ScaffoldOptions) {
fs.removeSync(root)
scaffold({
...options,
root
})
test.each(variations)('init %s', async (_, { theme, useTs }) => {
const root = getTempRoot()
await fs.remove(root)
scaffold({ root, theme, useTs, injectNpmScripts: false })
let server: ViteDevServer | Server
const port = await getPort()
const server = await createServer(root, { port })
await server.listen()
async function goto(path: string) {
await page.goto(`http://localhost:${port}${path}`)
await page.waitForSelector('#app div')
}
if (process.env['VITE_TEST_BUILD']) {
await build(root)
server = (await serve({ root, port })).server
} else {
server = await createServer(root, { port })
await server!.listen()
}
try {
await goto('/')
expect(await page.textContent('h1')).toMatch('My Awesome Project')
await page.click('a[href="/markdown-examples.html"]')
await page.waitForSelector('pre code')
await page.waitForFunction('document.querySelector("pre code")')
expect(await page.textContent('h1')).toMatch('Markdown Extension Examples')
await goto('/')
expect(await page.textContent('h1')).toMatch('My Awesome Project')
await page.click('a[href="/api-examples.html"]')
await page.waitForSelector('pre code')
await page.waitForFunction('document.querySelector("pre code")')
expect(await page.textContent('h1')).toMatch('Runtime API Examples')
// teardown
} finally {
fs.removeSync(root)
if ('ws' in server) {
await server.close()
} else {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
}
}
}
const themes = [
ScaffoldThemeType.Default,
ScaffoldThemeType.DefaultCustom,
ScaffoldThemeType.Custom
]
const usingTs = [false, true]
for (const theme of themes) {
for (const useTs of usingTs) {
test(`${theme}${useTs ? ` + TypeScript` : ``}`, () =>
testVariation({
root: '.',
theme,
useTs,
injectNpmScripts: false
}))
}
}
})

@ -1,6 +1,11 @@
{
"name": "tests-init",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run",
"watch": "DEBUG=1 vitest"
},
"devDependencies": {
"vitepress": "workspace:*"
}

@ -1,20 +1,9 @@
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vitest/config'
const dir = dirname(fileURLToPath(import.meta.url))
const timeout = 60_000
export default defineConfig({
resolve: {
alias: {
node: resolve(dir, '../../src/node')
}
},
test: {
watchExclude: ['**/node_modules/**', '**/temp/**'],
globalSetup: ['__tests__/init/vitestGlobalSetup.ts'],
testTimeout: timeout,
hookTimeout: timeout,
teardownTimeout: timeout,

@ -1,17 +0,0 @@
import { chromium, type BrowserServer } from 'playwright-chromium'
let browserServer: BrowserServer
export async function setup() {
browserServer = await chromium.launchServer({
headless: !process.env.DEBUG,
args: process.env.CI
? ['--no-sandbox', '--disable-setuid-sandbox']
: undefined
})
process.env['WS_ENDPOINT'] = browserServer.wsEndpoint()
}
export async function teardown() {
await browserServer.close()
}

@ -1,5 +1,11 @@
import { resolveHeaders } from 'client/theme-default/composables/outline'
const element = {
classList: {
contains: () => false
}
} as unknown as HTMLHeadElement
describe('client/theme-default/composables/outline', () => {
describe('resolveHeader', () => {
test('levels range', () => {
@ -9,12 +15,14 @@ describe('client/theme-default/composables/outline', () => {
{
level: 2,
title: 'h2 - 1',
link: '#h2-1'
link: '#h2-1',
element
},
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
link: '#h3-1',
element
}
],
[2, 3]
@ -28,9 +36,12 @@ describe('client/theme-default/composables/outline', () => {
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
link: '#h3-1',
children: [],
element
}
]
],
element
}
])
})
@ -42,12 +53,14 @@ describe('client/theme-default/composables/outline', () => {
{
level: 2,
title: 'h2 - 1',
link: '#h2-1'
link: '#h2-1',
element
},
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
link: '#h3-1',
element
}
],
2
@ -56,7 +69,9 @@ describe('client/theme-default/composables/outline', () => {
{
level: 2,
title: 'h2 - 1',
link: '#h2-1'
link: '#h2-1',
children: [],
element
}
])
})
@ -68,42 +83,50 @@ describe('client/theme-default/composables/outline', () => {
{
level: 2,
title: 'h2 - 1',
link: '#h2-1'
link: '#h2-1',
element
},
{
level: 3,
title: 'h3 - 1',
link: '#h3-1'
link: '#h3-1',
element
},
{
level: 4,
title: 'h4 - 1',
link: '#h4-1'
link: '#h4-1',
element
},
{
level: 3,
title: 'h3 - 2',
link: '#h3-2'
link: '#h3-2',
element
},
{
level: 4,
title: 'h4 - 2',
link: '#h4-2'
link: '#h4-2',
element
},
{
level: 2,
title: 'h2 - 2',
link: '#h2-2'
link: '#h2-2',
element
},
{
level: 3,
title: 'h3 - 3',
link: '#h3-3'
link: '#h3-3',
element
},
{
level: 4,
title: 'h4 - 3',
link: '#h4-3'
link: '#h4-3',
element
}
],
'deep'
@ -122,9 +145,12 @@ describe('client/theme-default/composables/outline', () => {
{
level: 4,
title: 'h4 - 1',
link: '#h4-1'
link: '#h4-1',
children: [],
element
}
]
],
element
},
{
level: 3,
@ -134,11 +160,15 @@ describe('client/theme-default/composables/outline', () => {
{
level: 4,
title: 'h4 - 2',
link: '#h4-2'
link: '#h4-2',
children: [],
element
}
]
],
element
}
]
],
element
},
{
level: 2,
@ -153,11 +183,15 @@ describe('client/theme-default/composables/outline', () => {
{
level: 4,
title: 'h4 - 3',
link: '#h4-3'
link: '#h4-3',
children: [],
element
}
]
],
element
}
]
],
element
}
])
})

@ -28,15 +28,19 @@ describe('client/theme-default/support/sidebar', () => {
}
test('gets `/` sidebar', () => {
expect(getSidebar(normalSidebar, '/')).toBe(root)
expect(getSidebar(normalSidebar, '/')).toStrictEqual(root)
})
test('gets `/multi-sidebar/` sidebar', () => {
expect(getSidebar(normalSidebar, '/multi-sidebar/')).toBe(another)
expect(getSidebar(normalSidebar, '/multi-sidebar/')).toStrictEqual(
another
)
})
test('gets `/` sidebar again', () => {
expect(getSidebar(normalSidebar, '/some-entry.html')).toBe(root)
expect(getSidebar(normalSidebar, '/some-entry.html')).toStrictEqual(
root
)
})
})
@ -47,15 +51,19 @@ describe('client/theme-default/support/sidebar', () => {
}
test('gets `/` sidebar', () => {
expect(getSidebar(reversedSidebar, '/')).toBe(root)
expect(getSidebar(reversedSidebar, '/')).toStrictEqual(root)
})
test('gets `/multi-sidebar/` sidebar', () => {
expect(getSidebar(reversedSidebar, '/multi-sidebar/')).toBe(another)
expect(getSidebar(reversedSidebar, '/multi-sidebar/')).toStrictEqual(
another
)
})
test('gets `/` sidebar again', () => {
expect(getSidebar(reversedSidebar, '/some-entry.html')).toBe(root)
expect(getSidebar(reversedSidebar, '/some-entry.html')).toStrictEqual(
root
)
})
})
@ -74,19 +82,25 @@ describe('client/theme-default/support/sidebar', () => {
}
test('gets `/` sidebar', () => {
expect(getSidebar(nestedSidebar, '/')).toBe(root)
expect(getSidebar(nestedSidebar, '/')).toStrictEqual(root)
})
test('gets `/multi-sidebar/` sidebar', () => {
expect(getSidebar(nestedSidebar, '/multi-sidebar/')).toBe(another)
expect(getSidebar(nestedSidebar, '/multi-sidebar/')).toStrictEqual(
another
)
})
test('gets `/multi-sidebar/nested/` sidebar', () => {
expect(getSidebar(nestedSidebar, '/multi-sidebar/nested/')).toBe(nested)
expect(
getSidebar(nestedSidebar, '/multi-sidebar/nested/')
).toStrictEqual(nested)
})
test('gets `/` sidebar again', () => {
expect(getSidebar(nestedSidebar, '/some-entry.html')).toBe(root)
expect(getSidebar(nestedSidebar, '/some-entry.html')).toStrictEqual(
root
)
})
})
})

@ -1,4 +1,45 @@
import { dedent } from 'node/markdown/plugins/snippet'
import {
dedent,
findRegion,
rawPathToToken
} from 'node/markdown/plugins/snippet'
import { expect } from 'vitest'
const removeEmptyKeys = <T extends Record<string, unknown>>(obj: T) => {
return Object.fromEntries(
Object.entries(obj).filter(([, value]) => value !== '')
) as T
}
/* prettier-ignore */
const rawPathTokenMap: [string, Partial<{ filepath: string, extension: string, title: string, region: string, lines: string, lang: string }>][] = [
['/path/to/file.extension', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension' }],
['./path/to/file.extension', { filepath: './path/to/file.extension', extension: 'extension', title: 'file.extension' }],
['/path to/file.extension', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension' }],
['./path to/file.extension', { filepath: './path to/file.extension', extension: 'extension', title: 'file.extension' }],
['/path.to/file.extension', { filepath: '/path.to/file.extension', extension: 'extension', title: 'file.extension' }],
['./path.to/file.extension', { filepath: './path.to/file.extension', extension: 'extension', title: 'file.extension' }],
['/path .to/file.extension', { filepath: '/path .to/file.extension', extension: 'extension', title: 'file.extension' }],
['./path .to/file.extension', { filepath: './path .to/file.extension', extension: 'extension', title: 'file.extension' }],
['/path/to/file', { filepath: '/path/to/file', title: 'file' }],
['./path/to/file', { filepath: './path/to/file', title: 'file' }],
['/path to/file', { filepath: '/path to/file', title: 'file' }],
['./path to/file', { filepath: './path to/file', title: 'file' }],
['/path.to/file', { filepath: '/path.to/file', title: 'file' }],
['./path.to/file', { filepath: './path.to/file', title: 'file' }],
['/path .to/file', { filepath: '/path .to/file', title: 'file' }],
['./path .to/file', { filepath: './path .to/file', title: 'file' }],
['/path/to/file.extension#region', { filepath: '/path/to/file.extension', extension: 'extension', title: 'file.extension', region: '#region' }],
['./path/to/file.extension {c#}', { filepath: './path/to/file.extension', extension: 'extension', title: 'file.extension', lang: 'c#' }],
['/path to/file.extension {1,2,4-6}', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension', lines: '1,2,4-6' }],
['/path to/file.extension {1,2,4-6 c#}', { filepath: '/path to/file.extension', extension: 'extension', title: 'file.extension', lines: '1,2,4-6', lang: 'c#' }],
['/path.to/file.extension [title]', { filepath: '/path.to/file.extension', extension: 'extension', title: 'title' }],
['./path.to/file.extension#region {c#}', { filepath: './path.to/file.extension', extension: 'extension', title: 'file.extension', region: '#region', lang: 'c#' }],
['/path/to/file#region {1,2,4-6}', { filepath: '/path/to/file', title: 'file', region: '#region', lines: '1,2,4-6' }],
['./path/to/file#region {1,2,4-6 c#}', { filepath: './path/to/file', title: 'file', region: '#region', lines: '1,2,4-6', lang: 'c#' }],
['/path to/file {1,2,4-6 c#} [title]', { filepath: '/path to/file', title: 'title', lines: '1,2,4-6', lang: 'c#' }],
['./path to/file#region {1,2,4-6 c#} [title]', { filepath: './path to/file', title: 'title', region: '#region', lines: '1,2,4-6', lang: 'c#' }],
]
describe('node/markdown/plugins/snippet', () => {
describe('dedent', () => {
@ -14,7 +55,7 @@ describe('node/markdown/plugins/snippet', () => {
)
).toMatchInlineSnapshot(`
"fn main() {
println!(\\"Hello\\");
println!("Hello");
}"
`)
})
@ -57,4 +98,229 @@ describe('node/markdown/plugins/snippet', () => {
`)
})
})
describe('rawPathToToken', () => {
test.each(rawPathTokenMap)('%s', (rawPath, token) => {
expect(removeEmptyKeys(rawPathToToken(rawPath))).toEqual(token)
})
})
describe('findRegion', () => {
it('returns null when no region markers are present', () => {
const lines = ['function foo() {', ' console.log("hello");', '}']
expect(findRegion(lines, 'foo')).toBeNull()
})
it('ignores non-matching region names', () => {
const lines = [
'// #region regionA',
'some code here',
'// #endregion regionA'
]
expect(findRegion(lines, 'regionC')).toBeNull()
})
it('returns null if a region start marker exists without a matching end marker', () => {
const lines = [
'// #region missingEnd',
'console.log("inside region");',
'console.log("still inside");'
]
expect(findRegion(lines, 'missingEnd')).toBeNull()
})
it('returns null if an end marker exists without a preceding start marker', () => {
const lines = [
'// #endregion ghostRegion',
'console.log("stray end marker");'
]
expect(findRegion(lines, 'ghostRegion')).toBeNull()
})
it('detects C#/JavaScript style region markers with matching tags', () => {
const lines = [
'Console.WriteLine("Before region");',
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion hello',
'Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");'
)
}
})
it('detects region markers even when the end marker omits the region name', () => {
const lines = [
'Console.WriteLine("Before region");',
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion',
'Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");'
)
}
})
it('handles indented region markers correctly', () => {
const lines = [
' Console.WriteLine("Before region");',
' #region hello',
' Console.WriteLine("Hello, World!");',
' #endregion hello',
' Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Hello, World!");'
)
}
})
it('detects TypeScript style region markers', () => {
const lines = [
'let regexp: RegExp[] = [];',
'// #region foo',
'let start = -1;',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'let start = -1;'
)
}
})
it('detects CSS style region markers', () => {
const lines = [
'.body-content {',
'/* #region foo */',
' padding-left: 15px;',
'/* #endregion foo */',
' padding-right: 15px;',
'}'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' padding-left: 15px;'
)
}
})
it('detects HTML style region markers', () => {
const lines = [
'<div>Some content</div>',
'<!-- #region foo -->',
' <h1>Hello world</h1>',
'<!-- #endregion foo -->',
'<div>Other content</div>'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' <h1>Hello world</h1>'
)
}
})
it('detects Visual Basic style region markers (with case-insensitive "End")', () => {
const lines = [
'Console.WriteLine("VB")',
'#Region VBRegion',
' Console.WriteLine("Inside region")',
'#End Region VBRegion',
'Console.WriteLine("Done")'
]
const result = findRegion(lines, 'VBRegion')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Inside region")'
)
}
})
it('detects Bat style region markers', () => {
const lines = ['::#region foo', 'echo off', '::#endregion foo']
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'echo off'
)
}
})
it('detects C/C++ style region markers using #pragma', () => {
const lines = [
'#pragma region foo',
'int a = 1;',
'#pragma endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'int a = 1;'
)
}
})
it('returns the first complete region when multiple regions exist', () => {
const lines = [
'// #region foo',
'first region content',
'// #endregion foo',
'// #region foo',
'second region content',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'first region content'
)
}
})
it('handles nested regions with different names properly', () => {
const lines = [
'// #region foo',
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
const extracted = lines.slice(result.start, result.end).join('\n')
const expected = [
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar'
].join('\n')
expect(extracted).toBe(expected)
}
})
})
})

@ -0,0 +1,72 @@
import { ModuleGraph } from 'node/utils/moduleGraph'
describe('node/utils/moduleGraph', () => {
let graph: ModuleGraph
beforeEach(() => {
graph = new ModuleGraph()
})
it('should correctly delete a module and its dependents', () => {
graph.add('A', ['B', 'C'])
graph.add('B', ['D'])
graph.add('C', [])
graph.add('D', [])
expect(graph.delete('D')).toEqual(new Set(['D', 'B', 'A']))
})
it('should handle shared dependencies correctly', () => {
graph.add('A', ['B', 'C'])
graph.add('B', ['D'])
graph.add('C', ['D']) // Shared dependency
graph.add('D', [])
expect(graph.delete('D')).toEqual(new Set(['A', 'B', 'C', 'D']))
})
it('merges dependencies correctly', () => {
// Add module A with dependency B
graph.add('A', ['B'])
// Merge new dependency C into module A (B should remain)
graph.add('A', ['C'])
// Deleting B should remove A as well, since A depends on B.
expect(graph.delete('B')).toEqual(new Set(['B', 'A']))
})
it('handles cycles gracefully', () => {
// Create a cycle: A -> B, B -> C, C -> A.
graph.add('A', ['B'])
graph.add('B', ['C'])
graph.add('C', ['A'])
// Deleting any module in the cycle should delete all modules in the cycle.
expect(graph.delete('A')).toEqual(new Set(['A', 'B', 'C']))
})
it('cleans up dependencies when deletion', () => {
// Setup A -> B relationship.
graph.add('A', ['B'])
graph.add('B', [])
// Deleting B should remove both B and A from the graph.
expect(graph.delete('B')).toEqual(new Set(['B', 'A']))
// After deletion, add modules again.
graph.add('C', [])
graph.add('A', ['C']) // Now A depends only on C.
expect(graph.delete('C')).toEqual(new Set(['C', 'A']))
})
it('handles independent modules', () => {
// Modules with no dependencies.
graph.add('X', [])
graph.add('Y', [])
// Deletion of one should only remove that module.
expect(graph.delete('X')).toEqual(new Set(['X']))
expect(graph.delete('Y')).toEqual(new Set(['Y']))
})
})

@ -1,17 +1,26 @@
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import vue from '@vitejs/plugin-vue'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
const dir = dirname(fileURLToPath(import.meta.url))
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@siteData': resolve(dir, './shims.ts'),
client: resolve(dir, '../../src/client'),
node: resolve(dir, '../../src/node'),
vitepress: resolve(dir, '../../src/client')
alias: [
{ find: '@siteData', replacement: resolve(dir, './shims.ts') },
{ find: 'client', replacement: resolve(dir, '../../src/client') },
{ find: 'node', replacement: resolve(dir, '../../src/node') },
{
find: /^vitepress$/,
replacement: resolve(dir, '../../src/client/index.js')
},
{
find: /^vitepress\/theme$/,
replacement: resolve(dir, '../../src/client/theme-default/index.js')
}
]
},
test: {
globals: true

@ -0,0 +1,21 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.03628 7.87818C4.75336 5.83955 6.15592 3.95466 8.16899 3.66815L33.6838 0.0367403C35.6969 -0.24977 37.5581 1.1706 37.841 3.20923L42.9637 40.1218C43.2466 42.1604 41.8441 44.0453 39.831 44.3319L14.3162 47.9633C12.3031 48.2498 10.4419 46.8294 10.159 44.7908L5.03628 7.87818Z" fill="url(#paint0_linear_1287_1214)"/>
<path d="M6.85877 7.6188C6.71731 6.59948 7.41859 5.65703 8.42512 5.51378L33.9399 1.88237C34.9465 1.73911 35.8771 2.4493 36.0186 3.46861L41.1412 40.3812C41.2827 41.4005 40.5814 42.343 39.5749 42.4862L14.0601 46.1176C13.0535 46.2609 12.1229 45.5507 11.9814 44.5314L6.85877 7.6188Z" fill="white"/>
<path d="M33.1857 14.9195L25.8505 34.1576C25.6991 34.5547 25.1763 34.63 24.9177 34.2919L12.3343 17.8339C12.0526 17.4655 12.3217 16.9339 12.7806 16.9524L22.9053 17.3607C22.9698 17.3633 23.0344 17.3541 23.0956 17.3337L32.5088 14.1992C32.9431 14.0546 33.3503 14.4878 33.1857 14.9195Z" fill="url(#paint1_linear_1287_1214)"/>
<path d="M27.0251 12.5756L19.9352 15.0427C19.8187 15.0832 19.7444 15.1986 19.7546 15.3231L20.3916 23.063C20.4066 23.2453 20.5904 23.3628 20.7588 23.2977L22.7226 22.5392C22.9064 22.4682 23.1021 22.6138 23.0905 22.8128L22.9102 25.8903C22.8982 26.0974 23.1093 26.2436 23.295 26.1567L24.4948 25.5953C24.6808 25.5084 24.892 25.6549 24.8795 25.8624L24.5855 30.6979C24.5671 31.0004 24.9759 31.1067 25.1013 30.8321L25.185 30.6487L29.4298 17.8014C29.5008 17.5863 29.2968 17.3809 29.0847 17.454L27.0519 18.1547C26.8609 18.2205 26.6675 18.0586 26.6954 17.8561L27.3823 12.8739C27.4103 12.6712 27.2163 12.5091 27.0251 12.5756Z" fill="url(#paint2_linear_1287_1214)"/>
<defs>
<linearGradient id="paint0_linear_1287_1214" x1="6.48163" y1="1.9759" x2="39.05" y2="48.2064" gradientUnits="userSpaceOnUse">
<stop stop-color="#49C7FF"/>
<stop offset="1" stop-color="#BD36FF"/>
</linearGradient>
<linearGradient id="paint1_linear_1287_1214" x1="11.8848" y1="16.4266" x2="26.7246" y2="31.4177" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint2_linear_1287_1214" x1="21.8138" y1="13.7046" x2="26.2464" y2="28.8069" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1,41 @@
<svg viewBox="0 0 160 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1288_1252)">
<path d="M0.12699 43.8989C-0.863275 37.1255 4.04575 30.863 11.0915 29.911L113.152 16.1221C120.197 15.1702 126.712 19.8893 127.702 26.6627L147.873 164.635C148.863 171.409 143.954 177.671 136.908 178.623L39.1006 191.837C29.7063 193.107 21.0203 186.814 19.7001 177.784L0.12699 43.8989Z" fill="url(#paint0_linear_1288_1252)"/>
<path d="M10.1376 30.5728C9.06487 22.8787 14.3827 15.7647 22.0152 14.6834L124.678 0.138664C132.31 -0.942678 139.367 4.41806 140.44 12.1122L159.862 151.427C160.935 159.121 155.617 166.235 147.985 167.317L45.3224 181.861C37.6899 182.943 30.6329 177.582 29.5602 169.888L10.1376 30.5728Z" fill="url(#paint1_linear_1288_1252)"/>
<path d="M19.0979 33.7417C18.3346 28.2753 22.1183 23.221 27.5489 22.4527L121.946 9.09852C127.376 8.33025 132.397 12.1389 133.161 17.6054L151.402 148.258C152.165 153.725 148.382 158.779 142.951 159.547L48.5544 172.901C43.1238 173.67 38.1027 169.861 37.3394 164.395L19.0979 33.7417Z" fill="white"/>
<path d="M118.649 103.244L56.8226 111.376C54.3836 111.697 52.6843 113.807 53.027 116.09L57.0613 142.955C57.4041 145.237 59.6591 146.827 62.0981 146.507L123.924 138.375C126.363 138.054 128.063 135.943 127.72 133.661L123.686 106.796C123.343 104.513 121.088 102.923 118.649 103.244Z" stroke="url(#paint2_linear_1288_1252)" stroke-width="4"/>
<path d="M64.6477 140.356L61.2921 117.143L68.5741 116.148L77.0898 123.687L83.1382 114.157L90.4202 113.163L93.7764 136.375L86.4939 137.37L84.5693 124.057L78.5209 133.587L70.0052 126.047L71.9298 139.36L64.6477 140.356ZM110.16 134.136L97.609 124.365L104.891 123.369L103.163 111.421L110.446 110.427L112.173 122.374L119.455 121.379L110.16 134.136Z" fill="url(#paint3_linear_1288_1252)"/>
<path d="M106.461 37.6734L85.0897 89.1815C84.6484 90.245 83.1468 90.4541 82.4101 89.555L46.5625 45.7851C45.76 44.8053 46.5412 43.3811 47.858 43.4233L76.9122 44.3556C77.0976 44.3617 77.2828 44.3363 77.459 44.2807L104.529 35.76C105.778 35.3669 106.94 36.518 106.461 37.6734Z" fill="url(#paint4_linear_1288_1252)"/>
<path d="M88.8117 31.4996L68.4122 38.2029C68.0769 38.313 67.8619 38.6223 67.8893 38.955L69.591 59.6271C69.6311 60.114 70.1569 60.4249 70.6418 60.2485L76.2932 58.1908C76.8221 57.9984 77.3816 58.3843 77.3448 58.9164L76.7769 67.1425C76.7388 67.6961 77.3423 68.0835 77.8772 67.8484L81.3316 66.3297C81.8672 66.0943 82.4715 66.4829 82.4319 67.0371L81.508 79.9629C81.4502 80.7715 82.6224 81.0495 82.987 80.3136L83.2305 79.8222L95.63 45.426C95.8377 44.85 95.2552 44.3043 94.6453 44.503L88.7963 46.4074C88.2468 46.5862 87.6946 46.1564 87.778 45.615L89.8327 32.2912C89.9162 31.7488 89.3612 31.3188 88.8117 31.4996Z" fill="url(#paint5_linear_1288_1252)"/>
</g>
<defs>
<linearGradient id="paint0_linear_1288_1252" x1="0.447599" y1="16.413" x2="148.448" y2="191.541" gradientUnits="userSpaceOnUse">
<stop stop-color="#508AB7"/>
<stop offset="1" stop-color="#2D1342"/>
</linearGradient>
<linearGradient id="paint1_linear_1288_1252" x1="11.1858" y1="19.1536" x2="158.875" y2="162.67" gradientUnits="userSpaceOnUse">
<stop stop-color="#49C7FF"/>
<stop offset="1" stop-color="#BD36FF"/>
</linearGradient>
<linearGradient id="paint2_linear_1288_1252" x1="56.6798" y1="114.377" x2="122.209" y2="145.088" gradientUnits="userSpaceOnUse">
<stop stop-color="#49C8FF"/>
<stop offset="1" stop-color="#BE35FF"/>
</linearGradient>
<linearGradient id="paint3_linear_1288_1252" x1="61.7655" y1="117.538" x2="121.328" y2="133.876" gradientUnits="userSpaceOnUse">
<stop stop-color="#4BC7FF"/>
<stop offset="1" stop-color="#B93DFF"/>
</linearGradient>
<linearGradient id="paint4_linear_1288_1252" x1="45.2957" y1="42.0327" x2="84.5852" y2="84.667" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint5_linear_1288_1252" x1="73.8285" y1="34.5977" x2="85.1242" y2="75.2134" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
<clipPath id="clip0_1288_1252">
<rect width="160" height="192" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

@ -1,2 +1,16 @@
#!/usr/bin/env node
// @ts-check
import module from 'node:module'
// https://github.com/vitejs/vite/blob/6c8a5a27e645a182f5b03a4ed6aa726eab85993f/packages/vite/bin/vite.js#L48-L63
try {
module.enableCompileCache?.()
setTimeout(() => {
try {
module.flushCompileCache?.()
} catch {}
}, 10 * 1000).unref()
} catch {}
import('../dist/node/cli.js')

@ -0,0 +1,8 @@
{
"plugins": {
"postcss-rtlcss": {
"ltrPrefix": ":where([dir=\"ltr\"])",
"rtlPrefix": ":where([dir=\"rtl\"])"
}
}
}

@ -1,227 +1,148 @@
import { createRequire } from 'module'
import { defineConfig } from 'vitepress'
import {
defineConfig,
resolveSiteDataByRoute,
type HeadConfig
} from 'vitepress'
import {
groupIconMdPlugin,
groupIconVitePlugin,
localIconLoader
} from 'vitepress-plugin-group-icons'
import llmstxt from 'vitepress-plugin-llms'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
const prod = !!process.env.NETLIFY
export default defineConfig({
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
rewrites: {
'en/:rest*': ':rest*'
},
lastUpdated: true,
cleanUrls: true,
metaChunk: true,
head: [
['meta', { name: 'theme-color', content: '#3c8772' }],
[
'script',
markdown: {
math: true,
codeTransformers: [
// We use `[!!code` in demo to prevent transformation, here we revert it back.
{
src: 'https://cdn.usefathom.com/script.js',
'data-site': 'AZBRSFGG',
'data-spa': 'auto',
defer: ''
postprocess(code) {
return code.replace(/\[\!\!code/g, '[!code')
}
}
]
],
themeConfig: {
nav: nav(),
sidebar: {
'/guide/': sidebarGuide(),
'/reference/': sidebarReference()
config(md) {
// TODO: remove when https://github.com/vuejs/vitepress/issues/4431 is fixed
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
const { localeIndex = 'root' } = env
const codeCopyButtonTitle = (() => {
switch (localeIndex) {
case 'es':
return 'Copiar código'
case 'fa':
return 'کپی کد'
case 'ko':
return '코드 복사'
case 'pt':
return 'Copiar código'
case 'ru':
return 'Скопировать код'
case 'zh':
return '复制代码'
default:
return 'Copy code'
}
})()
return fence(tokens, idx, options, env, self).replace(
'<button title="Copy Code" class="copy"></button>',
`<button title="${codeCopyButtonTitle}" class="copy"></button>`
)
}
md.use(groupIconMdPlugin)
}
},
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path',
text: 'Edit this page on GitHub'
sitemap: {
hostname: 'https://vitepress.dev',
transformItems(items) {
return items.filter((item) => !item.url.includes('migration'))
}
},
/* prettier-ignore */
head: [
['link', { rel: 'icon', type: 'image/svg+xml', href: '/vitepress-logo-mini.svg' }],
['link', { rel: 'icon', type: 'image/png', href: '/vitepress-logo-mini.png' }],
['meta', { name: 'theme-color', content: '#5f67ee' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'VitePress' }],
['meta', { property: 'og:image', content: 'https://vitepress.dev/vitepress-og.jpg' }],
['meta', { property: 'og:url', content: 'https://vitepress.dev/' }],
['script', { src: 'https://cdn.usefathom.com/script.js', 'data-site': 'AZBRSFGG', 'data-spa': 'auto', defer: '' }]
],
themeConfig: {
logo: { src: '/vitepress-logo-mini.svg', width: 24, height: 24 },
socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
],
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2019-present Evan You'
},
search: {
provider: 'algolia',
options: {
appId: '8J64VVRP8K',
apiKey: 'a18e2f4cc5665f6602c5631fd868adfd',
apiKey: '52f578a92b88ad6abde815aae2b0ad7c',
indexName: 'vitepress'
}
},
carbonAds: {
code: 'CEBDT27Y',
placement: 'vuejsorg'
}
}
})
function nav() {
return [
{ text: 'Guide', link: '/guide/what-is-vitepress', activeMatch: '/guide/' },
{
text: 'Reference',
link: '/reference/site-config',
activeMatch: '/reference/'
},
{
text: pkg.version,
items: [
{
text: 'Changelog',
link: 'https://github.com/vuejs/vitepress/blob/main/CHANGELOG.md'
carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' }
},
{
text: 'Contributing',
link: 'https://github.com/vuejs/vitepress/blob/main/.github/contributing.md'
}
]
}
]
}
function sidebarGuide() {
return [
{
text: 'Introduction',
collapsed: false,
items: [
{ text: 'What is VitePress?', link: '/guide/what-is-vitepress' },
{ text: 'Getting Started', link: '/guide/getting-started' },
{ text: 'Routing', link: '/guide/routing' },
{ text: 'Deploy', link: '/guide/deploy' }
]
},
{
text: 'Writing',
collapsed: false,
items: [
{ text: 'Markdown Extensions', link: '/guide/markdown' },
{ text: 'Asset Handling', link: '/guide/asset-handling' },
{ text: 'Frontmatter', link: '/guide/frontmatter' },
{ text: 'Using Vue in Markdown', link: '/guide/using-vue' },
{ text: 'Internationalization', link: '/guide/i18n' }
]
locales: {
root: { label: 'English' },
zh: { label: '简体中文' },
pt: { label: 'Português' },
ru: { label: 'Русский' },
es: { label: 'Español' },
ko: { label: '한국어' },
fa: { label: 'فارسی' }
},
{
text: 'Customization',
collapsed: false,
items: [
{ text: 'Using a Custom Theme', link: '/guide/custom-theme' },
{
text: 'Extending the Default Theme',
link: '/guide/extending-default-theme'
},
{ text: 'Build-Time Data Loading', link: '/guide/data-loading' },
{ text: 'SSR Compatibility', link: '/guide/ssr-compat' },
{ text: 'Connecting to a CMS', link: '/guide/cms' }
]
},
{
text: 'Experimental',
collapsed: false,
items: [
{
text: 'MPA Mode',
link: '/guide/mpa-mode'
vite: {
plugins: [
groupIconVitePlugin({
customIcon: {
vitepress: localIconLoader(
import.meta.url,
'../public/vitepress-logo-mini.svg'
),
firebase: 'logos:firebase'
}
}),
prod &&
llmstxt({
workDir: 'en',
ignoreFiles: ['index.md']
})
]
},
// {
// text: 'Migrations',
// collapsed: false,
// items: [
// {
// text: 'Migration from VuePress',
// link: '/guide/migration-from-vuepress'
// },
// {
// text: 'Migration from VitePress 0.x',
// link: '/guide/migration-from-vitepress-0'
// }
// ]
// },
{
text: 'Config & API Reference',
link: '/reference/site-config'
}
]
}
function sidebarReference() {
return [
{
text: 'Reference',
items: [
{ text: 'Site Config', link: '/reference/site-config' },
{ text: 'Frontmatter Config', link: '/reference/frontmatter-config' },
{ text: 'Runtime API', link: '/reference/runtime-api' },
{ text: 'CLI', link: '/reference/cli' },
{
text: 'Default Theme',
items: [
{
text: 'Overview',
link: '/reference/default-theme-config'
},
{
text: 'Nav',
link: '/reference/default-theme-nav'
},
{
text: 'Sidebar',
link: '/reference/default-theme-sidebar'
},
{
text: 'Home Page',
link: '/reference/default-theme-home-page'
},
{
text: 'Footer',
link: '/reference/default-theme-footer'
},
{
text: 'Layout',
link: '/reference/default-theme-layout'
},
{
text: 'Badge',
link: '/reference/default-theme-badge'
},
{
text: 'Team Page',
link: '/reference/default-theme-team-page'
},
{
text: 'Prev / Next Links',
link: '/reference/default-theme-prev-next-links'
},
{
text: 'Edit Link',
link: '/reference/default-theme-edit-link'
},
{
text: 'Last Updated Timestamp',
link: '/reference/default-theme-last-updated'
},
{
text: 'Search',
link: '/reference/default-theme-search'
},
{
text: 'Carbon Ads',
link: '/reference/default-theme-carbon-ads'
}
]
}
]
transformPageData: prod
? (pageData, ctx) => {
const site = resolveSiteDataByRoute(
ctx.siteConfig.site,
pageData.relativePath
)
const title = `${pageData.title || site.title} | ${pageData.description || site.description}`
;((pageData.frontmatter.head ??= []) as HeadConfig[]).push(
['meta', { property: 'og:locale', content: site.lang }],
['meta', { property: 'og:title', content: title }]
)
}
]
}
: undefined
})

@ -0,0 +1,5 @@
import Theme from 'vitepress/theme'
import 'virtual:group-icons.css'
import './styles.css'
export default Theme

@ -0,0 +1,43 @@
:root:where(:lang(fa)) {
--vp-font-family-base:
'Vazirmatn', 'Inter', ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#bd34fe 30%,
#41d1ff
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#bd34fe 50%,
#47caff 50%
);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
.VPHero .VPImage {
filter: drop-shadow(-2px 4px 6px rgba(0, 0, 0, 0.2));
padding: 18px;
}
/* used in reference/default-theme-search */
img[src='/search.png'] {
width: 100%;
aspect-ratio: 1 / 1;
}

@ -0,0 +1,139 @@
import { createRequire } from 'module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'en-US',
description: 'Vite & Vue powered static site generator.',
themeConfig: {
nav: nav(),
sidebar: {
'/guide/': { base: '/guide/', items: sidebarGuide() },
'/reference/': { base: '/reference/', items: sidebarReference() }
},
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path',
text: 'Edit this page on GitHub'
},
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2019-present Evan You'
}
}
})
function nav(): DefaultTheme.NavItem[] {
return [
{
text: 'Guide',
link: '/guide/what-is-vitepress',
activeMatch: '/guide/'
},
{
text: 'Reference',
link: '/reference/site-config',
activeMatch: '/reference/'
},
{
text: pkg.version,
items: [
{
text: 'Changelog',
link: 'https://github.com/vuejs/vitepress/blob/main/CHANGELOG.md'
},
{
text: 'Contributing',
link: 'https://github.com/vuejs/vitepress/blob/main/.github/contributing.md'
}
]
}
]
}
function sidebarGuide(): DefaultTheme.SidebarItem[] {
return [
{
text: 'Introduction',
collapsed: false,
items: [
{ text: 'What is VitePress?', link: 'what-is-vitepress' },
{ text: 'Getting Started', link: 'getting-started' },
{ text: 'Routing', link: 'routing' },
{ text: 'Deploy', link: 'deploy' }
]
},
{
text: 'Writing',
collapsed: false,
items: [
{ text: 'Markdown Extensions', link: 'markdown' },
{ text: 'Asset Handling', link: 'asset-handling' },
{ text: 'Frontmatter', link: 'frontmatter' },
{ text: 'Using Vue in Markdown', link: 'using-vue' },
{ text: 'Internationalization', link: 'i18n' }
]
},
{
text: 'Customization',
collapsed: false,
items: [
{ text: 'Using a Custom Theme', link: 'custom-theme' },
{
text: 'Extending the Default Theme',
link: 'extending-default-theme'
},
{ text: 'Build-Time Data Loading', link: 'data-loading' },
{ text: 'SSR Compatibility', link: 'ssr-compat' },
{ text: 'Connecting to a CMS', link: 'cms' }
]
},
{
text: 'Experimental',
collapsed: false,
items: [
{ text: 'MPA Mode', link: 'mpa-mode' },
{ text: 'Sitemap Generation', link: 'sitemap-generation' }
]
},
{ text: 'Config & API Reference', base: '/reference/', link: 'site-config' }
]
}
function sidebarReference(): DefaultTheme.SidebarItem[] {
return [
{
text: 'Reference',
items: [
{ text: 'Site Config', link: 'site-config' },
{ text: 'Frontmatter Config', link: 'frontmatter-config' },
{ text: 'Runtime API', link: 'runtime-api' },
{ text: 'CLI', link: 'cli' },
{
text: 'Default Theme',
base: '/reference/default-theme-',
items: [
{ text: 'Overview', link: 'config' },
{ text: 'Nav', link: 'nav' },
{ text: 'Sidebar', link: 'sidebar' },
{ text: 'Home Page', link: 'home-page' },
{ text: 'Footer', link: 'footer' },
{ text: 'Layout', link: 'layout' },
{ text: 'Badge', link: 'badge' },
{ text: 'Team Page', link: 'team-page' },
{ text: 'Prev / Next Links', link: 'prev-next-links' },
{ text: 'Edit Link', link: 'edit-link' },
{ text: 'Last Updated Timestamp', link: 'last-updated' },
{ text: 'Search', link: 'search' },
{ text: 'Carbon Ads', link: 'carbon-ads' }
]
}
]
}
]
}

@ -12,6 +12,10 @@ You can reference static assets in your markdown files, your `*.vue` components
Common image, media, and font filetypes are detected and included as assets automatically.
::: tip Linked files are not treated as assets
PDFs or other documents referenced by links within markdown files are not automatically treated as assets. To make linked files accessible, you must manually place them within the [`public`](#the-public-directory) directory of your project.
:::
All referenced assets, including those using absolute paths, will be copied to the output directory with a hashed file name in the production build. Never-referenced assets will not be copied. Image assets smaller than 4kb will be base64 inlined - this can be configured via the [`vite`](../reference/site-config#vite) config option.
All **static** path references, including absolute paths, should be based on your working directory structure.
@ -26,11 +30,6 @@ Assets placed in `public` will be copied to the root of the output directory as-
Note that you should reference files placed in `public` using root absolute path - for example, `public/icon.png` should always be referenced in source code as `/icon.png`.
There is one exception to this: if you have an HTML page in `public` and link to it from the main site, the router will yield a 404 by default. To get around this, VitePress provides a `pathname://` protocol which allows you to link to another page in the same domain as if the link is external. Compare these two links:
- [/pure.html](/pure.html)
- <pathname:///pure.html>
## Base URL
If your site is deployed to a non-root URL, you will need to set the `base` option in `.vitepress/config.js`. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'` (it should always start and end with a slash).

@ -49,8 +49,7 @@ interface EnhanceAppContext {
The theme entry file should export the theme as its default export:
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
// You can directly import Vue files in the theme entry
// VitePress is pre-configured with @vitejs/plugin-vue.
@ -72,8 +71,7 @@ Inside your layout component, it works just like a normal Vite + Vue 3 applicati
The most basic layout component needs to contain a [`<Content />`](../reference/runtime-api#content) component:
```vue
<!-- .vitepress/theme/Layout.vue -->
```vue [.vitepress/theme/Layout.vue]
<template>
<h1>Custom Layout!</h1>
@ -172,8 +170,7 @@ If you wish to distribute the theme as an npm package, follow these steps:
To consume an external theme, import and re-export it from the custom theme entry:
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
import Theme from 'awesome-vitepress-theme'
export default Theme
@ -181,8 +178,7 @@ export default Theme
If the theme needs to be extended:
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
import Theme from 'awesome-vitepress-theme'
export default {
@ -195,8 +191,7 @@ export default {
If the theme requires special VitePress config, you will need to also extend it in your own config:
```ts
// .vitepress/theme/config.ts
```ts [.vitepress/config.ts]
import baseConfig from 'awesome-vitepress-theme/config'
export default {
@ -207,8 +202,7 @@ export default {
Finally, if the theme provides types for its theme config:
```ts
// .vitepress/theme/config.ts
```ts [.vitepress/config.ts]
import baseConfig from 'awesome-vitepress-theme/config'
import { defineConfigWithTheme } from 'vitepress'
import type { ThemeConfig } from 'awesome-vitepress-theme'

@ -8,12 +8,11 @@ Data loaders can be used to fetch remote data, or generate metadata based on loc
A data loader file must end with either `.data.js` or `.data.ts`. The file should provide a default export of an object with the `load()` method:
```js
// example.data.js
```js [example.data.js]
export default {
load() {
return {
data: 'hello'
hello: 'world'
}
}
}
@ -23,7 +22,7 @@ The loader module is evaluated only in Node.js, so you can import Node APIs and
You can then import data from this file in `.md` pages and `.vue` components using the `data` named export:
```html
```vue
<script setup>
import { data } from './example.data.js'
</script>
@ -35,7 +34,7 @@ Output:
```json
{
"data": "hello"
"hello": "world"
}
```
@ -70,7 +69,7 @@ export default {
// watchedFiles will be an array of absolute paths of the matched files.
// generate an array of blog post metadata that can be used to render
// a list in the theme layout
return watchedFiles.map(file => {
return watchedFiles.map((file) => {
return parse(fs.readFileSync(file, 'utf-8'), {
columns: true,
skip_empty_lines: true
@ -84,14 +83,13 @@ export default {
When building a content focused site, we often need to create an "archive" or "index" page: a page where we list all available entries in our content collection, for example blog posts or API pages. We **can** implement this directly with the data loader API, but since this is such a common use case, VitePress also provides a `createContentLoader` helper to simplify this:
```js
// posts.data.js
```js [posts.data.js]
import { createContentLoader } from 'vitepress'
export default createContentLoader('posts/*.md', /* options */)
```
The helper takes a glob pattern relative to [project root](./routing#project-root), and returns a `{ watch, load }` data loader object that can be used as the default export in a data loader file. It also implements caching based on file modified timestamps to improve dev performance.
The helper takes a glob pattern relative to the [source directory](./routing#source-directory), and returns a `{ watch, load }` data loader object that can be used as the default export in a data loader file. It also implements caching based on file modified timestamps to improve dev performance.
Note the loader only works with Markdown files - matched non-Markdown files will be skipped.
@ -99,7 +97,8 @@ The loaded data will be an array with the type of `ContentData[]`:
```ts
interface ContentData {
// mapped absolute URL for the page. e.g. /posts/hello.html
// mapped URL for the page. e.g. /posts/hello.html (does not include base)
// manually iterate or use custom `transform` to normalize the paths
url: string
// frontmatter data of the page
frontmatter: Record<string, any>
@ -134,8 +133,7 @@ import { data as posts } from './posts.data.js'
The default data may not suit all needs - you can opt-in to transform the data using options:
```js
// posts.data.js
```js [posts.data.js]
import { createContentLoader } from 'vitepress'
export default createContentLoader('posts/*.md', {
@ -147,7 +145,7 @@ export default createContentLoader('posts/*.md', {
// the final result is what will be shipped to the client.
return rawData.sort((a, b) => {
return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
}).map(page => {
}).map((page) => {
page.src // raw markdown source
page.html // rendered full page HTML
page.excerpt // rendered excerpt HTML (content above first `---`)
@ -159,10 +157,9 @@ export default createContentLoader('posts/*.md', {
Check out how it is used in the [Vue.js blog](https://github.com/vuejs/blog/blob/main/.vitepress/theme/posts.data.ts).
The `createContentLoader` API can also be used inside [build hooks](/reference/site-config#build-hooks):
The `createContentLoader` API can also be used inside [build hooks](../reference/site-config#build-hooks):
```js
// .vitepress/config.js
```js [.vitepress/config.js]
export default {
async buildEnd() {
const posts = await createContentLoader('posts/*.md').load()
@ -171,6 +168,48 @@ export default {
}
```
**Types**
```ts
interface ContentOptions<T = ContentData[]> {
/**
* Include src?
* @default false
*/
includeSrc?: boolean
/**
* Render src to HTML and include in data?
* @default false
*/
render?: boolean
/**
* If `boolean`, whether to parse and include excerpt? (rendered as HTML)
*
* If `function`, control how the excerpt is extracted from the content.
*
* If `string`, define a custom separator to be used for extracting the
* excerpt. Default separator is `---` if `excerpt` is `true`.
*
* @see https://github.com/jonschlinkert/gray-matter#optionsexcerpt
* @see https://github.com/jonschlinkert/gray-matter#optionsexcerpt_separator
*
* @default false
*/
excerpt?:
| boolean
| ((file: { data: { [key: string]: any }; content: string; excerpt?: string }, options?: any) => void)
| string
/**
* Transform the data. Note the data will be inlined as JSON in the client
* bundle if imported from components or markdown files.
*/
transform?: (data: ContentData[]) => T | Promise<T>
}
```
## Typed Data Loaders
When using TypeScript, you can type your loader and `data` export like so:
@ -187,7 +226,7 @@ export { data }
export default defineLoader({
// type checked loader options
glob: ['...'],
watch: ['...'],
async load(): Promise<Data> {
// ...
}

@ -0,0 +1,339 @@
---
outline: deep
---
# Deploy Your VitePress Site
The following guides are based on some shared assumptions:
- The VitePress site is inside the `docs` directory of your project.
- You are using the default build output directory (`.vitepress/dist`).
- VitePress is installed as a local dependency in your project, and you have set up the following scripts in your `package.json`:
```json [package.json]
{
"scripts": {
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
}
}
```
## Build and Test Locally
1. Run this command to build the docs:
```sh
$ npm run docs:build
```
2. Once built, preview it locally by running:
```sh
$ npm run docs:preview
```
The `preview` command will boot up a local static web server that will serve the output directory `.vitepress/dist` at `http://localhost:4173`. You can use this to make sure everything looks good before pushing to production.
3. You can configure the port of the server by passing `--port` as an argument.
```json
{
"scripts": {
"docs:preview": "vitepress preview docs --port 8080"
}
}
```
Now the `docs:preview` method will launch the server at `http://localhost:8080`.
## Setting a Public Base Path
By default, we assume the site is going to be deployed at the root path of a domain (`/`). If your site is going to be served at a sub-path, e.g. `https://mywebsite.com/blog/`, then you need to set the [`base`](../reference/site-config#base) option to `'/blog/'` in the VitePress config.
**Example:** If you're using Github (or GitLab) Pages and deploying to `user.github.io/repo/`, then set your `base` to `/repo/`.
## HTTP Cache Headers
If you have control over the HTTP headers on your production server, you can configure `cache-control` headers to achieve better performance on repeated visits.
The production build uses hashed file names for static assets (JavaScript, CSS and other imported assets not in `public`). If you inspect the production preview using your browser devtools' network tab, you will see files like `app.4f283b18.js`.
This `4f283b18` hash is generated from the content of this file. The same hashed URL is guaranteed to serve the same file content - if the contents change, the URLs change too. This means you can safely use the strongest cache headers for these files. All such files will be placed under `assets/` in the output directory, so you can configure the following header for them:
```
Cache-Control: max-age=31536000,immutable
```
::: details Example Netlify `_headers` file
```
/assets/*
cache-control: max-age=31536000
cache-control: immutable
```
Note: the `_headers` file should be placed in the [public directory](./asset-handling#the-public-directory) - in our case, `docs/public/_headers` - so that it is copied verbatim to the output directory.
[Netlify custom headers documentation](https://docs.netlify.com/routing/headers/)
:::
::: details Example Vercel config in `vercel.json`
```json
{
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=31536000, immutable"
}
]
}
]
}
```
Note: the `vercel.json` file should be placed at the root of your **repository**.
[Vercel documentation on headers config](https://vercel.com/docs/concepts/projects/project-configuration#headers)
:::
## Platform Guides
### Netlify / Vercel / Cloudflare Pages / AWS Amplify / Render
Set up a new project and change these settings using your dashboard:
- **Build Command:** `npm run docs:build`
- **Output Directory:** `docs/.vitepress/dist`
- **Node Version:** `20` (or above)
::: warning
Don't enable options like _Auto Minify_ for HTML code. It will remove comments from output which have meaning to Vue. You may see hydration mismatch errors if they get removed.
:::
### GitHub Pages
1. Create a file named `deploy.yml` inside `.github/workflows` directory of your project with some content like this:
```yaml [.github/workflows/deploy.yml]
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v3 # Uncomment this block if you're using pnpm
# with:
# version: 9 # Not needed if you've set "packageManager" in package.json
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # or pnpm install / yarn install / bun install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
```
::: warning
Make sure the `base` option in your VitePress is properly configured. See [Setting a Public Base Path](#setting-a-public-base-path) for more details.
:::
2. In your repository's settings under "Pages" menu item, select "GitHub Actions" in "Build and deployment > Source".
3. Push your changes to the `main` branch and wait for the GitHub Actions workflow to complete. You should see your site deployed to `https://<username>.github.io/[repository]/` or `https://<custom-domain>/` depending on your settings. Your site will automatically be deployed on every push to the `main` branch.
### GitLab Pages
1. Set `outDir` in VitePress config to `../public`. Configure `base` option to `'/<repository>/'` if you want to deploy to `https://<username>.gitlab.io/<repository>/`. You don't need `base` if you're deploying to custom domain, user or group pages, or have "Use unique domain" setting enabled in GitLab.
2. Create a file named `.gitlab-ci.yml` in the root of your project with the content below. This will build and deploy your site whenever you make changes to your content:
```yaml [.gitlab-ci.yml]
image: node:18
pages:
cache:
paths:
- node_modules/
script:
# - apk add git # Uncomment this if you're using small docker images like alpine and have lastUpdated enabled
- npm install
- npm run docs:build
artifacts:
paths:
- public
only:
- main
```
### Azure Static Web Apps
1. Follow the [official documentation](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration).
2. Set these values in your configuration file (and remove the ones you don't require, like `api_location`):
- **`app_location`**: `/`
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `npm run docs:build`
### Firebase
1. Create `firebase.json` and `.firebaserc` at the root of your project:
`firebase.json`:
```json [firebase.json]
{
"hosting": {
"public": "docs/.vitepress/dist",
"ignore": []
}
}
```
`.firebaserc`:
```json [.firebaserc]
{
"projects": {
"default": "<YOUR_FIREBASE_ID>"
}
}
```
2. After running `npm run docs:build`, run this command to deploy:
```sh
firebase deploy
```
### Surge
1. After running `npm run docs:build`, run this command to deploy:
```sh
npx surge docs/.vitepress/dist
```
### Heroku
1. Follow documentation and guide given in [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static).
2. Create a file called `static.json` in the root of your project with the below content:
```json [static.json]
{
"root": "docs/.vitepress/dist"
}
```
### Edgio
Refer [Creating and Deploying a VitePress App To Edgio](https://docs.edg.io/guides/vitepress).
### Kinsta Static Site Hosting
You can deploy your VitePress website on [Kinsta](https://kinsta.com/static-site-hosting/) by following these [instructions](https://kinsta.com/docs/vitepress-static-site-example/).
### Stormkit
You can deploy your VitePress project to [Stormkit](https://www.stormkit.io) by following these [instructions](https://stormkit.io/blog/how-to-deploy-vitepress).
### Nginx
Here is a example of an Nginx server block configuration. This setup includes gzip compression for common text-based assets, rules for serving your VitePress site's static files with proper caching headers as well as handling `cleanUrls: true`.
```nginx
server {
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
listen 80;
server_name _;
index index.html;
location / {
# content location
root /app;
# exact matches -> reverse clean urls -> folders -> not found
try_files $uri $uri.html $uri/ =404;
# non existent pages
error_page 404 /404.html;
# a folder without index.html raises 403 in this setup
error_page 403 /404.html;
# adjust caching headers
# files in the assets folder have hashes filenames
location ~* ^/assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}
```
This configuration assumes that your built VitePress site is located in the `/app` directory on your server. Adjust the `root` directive accordingly if your site's files are located elsewhere.
::: warning Do not default to index.html
The try_files resolution must not default to index.html like in other Vue applications. This would result in an invalid page state.
:::
Further information can be found in the [official nginx documentation](https://nginx.org/en/docs/), in these issues [#2837](https://github.com/vuejs/vitepress/discussions/2837), [#3235](https://github.com/vuejs/vitepress/issues/3235) as well as in this [blog post](https://blog.mehdi.cc/articles/vitepress-cleanurls-on-nginx-environment#readings) by Mehdi Merah.

@ -1,3 +1,7 @@
---
outline: deep
---
# Extending the Default Theme
VitePress' default theme is optimized for documentation, and can be customized. Consult the [Default Theme Config Overview](../reference/default-theme-config) for a comprehensive list of options.
@ -10,7 +14,7 @@ However, there are a number of cases where configuration alone won't be enough.
These advanced customizations will require using a custom theme that "extends" the default theme.
:::tip
::: tip
Before proceeding, make sure to first read [Using a Custom Theme](./custom-theme) to understand how custom themes work.
:::
@ -18,8 +22,7 @@ Before proceeding, make sure to first read [Using a Custom Theme](./custom-theme
The default theme CSS is customizable by overriding root level CSS variables:
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
import DefaultTheme from 'vitepress/theme'
import './custom.css'
@ -29,8 +32,8 @@ export default DefaultTheme
```css
/* .vitepress/theme/custom.css */
:root {
--vp-c-brand: #646cff;
--vp-c-brand-light: #747bff;
--vp-c-brand-1: #646cff;
--vp-c-brand-2: #747bff;
}
```
@ -42,8 +45,7 @@ VitePress uses [Inter](https://rsms.me/inter/) as the default font, and will inc
To avoid including Inter in the build output, import the theme from `vitepress/theme-without-fonts` instead:
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
import DefaultTheme from 'vitepress/theme-without-fonts'
import './my-fonts.css'
@ -51,25 +53,24 @@ export default DefaultTheme
```
```css
/* .vitepress/theme/custom.css */
/* .vitepress/theme/my-fonts.css */
:root {
--vp-font-family-base: /* normal text font */
--vp-font-family-mono: /* code font */
}
```
:::warning
If you are using optional components like the [Team Page](/reference/default-theme-team-page) components, make sure to also import them from `vitepress/theme-without-fonts`!
::: warning
If you are using optional components like the [Team Page](../reference/default-theme-team-page) components, make sure to also import them from `vitepress/theme-without-fonts`!
:::
If your font is a local file referenced via `@font-face`, it will be processed as an asset and included under `.vitepress/dist/assets` with hashed filename. To preload this file, use the [transformHead](/reference/site-config#transformhead) build hook:
If your font is a local file referenced via `@font-face`, it will be processed as an asset and included under `.vitepress/dist/assets` with hashed filename. To preload this file, use the [transformHead](../reference/site-config#transformhead) build hook:
```js
// .vitepress/config.js
```js [.vitepress/config.js]
export default {
transformHead({ assets }) {
// adjust the regex accordingly to match your font
const myFontFile = assets.find(file => /font-name\.\w+\.woff2/)
const myFontFile = assets.find(file => /font-name\.[\w-]+\.woff2/.test(file))
if (myFontFile) {
return [
[
@ -90,40 +91,52 @@ export default {
## Registering Global Components
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
import DefaultTheme from 'vitepress/theme'
/** @type {import('vitepress').Theme} */
export default {
extends: DefaultTheme,
enhanceApp(ctx) {
enhanceApp({ app }) {
// register your custom global components
ctx.app.component('MyGlobalComponent' /* ... */)
app.component('MyGlobalComponent' /* ... */)
}
}
```
If you're using TypeScript:
```ts [.vitepress/theme/index.ts]
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
// register your custom global components
app.component('MyGlobalComponent' /* ... */)
}
} satisfies Theme
```
Since we are using Vite, you can also leverage Vite's [glob import feature](https://vitejs.dev/guide/features.html#glob-import) to auto register a directory of components.
## Layout Slots
The default theme's `<Layout/>` component has a few slots that can be used to inject content at certain locations of the page. Here's an example of injecting a component into the before outline:
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
import DefaultTheme from 'vitepress/theme'
import MyLayout from './MyLayout.vue'
export default {
...DefaultTheme,
extends: DefaultTheme,
// override the Layout with a wrapper component that
// injects the slots
Layout: MyLayout
}
```
```vue
<!--.vitepress/theme/MyLayout.vue-->
```vue [.vitepress/theme/MyLayout.vue]
<script setup>
import DefaultTheme from 'vitepress/theme'
@ -141,14 +154,13 @@ const { Layout } = DefaultTheme
Or you could use render function as well.
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import MyComponent from './MyComponent.vue'
export default {
...DefaultTheme,
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'aside-outline-before': () => h(MyComponent)
@ -175,7 +187,10 @@ Full list of slots available in the default theme layout:
- `aside-ads-after`
- When `layout: 'home'` is enabled via frontmatter:
- `home-hero-before`
- `home-hero-info-before`
- `home-hero-info`
- `home-hero-info-after`
- `home-hero-actions-after`
- `home-hero-image`
- `home-hero-after`
- `home-features-before`
@ -195,6 +210,100 @@ Full list of slots available in the default theme layout:
- `nav-screen-content-before`
- `nav-screen-content-after`
## Using View Transitions API
### On Appearance Toggle
You can extend the default theme to provide a custom transition when the color mode is toggled. An example:
```vue [.vitepress/theme/Layout.vue]
<script setup lang="ts">
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import { nextTick, provide } from 'vue'
const { isDark } = useData()
const enableTransitions = () =>
'startViewTransition' in document &&
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
if (!enableTransitions()) {
isDark.value = !isDark.value
return
}
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
)}px at ${x}px ${y}px)`
]
await document.startViewTransition(async () => {
isDark.value = !isDark.value
await nextTick()
}).ready
document.documentElement.animate(
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
{
duration: 300,
easing: 'ease-in',
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`
}
)
})
</script>
<template>
<DefaultTheme.Layout />
</template>
<style>
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root),
.dark::view-transition-new(root) {
z-index: 1;
}
::view-transition-new(root),
.dark::view-transition-old(root) {
z-index: 9999;
}
.VPSwitchAppearance {
width: 22px !important;
}
.VPSwitchAppearance .check {
transform: none !important;
}
</style>
```
Result (**warning!**: flashing colors, sudden movements, bright lights):
<details>
<summary>Demo</summary>
![Appearance Toggle Transition Demo](/appearance-toggle-transition.webp)
</details>
Refer [Chrome Docs](https://developer.chrome.com/docs/web-platform/view-transitions/) from more details on view transitions.
### On Route Change
Coming soon.
## Overriding Internal Components
You can use Vite's [aliases](https://vitejs.dev/config/shared-options.html#resolve-alias) to replace default theme components with your custom ones:

@ -8,7 +8,7 @@ You can try VitePress directly in your browser on [StackBlitz](https://vitepress
### Prerequisites
- [Node.js](https://nodejs.org/) version 16 or higher.
- [Node.js](https://nodejs.org/) version 18 or higher.
- Terminal for accessing VitePress via its command line interface (CLI).
- Text Editor with [Markdown](https://en.wikipedia.org/wiki/Markdown) syntax support.
- [VSCode](https://code.visualstudio.com/) is recommended, along with the [official Vue extension](https://marketplace.visualstudio.com/items?itemName=Vue.volar).
@ -18,7 +18,7 @@ VitePress can be used on its own, or be installed into an existing project. In b
::: code-group
```sh [npm]
$ npm install -D vitepress
$ npm add -D vitepress
```
```sh [pnpm]
@ -29,6 +29,14 @@ $ pnpm add -D vitepress
$ yarn add -D vitepress
```
```sh [yarn (pnp)]
$ yarn add -D vitepress vue
```
```sh [bun]
$ bun add -D vitepress
```
:::
::: details Getting missing peer deps warnings?
@ -38,7 +46,8 @@ If using PNPM, you will notice a missing peer warning for `@docsearch/js`. This
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"@algolia/client-search"
"@algolia/client-search",
"search-insights"
]
}
}
@ -46,6 +55,12 @@ If using PNPM, you will notice a missing peer warning for `@docsearch/js`. This
:::
::: tip NOTE
VitePress is an ESM-only package. Don't use `require()` to import it, and make sure your nearest `package.json` contains `"type": "module"`, or change the file extension of your relevant files like `.vitepress/config.js` to `.mjs`/`.mts`. Refer to [Vite's troubleshooting guide](http://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only) for more details. Also, inside async CJS contexts, you can use `await import('vitepress')` instead.
:::
### Setup Wizard
VitePress ships with a command line setup wizard that will help you scaffold a basic project. After installation, start the wizard by running:
@ -57,19 +72,25 @@ $ npx vitepress init
```
```sh [pnpm]
$ pnpm exec vitepress init
$ pnpm vitepress init
```
```sh [yarn]
$ yarn vitepress init
```
```sh [bun]
$ bun vitepress init
```
:::
You will be greeted with a few simple questions:
<p>
<img src="./vitepress-init.png" alt="vitepress init screenshot" style="border-radius:8px">
</p>
<<< @/snippets/init.ansi
:::tip Vue as Peer Dependency
If you intend to perform customization that uses Vue components or APIs, you should also explicitly install `vue` as a peer dependency.
::: tip Vue as Peer Dependency
If you intend to perform customization that uses Vue components or APIs, you should also explicitly install `vue` as a dependency.
:::
## File Structure
@ -91,7 +112,7 @@ Assuming you chose to scaffold the VitePress project in `./docs`, the generated
The `docs` directory is considered the **project root** of the VitePress site. The `.vitepress` directory is a reserved location for VitePress' config file, dev server cache, build output, and optional theme customization code.
:::tip
::: tip
By default, VitePress stores its dev server cache in `.vitepress/cache`, and the production build output in `.vitepress/dist`. If using Git, you should add them to your `.gitignore` file. These locations can also be [configured](../reference/site-config#outdir).
:::
@ -99,8 +120,7 @@ By default, VitePress stores its dev server cache in `.vitepress/cache`, and the
The config file (`.vitepress/config.js`) allows you to customize various aspects of your VitePress site, with the most basic options being the title and description of the site:
```js
// .vitepress/config.js
```js [.vitepress/config.js]
export default {
// site-level options
title: 'VitePress',
@ -126,7 +146,7 @@ VitePress also provides the ability to generate clean URLs, rewrite paths, and d
The tool should have also injected the following npm scripts to your `package.json` if you allowed it to do so during the setup process:
```json
```json [package.json]
{
...
"scripts": {
@ -154,6 +174,10 @@ $ pnpm run docs:dev
$ yarn docs:dev
```
```sh [bun]
$ bun run docs:dev
```
:::
Instead of npm scripts, you can also invoke VitePress directly with:
@ -165,7 +189,15 @@ $ npx vitepress dev docs
```
```sh [pnpm]
$ pnpm exec vitepress dev docs
$ pnpm vitepress dev docs
```
```sh [yarn]
$ yarn vitepress dev docs
```
```sh [bun]
$ bun vitepress dev docs
```
:::
@ -178,7 +210,7 @@ The dev server should be running at `http://localhost:5173`. Visit the URL in yo
- To better understand how markdown files are mapped to generated HTML, proceed to the [Routing Guide](./routing).
- To discover more about what you can do on the page, such as writing markdown content or using Vue Component, refer to the "Writing" section of the guide. A great place to start would be to learn about [Markdown Extensions](./markdown).
- To discover more about what you can do on the page, such as writing markdown content or using Vue Components, refer to the "Writing" section of the guide. A great place to start would be to learn about [Markdown Extensions](./markdown).
- To explore the features provided by the default documentation theme, check out the [Default Theme Config Reference](../reference/default-theme-config).

@ -13,7 +13,7 @@ docs/
Then in `docs/.vitepress/config.ts`:
```ts
```ts [docs/.vitepress/config.ts]
import { defineConfig } from 'vitepress'
export default defineConfig({
@ -75,23 +75,35 @@ However, VitePress won't redirect `/` to `/en/` by default. You'll need to confi
/* /en/:splat 302
```
**Pro tip:** If using the above approach, you can use `nf_lang` cookie to persist user's language choice. A very basic way to do this is register a watcher inside the [setup](./custom-theme#using-a-custom-theme) function of custom theme:
**Pro tip:** If using the above approach, you can use `nf_lang` cookie to persist user's language choice:
```ts
// docs/.vitepress/theme/index.ts
```ts [docs/.vitepress/theme/index.ts]
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'
export default {
...DefaultTheme,
setup() {
const { lang } = useData()
watchEffect(() => {
extends: DefaultTheme,
Layout
}
```
```vue [docs/.vitepress/theme/Layout.vue]
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import { useData, inBrowser } from 'vitepress'
import { watchEffect } from 'vue'
const { lang } = useData()
watchEffect(() => {
if (inBrowser) {
document.cookie = `nf_lang=${lang.value}; expires=Mon, 1 Jan 2024 00:00:00 UTC; path=/`
}
})
document.cookie = `nf_lang=${lang.value}; expires=Mon, 1 Jan 2030 00:00:00 UTC; path=/`
}
}
})
</script>
<template>
<DefaultTheme.Layout />
</template>
```
## RTL Support (Experimental)

@ -18,11 +18,11 @@ This allows you to link to the heading as `#my-anchor` instead of the default `#
## Links
Both internal and external links gets special treatments.
Both internal and external links get special treatment.
### Internal Links
Internal links are converted to router link for SPA navigation. Also, every `index.md` contained in each sub-directory will automatically be converted to `index.html`, with corresponding URL `/`.
Internal links are converted to router links for SPA navigation. Also, every `index.md` contained in each sub-directory will automatically be converted to `index.html`, with corresponding URL `/`.
For example, given the following directory structure:
@ -80,7 +80,7 @@ For more details, see [Frontmatter](../reference/frontmatter-config).
**Input**
```
```md
| Tables | Are | Cool |
| ------------- | :-----------: | ----: |
| col 3 is | right-aligned | $1600 |
@ -108,7 +108,7 @@ For more details, see [Frontmatter](../reference/frontmatter-config).
:tada: :100:
A [list of all emojis](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.json) is available.
A [list of all emojis](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/data/full.mjs) is available.
## Table of Contents
@ -187,7 +187,7 @@ You may set custom title by appending the text right after the "type" of the con
Danger zone, do not proceed
:::
::: details Click me to view the code
::: details Click me to toggle the code
```js
console.log('Hello, VitePress!')
```
@ -200,7 +200,48 @@ console.log('Hello, VitePress!')
Danger zone, do not proceed
:::
::: details Click me to view the code
::: details Click me to toggle the code
```js
console.log('Hello, VitePress!')
```
:::
Also, you may set custom titles globally by adding the following content in site config, helpful if not writing in English:
```ts
// config.ts
export default defineConfig({
// ...
markdown: {
container: {
tipLabel: '提示',
warningLabel: '警告',
dangerLabel: '危险',
infoLabel: '信息',
detailsLabel: '详细信息'
}
}
// ...
})
```
### Additional Attributes
You can add additional attributes to the custom containers. We use [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs) for this feature, and it is supported on almost all markdown elements. For example, you can set the `open` attribute to make the details block open by default:
**Input**
````md
::: details Click me to toggle the code {open}
```js
console.log('Hello, VitePress!')
```
:::
````
**Output**
::: details Click me to toggle the code {open}
```js
console.log('Hello, VitePress!')
```
@ -214,42 +255,75 @@ This is a special container that can be used to prevent style and router conflic
```md
::: raw
Wraps in a <div class="vp-raw">
Wraps in a `<div class="vp-raw">`
:::
```
`vp-raw` class can be directly used on elements too. Style isolation is currently opt-in:
::: details
- Install required deps with your preferred package manager:
- Install `postcss` with your preferred package manager:
```sh
$ npm install -D postcss postcss-prefix-selector
$ npm add -D postcss
```
- Create a file named `docs/.postcssrc.cjs` and add this to it:
- Create a file named `docs/postcss.config.mjs` and add this to it:
```js
module.exports = {
plugins: {
'postcss-prefix-selector': {
prefix: ':not(:where(.vp-raw *))',
includeFiles: [/vp-doc\.css/],
transform(prefix, _selector) {
const [selector, pseudo = ''] = _selector.split(/(:\S*)$/)
return selector + prefix + pseudo
}
}
}
import { postcssIsolateStyles } from 'vitepress'
export default {
plugins: [postcssIsolateStyles()]
}
```
:::
It uses [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) under the hood. You can pass its options like this:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // defaults to /base\.css/
})
```
## GitHub-flavored Alerts
VitePress also supports [GitHub-flavored alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) to render as callouts. They will be rendered the same as the [custom containers](#custom-containers).
```md
> [!NOTE]
> Highlights information that users should take into account, even when skimming.
> [!TIP]
> Optional information to help a user be more successful.
> [!IMPORTANT]
> Crucial information necessary for users to succeed.
> [!WARNING]
> Critical content demanding immediate user attention due to potential risks.
> [!CAUTION]
> Negative potential consequences of an action.
```
> [!NOTE]
> Highlights information that users should take into account, even when skimming.
> [!TIP]
> Optional information to help a user be more successful.
> [!IMPORTANT]
> Crucial information necessary for users to succeed.
> [!WARNING]
> Critical content demanding immediate user attention due to potential risks.
> [!CAUTION]
> Negative potential consequences of an action.
## Syntax Highlighting in Code Blocks
VitePress uses [Shiki](https://shiki.matsu.io/) to highlight language syntax in Markdown code blocks, using coloured text. Shiki supports a wide variety of programming languages. All you need to do is append a valid language alias to the beginning backticks for the code block:
VitePress uses [Shiki](https://github.com/shikijs/shiki) to highlight language syntax in Markdown code blocks, using coloured text. Shiki supports a wide variety of programming languages. All you need to do is append a valid language alias to the beginning backticks for the code block:
**Input**
@ -289,7 +363,7 @@ export default {
</ul>
```
A [list of valid languages](https://github.com/shikijs/shiki/blob/main/docs/languages.md) is available on Shiki's repository.
A [list of valid languages](https://shiki.style/languages) is available on Shiki's repository.
You may also customize syntax highlight theme in app config. Please see [`markdown` options](../reference/site-config#markdown) for more details.
@ -361,7 +435,7 @@ export default { // Highlighted
}
```
Alternatively, it's possible to highlight directly in the line by using the `// [!code hl]` comment.
Alternatively, it's possible to highlight directly in the line by using the `// [!code highlight]` comment.
**Input**
@ -370,7 +444,7 @@ Alternatively, it's possible to highlight directly in the line by using the `//
export default {
data () {
return {
msg: 'Highlighted!' // [!code hl]
msg: 'Highlighted!' // [!!code highlight]
}
}
}
@ -383,7 +457,7 @@ export default {
export default {
data() {
return {
msg: 'Highlighted!' // [!code hl]
msg: 'Highlighted!' // [!code highlight]
}
}
}
@ -397,14 +471,12 @@ Additionally, you can define a number of lines to focus using `// [!code focus:<
**Input**
Note that only one space is required after `!code`, here are two to prevent processing.
````
```js
export default {
data () {
return {
msg: 'Focused!' // [!code focus]
msg: 'Focused!' // [!!code focus]
}
}
}
@ -429,15 +501,13 @@ Adding the `// [!code --]` or `// [!code ++]` comments on a line will create a d
**Input**
Note that only one space is required after `!code`, here are two to prevent processing.
````
```js
export default {
data () {
return {
msg: 'Removed' // [!code --]
msg: 'Added' // [!code ++]
msg: 'Removed' // [!!code --]
msg: 'Added' // [!!code ++]
}
}
}
@ -463,15 +533,13 @@ Adding the `// [!code warning]` or `// [!code error]` comments on a line will co
**Input**
Note that only one space is required after `!code`, here are two to prevent processing.
````
```js
export default {
data () {
return {
msg: 'Error', // [!code error]
msg: 'Warning' // [!code warning]
msg: 'Error', // [!!code error]
msg: 'Warning' // [!!code warning]
}
}
}
@ -507,6 +575,8 @@ Please see [`markdown` options](../reference/site-config#markdown) for more deta
You can add `:line-numbers` / `:no-line-numbers` mark in your fenced code blocks to override the value set in config.
You can also customize the starting line number by adding `=` after `:line-numbers`. For example, `:line-numbers=2` means the line numbers in code blocks will start from `2`.
**Input**
````md
@ -521,6 +591,12 @@ const line3 = 'This is line 3'
const line2 = 'This is line 2'
const line3 = 'This is line 3'
```
```ts:line-numbers=2 {1}
// line-numbers is enabled and start from 2
const line3 = 'This is line 3'
const line4 = 'This is line 4'
```
````
**Output**
@ -537,6 +613,12 @@ const line2 = 'This is line 2'
const line3 = 'This is line 3'
```
```ts:line-numbers=2 {1}
// line-numbers is enabled and start from 2
const line3 = 'This is line 3'
const line4 = 'This is line 4'
```
## Import Code Snippets
You can import code snippets from existing files via following syntax:
@ -696,10 +778,10 @@ You can also [import snippets](#import-code-snippets) in code groups:
## Markdown File Inclusion
You can include a markdown file in another markdown file.
You can include a markdown file in another markdown file, even nested.
::: tip
You can also prefix the markdown path with `@`, it will act as the source root. By default it's the VitePress project root, unless `srcDir` is configured.
You can also prefix the markdown path with `@`, it will act as the source root. By default, it's the VitePress project root, unless `srcDir` is configured.
:::
For example, you can include a relative markdown file using this:
@ -738,23 +820,204 @@ Some getting started stuff.
Can be created using `.foorc.json`.
```
It also supports selecting a line range:
**Input**
```md:line-numbers
# Docs
## Basics
<!--@include: ./parts/basics.md{3,}-->
```
**Part file** (`parts/basics.md`)
```md:line-numbers
Some getting started stuff.
### Configuration
Can be created using `.foorc.json`.
```
**Equivalent code**
```md:line-numbers
# Docs
## Basics
### Configuration
Can be created using `.foorc.json`.
```
The format of the selected line range can be: `{3,}`, `{,10}`, `{1,10}`
You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) to only include the corresponding part of the code file. You can provide a custom region name after a `#` following the filepath:
**Input**
```md:line-numbers
# Docs
## Basics
<!--@include: ./parts/basics.md#basic-usage{,2}-->
<!--@include: ./parts/basics.md#basic-usage{5,}-->
```
**Part file** (`parts/basics.md`)
```md:line-numbers
<!-- #region basic-usage -->
## Usage Line 1
## Usage Line 2
## Usage Line 3
<!-- #endregion basic-usage -->
```
**Equivalent code**
```md:line-numbers
# Docs
## Basics
## Usage Line 1
## Usage Line 3
```
::: warning
Note that this does not throw errors if your file is not present. Hence, when using this feature make sure that the contents are being rendered as expected.
:::
Instead of VS Code regions, you can also use header anchors to include a specific section of the file. For example, if you have a header in your markdown file like this:
```md
## My Base Section
Some content here.
### My Sub Section
Some more content here.
## Another Section
Content outside `My Base Section`.
```
You can include the `My Base Section` section like this:
```md
## My Extended Section
<!--@include: ./parts/basics.md#my-base-section-->
```
**Equivalent code**
```md
## My Extended Section
Some content here.
### My Sub Section
Some more content here.
```
Here, `my-base-section` is the generated id of the heading element. In case it's not easily guessable, you can open the part file in your browser and click on the heading anchor (`#` symbol left to the heading when hovered) to see the id in the URL bar. Or use browser dev tools to inspect the element. Alternatively, you can also specify the id to the part file like this:
```md
## My Base Section {#custom-id}
```
and include it like this:
```md
<!--@include: ./parts/basics.md#custom-id-->
```
## Math Equations
This is currently opt-in. To enable it, you need to install `markdown-it-mathjax3` and set `markdown.math` to `true` in your config file:
```sh
npm add -D markdown-it-mathjax3
```
```ts [.vitepress/config.ts]
export default {
markdown: {
math: true
}
}
```
**Input**
```md
When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are
$$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
**Maxwell's equations:**
| equation | description |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| $\nabla \cdot \vec{\mathbf{B}} = 0$ | divergence of $\vec{\mathbf{B}}$ is zero |
| $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ | curl of $\vec{\mathbf{E}}$ is proportional to the rate of change of $\vec{\mathbf{B}}$ |
| $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ | _wha?_ |
```
**Output**
When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are
$$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
**Maxwell's equations:**
| equation | description |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| $\nabla \cdot \vec{\mathbf{B}} = 0$ | divergence of $\vec{\mathbf{B}}$ is zero |
| $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ | curl of $\vec{\mathbf{E}}$ is proportional to the rate of change of $\vec{\mathbf{B}}$ |
| $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ | _wha?_ |
## Image Lazy Loading
You can enable lazy loading for each image added via markdown by setting `lazyLoading` to `true` in your config file:
```js
export default {
markdown: {
image: {
// image lazy loading is disabled by default
lazyLoading: true
}
}
}
```
## Advanced Configuration
VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`:
```js
const anchor = require('markdown-it-anchor')
import { defineConfig } from 'vitepress'
import markdownItAnchor from 'markdown-it-anchor'
import markdownItFoo from 'markdown-it-foo'
module.exports = {
export default defineConfig({
markdown: {
// options for markdown-it-anchor
// https://github.com/valeriangalliat/markdown-it-anchor#usage
anchor: {
permalink: anchor.permalink.headerLink()
permalink: markdownItAnchor.permalink.headerLink()
},
// options for @mdit-vue/plugin-toc
@ -763,10 +1026,10 @@ module.exports = {
config: (md) => {
// use more markdown-it plugins!
md.use(require('markdown-it-xxx'))
md.use(markdownItFoo)
}
}
}
})
```
See full list of configurable properties in [Config Reference: App Config](../reference/site-config#markdown).

@ -93,19 +93,45 @@ You can use both absolute and relative paths when linking between pages. Note th
[Getting Started](./getting-started.html)
```
Learn more about linking to assets such images in [Asset Handling](asset-handling).
Learn more about linking to assets such images in [Asset Handling](./asset-handling).
### Linking to Non-VitePress Pages
If you want to link to a page in your site that is not generated by VitePress, you'll either need to use the full URL (opens in a new tab) or explicitly specify the target:
**Input**
```md
[Link to pure.html](/pure.html){target="_self"}
```
**Output**
[Link to pure.html](/pure.html){target="_self"}
::: tip Note
In Markdown links, the `base` is automatically prepended to the URL. This means that if you want to link to a page outside of your base, you'd need something like `../../pure.html` in the link (resolved relative to the current page by the browser).
Alternatively, you can directly use the anchor tag syntax:
```md
<a href="/pure.html" target="_self">Link to pure.html</a>
```
:::
## Generating Clean URL
:::warning Server Support Required
::: warning Server Support Required
To serve clean URLs with VitePress, server-side support is required.
:::
By default, VitePress resolves inbound links to URLs ending with `.html`. However, some users may prefer "Clean URLs" without the `.html` extension - for example, `example.com/path` instead of `example.com/path.html`.
Some servers or hosting platforms (for example Netlify or Vercel) provide the ability to map a URL like `/foo` to `/foo.html` if it exists, without a redirect:
Some servers or hosting platforms (for example Netlify, Vercel, GitHub Pages) provide the ability to map a URL like `/foo` to `/foo.html` if it exists, without a redirect:
- Netlify supports this by default.
- Netlify and GitHub Pages support this by default.
- Vercel requires enabling the [`cleanUrls` option in `vercel.json`](https://vercel.com/docs/concepts/projects/project-configuration#cleanurls).
If this feature is available to you, you can then also enable VitePress' own [`cleanUrls`](../reference/site-config#cleanurls) config option so that:
@ -113,7 +139,7 @@ If this feature is available to you, you can then also enable VitePress' own [`c
- Inbound links between pages are generated without the `.html` extension.
- If current path ends with `.html`, the router will perform a client-side redirect to the extension-less path.
If, however, you cannot configure your server with such support (e.g. GitHub pages), you will have to manually resort to the following directory structure:
If, however, you cannot configure your server with such support, you will have to manually resort to the following directory structure:
```
.
@ -130,32 +156,35 @@ You can customize the mapping between the source directory structure and the gen
```
.
─ packages
├─ pkg-a
│ └─ src
│ │ ├─ pkg-a-code.ts
│ │ └─ pkg-a-docs.md
└─ pkg-b
└─ src
│ ├─ pkg-b-code.ts
│ └─ pkg-b-docs.md
─ packages
├─ pkg-a
│ └─ src
│ ├─ foo.md
│ └─ index.md
└─ pkg-b
└─ src
├─ bar.md
└─ index.md
```
And you want the VitePress pages to be generated like this:
```
packages/pkg-a/src/pkg-a-docs.md --> /pkg-a/index.html
packages/pkg-b/src/pkg-b-docs.md --> /pkg-b/index.html
packages/pkg-a/src/index.md --> /pkg-a/index.html
packages/pkg-a/src/foo.md --> /pkg-a/foo.html
packages/pkg-b/src/index.md --> /pkg-b/index.html
packages/pkg-b/src/bar.md --> /pkg-b/bar.html
```
You can achieve this by configuring the [`rewrites`](../reference/site-config#rewrites) option like this:
```ts
// .vitepress/config.js
```ts [.vitepress/config.js]
export default {
rewrites: {
'packages/pkg-a/src/pkg-a-docs.md': 'pkg-a/index.md',
'packages/pkg-b/src/pkg-b-docs.md': 'pkg-b/index.md'
'packages/pkg-a/src/index.md': 'pkg-a/index.md',
'packages/pkg-a/src/foo.md': 'pkg-a/foo.md',
'packages/pkg-b/src/index.md': 'pkg-b/index.md',
'packages/pkg-b/src/bar.md': 'pkg-b/bar.md'
}
}
```
@ -165,14 +194,24 @@ The `rewrites` option also supports dynamic route parameters. In the above examp
```ts
export default {
rewrites: {
'packages/:pkg/src/(.*)': ':pkg/index.md'
'packages/:pkg/src/:slug*': ':pkg/:slug*'
}
}
```
The rewrite paths are compiled using the `path-to-regexp` package - consult [its documentation](https://github.com/pillarjs/path-to-regexp#parameters) for more advanced syntax.
The rewrite paths are compiled using the `path-to-regexp` package - consult [its documentation](https://github.com/pillarjs/path-to-regexp/tree/6.x#parameters) for more advanced syntax.
`rewrites` can also be a function that receives the original path and returns the new path:
```ts
export default {
rewrites(id) {
return id.replace(/^packages\/([^/]+)\/src\//, '$1/')
}
}
```
:::warning Relative Links with Rewrites
::: warning Relative Links with Rewrites
When rewrites are enabled, **relative links should be based on the rewritten paths**. For example, in order to create a relative link from `packages/pkg-a/src/pkg-a-code.md` to `packages/pkg-b/src/pkg-b-code.md`, you should use:
@ -327,7 +366,6 @@ Instead, you can pass such content to each page using the `content` property on
```js
export default {
paths() {
async paths() {
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
@ -338,7 +376,6 @@ export default {
}
})
}
}
}
```

@ -0,0 +1,58 @@
# Sitemap Generation
VitePress comes with out-of-the-box support for generating a `sitemap.xml` file for your site. To enable it, add the following to your `.vitepress/config.js`:
```ts
export default {
sitemap: {
hostname: 'https://example.com'
}
}
```
To have `<lastmod>` tags in your `sitemap.xml`, you can enable the [`lastUpdated`](../reference/default-theme-last-updated) option.
## Options
Sitemap support is powered by the [`sitemap`](https://www.npmjs.com/package/sitemap) module. You can pass any options supported by it to the `sitemap` option in your config file. These will be passed directly to the `SitemapStream` constructor. Refer to the [`sitemap` documentation](https://www.npmjs.com/package/sitemap#options-you-can-pass) for more details. Example:
```ts
export default {
sitemap: {
hostname: 'https://example.com',
lastmodDateOnly: false
}
}
```
If you're using `base` in your config, you should append it to the `hostname` option:
```ts
export default {
base: '/my-site/',
sitemap: {
hostname: 'https://example.com/my-site/'
}
}
```
## `transformItems` Hook
You can use the `sitemap.transformItems` hook to modify the sitemap items before they are written to the `sitemap.xml` file. This hook is called with an array of sitemap items and expects an array of sitemap items to be returned. Example:
```ts
export default {
sitemap: {
hostname: 'https://example.com',
transformItems: (items) => {
// add new items or modify/filter existing items
items.push({
url: '/extra-page',
changefreq: 'monthly',
priority: 0.8
})
return items
}
}
}
```

@ -6,7 +6,7 @@ outline: deep
VitePress pre-renders the app in Node.js during the production build, using Vue's Server-Side Rendering (SSR) capabilities. This means all custom code in theme components are subject to SSR Compatibility.
The [SSR section in official Vue docs](https://vuejs.org/guide/scaling-up/ssr.html) provides more context on what is SSR, the relationship between SSR / SSG, and common notes on writing SSR-friendly code. The rule of thumb is to only access browser / DOM APIs in `beforeMount` or `mounted` hooks of Vue components.
The [SSR section in official Vue docs](https://vuejs.org/guide/scaling-up/ssr.html) provides more context on what SSR is, the relationship between SSR / SSG, and common notes on writing SSR-friendly code. The rule of thumb is to only access browser / DOM APIs in `beforeMount` or `mounted` hooks of Vue components.
## `<ClientOnly>`
@ -48,21 +48,36 @@ if (!import.meta.env.SSR) {
}
```
Since [`Theme.enhanceApp`](/guide/custom-theme#theme-interface) can be async, you can conditionally import and register Vue plugins that access browser APIs on import:
Since [`Theme.enhanceApp`](./custom-theme#theme-interface) can be async, you can conditionally import and register Vue plugins that access browser APIs on import:
```js
// .vitepress/theme/index.js
```js [.vitepress/theme/index.js]
/** @type {import('vitepress').Theme} */
export default {
// ...
async enhanceApp({ app }) {
if (!import.meta.env.SSR) {
const plugin = await import('plugin-that-access-window-on-import')
app.use(plugin)
app.use(plugin.default)
}
}
}
```
If you're using TypeScript:
```ts [.vitepress/theme/index.ts]
import type { Theme } from 'vitepress'
export default {
// ...
async enhanceApp({ app }) {
if (!import.meta.env.SSR) {
const plugin = await import('plugin-that-access-window-on-import')
app.use(plugin.default)
}
}
} satisfies Theme
```
### `defineClientComponent`
VitePress provides a convenience helper for importing Vue components that access browser APIs on import.

@ -4,7 +4,7 @@ In VitePress, each Markdown file is compiled into HTML and then processed as a [
It's worth noting that VitePress leverages Vue's compiler to automatically detect and optimize the purely static parts of the Markdown content. Static contents are optimized into single placeholder nodes and eliminated from the page's JavaScript payload for initial visits. They are also skipped during client-side hydration. In short, you only pay for the dynamic parts on any given page.
:::tip SSR Compatibility
::: tip SSR Compatibility
All Vue usage needs to be SSR-compatible. See [SSR Compatibility](./ssr-compat) for details and common workarounds.
:::
@ -67,7 +67,7 @@ The count is: {{ count }}
</style>
```
:::warning Avoid `<style scoped>` in Markdown
::: warning Avoid `<style scoped>` in Markdown
When used in Markdown, `<style scoped>` requires adding special attributes to every element on the current page, which will significantly bloat the page size. `<style module>` is preferred when locally-scoped styling is needed in a page.
:::
@ -224,7 +224,7 @@ Then you can use the following in Markdown and theme components:
## Using Teleports
Vitepress currently has SSG support for teleports to body only. For other targets, you can wrap them inside the built-in `<ClientOnly>` component or inject the teleport markup into the correct location in your final page HTML through [`postRender` hook](../reference/site-config#postrender).
VitePress currently has SSG support for teleports to body only. For other targets, you can wrap them inside the built-in `<ClientOnly>` component or inject the teleport markup into the correct location in your final page HTML through [`postRender` hook](../reference/site-config#postrender).
<ModalDemo />
@ -243,7 +243,8 @@ Vitepress currently has SSG support for teleports to body only. For other target
```
<script setup>
import ModalDemo from '../components/ModalDemo.vue'
import ModalDemo from '../../components/ModalDemo.vue'
import ComponentInHeader from '../../components/ComponentInHeader.vue'
</script>
<style>
@ -253,3 +254,38 @@ import ModalDemo from '../components/ModalDemo.vue'
padding: 0 20px;
}
</style>
## VS Code IntelliSense Support
<!-- Based on https://github.com/vuejs/language-tools/pull/4321 -->
Vue provides IntelliSense support out of the box via the [Vue - Official VS Code plugin](https://marketplace.visualstudio.com/items?itemName=Vue.volar). However, to enable it for `.md` files, you need to make some adjustments to the configuration files.
1. Add `.md` pattern to the `include` and `vueCompilerOptions.vitePressExtensions` options in the tsconfig/jsconfig file:
::: code-group
```json [tsconfig.json]
{
"include": [
"docs/**/*.ts",
"docs/**/*.vue",
"docs/**/*.md",
],
"vueCompilerOptions": {
"vitePressExtensions": [".md"],
},
}
```
:::
2. Add `markdown` to the `vue.server.includeLanguages` option in the VS Code setting:
::: code-group
```json [.vscode/settings.json]
{
"vue.server.includeLanguages": ["vue", "markdown"]
}
```
:::

@ -12,7 +12,7 @@ Just want to try it out? Skip to the [Quickstart](./getting-started).
- **Documentation**
VitePress ships with a default theme designed for technical documentation, especially those that need to embed interactive demos. It powers this page you are reading right now, along with the documentation for [Vite](https://vitejs.dev/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Rollup](https://rollupjs.org/), [Mermaid](https://mermaid.js.org/), [Wikimedia Codex](https://doc.wikimedia.org/codex/latest/), and many more.
VitePress ships with a default theme designed for technical documentation. It powers this page you are reading right now, along with the documentation for [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) and [many more](https://www.vuetelescope.com/explore?framework.slug=vitepress).
The [official Vue.js documentation](https://vuejs.org/) is also based on VitePress, but uses a custom theme shared between multiple translations.
@ -34,11 +34,11 @@ VitePress aims to provide a great Developer Experience (DX) when working with Ma
## Performance
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).
Unlike many traditional SSGs where each navigation results in a full page reload, a website generated by VitePress serves static HTML on the initial visit, but becomes a [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) (SPA) for subsequent navigation within the site. This model, in our opinion, provides an optimal balance for performance:
- **Fast Initial Load**
The initial visit to any page will be served the static, pre-rendered HTML for blazing fast loading speed and optimal SEO. The page then loads 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/report?url=https%3A%2F%2Fvitepress.dev%2F), typical VitePress sites achieve near-perfect performance scores even on low-end mobile devices with a slow network.
The initial visit to any page will be served the static, pre-rendered HTML for fast loading speed and optimal SEO. The page then loads a JavaScript bundle that turns the page into a Vue SPA ("hydration"). Contrary to common assumptions of SPA hydration being slow, this process is actually extremely fast thanks to Vue 3's raw performance and compiler optimizations. On [PageSpeed Insights](https://pagespeed.web.dev/report?url=https%3A%2F%2Fvitepress.dev%2F), typical VitePress sites achieve near-perfect performance scores even on low-end mobile devices with a slow network.
- **Fast Post-load Navigation**

@ -0,0 +1,35 @@
---
layout: home
hero:
name: VitePress
text: Vite & Vue Powered Static Site Generator
tagline: Markdown to Beautiful Docs in Minutes
actions:
- theme: brand
text: What is VitePress?
link: /guide/what-is-vitepress
- theme: alt
text: Quickstart
link: /guide/getting-started
- theme: alt
text: GitHub
link: https://github.com/vuejs/vitepress
image:
src: /vitepress-logo-large.svg
alt: VitePress
features:
- icon: 📝
title: Focus on Your Content
details: Effortlessly create beautiful documentation sites with just markdown.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
title: Enjoy the Vite DX
details: Instant server start, lightning fast hot updates, and leverage Vite ecosystem plugins.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 220.8"><path fill="#41B883" d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36Z"/><path fill="#41B883" d="m0 0 128 220.8L256 0h-51.2L128 132.48 50.56 0H0Z"/><path fill="#35495E" d="M50.56 0 128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56Z"/></svg>
title: Customize with Vue
details: Use Vue syntax and components directly in markdown, or build custom themes with Vue.
- icon: 🚀
title: Ship Fast Sites
details: Fast initial load with static HTML, fast post-load navigation with client-side routing.
---

@ -0,0 +1,74 @@
# Command Line Interface
## `vitepress dev`
Start VitePress dev server using designated directory as root. Defaults to current directory. The `dev` command can also be omitted when running in current directory.
### Usage
```sh
# start in current directory, omitting `dev`
vitepress
# start in sub directory
vitepress dev [root]
```
### Options
| Option | Description |
| --------------- | ----------------------------------------------------------------- |
| `--open [path]` | Open browser on startup (`boolean \| string`) |
| `--port <port>` | Specify port (`number`) |
| `--base <path>` | Public base path (default: `/`) (`string`) |
| `--cors` | Enable CORS |
| `--strictPort` | Exit if specified port is already in use (`boolean`) |
| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) |
## `vitepress build`
Build the VitePress site for production.
### Usage
```sh
vitepress build [root]
```
### Options
| Option | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `--mpa` (experimental) | Build in [MPA mode](../guide/mpa-mode) without client-side hydration (`boolean`) |
| `--base <path>` | Public base path (default: `/`) (`string`) |
| `--target <target>` | Transpile target (default: `"modules"`) (`string`) |
| `--outDir <dir>` | Output directory relative to **cwd** (default: `<root>/.vitepress/dist`) (`string`) |
| `--minify [minifier]` | Enable/disable minification, or specify minifier to use (default: `"esbuild"`) (`boolean \| "terser" \| "esbuild"`) |
| `--assetsInlineLimit <number>` | Static asset base64 inline threshold in bytes (default: `4096`) (`number`) |
## `vitepress preview`
Locally preview the production build.
### Usage
```sh
vitepress preview [root]
```
### Options
| Option | Description |
| --------------- | ------------------------------------------ |
| `--base <path>` | Public base path (default: `/`) (`string`) |
| `--port <port>` | Specify port (`number`) |
## `vitepress init`
Start the [Setup Wizard](../guide/getting-started#setup-wizard) in current directory.
### Usage
```sh
vitepress init
```

@ -32,39 +32,25 @@ Code above renders like:
## Customize Type Color
You can customize the `background-color` of badges by overriding css variables. The following are the default values:
You can customize the style of badges by overriding css variables. The following are the default values:
```css
:root {
--vp-badge-info-border: var(--vp-c-divider-light);
--vp-badge-info-border: transparent;
--vp-badge-info-text: var(--vp-c-text-2);
--vp-badge-info-bg: var(--vp-c-white-soft);
--vp-badge-info-bg: var(--vp-c-default-soft);
--vp-badge-tip-border: var(--vp-c-green-dimm-1);
--vp-badge-tip-text: var(--vp-c-green-darker);
--vp-badge-tip-bg: var(--vp-c-green-dimm-3);
--vp-badge-tip-border: transparent;
--vp-badge-tip-text: var(--vp-c-brand-1);
--vp-badge-tip-bg: var(--vp-c-brand-soft);
--vp-badge-warning-border: var(--vp-c-yellow-dimm-1);
--vp-badge-warning-text: var(--vp-c-yellow-darker);
--vp-badge-warning-bg: var(--vp-c-yellow-dimm-3);
--vp-badge-warning-border: transparent;
--vp-badge-warning-text: var(--vp-c-warning-1);
--vp-badge-warning-bg: var(--vp-c-warning-soft);
--vp-badge-danger-border: var(--vp-c-red-dimm-1);
--vp-badge-danger-text: var(--vp-c-red-darker);
--vp-badge-danger-bg: var(--vp-c-red-dimm-3);
}
.dark {
--vp-badge-info-border: var(--vp-c-divider-light);
--vp-badge-info-bg: var(--vp-c-black-mute);
--vp-badge-tip-border: var(--vp-c-green-dimm-2);
--vp-badge-tip-text: var(--vp-c-green-light);
--vp-badge-warning-border: var(--vp-c-yellow-dimm-2);
--vp-badge-warning-text: var(--vp-c-yellow-light);
--vp-badge-danger-border: var(--vp-c-red-dimm-2);
--vp-badge-danger-text: var(--vp-c-red-light);
--vp-badge-danger-border: transparent;
--vp-badge-danger-text: var(--vp-c-danger-1);
--vp-badge-danger-bg: var(--vp-c-danger-soft);
}
```

@ -66,7 +66,7 @@ export default {
The configuration for the nav menu item. More details in [Default Theme: Nav](./default-theme-nav#navigation-links).
```js
```ts
export default {
themeConfig: {
nav: [
@ -93,6 +93,7 @@ interface NavItemWithLink {
activeMatch?: string
target?: string
rel?: string
noIcon?: boolean
}
interface NavItemChildren {
@ -113,7 +114,7 @@ interface NavItemWithChildren {
The configuration for the sidebar menu item. More details in [Default Theme: Sidebar](./default-theme-sidebar).
```js
```ts
export default {
themeConfig: {
sidebar: [
@ -134,7 +135,7 @@ export default {
export type Sidebar = SidebarItem[] | SidebarMulti
export interface SidebarMulti {
[path: string]: SidebarItem[]
[path: string]: SidebarItem[] | { items: SidebarItem[]; base: string }
}
export type SidebarItem = {
@ -161,6 +162,19 @@ export type SidebarItem = {
* If `false`, group is collapsible but expanded by default
*/
collapsed?: boolean
/**
* Base path for the children items.
*/
base?: string
/**
* Customize text that appears on the footer of previous/next page.
*/
docFooterText?: string
rel?: string
target?: string
}
```
@ -174,26 +188,33 @@ Setting this value to `false` prevents rendering of aside container.\
Setting this value to `true` renders the aside to the right.\
Setting this value to `left` renders the aside to the left.
## outline
If you want to disable it for all viewports, you should use `outline: false` instead.
- Type: `number | [number, number] | 'deep' | false`
- Default: `2`
- Can be overridden per page via [frontmatter](./frontmatter-config#outline)
## outline
The levels of header to display in the outline. You can specify a particular level by passing a number, or you can provide a level range by passing a tuple containing the bottom and upper limits. When passing `'deep'` which equals `[2, 6]`, all header levels are shown in the outline except `h1`. Set `false` to hide outline.
- Type: `Outline | Outline['level'] | false`
- Level can be overridden per page via [frontmatter](./frontmatter-config#outline)
## outlineTitle
Setting this value to `false` prevents rendering of outline container. Refer this interface for more details:
- Type: `string`
- Default: `On this page`
Can be used to customize the title of the right sidebar (on the top of outline links). This is useful when writing documentation in another language.
```ts
interface Outline {
/**
* The levels of headings to be displayed in the outline.
* Single number means only headings of that level will be displayed.
* If a tuple is passed, the first number is the minimum level and the second number is the maximum level.
* `'deep'` is same as `[2, 6]`, which means all headings from `<h2>` to `<h6>` will be displayed.
*
* @default 2
*/
level?: number | [number, number] | 'deep'
```js
export default {
themeConfig: {
outlineTitle: 'In hac pagina'
}
/**
* The title to be displayed on the outline.
*
* @default 'On this page'
*/
label?: string
}
```
@ -203,10 +224,11 @@ export default {
You may define this option to show your social account links with icons in nav.
```js
```ts
export default {
themeConfig: {
socialLinks: [
// You can add any icon from simple-icons (https://simpleicons.org/):
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
{ icon: 'twitter', link: '...' },
// You can also add custom icons by passing SVG as string:
@ -225,27 +247,16 @@ export default {
```ts
interface SocialLink {
icon: SocialLinkIcon
icon: string | { svg: string }
link: string
ariaLabel?: string
}
type SocialLinkIcon =
| 'discord'
| 'facebook'
| 'github'
| 'instagram'
| 'linkedin'
| 'mastodon'
| 'slack'
| 'twitter'
| 'youtube'
| { svg: string }
```
## footer
- Type: `Footer`
- Can be overridden per page via [frontmatter](./frontmatter-config#footer)
Footer configuration. You can add a message or copyright text on the footer, however, it will only be displayed when the page doesn't contain a sidebar. This is due to design concerns.
@ -274,7 +285,7 @@ export interface Footer {
Edit Link lets you display a link to edit the page on Git management services such as GitHub, or GitLab. See [Default Theme: Edit Link](./default-theme-edit-link) for more details.
```js
```ts
export default {
themeConfig: {
editLink: {
@ -292,18 +303,38 @@ export interface EditLink {
}
```
## lastUpdatedText
## lastUpdated
- Type: `string`
- Default: `Last updated`
- Type: `LastUpdatedOptions`
The prefix text showing right before the last updated time.
Allows customization for the last updated text and date format.
```ts
export default {
themeConfig: {
lastUpdatedText: 'Updated Date'
lastUpdated: {
text: 'Updated at',
formatOptions: {
dateStyle: 'full',
timeStyle: 'medium'
}
}
}
}
```
```ts
export interface LastUpdatedOptions {
/**
* @default 'Last updated'
*/
text?: string
/**
* @default
* { dateStyle: 'short', timeStyle: 'short' }
*/
formatOptions?: Intl.DateTimeFormatOptions & { forceLocale?: boolean }
}
```
@ -353,7 +384,7 @@ Learn more in [Default Theme: Carbon Ads](./default-theme-carbon-ads)
Can be used to customize text appearing above previous and next links. Helpful if not writing docs in English. Also can be used to disable prev/next links globally. If you want to selectively enable/disable prev/next links, you can use [frontmatter](./default-theme-prev-next-links).
```js
```ts
export default {
themeConfig: {
docFooter: {
@ -378,6 +409,20 @@ export interface DocFooter {
Can be used to customize the dark mode switch label. This label is only displayed in the mobile view.
## lightModeSwitchTitle
- Type: `string`
- Default: `Switch to light theme`
Can be used to customize the light mode switch title that appears on hovering.
## darkModeSwitchTitle
- Type: `string`
- Default: `Switch to dark theme`
Can be used to customize the dark mode switch title that appears on hovering.
## sidebarMenuLabel
- Type: `string`
@ -398,3 +443,52 @@ Can be used to customize the label of the return to top button. This label is on
- Default: `Change language`
Can be used to customize the aria-label of the language toggle button in navbar. This is only used if you're using [i18n](../guide/i18n).
## skipToContentLabel
- Type: `string`
- Default: `Skip to content`
Can be used to customize the label of the skip to content link. This link is shown when the user is navigating the site using a keyboard.
## externalLinkIcon
- Type: `boolean`
- Default: `false`
Whether to show an external link icon next to external links in markdown.
## `useLayout` <Badge type="info" text="composable" />
Returns layout-related data. The returned object has the following type:
```ts
interface {
isHome: ComputedRef<boolean>
sidebar: Readonly<ShallowRef<DefaultTheme.SidebarItem[]>>
sidebarGroups: ComputedRef<DefaultTheme.SidebarItem[]>
hasSidebar: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean>
hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean>
headers: Readonly<ShallowRef<DefaultTheme.OutlineItem[]>>
hasLocalNav: ComputedRef<boolean>
}
```
**Example:**
```vue
<script setup>
import { useLayout } from 'vitepress/theme'
const { hasSidebar } = useLayout()
</script>
<template>
<div v-if="hasSidebar">Only show when sidebar exists</div>
</template>
```

@ -41,3 +41,13 @@ Only inline elements can be used in `message` and `copyright` as they are render
:::
Note that footer will not be displayed when the [SideBar](./default-theme-sidebar) is visible.
## Frontmatter Config
This can be disabled per-page using the `footer` option on frontmatter:
```yaml
---
footer: false
---
```

@ -69,12 +69,18 @@ interface HeroAction {
// Destination link of the button.
link: string
// Link target attribute.
target?: string
// Link rel attribute.
rel?: string
}
```
### Customizing the name color
VitePress uses the brand color (`--vp-c-brand`) for the `name`. However, you may customize this color by overriding `--vp-home-hero-name-color` variable.
VitePress uses the brand color (`--vp-c-brand-1`) for the `name`. However, you may customize this color by overriding `--vp-home-hero-name-color` variable.
```css
:root {
@ -131,7 +137,7 @@ interface Feature {
// Link when clicked on feature component. The link can
// be both internal or external.
//
// e.g. `guid/reference/default-theme-home-page` or `htttps://example.com`
// e.g. `guide/reference/default-theme-home-page` or `https://example.com`
link?: string
// Link text to be shown inside feature component. Best
@ -139,6 +145,14 @@ interface Feature {
//
// e.g. `Learn more`, `Visit page`, etc.
linkText?: string
// Link rel attribute for the `link` option.
//
// e.g. `external`
rel?: string
// Link target attribute for the `link` option.
target?: string
}
type FeatureIcon =
@ -152,3 +166,30 @@ type FeatureIcon =
height: string
}
```
## Markdown Content
You can add additional content to your site's homepage just by adding Markdown below the `---` frontmatter divider.
````md
---
layout: home
hero:
name: VitePress
text: Vite & Vue powered static site generator.
---
## Getting Started
You can get started using VitePress right away using `npx`!
```sh
npm init
npx vitepress init
```
````
::: info
VitePress didn't always auto-style the extra content of the `layout: home` page. To revert to older behavior, you can add `markdownStyles: false` to the frontmatter.
:::

@ -2,6 +2,10 @@
The update time of the last content will be displayed in the lower right corner of the page. To enable it, add `lastUpdated` options to your config.
::: tip
You need to commit the markdown file to see the updated time.
:::
## Site-Level Config
```js
@ -20,3 +24,4 @@ lastUpdated: false
---
```
Also refer [Default Theme: Last Updated](./default-theme-config#lastupdated) for more details. Any truthy value at theme-level will also enable the feature unless explicitly disabled at site or page level.

@ -36,3 +36,27 @@ Option `home` will generate templated "Homepage". In this layout, you can set ex
## No Layout
If you don't want any layout, you can pass `layout: false` through frontmatter. This option is helpful if you want a fully-customizable landing page (without any sidebar, navbar, or footer by default).
## Custom Layout
You can also use a custom layout:
```md
---
layout: foo
---
```
This will look for a component named `foo` registered in context. For example, you can register your component globally in `.vitepress/theme/index.ts`:
```ts
import DefaultTheme from 'vitepress/theme'
import Foo from './Foo.vue'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('foo', Foo)
}
}
```

@ -160,3 +160,55 @@ export default {
## Social Links
Refer [`socialLinks`](./default-theme-config#sociallinks).
## Custom Components
You can include custom components in the navigation bar by using the `component` option. The `component` key should be the Vue component name, and must be registered globally using [Theme.enhanceApp](../guide/custom-theme#theme-interface).
```js [.vitepress/config.js]
export default {
themeConfig: {
nav: [
{
text: 'My Menu',
items: [
{
component: 'MyCustomComponent',
// Optional props to pass to the component
props: {
title: 'My Custom Component'
}
}
]
},
{
component: 'AnotherCustomComponent'
}
]
}
}
```
Then, you need to register the component globally:
```js [.vitepress/theme/index.js]
import DefaultTheme from 'vitepress/theme'
import MyCustomComponent from './components/MyCustomComponent.vue'
import AnotherCustomComponent from './components/AnotherCustomComponent.vue'
/** @type {import('vitepress').Theme} */
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('MyCustomComponent', MyCustomComponent)
app.component('AnotherCustomComponent', AnotherCustomComponent)
}
}
```
Your component will be rendered in the navigation bar. VitePress will provide the following additional props to the component:
- `screenMenu`: an optional boolean indicating whether the component is inside mobile navigation menu
You can check an example in the e2e tests [here](https://github.com/vuejs/vitepress/tree/main/__tests__/e2e/.vitepress).

@ -0,0 +1,383 @@
---
outline: deep
---
# Search
## Local Search
VitePress supports fuzzy full-text search using a in-browser index thanks to [minisearch](https://github.com/lucaong/minisearch/). To enable this feature, simply set the `themeConfig.search.provider` option to `'local'` in your `.vitepress/config.ts` file:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'local'
}
}
})
```
Example result:
![screenshot of the search modal](/search.png)
Alternatively, you can use [Algolia DocSearch](#algolia-search) or some community plugins like:
- <https://www.npmjs.com/package/vitepress-plugin-search>
- <https://www.npmjs.com/package/vitepress-plugin-pagefind>
- <https://www.npmjs.com/package/@orama/plugin-vitepress>
### i18n {#local-search-i18n}
You can use a config like this to use multilingual search:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
locales: {
zh: { // make this `root` if you want to translate the default locale
translations: {
button: {
buttonText: '搜索',
buttonAriaLabel: '搜索'
},
modal: {
displayDetails: '显示详细列表',
resetButtonTitle: '重置搜索',
backButtonTitle: '关闭搜索',
noResultsText: '没有结果',
footer: {
selectText: '选择',
selectKeyAriaLabel: '输入',
navigateText: '导航',
navigateUpKeyAriaLabel: '上箭头',
navigateDownKeyAriaLabel: '下箭头',
closeText: '关闭',
closeKeyAriaLabel: 'esc'
}
}
}
}
}
}
}
}
})
```
### miniSearch options
You can configure MiniSearch like this:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
miniSearch: {
/**
* @type {Pick<import('minisearch').Options, 'extractField' | 'tokenize' | 'processTerm'>}
*/
options: {
/* ... */
},
/**
* @type {import('minisearch').SearchOptions}
* @default
* { fuzzy: 0.2, prefix: true, boost: { title: 4, text: 2, titles: 1 } }
*/
searchOptions: {
/* ... */
}
}
}
}
}
})
```
Learn more in [MiniSearch docs](https://lucaong.github.io/minisearch/classes/MiniSearch.MiniSearch.html).
### Custom content renderer
You can customize the function used to render the markdown content before indexing it:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
/**
* @param {string} src
* @param {import('vitepress').MarkdownEnv} env
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// return html string
}
}
}
}
})
```
This function will be stripped from client-side site data, so you can use Node.js APIs in it.
#### Example: Excluding pages from search
You can exclude pages from search by adding `search: false` to the frontmatter of the page. Alternatively:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.search === false) return ''
if (env.relativePath.startsWith('some/path')) return ''
return html
}
}
}
}
})
```
::: warning Note
In case a custom `_render` function is provided, you need to handle the `search: false` frontmatter yourself. Also, the `env` object won't be completely populated before `md.renderAsync` is called, so any checks on optional `env` properties like `frontmatter` should be done after that.
:::
#### Example: Transforming content - adding anchors
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.title)
return await md.renderAsync(`# ${env.frontmatter.title}`) + html
return html
}
}
}
}
})
```
## Algolia Search
VitePress supports searching your docs site using [Algolia DocSearch](https://docsearch.algolia.com/docs/what-is-docsearch). Refer their getting started guide. In your `.vitepress/config.ts` you'll need to provide at least the following to make it work:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...'
}
}
}
})
```
### i18n {#algolia-search-i18n}
You can use a config like this to use multilingual search:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
}
}
}
}
}
}
}
}
})
```
[These options](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) can be overridden. Refer official Algolia docs to learn more about them.
### Crawler Config
Here is an example config based on what this site uses:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```

@ -53,12 +53,12 @@ const members = [
Say hello to our awesome team.
<VPTeamMembers size="small" :members="members" />
<VPTeamMembers size="small" :members />
```
The above will display a team member in card looking element. It should display something similar to below.
<VPTeamMembers size="small" :members="members" />
<VPTeamMembers size="small" :members />
`<VPTeamMembers>` component comes in 2 different sizes, `small` and `medium`. While it boils down to your preference, usually `small` size should fit better when used in doc page. Also, you may add more properties to each member such as adding "description" or "sponsor" button. Learn more about it in [`<VPTeamMembers>`](#vpteammembers).
@ -107,9 +107,7 @@ const members = [
team, some of whom have chosen to be featured below.
</template>
</VPTeamPageTitle>
<VPTeamMembers
:members="members"
/>
<VPTeamMembers :members />
</VPTeamPage>
```
@ -212,6 +210,9 @@ interface TeamMember {
// URL for the sponsor page for the member.
sponsor?: string
// Text for the sponsor link. Defaults to 'Sponsor'.
actionText?: string
}
```

@ -86,7 +86,7 @@ type HeadConfig =
The following frontmatter options are only applicable when using the default theme.
### layout <Badge type="info" text="default theme only" />
### layout
- Type: `doc | home | page`
- Default: `doc`
@ -103,15 +103,41 @@ layout: doc
---
```
### hero <Badge type="info" text="default theme only" /> <Badge type="info" text="Home page only" />
### hero <Badge type="info" text="home page only" />
Defines contents of home hero section when `layout` is set to `home`. More details in [Default Theme: Home Page](./default-theme-home-page).
### features <Badge type="info" text="default theme only" /> <Badge type="info" text="Home page only" />
### features <Badge type="info" text="home page only" />
Defines items to display in features section when `layout` is set to `home`. More details in [Default Theme: Home Page](./default-theme-home-page).
### aside <Badge type="info" text="default theme only" />
### navbar
- Type: `boolean`
- Default: `true`
Whether to display [navbar](./default-theme-nav).
```yaml
---
navbar: false
---
```
### sidebar
- Type: `boolean`
- Default: `true`
Whether to display [sidebar](./default-theme-sidebar).
```yaml
---
sidebar: false
---
```
### aside
- Type: `boolean | 'left'`
- Default: `true`
@ -128,19 +154,25 @@ aside: false
---
```
### outline <Badge type="info" text="default theme only" />
### outline
- Type: `number | [number, number] | 'deep' | false`
- Default: `2`
The levels of header in the outline to display for the page. It's same as [config.themeConfig.outline](./default-theme-config#outline), and it overrides the theme config.
The levels of header in the outline to display for the page. It's same as [config.themeConfig.outline.level](./default-theme-config#outline), and it overrides the value set in site-level config.
```yaml
---
outline: [2, 4]
---
```
### lastUpdated <Badge type="info" text="default theme only" />
### lastUpdated
- Type: `boolean`
- Type: `boolean | Date`
- Default: `true`
Whether to display [Last Updated](./default-theme-last-updated) text in the footer of the current page.
Whether to display [last updated](./default-theme-last-updated) text in the footer of the current page. If a datetime is specified, it will be displayed instead of the last git modified timestamp.
```yaml
---
@ -148,15 +180,61 @@ lastUpdated: false
---
```
### editLink <Badge type="info" text="default theme only" />
### editLink
- Type: `boolean`
- Default: `true`
Whether to display [Edit Link](./default-theme-edit-link) in the footer of the current page.
Whether to display [edit link](./default-theme-edit-link) in the footer of the current page.
```yaml
---
editLink: false
---
```
### footer
- Type: `boolean`
- Default: `true`
Whether to display [footer](./default-theme-footer).
```yaml
---
footer: false
---
```
### pageClass
- Type: `string`
Add extra class name to a specific page.
```yaml
---
pageClass: custom-page-class
---
```
Then you can customize styles of this specific page in `.vitepress/theme/custom.css` file:
```css
.custom-page-class {
/* page-specific styles */
}
```
### isHome
- Type: `boolean`
The default theme relies on checks like `frontmatter.layout === 'home'` to determine if the current page is the home page.\
This is useful when you want to force show the home page elements in a custom layout.
```yaml
---
isHome: true
---
```

@ -38,6 +38,10 @@ interface VitePressData<T = any> {
isDark: Ref<boolean>
dir: Ref<string>
localeIndex: Ref<string>
/**
* Current location hash
*/
hash: Ref<string>
}
interface PageData {
@ -86,8 +90,31 @@ Returns the VitePress router instance so you can programmatically navigate to an
```ts
interface Router {
/**
* Current route.
*/
route: Route
go: (href?: string) => Promise<void>
/**
* Navigate to a new URL.
*/
go: (to?: string) => Promise<void>
/**
* Called before the route changes. Return `false` to cancel the navigation.
*/
onBeforeRouteChange?: (to: string) => Awaitable<void | boolean>
/**
* Called before the page component is loaded (after the history state is updated).
* Return `false` to cancel the navigation.
*/
onBeforePageLoad?: (to: string) => Awaitable<void | boolean>
/**
* Called after the page component is loaded (before the page component is updated).
*/
onAfterPageLoad?: (to: string) => Awaitable<void>
/**
* Called after the route changes.
*/
onAfterRouteChange?: (to: string) => Awaitable<void>
}
```
@ -122,7 +149,7 @@ If you are using or demoing components that are not SSR-friendly (for example, c
</ClientOnly>
```
- Related: [SSR Compatibility](/guide/ssr-compat)
- Related: [SSR Compatibility](../guide/ssr-compat)
## `$frontmatter` <Badge type="info" text="template global" />

@ -10,7 +10,7 @@ Site config is where you can define the global settings of the site. App config
### Config Resolution
The config file is always resolved from `<root>/.vitepress/config.[ext]`, where `<root>` is your VitePress [project root](../guide/routing#root-and-source-directory), and `[ext]` is one of the supported file extensions. TypeScript is supported out of the box. Supported extensions include `.js`, `.ts`, `.cjs`, `.mjs`, `.cts`, and `.mts`.
The config file is always resolved from `<root>/.vitepress/config.[ext]`, where `<root>` is your VitePress [project root](../guide/routing#root-and-source-directory), and `[ext]` is one of the supported file extensions. TypeScript is supported out of the box. Supported extensions include `.js`, `.ts`, `.mjs`, and `.mts`.
It is recommended to use ES modules syntax in config files. The config file should default export an object:
@ -24,6 +24,62 @@ export default {
}
```
:::details Dynamic (Async) Config
If you need to dynamically generate the config, you can also default export a function. For example:
```ts
import { defineConfig } from 'vitepress'
export default async () => {
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
return defineConfig({
// app level config options
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// theme level config options
themeConfig: {
sidebar: [
...posts.map((post) => ({
text: post.name,
link: `/posts/${post.name}`
}))
]
}
})
}
```
You can also use top-level `await`. For example:
```ts
import { defineConfig } from 'vitepress'
const posts = await (await fetch('https://my-cms.com/blog-posts')).json()
export default defineConfig({
// app level config options
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
// theme level config options
themeConfig: {
sidebar: [
...posts.map((post) => ({
text: post.name,
link: `/posts/${post.name}`
}))
]
}
})
```
:::
### Config Intellisense
Using the `defineConfig` helper will provide TypeScript-powered intellisense for config options. Assuming your IDE supports it, this should work in both JavaScript and TypeScript.
@ -155,17 +211,56 @@ export default {
Additional elements to render in the `<head>` tag in the page HTML. The user-added tags are rendered before the closing `head` tag, after VitePress tags.
```ts
type HeadConfig =
| [string, Record<string, string>]
| [string, Record<string, string>, string]
```
#### Example: Adding a favicon
```ts
export default {
head: [['link', { rel: 'icon', href: '/favicon.ico' }]]
} // put favicon.ico in public directory, if base is set, use /base/favicon.ico
/* Would render:
<link rel="icon" href="/favicon.ico">
*/
```
#### Example: Adding Google Fonts
```ts
export default {
head: [
[
'link',
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' }
],
[
'link',
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }
// would render:
//
// <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
],
[
'link',
{ href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap', rel: 'stylesheet' }
]
]
}
/* Would render:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
*/
```
#### Example: Registering a service worker
```ts
export default {
head: [
[
'script',
{ id: 'register-sw' },
@ -174,24 +269,50 @@ export default {
navigator.serviceWorker.register('/sw.js')
}
})()`
// would render:
//
// <script id="register-sw">
// ;(() => {
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/sw.js')
// }
// })()
// </script>
]
]
}
/* Would render:
<script id="register-sw">
;(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
})()
</script>
*/
```
#### Example: Using Google Analytics
```ts
type HeadConfig =
| [string, Record<string, string>]
| [string, Record<string, string>, string]
export default {
head: [
[
'script',
{ async: '', src: 'https://www.googletagmanager.com/gtag/js?id=TAG_ID' }
],
[
'script',
{},
`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'TAG_ID');`
]
]
}
/* Would render:
<script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'TAG_ID');
</script>
*/
```
### lang
@ -239,7 +360,7 @@ Enabling this may require additional configuration on your hosting platform. For
- Type: `Record<string, string>`
Defines custom directory <-> URL mappings. See [Routing: Route Rewrites](../guide/routing#route-rewrites) for more details.
Defines custom directory &lt;-&gt; URL mappings. See [Routing: Route Rewrites](../guide/routing#route-rewrites) for more details.
```ts
export default {
@ -266,7 +387,7 @@ export default {
### srcExclude
- Type: `string`
- Type: `string[]`
- Default: `undefined`
A [glob pattern](https://github.com/mrmlnc/fast-glob#pattern-syntax) for matching markdown files that should be excluded as source content.
@ -290,6 +411,19 @@ export default {
}
```
### assetsDir
- Type: `string`
- Default: `assets`
Specify the directory to nest generated assets under. The path should be inside [`outDir`](#outdir) and is resolved relative to it.
```ts
export default {
assetsDir: 'static'
}
```
### cacheDir
- Type: `string`
@ -337,6 +471,13 @@ export default {
}
```
### metaChunk <Badge type="warning" text="experimental" />
- Type: `boolean`
- Default: `false`
When set to `true`, extract pages metadata to a separate JavaScript chunk instead of inlining it in the initial HTML. This makes each page's HTML payload smaller and makes the pages metadata cacheable, thus reducing server bandwidth when you have many pages in the site.
### mpa <Badge type="warning" text="experimental" />
- Type: `boolean`
@ -348,7 +489,7 @@ When set to `true`, the production app will be built in [MPA Mode](../guide/mpa-
### appearance
- Type: `boolean | 'dark'`
- Type: `boolean | 'dark' | 'force-dark' | 'force-auto' | import('@vueuse/core').UseDarkOptions`
- Default: `true`
Whether to enable dark mode (by adding the `.dark` class to the `<html>` element).
@ -356,9 +497,13 @@ Whether to enable dark mode (by adding the `.dark` class to the `<html>` element
- If the option is set to `true`, the default theme will be determined by the user's preferred color scheme.
- If the option is set to `dark`, the theme will be dark by default, unless the user manually toggles it.
- If the option is set to `false`, users will not be able to toggle the theme.
- If the option is set to `'force-dark'`, the theme will always be dark and users will not be able to toggle it.
- If the option is set to `'force-auto'`, the theme will always be determined by the user's preferred color scheme and users will not be able to toggle it.
This option injects an inline script that restores users settings from local storage using the `vitepress-theme-appearance` key. This ensures the `.dark` class is applied before the page is rendered to avoid flickering.
`appearance.initialValue` can only be `'dark' | undefined`. Refs or getters are not supported.
### lastUpdated
- Type: `boolean`
@ -374,81 +519,15 @@ When using the default theme, enabling this option will display each page's last
- Type: `MarkdownOption`
Configure Markdown parser options. VitePress uses [Markdown-it](https://github.com/markdown-it/markdown-it) as the parser, and [Shiki](https://shiki.matsu.io/) to highlight language syntax. Inside this option, you may pass various Markdown related options to fit your needs.
Configure Markdown parser options. VitePress uses [Markdown-it](https://github.com/markdown-it/markdown-it) as the parser, and [Shiki](https://github.com/shikijs/shiki) to highlight language syntax. Inside this option, you may pass various Markdown related options to fit your needs.
```js
export default {
markdown: {
theme: 'material-theme-palenight',
lineNumbers: true,
// adjust how header anchors are generated,
// useful for integrating with tools that use different conventions
anchor: {
slugify(str) {
return encodeURIComponent(str)
}
}
}
markdown: {...}
}
```
Below are all the options that you can have in this object:
```ts
interface MarkdownOptions extends MarkdownIt.Options {
// Custom theme for syntax highlighting.
// You can use an existing theme.
// See: https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes
// Or add your own theme.
// See: https://github.com/shikijs/shiki/blob/main/docs/themes.md#loading-theme
theme?:
| Shiki.IThemeRegistration
| { light: Shiki.IThemeRegistration; dark: Shiki.IThemeRegistration }
// Enable line numbers in code block.
lineNumbers?: boolean
// Add support for your own languages.
// https://github.com/shikijs/shiki/blob/main/docs/languages.md#supporting-your-own-languages-with-shiki
languages?: Shiki.ILanguageRegistration
// markdown-it-anchor plugin options.
// See: https://github.com/valeriangalliat/markdown-it-anchor#usage
anchor?: anchorPlugin.AnchorOptions
// markdown-it-attrs plugin options.
// See: https://github.com/arve0/markdown-it-attrs
attrs?: {
leftDelimiter?: string
rightDelimiter?: string
allowedAttributes?: string[]
disable?: boolean
}
// specify default language for syntax highlighter
defaultHighlightLang?: string
// @mdit-vue/plugin-frontmatter plugin options.
// See: https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter#options
frontmatter?: FrontmatterPluginOptions
// @mdit-vue/plugin-headers plugin options.
// See: https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers#options
headers?: HeadersPluginOptions
// @mdit-vue/plugin-sfc plugin options.
// See: https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-sfc#options
sfc?: SfcPluginOptions
// @mdit-vue/plugin-toc plugin options.
// See: https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-toc#options
toc?: TocPluginOptions
// Configure the Markdown-it instance.
config?: (md: MarkdownIt) => void
}
```
Check the [type declaration and jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/markdown.ts) for all the options available.
### vite
@ -530,7 +609,7 @@ interface SSGContext {
`transformHead` is a build hook to transform the head before generating each page. It will allow you to add head entries that cannot be statically added to your VitePress config. You only need to return extra entries, they will be merged automatically with the existing ones.
::: warning
Don't mutate anything inside the `ctx`.
Don't mutate anything inside the `context`.
:::
```ts
@ -555,14 +634,52 @@ interface TransformContext {
}
```
Note that this hook is only called when generating the site statically. It is not called during dev. If you need to add dynamic head entries during dev, you can use the [`transformPageData`](#transformpagedata) hook instead:
```ts
export default {
transformPageData(pageData) {
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'meta',
{
name: 'og:title',
content:
pageData.frontmatter.layout === 'home'
? `VitePress`
: `${pageData.title} | VitePress`
}
])
}
}
```
#### Example: Adding a canonical URL `<link>`
```ts
export default {
transformPageData(pageData) {
const canonicalUrl = `https://example.com/${pageData.relativePath}`
.replace(/index\.md$/, '')
.replace(/\.md$/, '.html')
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'link',
{ rel: 'canonical', href: canonicalUrl }
])
}
}
```
### transformHtml
- Type: `(code: string, id: string, ctx: TransformContext) => Awaitable<string | void>`
- Type: `(code: string, id: string, context: TransformContext) => Awaitable<string | void>`
`transformHtml` is a build hook to transform the content of each page before saving to disk.
::: warning
Don't mutate anything inside the `ctx`. Also, modifying the html content may cause hydration problems in runtime.
Don't mutate anything inside the `context`. Also, modifying the html content may cause hydration problems in runtime.
:::
```ts
@ -575,12 +692,12 @@ export default {
### transformPageData
- Type: `(pageData: PageData, ctx: TransformPageContext) => Awaitable<Partial<PageData> | { [key: string]: any } | void>`
- Type: `(pageData: PageData, context: TransformPageContext) => Awaitable<Partial<PageData> | { [key: string]: any } | void>`
`transformPageData` is a hook to transform the `pageData` of each page. You can directly mutate `pageData` or return changed values which will be merged into PageData.
`transformPageData` is a hook to transform the `pageData` of each page. You can directly mutate `pageData` or return changed values which will be merged into the page data.
::: warning
Don't mutate anything inside the `ctx`.
Don't mutate anything inside the `context` and be careful that this might impact the performance of dev server, especially if you have some network requests or heavy computations (like generating images) in the hook. You can check for `process.env.NODE_ENV === 'production'` for conditional logic.
:::
```ts

@ -0,0 +1,222 @@
import { createRequire } from 'module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'es-CO',
description: 'Generador de Sitios Estaticos desarrollado con Vite y Vue.',
themeConfig: {
nav: nav(),
search: { options: searchOptions() },
sidebar: {
'/es/guide/': { base: '/es/guide/', items: sidebarGuide() },
'/es/reference/': { base: '/es/reference/', items: sidebarReference() }
},
editLink: {
pattern: 'https://github.com/vuejs/vitepress/edit/main/docs/:path',
text: 'Editar esta página en GitHub'
},
footer: {
message: 'Liberado bajo la licencia MIT',
copyright: `Derechos reservados © 2019-${new Date().getFullYear()} Evan You`
},
docFooter: {
prev: 'Anterior',
next: 'Siguiente'
},
outline: {
label: 'En esta página'
},
lastUpdated: {
text: 'Actualizado en'
},
notFound: {
title: 'PÁGINA NO ENCONTRADA',
quote:
'Pero si no cambias de dirección y sigues buscando, podrías terminar donde te diriges.',
linkLabel: 'ir a inicio',
linkText: 'Llévame a casa'
},
langMenuLabel: 'Cambiar Idioma',
returnToTopLabel: 'Volver arriba',
sidebarMenuLabel: 'Menu Lateral',
darkModeSwitchLabel: 'Tema Oscuro',
lightModeSwitchTitle: 'Cambiar a modo claro',
darkModeSwitchTitle: 'Cambiar a modo oscuro',
skipToContentLabel: 'Saltar al contenido'
}
})
function nav(): DefaultTheme.NavItem[] {
return [
{
text: 'Guia',
link: '/es/guide/what-is-vitepress',
activeMatch: '/es/guide/'
},
{
text: 'Referencia',
link: '/es/reference/site-config',
activeMatch: '/es/reference/'
},
{
text: pkg.version,
items: [
{
text: 'Registro de cambios',
link: 'https://github.com/vuejs/vitepress/blob/main/CHANGELOG.md'
},
{
text: 'Contribuir',
link: 'https://github.com/vuejs/vitepress/blob/main/.github/contributing.md'
}
]
}
]
}
function sidebarGuide(): DefaultTheme.SidebarItem[] {
return [
{
text: 'Introducción',
collapsed: false,
items: [
{ text: 'Qué es VitePress', link: 'what-is-vitepress' },
{ text: 'Iniciando', link: 'getting-started' },
{ text: 'Enrutamiento', link: 'routing' },
{ text: 'Despliegue', link: 'deploy' }
]
},
{
text: 'Escribiendo',
collapsed: false,
items: [
{ text: 'Extensiones Markdown', link: 'markdown' },
{ text: 'Manejo de assets', link: 'asset-handling' },
{ text: 'Frontmatter', link: 'frontmatter' },
{ text: 'Usando Vue en Markdown', link: 'using-vue' },
{ text: 'Internacionalización', link: 'i18n' }
]
},
{
text: 'Pesonalización',
collapsed: false,
items: [
{ text: 'Usando un tema personalizado', link: 'custom-theme' },
{
text: 'Extendiendo el tema por defecto',
link: 'extending-default-theme'
},
{
text: 'Carga de datos en tiempo de compilación',
link: 'data-loading'
},
{ text: 'Compatibilidad SSR', link: 'ssr-compat' },
{ text: 'Conectando a un CMS', link: 'cms' }
]
},
{
text: 'Experimental',
collapsed: false,
items: [
{ text: 'Modo MPA', link: 'mpa-mode' },
{ text: 'Generación de Sitemap', link: 'sitemap-generation' }
]
},
{
text: 'Configuración y Referencia del API',
base: '/es/reference/',
link: 'site-config'
}
]
}
function sidebarReference(): DefaultTheme.SidebarItem[] {
return [
{
text: 'Referencia',
items: [
{ text: 'Configuración del sitio', link: 'site-config' },
{ text: 'Configuración Frontmatter', link: 'frontmatter-config' },
{ text: 'API de tiempo de ejecución', link: 'runtime-api' },
{ text: 'CLI', link: 'cli' },
{
text: 'Tema por defecto',
base: '/es/reference/default-theme-',
items: [
{ text: 'Visión general', link: 'config' },
{ text: 'Navegación', link: 'nav' },
{ text: 'Barra Lateral', link: 'sidebar' },
{ text: 'Página Inicial', link: 'home-page' },
{ text: 'Pie de página', link: 'footer' },
{ text: 'Layout', link: 'layout' },
{ text: 'Distintivo', link: 'badge' },
{ text: 'Página del equipo', link: 'team-page' },
{ text: 'Links Anterior / Siguiente', link: 'prev-next-links' },
{ text: 'Editar Link', link: 'edit-link' },
{ text: 'Sello temporal de actualización', link: 'last-updated' },
{ text: 'Busqueda', link: 'search' },
{ text: 'Carbon Ads', link: 'carbon-ads' }
]
}
]
}
]
}
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'Buscar documentos',
translations: {
button: {
buttonText: 'Buscar',
buttonAriaLabel: 'Buscar'
},
modal: {
searchBox: {
resetButtonTitle: 'Limpiar búsqueda',
resetButtonAriaLabel: 'Limpiar búsqueda',
cancelButtonText: 'Cancelar',
cancelButtonAriaLabel: 'Cancelar'
},
startScreen: {
recentSearchesTitle: 'Historial de búsqueda',
noRecentSearchesText: 'Ninguna búsqueda reciente',
saveRecentSearchButtonTitle: 'Guardar en el historial de búsqueda',
removeRecentSearchButtonTitle: 'Borrar del historial de búsqueda',
favoriteSearchesTitle: 'Favoritos',
removeFavoriteSearchButtonTitle: 'Borrar de favoritos'
},
errorScreen: {
titleText: 'No fue posible obtener resultados',
helpText: 'Verifique su conexión de red'
},
footer: {
selectText: 'Seleccionar',
navigateText: 'Navegar',
closeText: 'Cerrar',
searchByText: 'Busqueda por'
},
noResultsScreen: {
noResultsText: 'No fue posible encontrar resultados',
suggestedQueryText: 'Puede intentar una nueva búsqueda',
reportMissingResultsText:
'Deberian haber resultados para esa consulta?',
reportMissingResultsLinkText: 'Click para enviar feedback'
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save