Skip to content

Commit

Permalink
feat: createContentLoader
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Mar 13, 2023
1 parent 905f58b commit d2838e3
Show file tree
Hide file tree
Showing 20 changed files with 335 additions and 47 deletions.
4 changes: 2 additions & 2 deletions __tests__/e2e/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
]
},
{
text: 'Static Data',
text: 'Data Loading',
items: [
{
text: 'Test Page',
link: '/static-data/data'
link: '/data-loading/data'
}
]
},
Expand Down
File renamed without changes.
9 changes: 9 additions & 0 deletions __tests__/e2e/data-loading/content/bar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: bar
---

Hello

---

world
9 changes: 9 additions & 0 deletions __tests__/e2e/data-loading/content/foo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: foo
---

Hello

---

world
13 changes: 13 additions & 0 deletions __tests__/e2e/data-loading/contentLoader.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createContentLoader } from 'vitepress'

export default createContentLoader('data-loading/content/*.md', {
includeSrc: true,
excerpt: true,
render: true,
transform(data) {
return data.map((item) => ({
...item,
transformed: true
}))
}
})
10 changes: 10 additions & 0 deletions __tests__/e2e/data-loading/data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Static Data

<script setup lang="ts">
import { data } from './basic.data.js'
import { data as contentData } from './contentLoader.data.js'
</script>

<pre id="basic">{{ data }}</pre>

