Merge branch 'chroe/vite-3' into feature/mermaid

pull/930/head
DAIKI URATA 3 years ago
commit 9fd1900fd7

@ -48,7 +48,7 @@ The easiest way to start testing out VitePress is to tweak the VitePress docs. Y
$ pnpm run docs $ pnpm run docs
``` ```
After executing the above command, visit http://localhost:3000 and try modifying the source code. You'll get live update. After executing the above command, visit http://localhost:5173 and try modifying the source code. You'll get live update.
If you don't need docs site up and running, you may start VitePress local dev environment with `pnpm run dev`. If you don't need docs site up and running, you may start VitePress local dev environment with `pnpm run dev`.

@ -1,17 +1,18 @@
name: Release
on: on:
push: push:
tags: tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Create Release
jobs: jobs:
build: release:
name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout
uses: actions/checkout@master uses: actions/checkout@v3
- name: Create Release for Tag - name: Create Release for Tag
id: release_tag id: release_tag
uses: yyx990803/release-tag@master uses: yyx990803/release-tag@master

@ -5,17 +5,17 @@ on: [push]
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [14, 16] node-version: [14, 16, 18]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2
with:
version: 7.0.1
- name: Set node version to ${{ matrix.node_version }} - name: Set node version to ${{ matrix.node_version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3

2
.gitignore vendored

@ -10,3 +10,5 @@ dist
node_modules node_modules
pnpm-global pnpm-global
TODOs.md TODOs.md
.temp
*.tgz

@ -1,5 +1,3 @@
/docs
/examples
*.css *.css
*.md *.md
*.vue *.vue

@ -8,6 +8,7 @@ export default defineConfig({
description: 'Vite & Vue powered static site generator.', description: 'Vite & Vue powered static site generator.',
lastUpdated: true, lastUpdated: true,
cleanUrls: 'without-subfolders',
themeConfig: { themeConfig: {
nav: nav(), nav: nav(),
@ -58,9 +59,9 @@ function nav() {
{ {
text: 'Contributing', text: 'Contributing',
link: 'https://github.com/vuejs/vitepress/blob/main/.github/contributing.md' link: 'https://github.com/vuejs/vitepress/blob/main/.github/contributing.md'
}, }
], ]
}, }
] ]
} }

@ -115,7 +115,7 @@ Below shows the the full option you may define within this object.
interface MarkdownOptions extends MarkdownIt.Options { interface MarkdownOptions extends MarkdownIt.Options {
// Syntax highlight theme for Shiki. // Syntax highlight theme for Shiki.
// See: https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes // See: https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes
theme?: Shiki.Theme | { light: Shiki.Theme, dark: Shiki.Theme } theme?: Shiki.Theme | { light: Shiki.Theme; dark: Shiki.Theme }
// Enable line numbers in code block. // Enable line numbers in code block.
lineNumbers?: boolean lineNumbers?: boolean
@ -179,3 +179,27 @@ export default {
} }
``` ```
## cleanUrls (Experimental)
- Type: `'disabled' | 'without-subfolders' | 'with-subfolders'`
- Default: `'disabled'`
Allows removing trailing `.html` from URLs and, optionally, generating clean directory structure. Available modes:
| Mode | Page | Generated Page | URL |
| :--------------------: | :-------: | :---------------: | :---------: |
| `'disabled'` | `/foo.md` | `/foo.html` | `/foo.html` |
| `'without-subfolders'` | `/foo.md` | `/foo.html` | `/foo` |
| `'with-subfolders'` | `/foo.md` | `/foo/index.html` | `/foo` |
::: warning
Enabling this may require additional configuration on your hosting platform. For it to work, your server must serve the generated page on requesting the URL (see above table) **without a redirect**.
:::
```ts
export default {
cleanUrls: 'with-subfolders'
}
```

@ -130,6 +130,22 @@ interface SidebarItem {
} }
``` ```
## outlineTitle
- 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.
```js
export default {
themeConfig: {
outlineTitle: 'In hac pagina'
}
}
```
## socialLinks ## socialLinks
- Type: `SocialLink` - Type: `SocialLink`

