Skip to content

Commit

Permalink
feat(build): support rewrites (#1798)
Browse files Browse the repository at this point in the history
Co-authored-by: Kia Ishii <[email protected]>
  • Loading branch information
brc-dd and kiaking authored Jan 27, 2023
1 parent 99858d7 commit 00abac6
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 14 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ function sidebarGuide() {
{ text: 'What is VitePress?', link: '/guide/what-is-vitepress' },
{ text: 'Getting Started', link: '/guide/getting-started' },
{ text: 'Configuration', link: '/guide/configuration' },
{ text: 'Routing', link: '/guide/routing' },
{ text: 'Deploying', link: '/guide/deploying' },
{ text: 'Internationalization', link: '/guide/i18n' }
]
Expand Down
32 changes: 23 additions & 9 deletions docs/config/app-configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,23 +271,37 @@ export default {
- Type: `'disabled' | 'without-subfolders' | 'with-subfolders'`
- Default: `'disabled'`

Allows removing trailing `.html` from URLs and, optionally, generating clean directory structure. Available modes:
Allows removing trailing `.html` from URLs and, optionally, generating clean directory structure.

| 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` |
```ts
export default {
cleanUrls: 'with-subfolders'
}
```

::: warning
This option has several modes you can choose. Here is the list of all modes available.

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**.
| 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 **without a redirect**.
:::

## rewrites

- Type: `Record<string, string>`

Defines custom directory <-> URL mappings. See [Routing: Customize the Mappings](/guide/routing#customize-the-mappings) for more details.

```ts
export default {
cleanUrls: 'with-subfolders'
rewrites: {
'source/:page': 'destination/:page'
}
}
```

Expand Down
185 changes: 185 additions & 0 deletions docs/guide/routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Routing

VitePress is built with file system based routing, which means the directory structure of the source file corresponds to the final URL. You may customize the mapping of the directory structure and URL too. Read through this page to learn everything about the VitePress routing system.

## Basic Routing

By default, VitePress assumes your page files are stored in project root. Here you may add markdown files with the name being the URL path. For example, when you have following directory structure:

```
.
├─ guide
│ ├─ getting-started.md
│ └─ index.md
├─ index.md
└─ prologue.md
```

Then you can access the pages by the below URL.

```
index.md -> /
prologue.md -> /prologue.html
guide/index.md -> /guide/
getting-started.md -> /guide/getting-started.html
```

As you can see, the directory structure corresponds to the final URL, as same as hosting plain HTML from a typical web server.

## Changing the Root Directory

To change the root directory for your page files, you may pass the directory name to the `vitepress` command. For example, if you want to store your page files under `docs` directory, then you should run `vitepress dev docs` command.

```
.
├─ docs
│ ├─ getting-started.md
│ └─ index.md
└─ ...
```

```
vitepress dev docs
```

This is going to map the URL as follows.

```
docs/index.md -> /
docs/getting-started.md -> /getting-started.html
```

You may also customize the root directory in config file via [`srcDir`](/config/app-configs#srcdir) option too. Running `vitepress dev` with the following setting acts same as running `vitepress dev docs` command.

```ts
export default {
srcDir: './docs'
}
```

## Linking Between Pages

When adding links in pages, omit extension from the path and use either absolute path from the root, or relative path from the page. VitePress will handle the extension according to your configuration setup.

```md
<!-- Do -->
[Getting Started](/guide/getting-started)
[Getting Started](../guide/getting-started)

<!-- Don't -->
[Getting Started](/guide/getting-started.md)
[Getting Started](/guide/getting-started.html)
```

Learn more about page links and links to assets, such as link to images, at [Asset Handling](asset-handling).

## Generate Clean URL

A "Clean URL" is commonly known as URL without `.html` extension, for example, `example.com/path` instead of `example.com/path.html`.

By default, VitePress generates the final static page files by adding `.html` extension to each file. If you would like to have clean URL, you may structure your directory by only using `index.html` file.

```
.
├─ getting-started
│ └─ index.md
├─ installation
│ └─ index.md
└─ index.md
```

However, you may also generate a clean URL by setting up [`cleanUrls`](/config/app-configs#cleanurls-experimental) option.

```ts
export default {
cleanUrls: 'with-subfolders'
}
```

This option has several modes you can choose. Here is the list of all modes available. The default behavior is `disabled` mode.

| 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 **without a redirect**.
:::

## Customize the Mappings

You may customize the mapping between directory structure and URL. It's useful when you have complex document structure. For example, let's say you have several packages and would like to place documentations along with the source files like this.

```
.
├─ packages
│ ├─ pkg-a
│ │ └─ src
│ │ ├─ pkg-a-code.ts
│ │ └─ pkg-a-code.md
│ └─ pkg-b
│ └─ src
│ ├─ pkg-b-code.ts
│ └─ pkg-b-code.md
```

And you want the VitePress pages to be generated as follows.

```
packages/pkg-a/src/pkg-a-code.md -> /pkg-a/pkg-a-code.md
packages/pkg-b/src/pkg-b-code.md -> /pkg-b/pkg-b-code.md
```

You may configure the mapping via [`rewrites`](/config/app-configs#rewrites) option like this.

```ts
export default {
rewrites: {
'packages/pkg-a/src/pkg-a-code.md': 'pkg-a/pkg-a-code',
'packages/pkg-b/src/pkg-b-code.md': 'pkg-b/pkg-b-code'
}
}
```

The `rewrites` option can also have dynamic route parameters. In this example, we have fixed path `packages` and `src` which stays the same on all pages, and it might be verbose to have to list all pages in your config as you add pages. You may configure the above mapping as below and get the same result.

```ts
export default {
rewrites: {
'packages/:pkg/src/:page': ':pkg/:page'
}
}
```

Route parameters are prefixed by `:` (e.g. `:pkg`). The name of the parameter is just a placeholder and can be anything.

In addition, you may add `*` at the end of the parameter to map all sub directories from there on.

```ts
export default {
rewrites: {
'packages/:pkg/src/:page*': ':pkg/:page*'
}
}
```

The above will create mapping as below.

```
packages/pkg-a/src/pkg-a-code.md -> /pkg-a/pkg-a-code
packages/pkg-b/src/folder/file.md -> /pkg-b/folder/file
```

::: warning You need server restart on page addition
At the moment, VitePress doesn't detect page additions to the mapped directory. You need to restart your server when adding or removing files from the directory during the dev mode. Updating the already existing files gets updated as usual.
:::

### Relative Link Handling in Page

Note that when enabling rewrites, **relative links in the markdown are resolved relative to the final path**. For example, in order to create relative link from `packages/pkg-a/src/pkg-a-code.md` to `packages/pkg-b/src/pkg-b-code.md`, you should define link as below.

```md
[Link to PKG B](../pkg-b/pkg-b-code)
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"nanoid": "3.3.4",
"npm-run-all": "^4.1.5",
"ora": "5.4.1",
"path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0",
"pkg-dir": "5.0.0",
"playwright-chromium": "^1.29.2",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/node/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export async function build(
// as JS object literal.
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))

const pages = ['404.md', ...siteConfig.pages]
const pages = ['404.md', ...siteConfig.pages].map(
(page) => siteConfig.rewrites.map[page] || page
)

await Promise.all(
pages.map((page) =>
Expand Down
3 changes: 2 additions & 1 deletion src/node/build/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export async function bundle(
config.pages.forEach((file) => {
// page filename conversion
// foo/bar.md -> foo_bar.md
input[slash(file).replace(/\//g, '_')] = path.resolve(config.srcDir, file)
const alias = config.rewrites.map[file] || file
input[slash(alias).replace(/\//g, '_')] = path.resolve(config.srcDir, file)
})

// resolve options to pass to vite
Expand Down
1 change: 1 addition & 0 deletions src/node/build/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ function resolvePageImports(
result: RollupOutput,
appChunk: OutputChunk
) {
page = config.rewrites.inv[page] || page
// find the page's js chunk and inject script tags for its imports so that
// they start fetching as early as possible
const srcPath = normalizePath(
Expand Down
41 changes: 40 additions & 1 deletion src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import _debug from 'debug'
import fg from 'fast-glob'
import fs from 'fs-extra'
import path from 'path'
import { match, compile } from 'path-to-regexp'
import c from 'picocolors'
import {
loadConfigFromFile,
Expand Down Expand Up @@ -98,6 +99,13 @@ export interface UserConfig<ThemeConfig = any>
*/
useWebFonts?: boolean

/**
* @experimental
*
* source -> destination
*/
rewrites?: Record<string, string>

/**
* Build end hook: called when SSG finish.
* @param siteConfig The resolved configuration.
Expand Down Expand Up @@ -175,6 +183,10 @@ export interface SiteConfig<ThemeConfig = any>
cacheDir: string
tempDir: string
pages: string[]
rewrites: {
map: Record<string, string | undefined>
inv: Record<string, string | undefined>
}
}

const resolve = (root: string, file: string) =>
Expand Down Expand Up @@ -234,6 +246,21 @@ export async function resolveConfig(
})
).sort()

const rewriteEntries = Object.entries(userConfig.rewrites || {})

const rewrites = rewriteEntries.length
? Object.fromEntries(
pages
.map((src) => {
for (const [from, to] of rewriteEntries) {
const dest = rewrite(src, from, to)
if (dest) return [src, dest]
}
})
.filter((e) => e != null) as [string, string][]
)
: {}

const config: SiteConfig = {
root,
srcDir,
Expand All @@ -260,7 +287,11 @@ export async function resolveConfig(
buildEnd: userConfig.buildEnd,
transformHead: userConfig.transformHead,
transformHtml: userConfig.transformHtml,
transformPageData: userConfig.transformPageData
transformPageData: userConfig.transformPageData,
rewrites: {
map: rewrites,
inv: Object.fromEntries(Object.entries(rewrites).map((a) => a.reverse()))
}
}

return config
Expand Down Expand Up @@ -395,3 +426,11 @@ function resolveSiteDataHead(userConfig?: UserConfig): HeadConfig[] {

return head
}

function rewrite(src: string, from: string, to: string) {
const urlMatch = match(from)
const res = urlMatch(src)
if (!res) return false
const toPath = compile(to)
return toPath(res.params)
}
Loading

0 comments on commit 00abac6

Please sign in to comment.