Skip to content

Commit 00abac6

Browse files
brc-ddkiaking
andauthoredJan 27, 2023
feat(build): support rewrites (#1798)
Co-authored-by: Kia Ishii <kia.king.08@gmail.com>
1 parent 99858d7 commit 00abac6

File tree

11 files changed

+277
-14
lines changed

11 files changed

+277
-14
lines changed
 

‎docs/.vitepress/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ function sidebarGuide() {
8484
{ text: 'What is VitePress?', link: '/guide/what-is-vitepress' },
8585
{ text: 'Getting Started', link: '/guide/getting-started' },
8686
{ text: 'Configuration', link: '/guide/configuration' },
87+
{ text: 'Routing', link: '/guide/routing' },
8788
{ text: 'Deploying', link: '/guide/deploying' },
8889
{ text: 'Internationalization', link: '/guide/i18n' }
8990
]

‎docs/config/app-configs.md

+23-9
Original file line numberDiff line numberDiff line change
@@ -271,23 +271,37 @@ export default {
271271
- Type: `'disabled' | 'without-subfolders' | 'with-subfolders'`
272272
- Default: `'disabled'`
273273

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

276-
| Mode | Page | Generated Page | URL |
277-
| :--------------------: | :-------: | :---------------: | :---------: |
278-
| `'disabled'` | `/foo.md` | `/foo.html` | `/foo.html` |
279-
| `'without-subfolders'` | `/foo.md` | `/foo.html` | `/foo` |
280-
| `'with-subfolders'` | `/foo.md` | `/foo/index.html` | `/foo` |
276+
```ts
277+
export default {
278+
cleanUrls: 'with-subfolders'
279+
}
280+
```
281281

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

284-
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**.
284+
| Mode | Page | Generated Page | URL |
285+
| :--------------------- | :-------- | :---------------- | :---------- |
286+
| `'disabled'` | `/foo.md` | `/foo.html` | `/foo.html` |
287+
| `'without-subfolders'` | `/foo.md` | `/foo.html` | `/foo` |
288+
| `'with-subfolders'` | `/foo.md` | `/foo/index.html` | `/foo` |
285289

290+
::: warning
291+
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**.
286292
:::
287293

294+
## rewrites
295+
296+
- Type: `Record<string, string>`
297+
298+
Defines custom directory <-> URL mappings. See [Routing: Customize the Mappings](/guide/routing#customize-the-mappings) for more details.
299+
288300
```ts
289301
export default {
290-
cleanUrls: 'with-subfolders'
302+
rewrites: {
303+
'source/:page': 'destination/:page'
304+
}
291305
}
292306
```
293307

‎docs/guide/routing.md

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Routing
2+
3+
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.
4+
5+
## Basic Routing
6+
7+
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:
8+
9+
```
10+
.
11+
├─ guide
12+
│ ├─ getting-started.md
13+
│ └─ index.md
14+
├─ index.md
15+
└─ prologue.md
16+
```
17+
18+
Then you can access the pages by the below URL.
19+
20+
```
21+
index.md -> /
22+
prologue.md -> /prologue.html
23+
guide/index.md -> /guide/
24+
getting-started.md -> /guide/getting-started.html
25+
```
26+
27+
As you can see, the directory structure corresponds to the final URL, as same as hosting plain HTML from a typical web server.
28+
29+
## Changing the Root Directory
30+
31+
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.
32+
33+
```
34+
.
35+
├─ docs
36+
│ ├─ getting-started.md
37+
│ └─ index.md
38+
└─ ...
39+
```
40+
41+
```
42+
vitepress dev docs
43+
```
44+
45+
This is going to map the URL as follows.
46+
47+
```
48+
docs/index.md -> /
49+
docs/getting-started.md -> /getting-started.html
50+
```
51+
52+
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.
53+
54+
```ts
55+
export default {
56+
srcDir: './docs'
57+
}
58+
```
59+
60+
## Linking Between Pages
61+
62+
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.
63+
64+
```md
65+
<!-- Do -->
66+
[Getting Started](/guide/getting-started)
67+
[Getting Started](../guide/getting-started)
68+
69+
<!-- Don't -->
70+
[Getting Started](/guide/getting-started.md)
71+
[Getting Started](/guide/getting-started.html)
72+
```
73+
74+
Learn more about page links and links to assets, such as link to images, at [Asset Handling](asset-handling).
75+
76+
## Generate Clean URL
77+
78+
A "Clean URL" is commonly known as URL without `.html` extension, for example, `example.com/path` instead of `example.com/path.html`.
79+
80+
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.
81+
82+
```
83+
.
84+
├─ getting-started
85+
│ └─ index.md
86+
├─ installation
87+
│ └─ index.md
88+
└─ index.md
89+
```
90+
91+
However, you may also generate a clean URL by setting up [`cleanUrls`](/config/app-configs#cleanurls-experimental) option.
92+
93+
```ts
94+
export default {
95+
cleanUrls: 'with-subfolders'
96+
}
97+
```
98+
99+
This option has several modes you can choose. Here is the list of all modes available. The default behavior is `disabled` mode.
100+
101+
| Mode | Page | Generated Page | URL |
102+
| :--------------------- | :-------- | :---------------- | :---------- |
103+
| `'disabled'` | `/foo.md` | `/foo.html` | `/foo.html` |
104+
| `'without-subfolders'` | `/foo.md` | `/foo.html` | `/foo` |
105+
| `'with-subfolders'` | `/foo.md` | `/foo/index.html` | `/foo` |
106+
107+
::: warning
108+
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**.
109+
:::
110+
111+
## Customize the Mappings
112+
113+
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.
114+
115+
```
116+
.
117+
├─ packages
118+
│ ├─ pkg-a
119+
│ │ └─ src
120+
│ │ ├─ pkg-a-code.ts
121+
│ │ └─ pkg-a-code.md
122+
│ └─ pkg-b
123+
│ └─ src
124+
│ ├─ pkg-b-code.ts
125+
│ └─ pkg-b-code.md
126+
```
127+
128+
And you want the VitePress pages to be generated as follows.
129+
130+
```
131+
packages/pkg-a/src/pkg-a-code.md -> /pkg-a/pkg-a-code.md
132+
packages/pkg-b/src/pkg-b-code.md -> /pkg-b/pkg-b-code.md
133+
```
134+
135+
You may configure the mapping via [`rewrites`](/config/app-configs#rewrites) option like this.
136+
137+
```ts
138+
export default {
139+
rewrites: {
140+
'packages/pkg-a/src/pkg-a-code.md': 'pkg-a/pkg-a-code',
141+
'packages/pkg-b/src/pkg-b-code.md': 'pkg-b/pkg-b-code'
142+
}
143+
}
144+
```
145+
146+
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.
147+
148+
```ts
149+
export default {
150+
rewrites: {
151+
'packages/:pkg/src/:page': ':pkg/:page'
152+
}
153+
}
154+
```
155+
156+
Route parameters are prefixed by `:` (e.g. `:pkg`). The name of the parameter is just a placeholder and can be anything.
157+
158+
In addition, you may add `*` at the end of the parameter to map all sub directories from there on.
159+
160+
```ts
161+
export default {
162+
rewrites: {
163+
'packages/:pkg/src/:page*': ':pkg/:page*'
164+
}
165+
}
166+
```
167+
168+
The above will create mapping as below.
169+
170+
```
171+
packages/pkg-a/src/pkg-a-code.md -> /pkg-a/pkg-a-code
172+
packages/pkg-b/src/folder/file.md -> /pkg-b/folder/file
173+
```
174+
175+
::: warning You need server restart on page addition
176+
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.
177+
:::
178+
179+
### Relative Link Handling in Page
180+
181+
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.
182+
183+
```md
184+
[Link to PKG B](../pkg-b/pkg-b-code)
185+
```

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
"nanoid": "3.3.4",
144144
"npm-run-all": "^4.1.5",
145145
"ora": "5.4.1",
146+
"path-to-regexp": "^6.2.1",
146147
"picocolors": "^1.0.0",
147148
"pkg-dir": "5.0.0",
148149
"playwright-chromium": "^1.29.2",

‎pnpm-lock.yaml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/node/build/build.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export async function build(
6464
// as JS object literal.
6565
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
6666

67-
const pages = ['404.md', ...siteConfig.pages]
67+
const pages = ['404.md', ...siteConfig.pages].map(
68+
(page) => siteConfig.rewrites.map[page] || page
69+
)
6870

6971
await Promise.all(
7072
pages.map((page) =>

‎src/node/build/bundle.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export async function bundle(
3737
config.pages.forEach((file) => {
3838
// page filename conversion
3939
// foo/bar.md -> foo_bar.md
40-
input[slash(file).replace(/\//g, '_')] = path.resolve(config.srcDir, file)
40+
const alias = config.rewrites.map[file] || file
41+
input[slash(alias).replace(/\//g, '_')] = path.resolve(config.srcDir, file)
4142
})
4243

4344
// resolve options to pass to vite

‎src/node/build/render.ts

+1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ function resolvePageImports(
201201
result: RollupOutput,
202202
appChunk: OutputChunk
203203
) {
204+
page = config.rewrites.inv[page] || page
204205
// find the page's js chunk and inject script tags for its imports so that
205206
// they start fetching as early as possible
206207
const srcPath = normalizePath(

‎src/node/config.ts

+40-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import _debug from 'debug'
33
import fg from 'fast-glob'
44
import fs from 'fs-extra'
55
import path from 'path'
6+
import { match, compile } from 'path-to-regexp'
67
import c from 'picocolors'
78
import {
89
loadConfigFromFile,
@@ -98,6 +99,13 @@ export interface UserConfig<ThemeConfig = any>
9899
*/
99100
useWebFonts?: boolean
100101

102+
/**
103+
* @experimental
104+
*
105+
* source -> destination
106+
*/
107+
rewrites?: Record<string, string>
108+
101109
/**
102110
* Build end hook: called when SSG finish.
103111
* @param siteConfig The resolved configuration.
@@ -175,6 +183,10 @@ export interface SiteConfig<ThemeConfig = any>
175183
cacheDir: string
176184
tempDir: string
177185
pages: string[]
186+
rewrites: {
187+
map: Record<string, string | undefined>
188+
inv: Record<string, string | undefined>
189+
}
178190
}
179191

180192
const resolve = (root: string, file: string) =>
@@ -234,6 +246,21 @@ export async function resolveConfig(
234246
})
235247
).sort()
236248

249+
const rewriteEntries = Object.entries(userConfig.rewrites || {})
250+
251+
const rewrites = rewriteEntries.length
252+
? Object.fromEntries(
253+
pages
254+
.map((src) => {
255+
for (const [from, to] of rewriteEntries) {
256+
const dest = rewrite(src, from, to)
257+
if (dest) return [src, dest]
258+
}
259+
})
260+
.filter((e) => e != null) as [string, string][]
261+
)
262+
: {}
263+
237264
const config: SiteConfig = {
238265
root,
239266
srcDir,
@@ -260,7 +287,11 @@ export async function resolveConfig(
260287
buildEnd: userConfig.buildEnd,
261288
transformHead: userConfig.transformHead,
262289
transformHtml: userConfig.transformHtml,
263-
transformPageData: userConfig.transformPageData
290+
transformPageData: userConfig.transformPageData,
291+
rewrites: {
292+
map: rewrites,
293+
inv: Object.fromEntries(Object.entries(rewrites).map((a) => a.reverse()))
294+
}
264295
}
265296

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

396427
return head
397428
}
429+
430+
function rewrite(src: string, from: string, to: string) {
431+
const urlMatch = match(from)
432+
const res = urlMatch(src)
433+
if (!res) return false
434+
const toPath = compile(to)
435+
return toPath(res.params)
436+
}

‎src/node/markdownToVue.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export async function createMarkdownToVueRenderFn(
5555
file: string,
5656
publicDir: string
5757
): Promise<MarkdownCompileResult> => {
58+
const alias = siteConfig?.rewrites.map[file.slice(srcDir.length + 1)]
59+
file = alias ? path.join(srcDir, alias) : file
5860
const relativePath = slash(path.relative(srcDir, file))
5961
const dir = path.dirname(file)
6062
const cacheKey = JSON.stringify({ src, file })
@@ -125,13 +127,15 @@ export async function createMarkdownToVueRenderFn(
125127

126128
url = url.replace(/[?#].*$/, '').replace(/\.(html|md)$/, '')
127129
if (url.endsWith('/')) url += `index`
128-
const resolved = decodeURIComponent(
130+
let resolved = decodeURIComponent(
129131
slash(
130132
url.startsWith('/')
131133
? url.slice(1)
132134
: path.relative(srcDir, path.resolve(dir, url))
133135
)
134136
)
137+
resolved =
138+
siteConfig?.rewrites.inv[resolved + '.md']?.slice(0, -3) || resolved
135139
if (
136140
!pages.includes(resolved) &&
137141
!fs.existsSync(path.resolve(dir, publicDir, `${resolved}.html`))

‎src/node/plugin.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export async function createVitePressPlugin(
6363
pages,
6464
ignoreDeadLinks,
6565
lastUpdated,
66-
cleanUrls
66+
cleanUrls,
67+
rewrites
6768
} = siteConfig
6869

6970
let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
@@ -191,6 +192,14 @@ export async function createVitePressPlugin(
191192
configDeps.forEach((file) => server.watcher.add(file))
192193
}
193194

195+
server.middlewares.use((req, res, next) => {
196+
if (req.url) {
197+
const page = req.url.replace(/[?#].*$/, '').slice(1)
198+
req.url = req.url.replace(page, rewrites.inv[page] || page)
199+
}
200+
next()
201+
})
202+
194203
// serve our index.html after vite history fallback
195204
return () => {
196205
server.middlewares.use(async (req, res, next) => {

0 commit comments

Comments
 (0)
Please sign in to comment.