Skip to content

Commit d2838e3

Browse files
committed
feat: createContentLoader
1 parent 905f58b commit d2838e3

File tree

20 files changed

+335
-47
lines changed

20 files changed

+335
-47
lines changed

__tests__/e2e/.vitepress/config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
2222
]
2323
},
2424
{
25-
text: 'Static Data',
25+
text: 'Data Loading',
2626
items: [
2727
{
2828
text: 'Test Page',
29-
link: '/static-data/data'
29+
link: '/data-loading/data'
3030
}
3131
]
3232
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: bar
3+
---
4+
5+
Hello
6+
7+
---
8+
9+
world
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: foo
3+
---
4+
5+
Hello
6+
7+
---
8+
9+
world
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createContentLoader } from 'vitepress'
2+
3+
export default createContentLoader('data-loading/content/*.md', {
4+
includeSrc: true,
5+
excerpt: true,
6+
render: true,
7+
transform(data) {
8+
return data.map((item) => ({
9+
...item,
10+
transformed: true
11+
}))
12+
}
13+
})

__tests__/e2e/data-loading/data.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Static Data
2+
3+
<script setup lang="ts">
4+
import { data } from './basic.data.js'
5+
import { data as contentData } from './contentLoader.data.js'
6+
</script>
7+
8+
<pre id="basic">{{ data }}</pre>
9+
10+
<pre id="content">{{ contentData }}</pre>
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
describe('static data file support in vite 3', () => {
2+
beforeAll(async () => {
3+
await goto('/data-loading/data')
4+
})
5+
6+
test('render correct content', async () => {
7+
expect(await page.textContent('pre#basic')).toMatchInlineSnapshot(`
8+
"[
9+
{
10+
\\"foo\\": true
11+
},
12+
{
13+
\\"bar\\": true
14+
}
15+
]"
16+
`)
17+
expect(await page.textContent('pre#content')).toMatchInlineSnapshot(`
18+
"[
19+
{
20+
\\"src\\": \\"---\\\\ntitle: bar\\\\n---\\\\n\\\\nHello\\\\n\\\\n---\\\\n\\\\nworld\\\\n\\",
21+
\\"html\\": \\"<p>Hello</p>\\\\n<hr>\\\\n<p>world</p>\\\\n\\",
22+
\\"frontmatter\\": {
23+
\\"title\\": \\"bar\\"
24+
},
25+
\\"excerpt\\": \\"<p>Hello</p>\\\\n\\",
26+
\\"url\\": \\"/data-loading/content/bar.html\\",
27+
\\"transformed\\": true
28+
},
29+
{
30+
\\"src\\": \\"---\\\\ntitle: foo\\\\n---\\\\n\\\\nHello\\\\n\\\\n---\\\\n\\\\nworld\\\\n\\",
31+
\\"html\\": \\"<p>Hello</p>\\\\n<hr>\\\\n<p>world</p>\\\\n\\",
32+
\\"frontmatter\\": {
33+
\\"title\\": \\"foo\\"
34+
},
35+
\\"excerpt\\": \\"<p>Hello</p>\\\\n\\",
36+
\\"url\\": \\"/data-loading/content/foo.html\\",
37+
\\"transformed\\": true
38+
}
39+
]"
40+
`)
41+
})
42+
})