@ -2,279 +2,195 @@
The following guides are based on some shared assumptions: The following guides are based on some shared assumptions:
- You are placing your docs inside the `docs` directory of your project; - You are placing your docs inside the `docs` directory of your project.
- You are using the default build output location (`.vitepress/dist`); - You are using the default build output location (`.vitepress/dist`).
- VitePress is installed as a local dependency in your project, and you have setup the following npm scripts: - VitePress is installed as a local dependency in your project, and you have set up the following scripts in your `package.json`:
```json ```json
{ {
"scripts": { "scripts": {
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
"docs:serve": "vitepress serve docs" "docs:serve": "vitepress serve docs"
}
} }
} ```
```
## Build and test locally ::: tip
You may run `yarn docs:build` command to build the docs.
```bash
$ yarn docs:build
```
By default, the build output will be placed at `.vitepress/dist`. You may deploy this `dist` folder to any of your preferred platforms.
Once you've built the docs, you may test them locally by running `yarn docs:serve` command.
```bash
$ yarn docs:build
$ yarn docs:serve
```
The `serve` command will boot up local static web server that serves the files from `.vitepress/dist` at `http://localhost:5000`. It's an easy way to check if the production build looks OK in your local environment.
You may configure the port of the server by passing `--port` flag as an argument. If your site is to be served at a subdirectory (`https://example.com/subdir/`), then you have to set `'/subdir/'` as the [`base`](../config/app-configs#base) in your `docs/.vitepress/config.js`.
```json :::
{
"scripts": {
"docs:serve": "vitepress serve docs --port 8080"
}
}
```
Now the `docs:serve` method will launch the server at `http://localhost:8080`. ## Build and Test Locally
## GitHub Pages - You may run this command to build the docs:
1. Set the correct `base` in `docs/.vitepress/config.js`. ```sh
$ yarn docs:build
```
If you are deploying to `https://<USERNAME>.github.io/`, you can omit `base` as it defaults to `'/'`. - Once you've built the docs, you can test them locally by running:
If you are deploying to `https://<USERNAME>.github.io/<REPO>/`, for example your repository is at `https://github.com/<USERNAME>/<REPO>`, then set `base` to `'/<REPO>/'`. ```sh
$ yarn docs:serve
```
2. Inside your project, create `deploy.sh` with the following content (with highlighted lines uncommented appropriately), and run it to deploy: The `serve` command will boot up a local static web server that will serve the files from `.vitepress/dist` at `http://localhost:4173`. It's an easy way to check if the production build looks fine in your local environment.
```bash{13,20,23} - You can configure the port of the server by passing `--port` as an argument.
#!/usr/bin/env sh
# abort on errors ```json
set -e {
"scripts": {
"docs:serve": "vitepress serve docs --port 8080"
}
}
```
# build Now the `docs:serve` method will launch the server at `http://localhost:8080`.
npm run docs:build
# navigate into the build output directory ## Netlify, Vercel, AWS Amplify, Cloudflare Pages, Render
cd docs/.vitepress/dist
# if you are deploying to a custom domain Set up a new project and change these settings using your dashboard:
# echo 'www.example.com' > CNAME
git init - **Build Command:** `yarn docs:build`
git add -A - **Output Directory:** `docs/.vitepress/dist`
git commit -m 'deploy' - **Node Version:** `14` (or above, by default it usually will be 14 or 16, but on Cloudflare Pages the default is still 12, so you may need to [change that](https://developers.cloudflare.com/pages/platform/build-configuration/))
# if you are deploying to https://<USERNAME>.github.io ::: warning
# git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git main 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.
:::
# if you are deploying to https://<USERNAME>.github.io/<REPO> ## GitHub Pages
# git push -f git@github.com:<USERNAME>/<REPO>.git main:gh-pages
cd - ### Using GitHub Actions
```
::: tip 1. Create a file named `deploy.yml` inside `.github/workflow` directory of your project with the following content:
You can also run the above script in your CI setup to enable automatic deployment on each push.
:::
## GitHub Pages and Travis CI ```yaml
name: Deploy
1. Set the correct `base` in `docs/.vitepress/config.js`. on:
push:
branches:
- main
If you are deploying to `https://<USERNAME or GROUP>.github.io/`, you can omit `base` as it defaults to `'/'`. jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn
- run: yarn install --frozen-lockfile
If you are deploying to `https://<USERNAME or GROUP>.github.io/<REPO>/`, for example your repository is at `https://github.com/<USERNAME>/<REPO>`, then set `base` to `'/<REPO>/'`. - name: Build
run: yarn docs:build
2. Create a file named `.travis.yml` in the root of your project. - name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: docs/.vitepress/dist
```
3. Run `yarn` or `npm install` locally and commit the generated lockfile (that is `yarn.lock` or `package-lock.json`). 2. Now commit your code and push it to the `main` branch.
4. Use the GitHub Pages deploy provider template, and follow the [Travis CI documentation](https://docs.travis-ci.com/user/deployment/pages). 3. Wait for actions to complete. Then select `gh-pages` branch as GitHub Pages source in your repository settings. Now your docs will automatically deploy each time you push.
```yaml ## GitLab Pages
language: node_js
node_js:
- lts/*
install:
- yarn install # npm ci
script:
- yarn docs:build # npm run docs:build
deploy:
provider: pages
skip_cleanup: true
local_dir: docs/.vitepress/dist
# A token generated on GitHub allowing Travis to push code on you repository.
# Set in the Travis settings page of your repository, as a secure variable.
github_token: $GITHUB_TOKEN
keep_history: true
on:
branch: main
```
## GitLab Pages and GitLab CI ### Using GitLab CI
1. Set the correct `base` in `docs/.vitepress/config.js`. 1. Set the correct `base` in `docs/.vitepress/config.js`.
If you are deploying to `https://<USERNAME or GROUP>.gitlab.io/`, you can omit `base` as it defaults to `'/'`. If you are deploying to `https://<USERNAME or GROUP>.gitlab.io/`, you can omit `base` as it defaults to `'/'`.
If you are deploying to `https://<USERNAME or GROUP>.gitlab.io/<REPO>/`, for example your repository is at `https://gitlab.com/<USERNAME>/<REPO>`, then set `base` to `'/<REPO>/'`. If you are deploying to `https://<USERNAME or GROUP>.gitlab.io/<REPO>/` (your repository is at `https://gitlab.com/<USERNAME>/<REPO>`), then set `base` to `'/<REPO>/'`.
2. Set `outDir` in `.vitepress/config.js` to `../public`. 2. Set `outDir` in `docs/.vitepress/config.js` to `../public`.
3. Create a file called `.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: 3. Create a file called `.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 ```yaml
image: node:16 image: node:16
pages: pages:
cache: cache:
paths: paths:
- node_modules/ - node_modules/
script: script:
- yarn install # npm install - yarn install
- yarn docs:build # npm run docs:build - yarn docs:build
artifacts: artifacts:
paths: paths:
- public - public
only: only:
- main - main
``` ```
## Netlify ## Azure Static Web Apps
1. On [Netlify](https://www.netlify.com/), setup up a new project from GitHub with the following settings: 1. Follow the [official documentation](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration).
- **Build Command:** `vitepress build docs` or `yarn docs:build` or `npm run docs:build` 2. Set these values in your configuration file (and remove the ones you don't require, like `api_location`):
- **Publish directory:** `docs/.vitepress/dist`
2. Hit the deploy button. - **`app_location`**: `/`
- **`output_location`**: `docs/.vitepress/dist`
- **`app_build_command`**: `yarn docs:build`
## Google Firebase ## Firebase
1. Make sure you have [firebase-tools](https://www.npmjs.com/package/firebase-tools) installed. 1. Create `firebase.json` and `.firebaserc` at the root of your project:
2. Create `firebase.json` and `.firebaserc` at the root of your project with the following content: `firebase.json`:
`firebase.json`: ```json
{
"hosting": {
"public": "docs/.vitepress/dist",
"ignore": []
}
}
```
```json `.firebaserc`:
{
"hosting": {
"public": "./docs/.vitepress/dist",
"ignore": []
}
}
```
`.firebaserc`: ```json
{
"projects": {
"default": "<YOUR_FIREBASE_ID>"
}
}
```
```js 2. After running `yarn docs:build`, run this command to deploy:
{
"projects": {
"default": "<YOUR_FIREBASE_ID>"
}
}
```
3. After running `yarn docs:build` or `npm run docs:build`, deploy using the command `firebase deploy`. ```sh
firebase deploy
```
## Surge ## Surge
1. First install [surge](https://www.npmjs.com/package/surge), if you havent already. 1. After running `yarn docs:build`, run this command to deploy:
2. Run `yarn docs:build` or `npm run docs:build`.
3. Deploy to surge by typing `surge docs/.vitepress/dist`.
You can also deploy to a [custom domain](https://surge.sh/help/adding-a-custom-domain) by adding `surge docs/.vitepress/dist yourdomain.com`. ```sh
npx surge docs/.vitepress/dist
```
## Heroku ## Heroku
1. Install [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli). 1. Follow documentation and guide given in [`heroku-buildpack-static`](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-static).
2. Create a Heroku account by [signing up](https://signup.heroku.com).
3. Run `heroku login` and fill in your Heroku credentials:
```bash 2. Create a file called `static.json` in the root of your project with the below content:
$ heroku login
```
4. Create a file called `static.json` in the root of your project with the below content: ```json
{
`static.json`: "root": "docs/.vitepress/dist"
}
```json ```
{
"root": "./docs/.vitepress/dist"
}
```
This is the configuration of your site; read more at [heroku-buildpack-static](https://github.com/heroku/heroku-buildpack-static).
5. Set up your Heroku git remote:
```bash
# version change
$ git init
$ git add .
$ git commit -m "My site ready for deployment."
# creates a new app with a specified name
$ heroku apps:create example
# set buildpack for static sites
$ heroku buildpacks:set https://github.com/heroku/heroku-buildpack-static.git
```
6. Deploy your site:
```bash
# publish site
$ git push heroku main
# opens a browser to view the Dashboard version of Heroku CI
$ heroku open
```
## Vercel
To deploy your VitePress app with a [Vercel for Git](https://vercel.com/docs/concepts/git), make sure it has been pushed to a Git repository.
Go to https://vercel.com/new and import the project into Vercel using your Git of choice (GitHub, GitLab or BitBucket). Follow the wizard to select the project root with the project's `package.json` and override the build step using `yarn docs:build` or `npm run docs:build` and the output dir to be `./docs/.vitepress/dist`
![Override Vercel Configuration](../images/vercel-configuration.png)
After your project has been imported, all subsequent pushes to branches will generate Preview Deployments, and all changes made to the Production Branch (commonly "main") will result in a Production Deployment.
Once deployed, you will get a URL to see your app live, such as the following: https://vitepress.vercel.app
## Layer0 ## Layer0
See [Creating and Deploying a VitePress App with Layer0](https://docs.layer0.co/guides/vitepress). Refer [Creating and Deploying a VitePress App with Layer0](https://docs.layer0.co/guides/vitepress).
## Cloudflare Pages
1. Go to [Cloudflare dashboard](https://dash.cloudflare.com/) > Account Home > Pages and selecting **Create a project**.
2. You will see three options, just select first **Connect to a git provider**.
3. Click Connect GitHub or Connect GitLab. Then select the repo you want to deploy.
4. Set up build docs command, like `npm run build` or `npm run docs:build`.
5. Now deploy, you will get a domain like `my-project.pages.dev`.
::: warning Do not Auto Minify HTML
If you want or are using Cloudflare's Auto minify feature, you should not check the html box.
With Auto Minify, Cloudflare will automatically remove the comments in the html file, however, html comments for Vue has meanings. For example, it works as a placeholder for `v-if`.
If it gets removed, then you will probably see a hydration mismatch error.
:::

@ -31,26 +31,13 @@ $ yarn add --dev vitepress vue
::: details Getting missing peer deps warnings? ::: details Getting missing peer deps warnings?
`@docsearch/js` has certain issues with its peer dependencies. If you see some commands failing due to them, you can try this workaround for now: `@docsearch/js` has certain issues with its peer dependencies. If you see some commands failing due to them, you can try this workaround for now:
On Yarn v2/v3, add this inside your rc file (`.yarnrc.yml` by default): If using PNPM, add this in your `package.json`:
```yaml
packageExtensions:
'@docsearch/react@*':
peerDependenciesMeta:
'@types/react':
optional: true
'react':
optional: true
'react-dom':
optional: true
```
On PNPM, add this in your `package.json`:
```json ```json
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [ "ignoreMissing": [
"@algolia/client-search",
"@types/react", "@types/react",
"react", "react",
"react-dom" "react-dom"
@ -89,7 +76,7 @@ Serve the documentation site in the local server.
$ yarn docs:dev $ yarn docs:dev
``` ```
VitePress will start a hot-reloading development server at `http://localhost:3000`. VitePress will start a hot-reloading development server at `http://localhost:5173`.
## Step. 4: Add more pages ## Step. 4: Add more pages
@ -103,7 +90,7 @@ Let's add another page to the site. Create a file name `getting-started.md` alon
└─ package.json └─ package.json
``` ```
Then, try to access `http://localhost:3000/getting-started` and you should see the content of `getting-started` is shown. Then, try to access `http://localhost:5173/getting-started.html` and you should see the content of `getting-started` is shown.
This is how VitePress works basically. The directory structure corresponds with the URL path. You add files, and just try to access it. This is how VitePress works basically. The directory structure corresponds with the URL path. You add files, and just try to access it.

@ -388,6 +388,48 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
<!--lint enable strong-marker--> <!--lint enable strong-marker-->
## Markdown File Inclusion
You can include a markdown file in another markdown file like this:
**Input**
```md
# Docs
## Basics
<!--@include: ./parts/basics.md-->
```
**Part file** (`parts/basics.md`)
```md
Some getting started stuff.
### Configuration
Can be created using `.foorc.json`.
```
**Equivalent code**
```md
# Docs
## Basics
Some getting started stuff.
### Configuration
Can be created using `.foorc.json`.
```
::: 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.
:::
## Advanced Configuration ## 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`: 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`:

@ -12,7 +12,7 @@ If you're coming from VitePress 0.x version, there're several breaking changes d
- `children` key is now named `items`. - `children` key is now named `items`.
- Top level item may not contain `link` at the moment. We're planning to bring it back. - Top level item may not contain `link` at the moment. We're planning to bring it back.
- `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api. - `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api.
- For adding GitHub link with icon to the nav, use [Social Links](./theme-nav.html#navigation-links) feature. - For adding GitHub link with icon to the nav, use [Social Links](./theme-nav#navigation-links) feature.
- For adding "Edit this page" feature, use [Edit Link](./theme-edit-link) feature. - For adding "Edit this page" feature, use [Edit Link](./theme-edit-link) feature.
- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`. - `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`.
- `carbonAds.carbon` is changed to `carbonAds.code`. - `carbonAds.carbon` is changed to `carbonAds.code`.

@ -4,7 +4,7 @@
### Images ### Images
Unlike VuePress, VitePress handles [`base`](/guide/asset-handling.html#base-url) of your config automatically when you use static image. Unlike VuePress, VitePress handles [`base`](./asset-handling#base-url) of your config automatically when you use static image.
Hence, now you can render images without `img` tag. Hence, now you can render images without `img` tag.
@ -14,7 +14,7 @@ Hence, now you can render images without `img` tag.
``` ```
::: warning ::: warning
For dynamic images you still need `withBase` as shown in [Base URL guide](/guide/asset-handling.html#base-url). For dynamic images you still need `withBase` as shown in [Base URL guide](./asset-handling#base-url).
::: :::
Use `<img.*withBase\('(.*)'\).*alt="([^"]*)".*>` regex to find and replace it with `![$2]($1)` to replace all the images with `![](...)` syntax. Use `<img.*withBase\('(.*)'\).*alt="([^"]*)".*>` regex to find and replace it with `![$2]($1)` to replace all the images with `![](...)` syntax.

@ -38,6 +38,7 @@ interface Theme {
Layout: Component // Vue 3 component Layout: Component // Vue 3 component
NotFound?: Component NotFound?: Component
enhanceApp?: (ctx: EnhanceAppContext) => void enhanceApp?: (ctx: EnhanceAppContext) => void
setup?: () => void
} }
interface EnhanceAppContext { interface EnhanceAppContext {
@ -65,6 +66,11 @@ export default {
// router is VitePress' custom router. `siteData` is // router is VitePress' custom router. `siteData` is
// a `ref` of current site-level metadata. // a `ref` of current site-level metadata.
} }
setup() {
// this function will be executed inside VitePressApp's
// setup hook. all composition APIs are available here.
}
} }
``` ```
@ -201,3 +207,12 @@ Full list of slots available in the default theme layout:
- `home-hero-after` - `home-hero-after`
- `home-features-before` - `home-features-before`
- `home-features-after` - `home-features-after`
- Always:
- `layout-top`
- `layout-bottom`
- `nav-bar-title-before`
- `nav-bar-title-after`
- `nav-bar-content-before`
- `nav-bar-content-after`
- `nav-screen-content-before`
- `nav-screen-content-after`

@ -4,7 +4,7 @@ The Nav is the navigation bar displayed on top of the page. It contains the site
## Site Title and Logo ## Site Title and Logo
By default, nav shows the title of the site refferencing [`config.title`](../config/app-configs.html#title) value. If you would like to change what's displayed on nav, you may define custom text in `themeConfig.siteTitle` option. By default, nav shows the title of the site refferencing [`config.title`](../config/app-configs#title) value. If you would like to change what's displayed on nav, you may define custom text in `themeConfig.siteTitle` option.
```js ```js
export default { export default {
@ -114,7 +114,7 @@ export default {
### Customize link's "active" state ### Customize link's "active" state
Nav menu items will be highlighted when the current page is under the matching path. if you would like to customize the path to be mathced, define `activeMatch` property and regex as a string value. Nav menu items will be highlighted when the current page is under the matching path. if you would like to customize the path to be matched, define `activeMatch` property and regex as a string value.
```js ```js
export default { export default {

@ -60,9 +60,9 @@ The above will display a team member in card looking element. It should display
<VPTeamMembers size="small" :members="members" /> <VPTeamMembers size="small" :members="members" />
`<VPTeamMembers>` component comes in 2 different sizes, `small` and `medium`. While it boiles 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). `<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).
Embbeding team members in doc page is good for small size team where having dedicated full team page might be too much, or introducing partial members as a refference to documenation context. Embbeding team members in doc page is good for small size team where having dedicated full team page might be too much, or introducing partial members as a reference to documentation context.
If you have large number of members, or simply would like to have more space to show team members, consider [creating a full team page](#create-a-full-team-page). If you have large number of members, or simply would like to have more space to show team members, consider [creating a full team page](#create-a-full-team-page).
@ -217,7 +217,7 @@ interface TeamMember {
## `<VPTeamPage>` ## `<VPTeamPage>`
The root component when creating a full team page. It only accepts a single slot. It's will style all passed in team related components. The root component when creating a full team page. It only accepts a single slot. It will style all passed in team related components.
## `<VPTeamPageTitle>` ## `<VPTeamPageTitle>`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

@ -3,7 +3,7 @@
"version": "1.0.0-alpha.4", "version": "1.0.0-alpha.4",
"description": "Vite & Vue powered static site generator", "description": "Vite & Vue powered static site generator",
"type": "module", "type": "module",
"packageManager": "pnpm@7.1.7", "packageManager": "pnpm@7.5.0",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",
"exports": { "exports": {
@ -51,7 +51,7 @@
"dev-watch": "node scripts/watchAndCopy", "dev-watch": "node scripts/watchAndCopy",
"build": "run-s build-prepare build-client build-node", "build": "run-s build-prepare build-client build-node",
"build-prepare": "rimraf dist && node scripts/copyShared", "build-prepare": "rimraf dist && node scripts/copyShared",
"build-client": "tsc -p src/client && node scripts/copyClient", "build-client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient",
"build-node": "rollup --config rollup.config.ts --configPlugin esbuild", "build-node": "rollup --config rollup.config.ts --configPlugin esbuild",
"format": "prettier --check --write .", "format": "prettier --check --write .",
"format-fail": "prettier --check .", "format-fail": "prettier --check .",
@ -71,77 +71,77 @@
"ci-docs": "run-s docs-build" "ci-docs": "run-s docs-build"
}, },
"dependencies": { "dependencies": {
"@docsearch/css": "^3.0.0", "@docsearch/css": "^3.1.1",
"@docsearch/js": "^3.0.0", "@docsearch/js": "^3.1.1",
"@vitejs/plugin-vue": "^2.3.2", "@vitejs/plugin-vue": "^3.0.0-beta.1",
"@vue/devtools-api": "^6.1.4", "@vue/devtools-api": "^6.2.0",
"@vueuse/core": "^8.5.0", "@vueuse/core": "^8.9.1",
"body-scroll-lock": "^4.0.0-beta.0", "body-scroll-lock": "^4.0.0-beta.0",
"mermaid": "^9.1.3", "mermaid": "^9.1.3",
"shiki": "^0.10.1", "shiki": "^0.10.1",
"vite": "^2.9.7", "vite": "^3.0.0-beta.8",
"vue": "^3.2.33" "vue": "^3.2.37"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-alias": "^3.1.5", "@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-commonjs": "^22.0.1",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4", "@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
"@types/body-scroll-lock": "^3.1.0", "@types/body-scroll-lock": "^3.1.0",
"@types/compression": "^1.7.0", "@types/compression": "^1.7.2",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@types/fs-extra": "^9.0.11", "@types/fs-extra": "^9.0.13",
"@types/koa": "^2.13.1", "@types/koa": "^2.13.4",
"@types/koa-static": "^4.0.1", "@types/koa-static": "^4.0.2",
"@types/lru-cache": "^5.1.0", "@types/markdown-it": "^12.2.3",
"@types/markdown-it": "^12.0.1",
"@types/mermaid": "^8.2.9", "@types/mermaid": "^8.2.9",
"@types/micromatch": "^4.0.2", "@types/micromatch": "^4.0.2",
"@types/minimist": "^1.2.2", "@types/minimist": "^1.2.2",
"@types/node": "^15.6.1", "@types/node": "^18.0.3",
"@types/polka": "^0.5.3", "@types/polka": "^0.5.4",
"@types/prompts": "^2.0.14", "@types/prompts": "^2.0.14",
"chokidar": "^3.5.1", "chokidar": "^3.5.3",
"compression": "^1.7.4", "compression": "^1.7.4",
"conventional-changelog-cli": "^2.1.1", "conventional-changelog-cli": "^2.2.2",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"debug": "^4.3.2", "debug": "^4.3.4",
"diacritics": "^1.3.0", "diacritics": "^1.3.0",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"esbuild": "^0.14.0", "esbuild": "^0.14.48",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"execa": "^6.1.0", "execa": "^6.1.0",
"fast-glob": "^3.2.7", "fast-glob": "^3.2.11",
"fs-extra": "^10.0.0", "fs-extra": "^10.1.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lint-staged": "^11.0.0", "lint-staged": "^13.0.3",
"lru-cache": "^6.0.0", "lru-cache": "^7.12.0",
"markdown-it": "^12.3.2", "markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.4.1", "markdown-it-anchor": "^8.6.4",
"markdown-it-attrs": "^4.1.3", "markdown-it-attrs": "^4.1.4",
"markdown-it-container": "^3.0.0", "markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.0", "markdown-it-emoji": "^2.0.2",
"markdown-it-toc-done-right": "^4.2.0", "markdown-it-toc-done-right": "^4.2.0",
"micromatch": "^4.0.4", "micromatch": "^4.0.5",
"minimist": "^1.2.5", "minimist": "^1.2.6",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^5.4.0", "ora": "^5.4.1",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"polka": "^0.5.2", "polka": "^0.5.2",
"prettier": "^2.3.0", "prettier": "^2.7.1",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^2.56.3", "rollup": "^2.76.0",
"rollup-plugin-dts": "^4.2.2", "rollup-plugin-dts": "^4.2.2",
"rollup-plugin-esbuild": "^4.8.2", "rollup-plugin-esbuild": "^4.9.1",
"semver": "^7.3.5", "semver": "^7.3.7",
"simple-git-hooks": "^2.7.0", "simple-git-hooks": "^2.8.0",
"sirv": "^1.0.12", "sirv": "^2.0.2",
"supports-color": "^9.2.2", "supports-color": "^9.2.2",
"typescript": "^4.7.2", "typescript": "^4.7.4",
"vitest": "^0.14.2" "vitest": "^0.17.1",
"vue-tsc": "^0.38.3"
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {

File diff suppressed because it is too large Load Diff

@ -49,7 +49,7 @@ export function usePrefetch() {
return return
} }
const rIC = (window as any).requestIdleCallback || setTimeout const rIC = window.requestIdleCallback || setTimeout
let observer: IntersectionObserver | null = null let observer: IntersectionObserver | null = null
const observeLinks = () => { const observeLinks = () => {
@ -73,8 +73,8 @@ export function usePrefetch() {
}) })
rIC(() => { rIC(() => {
document.querySelectorAll('#app a').forEach((link) => { document.querySelectorAll<HTMLAnchorElement>('#app a').forEach((link) => {
const { target, hostname, pathname } = link as HTMLAnchorElement const { target, hostname, pathname } = link
const extMatch = pathname.match(/\.\w+$/) const extMatch = pathname.match(/\.\w+$/)
if (extMatch && extMatch[0] !== '.html') { if (extMatch && extMatch[0] !== '.html') {
return return

@ -2,11 +2,12 @@ import {
App, App,
createApp as createClientApp, createApp as createClientApp,
createSSRApp, createSSRApp,
defineComponent,
h, h,
onMounted, onMounted,
watch watch
} from 'vue' } from 'vue'
import Theme from '/@theme/index' import Theme from '../theme-default'
import { inBrowser, pathToFile } from './utils' import { inBrowser, pathToFile } from './utils'
import { Router, RouterSymbol, createRouter } from './router' import { Router, RouterSymbol, createRouter } from './router'
import { siteDataRef, useData } from './data' import { siteDataRef, useData } from './data'
@ -19,7 +20,7 @@ import { Mermaid } from './components/Mermaid'
const NotFound = Theme.NotFound || (() => '404 Not Found') const NotFound = Theme.NotFound || (() => '404 Not Found')
const VitePressApp = { const VitePressApp = defineComponent({
name: 'VitePressApp', name: 'VitePressApp',
setup() { setup() {
const { site } = useData() const { site } = useData()
@ -39,9 +40,11 @@ const VitePressApp = {
// in prod mode, enable intersectionObserver based pre-fetch // in prod mode, enable intersectionObserver based pre-fetch
usePrefetch() usePrefetch()
} }
if (Theme.setup) Theme.setup()
return () => h(Theme.Layout) return () => h(Theme.Layout)
} }
} })
export function createApp() { export function createApp() {
const router = newRouter() const router = newRouter()

@ -34,17 +34,20 @@ interface PageModule {
} }
export function createRouter( export function createRouter(
loadPageModule: (path: string) => PageModule | Promise<PageModule>, loadPageModule: (path: string) => Promise<PageModule>,
fallbackComponent?: Component fallbackComponent?: Component
): Router { ): Router {
const route = reactive(getDefaultRoute()) const route = reactive(getDefaultRoute())
function go(href: string = inBrowser ? location.href : '/') { function go(href: string = inBrowser ? location.href : '/') {
// ensure correct deep link so page refresh lands on correct files.
const url = new URL(href, fakeHost) const url = new URL(href, fakeHost)
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { if (siteDataRef.value.cleanUrls === 'disabled') {
url.pathname += '.html' // ensure correct deep link so page refresh lands on correct files.
href = url.pathname + url.search + url.hash // if cleanUrls is enabled, the server should handle this
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
url.pathname += '.html'
href = url.pathname + url.search + url.hash
}
} }
if (inBrowser) { if (inBrowser) {
// save scroll position before changing url // save scroll position before changing url
@ -60,16 +63,11 @@ export function createRouter(
const targetLoc = new URL(href, fakeHost) const targetLoc = new URL(href, fakeHost)
const pendingPath = (latestPendingPath = targetLoc.pathname) const pendingPath = (latestPendingPath = targetLoc.pathname)
try { try {
let page = loadPageModule(pendingPath) let page = await loadPageModule(pendingPath)
// only await if it returns a Promise - this allows sync resolution
// on initial render in SSR.
if ('then' in page && typeof page.then === 'function') {
page = await page
}
if (latestPendingPath === pendingPath) { if (latestPendingPath === pendingPath) {
latestPendingPath = null latestPendingPath = null
const { default: comp, __pageData } = page as PageModule const { default: comp, __pageData } = page
if (!comp) { if (!comp) {
throw new Error(`Invalid route component: ${comp}`) throw new Error(`Invalid route component: ${comp}`)
} }
@ -87,7 +85,7 @@ export function createRouter(
try { try {
target = document.querySelector( target = document.querySelector(
decodeURIComponent(targetLoc.hash) decodeURIComponent(targetLoc.hash)
) as HTMLElement )
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
} }
@ -101,7 +99,7 @@ export function createRouter(
} }
} }
} catch (err: any) { } catch (err: any) {
if (!err.message.match(/fetch/) && !href.match(/^[\\/]404\.html$/)) { if (!/fetch/.test(err.message) && !/^\/404(\.html|\/)?$/.test(href)) {
console.error(err) console.error(err)
} }
@ -187,7 +185,6 @@ export function useRouter(): Router {
if (!router) { if (!router) {
throw new Error('useRouter() is called without provider.') throw new Error('useRouter() is called without provider.')
} }
// @ts-ignore
return router return router
} }
@ -196,7 +193,7 @@ export function useRoute(): Route {
} }
function scrollTo(el: HTMLElement, hash: string, smooth = false) { function scrollTo(el: HTMLElement, hash: string, smooth = false) {
let target: Element | null = null let target: HTMLElement | null = null
try { try {
target = el.classList.contains('header-anchor') target = el.classList.contains('header-anchor')
@ -213,12 +210,12 @@ function scrollTo(el: HTMLElement, hash: string, smooth = false) {
document.querySelector(offset)!.getBoundingClientRect().bottom + 24 document.querySelector(offset)!.getBoundingClientRect().bottom + 24
} }
const targetPadding = parseInt( const targetPadding = parseInt(
window.getComputedStyle(target as HTMLElement).paddingTop, window.getComputedStyle(target).paddingTop,
10 10
) )
const targetTop = const targetTop =
window.scrollY + window.scrollY +
(target as HTMLElement).getBoundingClientRect().top - target.getBoundingClientRect().top -
offset + offset +
targetPadding targetPadding
// only smooth scroll if distance is smaller than screen height. // only smooth scroll if distance is smaller than screen height.

@ -12,4 +12,5 @@ export interface Theme {
Layout: Component Layout: Component
NotFound?: Component NotFound?: Component
enhanceApp?: (ctx: EnhanceAppContext) => void enhanceApp?: (ctx: EnhanceAppContext) => void
setup?: () => void
} }

@ -35,7 +35,8 @@ export function pathToFile(path: string): string {
// /foo/bar.html -> ./foo_bar.md // /foo/bar.html -> ./foo_bar.md
if (inBrowser) { if (inBrowser) {
const base = import.meta.env.BASE_URL const base = import.meta.env.BASE_URL
pagePath = pagePath.slice(base.length).replace(/\//g, '_') + '.md' pagePath =
(pagePath.slice(base.length).replace(/\//g, '_') || 'index') + '.md'
// client production build needs to account for page hash, which is // client production build needs to account for page hash, which is
// injected directly in the page's html // injected directly in the page's html
const pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()] const pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]

@ -14,9 +14,3 @@ declare module '@siteData' {
const data: SiteData const data: SiteData
export default data export default data
} }
// this module's typing is broken.
declare module '@docsearch/js' {
function docsearch<T = any>(props: T): void
export default docsearch
}

@ -29,7 +29,14 @@ provide('close-sidebar', closeSidebar)
<slot name="layout-top" /> <slot name="layout-top" />
<VPSkipLink /> <VPSkipLink />
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" /> <VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
<VPNav /> <VPNav>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
<template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
</VPNav>
<VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" /> <VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
<VPSidebar :open="isSidebarOpen" /> <VPSidebar :open="isSidebarOpen" />

@ -6,7 +6,7 @@ import { useRouter, useRoute, useData } from 'vitepress'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { theme } = useData() const { theme, site } = useData()
onMounted(() => { onMounted(() => {
initialize(theme.value.algolia) initialize(theme.value.algolia)
@ -29,14 +29,16 @@ function poll() {
}, 16) }, 16)
} }
type DocSearchProps = Parameters<typeof docsearch>[0]
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) { function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
// note: multi-lang search support is removed since the theme // note: multi-lang search support is removed since the theme
// doesn't support multiple locales as of now. // doesn't support multiple locales as of now.
const options = Object.assign({}, userOptions, { const options = Object.assign<{}, {}, DocSearchProps>({}, userOptions, {
container: '#docsearch', container: '#docsearch',
navigator: { navigator: {
navigate({ itemUrl }: { itemUrl: string }) { navigate({ itemUrl }) {
const { pathname: hitPathname } = new URL( const { pathname: hitPathname } = new URL(
window.location.origin + itemUrl window.location.origin + itemUrl
) )
@ -51,7 +53,7 @@ function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
} }
}, },
transformItems(items: any[]) { transformItems(items) {
return items.map((item) => { return items.map((item) => {
return Object.assign({}, item, { return Object.assign({}, item, {
url: getRelativePath(item.url) url: getRelativePath(item.url)
@ -59,9 +61,10 @@ function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
}) })
}, },
hitComponent({ hit, children }: { hit: any; children: any }) { // @ts-ignore
hitComponent({ hit, children }) {
const relativeHit = hit.url.startsWith('http') const relativeHit = hit.url.startsWith('http')
? getRelativePath(hit.url as string) ? getRelativePath(hit.url)
: hit.url : hit.url
return { return {
@ -117,7 +120,12 @@ function isSpecialClick(event: MouseEvent) {
function getRelativePath(absoluteUrl: string) { function getRelativePath(absoluteUrl: string) {
const { pathname, hash } = new URL(absoluteUrl) const { pathname, hash } = new URL(absoluteUrl)
return pathname + hash return (
pathname.replace(
/\.html$/,
site.value.cleanUrls !== 'disabled' ? '' : '.html'
) + hash
)
} }
</script> </script>

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { normalizeLink } from '../support/utils' import { normalizeLink } from '../support/utils'
import { EXTERNAL_URL_RE } from '../../shared'
const props = defineProps<{ const props = defineProps<{
tag?: string tag?: string
@ -15,7 +16,7 @@ const classes = computed(() => [
props.theme ?? 'brand' props.theme ?? 'brand'
]) ])
const isExternal = computed(() => props.href && /^[a-z]+:/i.test(props.href)) const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href))
const component = computed(() => { const component = computed(() => {
if (props.tag) { if (props.tag) {

@ -22,7 +22,7 @@ const resolvedHeaders = computed(() => {
function handleClick({ target: el }: Event) { function handleClick({ target: el }: Event) {
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1] const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.querySelector(id) as HTMLAnchorElement const heading = document.querySelector<HTMLAnchorElement>(id)
heading?.focus() heading?.focus()
} }
</script> </script>

@ -1,18 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Sponsors } from './VPSponsors.vue'
import type { Sponsor } from './VPSponsorsGrid.vue'
import VPSponsors from './VPSponsors.vue' import VPSponsors from './VPSponsors.vue'
export interface Sponsors {
tier?: string
size?: 'xmini' | 'mini' | 'small'
items: Sponsor[]
}
export interface Sponsor {
name: string
img: string
url: string
}
defineProps<{ defineProps<{
tier?: string tier?: string
size?: 'xmini' | 'mini' | 'small' size?: 'xmini' | 'mini' | 'small'
@ -22,11 +12,6 @@ defineProps<{
<template> <template>
<div class="VPDocAsideSponsors"> <div class="VPDocAsideSponsors">
<VPSponsors <VPSponsors mode="aside" :tier="tier" :size="size" :data="data" />
mode="aside"
:tier="tier"
:size="size"
:data="data"
/>
</div> </div>
</template> </template>

@ -2,13 +2,14 @@
import { computed } from 'vue' import { computed } from 'vue'
import { normalizeLink } from '../support/utils' import { normalizeLink } from '../support/utils'
import VPIconExternalLink from './icons/VPIconExternalLink.vue' import VPIconExternalLink from './icons/VPIconExternalLink.vue'
import { EXTERNAL_URL_RE } from '../../shared'
const props = defineProps<{ const props = defineProps<{
href?: string href?: string
noIcon?: boolean noIcon?: boolean
}>() }>()
const isExternal = computed(() => props.href && /^[a-z]+:/i.test(props.href)) const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href))
</script> </script>
<template> <template>

@ -13,8 +13,16 @@ provide('close-screen', closeScreen)
<template> <template>
<header class="VPNav" :class="{ 'no-sidebar' : !hasSidebar }"> <header class="VPNav" :class="{ 'no-sidebar' : !hasSidebar }">
<VPNavBar :is-screen-open="isScreenOpen" @toggle-screen="toggleScreen" /> <VPNavBar :is-screen-open="isScreenOpen" @toggle-screen="toggleScreen">
<VPNavScreen :open="isScreenOpen" /> <template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
<template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
</VPNavBar>
<VPNavScreen :open="isScreenOpen">
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
</VPNavScreen>
</header> </header>
</template> </template>

@ -23,15 +23,20 @@ const { hasSidebar } = useSidebar()
<template> <template>
<div class="VPNavBar" :class="{ 'has-sidebar' : hasSidebar }"> <div class="VPNavBar" :class="{ 'has-sidebar' : hasSidebar }">
<div class="container"> <div class="container">
<VPNavBarTitle /> <VPNavBarTitle>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
</VPNavBarTitle>
<div class="content"> <div class="content">
<slot name="nav-bar-content-before" />
<VPNavBarSearch class="search" /> <VPNavBarSearch class="search" />
<VPNavBarMenu class="menu" /> <VPNavBarMenu class="menu" />
<VPNavBarTranslations class="translations" /> <VPNavBarTranslations class="translations" />
<VPNavBarAppearance class="appearance" /> <VPNavBarAppearance class="appearance" />
<VPNavBarSocialLinks class="social-links" /> <VPNavBarSocialLinks class="social-links" />
<VPNavBarExtra class="extra" /> <VPNavBarExtra class="extra" />
<slot name="nav-bar-content-after" />
<VPNavBarHamburger <VPNavBarHamburger
class="hamburger" class="hamburger"
:active="isScreenOpen" :active="isScreenOpen"

@ -49,7 +49,7 @@ defineEmits<{
} }
.VPNavBarHamburger:hover .top { top: 0; left: 0; transform: translateX(4px); } .VPNavBarHamburger:hover .top { top: 0; left: 0; transform: translateX(4px); }
.VPNavBarHamburger:hover .middle { top: 6; left: 0; transform: translateX(0); } .VPNavBarHamburger:hover .middle { top: 6px; left: 0; transform: translateX(0); }
.VPNavBarHamburger:hover .bottom { top: 12px; left: 0; transform: translateX(8px); } .VPNavBarHamburger:hover .bottom { top: 12px; left: 0; transform: translateX(8px); }
.VPNavBarHamburger.active .top { top: 6px; transform: translateX(0) rotate(225deg); } .VPNavBarHamburger.active .top { top: 6px; transform: translateX(0) rotate(225deg); }

@ -10,9 +10,11 @@ const { hasSidebar } = useSidebar()
<template> <template>
<div class="VPNavBarTitle" :class="{ 'has-sidebar': hasSidebar }"> <div class="VPNavBarTitle" :class="{ 'has-sidebar': hasSidebar }">
<a class="title" :href="site.base"> <a class="title" :href="site.base">
<slot name="nav-bar-title-before" />
<VPImage class="logo" :image="theme.logo" /> <VPImage class="logo" :image="theme.logo" />
<template v-if="theme.siteTitle">{{ theme.siteTitle }}</template> <template v-if="theme.siteTitle">{{ theme.siteTitle }}</template>
<template v-else-if="theme.siteTitle === undefined">{{ site.title }}</template> <template v-else-if="theme.siteTitle === undefined">{{ site.title }}</template>
<slot name="nav-bar-title-after" />
</a> </a>
</div> </div>
</template> </template>

@ -29,10 +29,12 @@ function unlockBodyScroll() {
> >
<div v-if="open" class="VPNavScreen" ref="screen"> <div v-if="open" class="VPNavScreen" ref="screen">
<div class="container"> <div class="container">
<slot name="nav-screen-content-before" />
<VPNavScreenMenu class="menu" /> <VPNavScreenMenu class="menu" />
<VPNavScreenTranslations class="translations" /> <VPNavScreenTranslations class="translations" />
<VPNavScreenAppearance class="appearance" /> <VPNavScreenAppearance class="appearance" />
<VPNavScreenSocialLinks class="social-links" /> <VPNavScreenSocialLinks class="social-links" />
<slot name="nav-screen-content-after" />
</div> </div>
</div> </div>
</transition> </transition>

@ -8,9 +8,9 @@ const backToTop = ref()
watch(() => route.path, () => backToTop.value.focus()) watch(() => route.path, () => backToTop.value.focus())
function focusOnTargetAnchor({ target }: Event) { function focusOnTargetAnchor({ target }: Event) {
const el = document.querySelector( const el = document.querySelector<HTMLAnchorElement>(
(target as HTMLAnchorElement).hash! (target as HTMLAnchorElement).hash
) as HTMLAnchorElement )
if (el) { if (el) {
const removeTabIndex = () => { const removeTabIndex = () => {

@ -1,40 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid'
import type { Sponsor } from './VPSponsorsGrid.vue'
import { computed } from 'vue' import { computed } from 'vue'
import VPSponsorsGrid from './VPSponsorsGrid.vue' import VPSponsorsGrid from './VPSponsorsGrid.vue'
export interface Sponsors { export interface Sponsors {
tier?: string tier?: string
size?: 'small' | 'medium' | 'big' size?: GridSize
items: Sponsor[] items: Sponsor[]
} }
export interface Sponsor {
name: string
img: string
url: string
}
const props = defineProps<{ const props = defineProps<{
mode?: 'normal' | 'aside' mode?: 'normal' | 'aside'
tier?: string tier?: string
size?: 'xmini' | 'small' | 'medium' | 'big' size?: GridSize
data: Sponsors[] | Sponsor[] data: Sponsors[] | Sponsor[]
}>() }>()
const sponsors = computed(() => { const sponsors = computed(() => {
const isSponsors = props.data.some((s) => { const isSponsors = props.data.some((s) => {
return !!(s as Sponsors).items return 'items' in s
}) })
if (isSponsors) { if (isSponsors) {
return props.data return props.data as Sponsors[]
} }
return [{ return [
tier: props.tier, { tier: props.tier, size: props.size, items: props.data as Sponsor[] }
size: props.size, ]
items: props.data
}]
}) })
</script> </script>

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid'
import { ref } from 'vue' import { ref } from 'vue'
import { useSponsorsGrid } from '../composables/sponsor-grid' import { useSponsorsGrid } from '../composables/sponsor-grid'
@ -9,7 +10,7 @@ export interface Sponsor {
} }
const props = defineProps<{ const props = defineProps<{
size?: 'xmini' | 'mini' | 'small' | 'medium' | 'big' size?: GridSize
data: Sponsor[] data: Sponsor[]
}>() }>()
@ -19,12 +20,29 @@ useSponsorsGrid({ el, size: props.size })
</script> </script>
<template> <template>
<div class="VPSponsorsGrid vp-sponsor-grid" :class="[props.size ?? 'medium']" ref="el"> <div
<div v-for="sponsor in data" :key="sponsor.tier" class="vp-sponsor-grid-item"> class="VPSponsorsGrid vp-sponsor-grid"
<a class="vp-sponsor-grid-link" :href="sponsor.url" target="_blank" rel="sponsored noopener"> :class="[props.size ?? 'medium']"
ref="el"
>
<div
v-for="sponsor in data"
:key="sponsor.name"
class="vp-sponsor-grid-item"
>
<a
class="vp-sponsor-grid-link"
:href="sponsor.url"
target="_blank"
rel="sponsored noopener"
>
<article class="vp-sponsor-grid-box"> <article class="vp-sponsor-grid-box">
<h4 class="visually-hidden">{{ sponsor.name }}</h4> <h4 class="visually-hidden">{{ sponsor.name }}</h4>
<img class="vp-sponsor-grid-image" :src="sponsor.img" :alt="sponsor.name" /> <img
class="vp-sponsor-grid-image"
:src="sponsor.img"
:alt="sponsor.name"
/>
</article> </article>
</a> </a>
</div> </div>

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue' import { computed } from 'vue'
import type { DefaultTheme } from '..'
import VPTeamMembersItem from './VPTeamMembersItem.vue' import VPTeamMembersItem from './VPTeamMembersItem.vue'
const props = defineProps<{ const props = defineProps<{

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DefaultTheme } from '..' import type { DefaultTheme } from 'vitepress/theme'
import VPIconHeart from './icons/VPIconHeart.vue' import VPIconHeart from './icons/VPIconHeart.vue'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
import VPSocialLinks from './VPSocialLinks.vue' import VPSocialLinks from './VPSocialLinks.vue'

@ -20,7 +20,7 @@ export function useFlyout(options: UseFlyoutOptions) {
listeners++ listeners++
const unwatch = watch(focusedElement, (el) => { const unwatch = watch(focusedElement, (el) => {
if (el === options.el.value || options.el.value?.contains(el as Node)) { if (el === options.el.value || options.el.value?.contains(el!)) {
focus.value = true focus.value = true
options.onFocus?.() options.onFocus?.()
} else { } else {

@ -135,7 +135,7 @@ export function useActiveAnchor(
if (hash !== null) { if (hash !== null) {
prevActiveLink = container.value.querySelector( prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]` `a[href="${decodeURIComponent(hash)}"]`
) as HTMLAnchorElement )
} }
const activeLink = prevActiveLink const activeLink = prevActiveLink

@ -1,15 +1,15 @@
import { ref } from 'vue' import { ref } from 'vue'
import { withBase } from 'vitepress' import { withBase, useData } from 'vitepress'
import { EXTERNAL_URL_RE } from '../../shared'
export const HASH_RE = /#.*$/ export const HASH_RE = /#.*$/
export const EXT_RE = /(index)?\.(md|html)$/ export const EXT_RE = /(index)?\.(md|html)$/
export const OUTBOUND_RE = /^[a-z]+:/i
const inBrowser = typeof window !== 'undefined' const inBrowser = typeof window !== 'undefined'
const hashRef = ref(inBrowser ? location.hash : '') const hashRef = ref(inBrowser ? location.hash : '')
export function isExternal(path: string): boolean { export function isExternal(path: string): boolean {
return OUTBOUND_RE.test(path) return EXTERNAL_URL_RE.test(path)
} }
export function throttleAndDebounce(fn: () => void, delay: number): () => void { export function throttleAndDebounce(fn: () => void, delay: number): () => void {
@ -74,12 +74,16 @@ export function normalizeLink(url: string): string {
return url return url
} }
const { site } = useData()
const { pathname, search, hash } = new URL(url, 'http://example.com') const { pathname, search, hash } = new URL(url, 'http://example.com')
const normalizedPath = const normalizedPath =
pathname.endsWith('/') || pathname.endsWith('.html') pathname.endsWith('/') || pathname.endsWith('.html')
? url ? url
: `${pathname.replace(/(\.md)?$/, '.html')}${search}${hash}` : `${pathname.replace(
/(\.md)?$/,
site.value.cleanUrls === 'disabled' ? '.html' : ''
)}${search}${hash}`
return withBase(normalizedPath) return withBase(normalizedPath)
} }

@ -11,7 +11,6 @@
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
"types": ["vite/client"], "types": ["vite/client"],
"paths": { "paths": {
"/@theme/*": ["theme-default/*"],
"vitepress": ["index.ts"], "vitepress": ["index.ts"],
"vitepress/theme": ["../../types/default-theme.d"] "vitepress/theme": ["../../types/default-theme.d"]
} }

@ -1,9 +1,7 @@
import { createRequire } from 'module'
import { resolve, join } from 'path' import { resolve, join } from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { Alias, AliasOptions } from 'vite' import { Alias, AliasOptions } from 'vite'
const require = createRequire(import.meta.url)
const PKG_ROOT = resolve(fileURLToPath(import.meta.url), '../..') const PKG_ROOT = resolve(fileURLToPath(import.meta.url), '../..')
export const DIST_CLIENT_PATH = resolve(PKG_ROOT, 'client') export const DIST_CLIENT_PATH = resolve(PKG_ROOT, 'client')
@ -17,29 +15,12 @@ export const DEFAULT_THEME_PATH = join(DIST_CLIENT_PATH, 'theme-default')
export const SITE_DATA_ID = '@siteData' export const SITE_DATA_ID = '@siteData'
export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
const vueRuntimePath = 'vue/dist/vue.runtime.esm-bundler.js'
export function resolveAliases(root: string, themeDir: string): AliasOptions { export function resolveAliases(root: string, themeDir: string): AliasOptions {
const paths: Record<string, string> = {
'/@theme': themeDir,
'@theme': themeDir,
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
}
// prioritize vue installed in project root and fallback to
// vue that comes with vitepress itself.
let vuePath
try {
vuePath = require.resolve(vueRuntimePath, { paths: [root] })
} catch (e) {
vuePath = require.resolve(vueRuntimePath)
}
const aliases: Alias[] = [ const aliases: Alias[] = [
...Object.keys(paths).map((p) => ({ {
find: p, find: SITE_DATA_ID,
replacement: paths[p] replacement: SITE_DATA_REQUEST_PATH
})), },
{ {
find: /^vitepress$/, find: /^vitepress$/,
replacement: join(DIST_CLIENT_PATH, '/index') replacement: join(DIST_CLIENT_PATH, '/index')
@ -52,12 +33,6 @@ export function resolveAliases(root: string, themeDir: string): AliasOptions {
{ {
find: /^vitepress\//, find: /^vitepress\//,
replacement: PKG_ROOT + '/' replacement: PKG_ROOT + '/'
},
// make sure it always use the same vue dependency that comes
// with vitepress itself
{
find: /^vue$/,
replacement: vuePath
} }
] ]

@ -84,7 +84,8 @@ export async function build(
pageToHashMap pageToHashMap
) )
} finally { } finally {
await fs.remove(siteConfig.tempDir) if (!process.env.DEBUG)
fs.rmSync(siteConfig.tempDir, { recursive: true, force: true })
} }
console.log(`build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.`) console.log(`build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.`)

@ -52,9 +52,8 @@ export async function bundle(
pageToHashMap, pageToHashMap,
clientJSMap clientJSMap
), ),
// @ts-ignore
ssr: { ssr: {
noExternal: ['vitepress'] noExternal: ['vitepress', '@docsearch/css']
}, },
build: { build: {
...options, ...options,
@ -71,7 +70,10 @@ export async function bundle(
output: { output: {
...rollupOptions?.output, ...rollupOptions?.output,
...(ssr ...(ssr
? {} ? {
entryFileNames: `[name].js`,
chunkFileNames: `[name].[hash].js`
}
: { : {
chunkFileNames(chunk) { chunkFileNames(chunk) {
// avoid ads chunk being intercepted by adblock // avoid ads chunk being intercepted by adblock
@ -95,9 +97,7 @@ export async function bundle(
} }
}) })
} }
}, }
// minify with esbuild in MPA mode (for CSS)
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
} }
}) })
@ -138,7 +138,7 @@ export async function bundle(
} }
// build <script client> bundle // build <script client> bundle
if (Object.keys(clientJSMap).length) { if (Object.keys(clientJSMap).length) {
clientResult = (await buildMPAClient(clientJSMap, config)) as RollupOutput clientResult = await buildMPAClient(clientJSMap, config)
} }
} }
@ -167,7 +167,7 @@ function staticImportedByEntry(
importStack: string[] = [] importStack: string[] = []
): boolean { ): boolean {
if (cache.has(id)) { if (cache.has(id)) {
return cache.get(id) as boolean return !!cache.get(id)
} }
if (importStack.includes(id)) { if (importStack.includes(id)) {
// circular deps! // circular deps!

@ -1,4 +1,3 @@
import { createRequire } from 'module'
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import { pathToFileURL } from 'url' import { pathToFileURL } from 'url'
@ -9,8 +8,6 @@ import { HeadConfig, PageData, createTitle, notFoundPageData } from '../shared'
import { slash } from '../utils/slash' import { slash } from '../utils/slash'
import { SiteConfig, resolveSiteDataByRoute } from '../config' import { SiteConfig, resolveSiteDataByRoute } from '../config'
const require = createRequire(import.meta.url)
export async function renderPage( export async function renderPage(
config: SiteConfig, config: SiteConfig,
page: string, // foo.md page: string, // foo.md
@ -20,28 +17,16 @@ export async function renderPage(
pageToHashMap: Record<string, string>, pageToHashMap: Record<string, string>,
hashMapString: string hashMapString: string
) { ) {
const { createApp } = await import( const entryPath = path.join(config.tempDir, 'app.js')
pathToFileURL(path.join(config.tempDir, `app.js`)).toString() const { createApp } = await import(pathToFileURL(entryPath).toString())
)
const { app, router } = createApp() const { app, router } = createApp()
const routePath = `/${page.replace(/\.md$/, '')}` const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath) const siteData = resolveSiteDataByRoute(config.site, routePath)
router.go(routePath) await router.go(routePath)
// lazy require server-renderer for production build
// prioritize project root over vitepress' own dep
let rendererPath
try {
rendererPath = require.resolve('vue/server-renderer', {
paths: [config.root]
})
} catch (e) {
rendererPath = require.resolve('vue/server-renderer')
}
// render page // render page
const content = await import(pathToFileURL(rendererPath).toString()).then( const content = await import('vue/server-renderer').then(
(r) => r.renderToString(app) ({ renderToString: r }) => r(app)
) )
const pageName = page.replace(/\//g, '_') const pageName = page.replace(/\//g, '_')
@ -165,7 +150,15 @@ export async function renderPage(
${inlinedScript} ${inlinedScript}
</body> </body>
</html>`.trim() </html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) const createSubDirectory =
config.cleanUrls === 'with-subfolders' &&
!/(^|\/)(index|404).md$/.test(page)
const htmlFileName = path.join(
config.outDir,
page.replace(/\.md$/, createSubDirectory ? '/index.html' : '.html')
)
await fs.ensureDir(path.dirname(htmlFileName)) await fs.ensureDir(path.dirname(htmlFileName))
await fs.writeFile(htmlFileName, html) await fs.writeFile(htmlFileName, html)
} }

@ -16,7 +16,8 @@ import {
LocaleConfig, LocaleConfig,
DefaultTheme, DefaultTheme,
APPEARANCE_KEY, APPEARANCE_KEY,
createLangDictionary createLangDictionary,
CleanUrlsMode
} from './shared' } from './shared'
import { resolveAliases, DEFAULT_THEME_PATH } from './alias' import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown' import { MarkdownOptions } from './markdown/markdown'
@ -60,7 +61,7 @@ export interface UserConfig<ThemeConfig = any> {
scrollOffset?: number | string scrollOffset?: number | string
/** /**
* Enable MPA / zero-JS mode * Enable MPA / zero-JS mode.
* @experimental * @experimental
*/ */
mpa?: boolean mpa?: boolean
@ -71,6 +72,19 @@ export interface UserConfig<ThemeConfig = any> {
* @default false * @default false
*/ */
ignoreDeadLinks?: boolean ignoreDeadLinks?: boolean
/**
* @experimental
* Remove '.html' from URLs and generate clean directory structure.
*
* Available Modes:
* - `disabled`: generates `/foo.html` for every `/foo.md` and shows `/foo.html` in browser
* - `without-subfolders`: generates `/foo.html` for every `/foo.md` but shows `/foo` in browser
* - `with-subfolders`: generates `/foo/index.html` for every `/foo.md` and shows `/foo` in browser
*
* @default 'disabled'
*/
cleanUrls?: CleanUrlsMode
} }
export type RawConfigExports<ThemeConfig = any> = export type RawConfigExports<ThemeConfig = any> =
@ -88,6 +102,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'mpa' | 'mpa'
| 'lastUpdated' | 'lastUpdated'
| 'ignoreDeadLinks' | 'ignoreDeadLinks'
| 'cleanUrls'
> { > {
root: string root: string
srcDir: string srcDir: string
@ -166,7 +181,8 @@ export async function resolveConfig(
vite: userConfig.vite, vite: userConfig.vite,
shouldPreload: userConfig.shouldPreload, shouldPreload: userConfig.shouldPreload,
mpa: !!userConfig.mpa, mpa: !!userConfig.mpa,
ignoreDeadLinks: userConfig.ignoreDeadLinks ignoreDeadLinks: userConfig.ignoreDeadLinks,
cleanUrls: userConfig.cleanUrls || 'disabled'
} }
return config return config
@ -180,17 +196,12 @@ async function resolveUserConfig(
mode: string mode: string
): Promise<[UserConfig, string | undefined]> { ): Promise<[UserConfig, string | undefined]> {
// load user config // load user config
let configPath const configPath = supportedConfigExtensions
for (const ext of supportedConfigExtensions) { .map((ext) => resolve(root, `config.${ext}`))
const p = resolve(root, `config.${ext}`) .find(fs.pathExistsSync)
if (await fs.pathExists(p)) {
configPath = p
break
}
}
const userConfig: RawConfigExports = configPath const userConfig: RawConfigExports = configPath
? (( ? (
await loadConfigFromFile( await loadConfigFromFile(
{ {
command, command,
@ -199,7 +210,7 @@ async function resolveUserConfig(
configPath, configPath,
root root
) )
)?.config as any) )?.config!
: {} : {}
if (configPath) { if (configPath) {
@ -270,7 +281,8 @@ export async function resolveSiteData(
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}, locales: userConfig.locales || {},
langs: createLangDictionary(userConfig), langs: createLangDictionary(userConfig),
scrollOffset: userConfig.scrollOffset || 90 scrollOffset: userConfig.scrollOffset || 90,
cleanUrls: userConfig.cleanUrls || 'disabled'
} }
} }

@ -4,21 +4,14 @@ import MarkdownIt from 'markdown-it'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE } from '../../shared'
export const imagePlugin = (md: MarkdownIt) => { export const imagePlugin = (md: MarkdownIt) => {
const imageRule = md.renderer.rules.image!
md.renderer.rules.image = (tokens, idx, options, env, self) => { md.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx] const token = tokens[idx]
const url = token.attrGet('src') let url = token.attrGet('src')
if (url && !EXTERNAL_URL_RE.test(url) && !/^\.?\//.test(url)) { if (url && !EXTERNAL_URL_RE.test(url)) {
token.attrSet('src', './' + url) if (!/^\.?\//.test(url)) url = './' + url
token.attrSet('src', decodeURIComponent(url))
} }
return imageRule(tokens, idx, options, env, self)
if (token.attrIndex('alt') && token.children != null) {
token.attrs![token.attrIndex('alt')][1] = self.renderInlineAsText(
token.children,
options,
env
)
}
return self.renderToken(tokens, idx, options)
} }
} }

@ -5,7 +5,7 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { MarkdownRenderer } from '../markdown' import { MarkdownRenderer } from '../markdown'
import { URL } from 'url' import { URL } from 'url'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE, CleanUrlsMode } from '../../shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i const indexRE = /(^|.*\/)index.md(#?.*)$/i
@ -37,7 +37,7 @@ export const linkPlugin = (
// links to files (other than html/md) // links to files (other than html/md)
!/\.(?!html|md)\w+($|\?)/i.test(url) !/\.(?!html|md)\w+($|\?)/i.test(url)
) { ) {
normalizeHref(hrefAttr) normalizeHref(hrefAttr, env.cleanUrl)
} }
// encode vite-specific replace strings in case they appear in URLs // encode vite-specific replace strings in case they appear in URLs
@ -50,7 +50,10 @@ export const linkPlugin = (
return self.renderToken(tokens, idx, options) return self.renderToken(tokens, idx, options)
} }
function normalizeHref(hrefAttr: [string, string]) { function normalizeHref(
hrefAttr: [string, string],
shouldCleanUrls: CleanUrlsMode
) {
let url = hrefAttr[1] let url = hrefAttr[1]
const indexMatch = url.match(indexRE) const indexMatch = url.match(indexRE)
@ -59,12 +62,19 @@ export const linkPlugin = (
url = path + hash url = path + hash
} else { } else {
let cleanUrl = url.replace(/[?#].*$/, '') let cleanUrl = url.replace(/[?#].*$/, '')
// .md -> .html // transform foo.md -> foo[.html]
if (cleanUrl.endsWith('.md')) { if (cleanUrl.endsWith('.md')) {
cleanUrl = cleanUrl.replace(/\.md$/, '.html') cleanUrl = cleanUrl.replace(
/\.md$/,
shouldCleanUrls === 'disabled' ? '.html' : ''
)
} }
// ./foo -> ./foo.html // transform ./foo -> ./foo[.html]
if (!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/')) { if (
shouldCleanUrls === 'disabled' &&
!cleanUrl.endsWith('.html') &&
!cleanUrl.endsWith('/')
) {
cleanUrl += '.html' cleanUrl += '.html'
} }
const parsed = new URL(url, 'http://a.com') const parsed = new URL(url, 'http://a.com')

@ -3,7 +3,7 @@ import path from 'path'
import c from 'picocolors' import c from 'picocolors'
import matter from 'gray-matter' import matter from 'gray-matter'
import LRUCache from 'lru-cache' import LRUCache from 'lru-cache'
import { PageData, HeadConfig, EXTERNAL_URL_RE } from './shared' import { PageData, HeadConfig, EXTERNAL_URL_RE, CleanUrlsMode } from './shared'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import { deeplyParseHeader } from './utils/parseHeader' import { deeplyParseHeader } from './utils/parseHeader'
import { getGitTimestamp } from './utils/getGitTimestamp' import { getGitTimestamp } from './utils/getGitTimestamp'
@ -28,7 +28,8 @@ export async function createMarkdownToVueRenderFn(
userDefines: Record<string, any> | undefined, userDefines: Record<string, any> | undefined,
isBuild = false, isBuild = false,
base = '/', base = '/',
includeLastUpdatedData = false includeLastUpdatedData = false,
cleanUrls: CleanUrlsMode = 'disabled'
) { ) {
const md = await createMarkdownRenderer(srcDir, options, base) const md = await createMarkdownRenderer(srcDir, options, base)
@ -54,11 +55,15 @@ export async function createMarkdownToVueRenderFn(
// resolve includes // resolve includes
let includes: string[] = [] let includes: string[] = []
src = src.replace(includesRE, (_, m1) => { src = src.replace(includesRE, (m, m1) => {
const includePath = path.join(dir, m1) try {
const content = fs.readFileSync(includePath, 'utf-8') const includePath = path.join(dir, m1)
includes.push(slash(includePath)) const content = fs.readFileSync(includePath, 'utf-8')
return content includes.push(slash(includePath))
return content
} catch (error) {
return m // silently ignore error if file is not present
}
}) })
const { content, data: frontmatter } = matter(src) const { content, data: frontmatter } = matter(src)
@ -67,7 +72,12 @@ export async function createMarkdownToVueRenderFn(
md.__path = file md.__path = file
md.__relativePath = relativePath md.__relativePath = relativePath
const html = md.render(content) const html = md.render(content, {
path: file,
relativePath,
cleanUrls,
frontmatter
})
const data = md.__data const data = md.__data
// validate data.links // validate data.links

@ -45,7 +45,9 @@ export async function createVitePressPlugin(
vue: userVuePluginOptions, vue: userVuePluginOptions,
vite: userViteConfig, vite: userViteConfig,
pages, pages,
ignoreDeadLinks ignoreDeadLinks,
lastUpdated,
cleanUrls
} = siteConfig } = siteConfig
let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>> let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
@ -83,7 +85,8 @@ export async function createVitePressPlugin(
config.define, config.define,
config.command === 'build', config.command === 'build',
config.base, config.base,
siteConfig.lastUpdated lastUpdated,
cleanUrls
) )
}, },
@ -217,6 +220,14 @@ export async function createVitePressPlugin(
delete bundle[name] delete bundle[name]
} }
} }
if (config.ssr?.format === 'esm') {
this.emitFile({
type: 'asset',
fileName: 'package.json',
source: '{ "private": true, "type": "module" }'
})
}
} else { } else {
// client build: // client build:
// for each .md entry chunk, adjust its name to its correct path. // for each .md entry chunk, adjust its name to its correct path.

@ -22,7 +22,7 @@ export interface ServeOptions {
} }
export async function serve(options: ServeOptions = {}) { export async function serve(options: ServeOptions = {}) {
const port = options.port !== undefined ? options.port : 5000 const port = options.port !== undefined ? options.port : 4173
const site = await resolveConfig(options.root, 'serve', 'production') const site = await resolveConfig(options.root, 'serve', 'production')
const base = trimChar(options?.base ?? site?.site?.base ?? '', '/') const base = trimChar(options?.base ?? site?.site?.base ?? '', '/')

@ -1,3 +1,4 @@
import { setDefaultResultOrder } from 'node:dns'
import { createServer as createViteServer, ServerOptions } from 'vite' import { createServer as createViteServer, ServerOptions } from 'vite'
import { resolveConfig } from './config' import { resolveConfig } from './config'
import { createVitePressPlugin } from './plugin' import { createVitePressPlugin } from './plugin'
@ -13,6 +14,8 @@ export async function createServer(
delete serverOptions.base delete serverOptions.base
} }
setDefaultResultOrder('verbatim')
return createViteServer({ return createViteServer({
root: config.srcDir, root: config.srcDir,
base: config.site.base, base: config.site.base,

@ -7,13 +7,13 @@ export type {
LocaleConfig, LocaleConfig,
Header, Header,
DefaultTheme, DefaultTheme,
PageDataPayload PageDataPayload,
CleanUrlsMode
} from '../../types/shared' } from '../../types/shared'
export const EXTERNAL_URL_RE = /^https?:/i export const EXTERNAL_URL_RE = /^[a-z]+:/i
export const APPEARANCE_KEY = 'vitepress-theme-appearance' export const APPEARANCE_KEY = 'vitepress-theme-appearance'
// @ts-ignore
export const inBrowser = typeof window !== 'undefined' export const inBrowser = typeof window !== 'undefined'
export const notFoundPageData: PageData = { export const notFoundPageData: PageData = {

@ -1,5 +1,5 @@
{ {
"extends": "../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"lib": ["ESNext", "DOM"] "lib": ["ESNext", "DOM"]

18
theme.d.ts vendored

@ -1,14 +1,14 @@
// so that users can do `import DefaultTheme from 'vitepress/theme'` // so that users can do `import DefaultTheme from 'vitepress/theme'`
import { ComponentOptions } from 'vue' import type { ComponentOptions } from 'vue'
export const VPHomeHero = ComponentOptions export const VPHomeHero: ComponentOptions
export const VPHomeFeatures = ComponentOptions export const VPHomeFeatures: ComponentOptions
export const VPHomeSponsors = ComponentOptions export const VPHomeSponsors: ComponentOptions
export const VPDocAsideSponsors = ComponentOptions export const VPDocAsideSponsors: ComponentOptions
export const VPTeamPage = ComponentOptions export const VPTeamPage: ComponentOptions
export const VPTeamPageTitle = ComponentOptions export const VPTeamPageTitle: ComponentOptions
export const VPTeamPageSection = ComponentOptions export const VPTeamPageSection: ComponentOptions
export const VPTeamMembers = ComponentOptions export const VPTeamMembers: ComponentOptions
declare const theme: { declare const theme: {
Layout: ComponentOptions Layout: ComponentOptions

6
types/shared.d.ts vendored

@ -19,8 +19,14 @@ export interface Header {
slug: string slug: string
} }
export type CleanUrlsMode =
| 'disabled'
| 'without-subfolders'
| 'with-subfolders'
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {
base: string base: string
cleanUrls?: CleanUrlsMode
/** /**
* Language of the site as it should be set on the `html` element. * Language of the site as it should be set on the `html` element.

Loading…
Cancel
Save