<pre id="content">{{ contentData }}</pre>
42 changes: 42 additions & 0 deletions __tests__/e2e/data-loading/data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
describe('static data file support in vite 3', () => {
beforeAll(async () => {
await goto('/data-loading/data')
})

test('render correct content', async () => {
expect(await page.textContent('pre#basic')).toMatchInlineSnapshot(`
"[
{
\\"foo\\": true
},
{
\\"bar\\": true
}
]"
`)
expect(await page.textContent('pre#content')).toMatchInlineSnapshot(`
"[
{
\\"src\\": \\"---\\\\ntitle: bar\\\\n---\\\\n\\\\nHello\\\\n\\\\n---\\\\n\\\\nworld\\\\n\\",
\\"html\\": \\"<p>Hello</p>\\\\n<hr>\\\\n<p>world</p>\\\\n\\",
\\"frontmatter\\": {
\\"title\\": \\"bar\\"
},
\\"excerpt\\": \\"<p>Hello</p>\\\\n\\",
\\"url\\": \\"/data-loading/content/bar.html\\",
\\"transformed\\": true
},
{
\\"src\\": \\"---\\\\ntitle: foo\\\\n---\\\\n\\\\nHello\\\\n\\\\n---\\\\n\\\\nworld\\\\n\\",
\\"html\\": \\"<p>Hello</p>\\\\n<hr>\\\\n<p>world</p>\\\\n\\",
\\"frontmatter\\": {
\\"title\\": \\"foo\\"
},
\\"excerpt\\": \\"<p>Hello</p>\\\\n\\",
\\"url\\": \\"/data-loading/content/foo.html\\",
\\"transformed\\": true
}
]"
`)
})
})
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion __tests__/e2e/multi-sidebar/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('test multi sidebar sort root', () => {
expect(sidebarContent).toEqual([
'Frontmatter',
'& <Text Literals &> code',
'Static Data',
'Data Loading',
'Multi Sidebar Test',
'Dynamic Routes',
'Markdown Extensions'
Expand Down
14 changes: 0 additions & 14 deletions __tests__/e2e/static-data/__snapshots__/data.test.ts.snap

This file was deleted.

7 changes: 0 additions & 7 deletions __tests__/e2e/static-data/data.md

This file was deleted.

12 changes: 0 additions & 12 deletions __tests__/e2e/static-data/data.test.ts

This file was deleted.

1 change: 1 addition & 0 deletions __tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"isolatedModules": false,
"baseUrl": ".",
"types": ["node", "vitest/globals"],
"paths": {
Expand Down
111 changes: 100 additions & 11 deletions docs/guide/data-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,118 @@ export default {

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.

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:
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.

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!

```js
import fs from 'node:fs'
import parseFrontmatter from 'gray-matter'
import { parse } from 'csv-parse/sync'

export default {
// watch all blog posts
watch: ['./posts/*.md'],
watch: ['./data/*.csv'],
load(watchedFiles) {
// watchedFiles will be an array of absolute paths of the matched files.
// generate an array of blog post metadata that can be used to render
// a list in the theme layout
return watchedFiles.map(file => {
const content = fs.readFileSync(file, 'utf-8')
const { data, excerpt } = parseFrontmatter(content)
return {
file,
data,
excerpt
}
return parse(fs.readFileSync(file, 'utf-8'), {
columns: true,
skip_empty_lines: true
})
})
}
}
```

## `createContentLoader`

When building a content focused site, we often need to create an "archive" or "index" page: a page where we list all available entries in our content collection, for example blog posts or API pages. We **can** implement this directly with the data loader API, but since this is such a common use case, VitePress also provides a `createContentLoader` helper to simplify this:

```js
// posts.data.js
import { createContentLoader } from 'vitepress'

export default createContentLoader('posts/*.md', /* options */)
```

The helper takes a glob pattern relative to [project root](./routing#project-root), and returns a `{ watch, load }` data loader object that can be used as the default export in a data loader file. It also implements caching based on file modified timestamps to improve dev performance.

Note the loader only works with Markdown files - matched non-Markdown files will be skipped.

The loaded data will be an array with the type of `ContentData[]`:

```ts
interface ContentData {
// mapped absolute URL for the page. e.g. /posts/hello.html
url: string
// frontmatter data of the page
frontmatter: Record<string, any>

// the following are only present if relevant options are enabled
// we will discuss them below
src: string | undefined
html: string | undefined
excerpt: string | undefined
}
```

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:

```vue
<script setup>
import { data as posts } from './posts.data.js'
</script>
<template>
<h1>All Blog Posts</h1>
<ul>
<li v-for="post of posts">
<a :href="post.url">{{ post.frontmatter.title }}</a>
<span>by {{ post.frontmatter.author }}</span>
</li>
</ul>
</template>
```

### Options

The default data may not suit all needs - you can opt-in to transform the data using options:

```js
// posts.data.js
import { createContentLoader } from 'vitepress'

export default createContentLoader('posts/*.md', {
includeSrc: true, // include raw markdown source?
render: true, // include rendered full page HTML?
excerpt: true, // include excerpt?
transform(rawData) {
// map, sort, or filter the raw data as you wish.
// the final result is what will be shipped to the client.
return rawData.sort((a, b) => {
return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
}).map(page => {
page.src // raw markdown source
page.html // rendered full page HTML
page.excerpt // rendered excerpt HTML (content above first `---`)
return {/* ... */}
})
}
})
```

Check out how it is used in the [Vue.js blog](https://github.com/vuejs/blog/blob/main/.vitepress/theme/posts.data.ts).

The `createContentLoader` API can also be used inside [build hooks](/reference/site-config#build-hooks):

```js
// .vitepress/config.js
export default {
async buildEnd() {
const posts = await createContentLoader('posts/*.md').load()
// generate files based on posts metadata, e.g. RSS feed
}
}
```

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"fast-glob": "^3.2.12",
"fs-extra": "^11.1.0",
"get-port": "^6.1.2",
"gray-matter": "^4.0.3",
"lint-staged": "^13.2.0",
"lodash.template": "^4.5.0",
"lru-cache": "^7.18.3",
Expand Down
2 changes: 2 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: 4 additions & 0 deletions src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ export async function resolveConfig(
userConfig
}

// to be shared with content loaders
// @ts-ignore
global.VITEPRESS_CONFIG = config

return config
}

Expand Down
Loading

0 comments on commit d2838e3

Please sign in to comment.