__tests__/e2e/multi-sidebar/index.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('test multi sidebar sort root', () => {
1212
expect(sidebarContent).toEqual([
1313
'Frontmatter',
1414
'& <Text Literals &> code',
15-
'Static Data',
15+
'Data Loading',
1616
'Multi Sidebar Test',
1717
'Dynamic Routes',
1818
'Markdown Extensions'

__tests__/e2e/static-data/__snapshots__/data.test.ts.snap

-14
This file was deleted.

__tests__/e2e/static-data/data.md

-7
This file was deleted.

__tests__/e2e/static-data/data.test.ts

-12
This file was deleted.

__tests__/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"extends": "../tsconfig.json",
33
"compilerOptions": {
4+
"isolatedModules": false,
45
"baseUrl": ".",
56
"types": ["node", "vitest/globals"],
67
"paths": {

docs/guide/data-loading.md

+100-11
Original file line numberDiff line numberDiff line change
@@ -56,29 +56,118 @@ export default {
5656

5757
When you need to generate data based on local files, you should use the `watch` option in the data loader so that changes made to these files can trigger hot updates.
5858

59-
The `watch` option is also convenient in that you can use [glob patterns](https://github.com/mrmlnc/fast-glob#pattern-syntax) to match multiple files. The patterns can be relative to the loader file itself, and the `load()` function will receive the matched files as absolute paths:
59+
The `watch` option is also convenient in that you can use [glob patterns](https://github.com/mrmlnc/fast-glob#pattern-syntax) to match multiple files. The patterns can be relative to the loader file itself, and the `load()` function will receive the matched files as absolute paths.
60+
61+
The following example shows loading CSV files and transforming them into JSON using [csv-parse](https://github.com/adaltas/node-csv/tree/master/packages/csv-parse/). Because this file only executes at build time, you will not be shipping the CSV parser to the client!
6062

6163
```js
6264
import fs from 'node:fs'
63-
import parseFrontmatter from 'gray-matter'
65+
import { parse } from 'csv-parse/sync'
6466

6567
export default {
66-
// watch all blog posts
67-
watch: ['./posts/*.md'],
68+
watch: ['./data/*.csv'],
6869
load(watchedFiles) {
6970
// watchedFiles will be an array of absolute paths of the matched files.
7071
// generate an array of blog post metadata that can be used to render
7172
// a list in the theme layout
7273
return watchedFiles.map(file => {
73-
const content = fs.readFileSync(file, 'utf-8')
74-
const { data, excerpt } = parseFrontmatter(content)
75-
return {
76-
file,
77-
data,
78-
excerpt
79-
}
74+
return parse(fs.readFileSync(file, 'utf-8'), {
75+
columns: true,
76+
skip_empty_lines: true
77+
})
78+
})
79+
}
80+
}
81+
```
82+
83+
## `createContentLoader`
84+
85+
When building a content focused site, we often need to create an "archive" or "index" page: a page where we list all available entries in our content collection, for example blog posts or API pages. We **can** implement this directly with the data loader API, but since this is such a common use case, VitePress also provides a `createContentLoader` helper to simplify this:
86+
87+
```js
88+
// posts.data.js
89+
import { createContentLoader } from 'vitepress'
90+
91+
export default createContentLoader('posts/*.md', /* options */)
92+
```
93+
94+
The helper takes a glob pattern relative to [project root](./routing#project-root), and returns a `{ watch, load }` data loader object that can be used as the default export in a data loader file. It also implements caching based on file modified timestamps to improve dev performance.
95+
96+
Note the loader only works with Markdown files - matched non-Markdown files will be skipped.
97+
98+
The loaded data will be an array with the type of `ContentData[]`:
99+
100+
```ts
101+
interface ContentData {
102+
// mapped absolute URL for the page. e.g. /posts/hello.html
103+
url: string
104+
// frontmatter data of the page
105+
frontmatter: Record<string, any>
106+
107+
// the following are only present if relevant options are enabled
108+
// we will discuss them below
109+
src: string | undefined
110+
html: string | undefined
111+
excerpt: string | undefined
112+
}
113+
```
114+
115+
By default, only `url` and `frontmatter` are provided. This is because the loaded data will be inlined as JSON in the client bundle, so we need to be cautious about its size. Here's an example using the data to build a minimal blog index page:
116+
117+
```vue
118+
<script setup>
119+
import { data as posts } from './posts.data.js'
120+
</script>
121+
122+
<template>
123+
<h1>All Blog Posts</h1>
124+
<ul>
125+
<li v-for="post of posts">
126+
<a :href="post.url">{{ post.frontmatter.title }}</a>
127+
<span>by {{ post.frontmatter.author }}</span>
128+
</li>
129+
</ul>
130+
</template>
131+
```
132+
133+
### Options
134+
135+
The default data may not suit all needs - you can opt-in to transform the data using options:
136+
137+
```js
138+
// posts.data.js
139+
import { createContentLoader } from 'vitepress'
140+
141+
export default createContentLoader('posts/*.md', {
142+
includeSrc: true, // include raw markdown source?
143+
render: true, // include rendered full page HTML?
144+
excerpt: true, // include excerpt?
145+
transform(rawData) {
146+
// map, sort, or filter the raw data as you wish.
147+
// the final result is what will be shipped to the client.
148+
return rawData.sort((a, b) => {
149+
return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
150+
}).map(page => {
151+
page.src // raw markdown source
152+
page.html // rendered full page HTML
153+
page.excerpt // rendered excerpt HTML (content above first `---`)
154+
return {/* ... */}
80155
})
81156
}
157+
})
158+
```
159+
160+
Check out how it is used in the [Vue.js blog](https://github.com/vuejs/blog/blob/main/.vitepress/theme/posts.data.ts).
161+
162+
The `createContentLoader` API can also be used inside [build hooks](/reference/site-config#build-hooks):
163+
164+
```js
165+
// .vitepress/config.js
166+
export default {
167+
async buildEnd() {
168+
const posts = await createContentLoader('posts/*.md').load()
169+
// generate files based on posts metadata, e.g. RSS feed
170+
}
82171
}
83172
```
84173

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
"fast-glob": "^3.2.12",
136136
"fs-extra": "^11.1.0",
137137
"get-port": "^6.1.2",
138+
"gray-matter": "^4.0.3",
138139
"lint-staged": "^13.2.0",
139140
"lodash.template": "^4.5.0",
140141
"lru-cache": "^7.18.3",

pnpm-lock.yaml

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

src/node/config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ export async function resolveConfig(
287287
userConfig
288288
}
289289

290+
// to be shared with content loaders
291+
// @ts-ignore
292+
global.VITEPRESS_CONFIG = config
293+
290294
return config
291295
}
292296

0 commit comments

Comments
 (0)