From 13f94a6663d5b4472ce380ee1c27e6124da8fec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=83=BD=E5=AE=81?= Date: Sat, 29 Jul 2023 18:26:47 +0800 Subject: [PATCH] feat(build): custom excerpt for `createContentLoader` (#2698) Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> --- docs/guide/data-loading.md | 54 ++++++++++++++++++++++++--- docs/guide/deploy.md | 2 +- docs/guide/extending-default-theme.md | 4 +- docs/guide/ssr-compat.md | 2 +- docs/reference/runtime-api.md | 2 +- src/node/contentLoader.ts | 42 +++++++++++++++++---- 6 files changed, 87 insertions(+), 19 deletions(-) diff --git a/docs/guide/data-loading.md b/docs/guide/data-loading.md index d23e25d6..6d54b2ca 100644 --- a/docs/guide/data-loading.md +++ b/docs/guide/data-loading.md @@ -23,7 +23,7 @@ The loader module is evaluated only in Node.js, so you can import Node APIs and You can then import data from this file in `.md` pages and `.vue` components using the `data` named export: -```html +```vue @@ -70,7 +70,7 @@ export default { // 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 => { + return watchedFiles.map((file) => { return parse(fs.readFileSync(file, 'utf-8'), { columns: true, skip_empty_lines: true @@ -147,9 +147,9 @@ export default createContentLoader('posts/*.md', { // 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 + }).map((page) => { + page.src // raw markdown source + page.html // rendered full page HTML page.excerpt // rendered excerpt HTML (content above first `---`) return {/* ... */} }) @@ -159,7 +159,7 @@ export default createContentLoader('posts/*.md', { 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): +The `createContentLoader` API can also be used inside [build hooks](../reference/site-config#build-hooks): ```js // .vitepress/config.js @@ -171,6 +171,48 @@ export default { } ``` +**Types** + +```ts +interface ContentOptions { + /** + * Include src? + * @default false + */ + includeSrc?: boolean + + /** + * Render src to HTML and include in data? + * @default false + */ + render?: boolean + + /** + * If `boolean`, whether to parse and include excerpt? (rendered as HTML) + * + * If `function`, control how the excerpt is extracted from the content. + * + * If `string`, define a custom separator to be used for extracting the + * excerpt. Default separator is `---` if `excerpt` is `true`. + * + * @see https://github.com/jonschlinkert/gray-matter#optionsexcerpt + * @see https://github.com/jonschlinkert/gray-matter#optionsexcerpt_separator + * + * @default false + */ + excerpt?: + | boolean + | ((file: { data: { [key: string]: any }; content: string; excerpt?: string }, options?: any) => void) + | string + + /** + * Transform the data. Note the data will be inlined as JSON in the client + * bundle if imported from components or markdown files. + */ + transform?: (data: ContentData[]) => T | Promise +} +``` + ## Typed Data Loaders When using TypeScript, you can type your loader and `data` export like so: diff --git a/docs/guide/deploy.md b/docs/guide/deploy.md index 7ebbc314..679d9ff9 100644 --- a/docs/guide/deploy.md +++ b/docs/guide/deploy.md @@ -73,7 +73,7 @@ Cache-Control: max-age=31536000,immutable cache-control: immutable ``` -Note: the `_headers` file should be placed in the [public directory](/guide/asset-handling#the-public-directory) - in our case, `docs/public/_headers` - so that it is copied verbatim to the output directory. +Note: the `_headers` file should be placed in the [public directory](./asset-handling#the-public-directory) - in our case, `docs/public/_headers` - so that it is copied verbatim to the output directory. [Netlify custom headers documentation](https://docs.netlify.com/routing/headers/) diff --git a/docs/guide/extending-default-theme.md b/docs/guide/extending-default-theme.md index a0ccfd9e..332fa971 100644 --- a/docs/guide/extending-default-theme.md +++ b/docs/guide/extending-default-theme.md @@ -59,10 +59,10 @@ export default DefaultTheme ``` ::: warning -If you are using optional components like the [Team Page](/reference/default-theme-team-page) components, make sure to also import them from `vitepress/theme-without-fonts`! +If you are using optional components like the [Team Page](../reference/default-theme-team-page) components, make sure to also import them from `vitepress/theme-without-fonts`! ::: -If your font is a local file referenced via `@font-face`, it will be processed as an asset and included under `.vitepress/dist/assets` with hashed filename. To preload this file, use the [transformHead](/reference/site-config#transformhead) build hook: +If your font is a local file referenced via `@font-face`, it will be processed as an asset and included under `.vitepress/dist/assets` with hashed filename. To preload this file, use the [transformHead](../reference/site-config#transformhead) build hook: ```js // .vitepress/config.js diff --git a/docs/guide/ssr-compat.md b/docs/guide/ssr-compat.md index 11c6a081..82eb58e5 100644 --- a/docs/guide/ssr-compat.md +++ b/docs/guide/ssr-compat.md @@ -48,7 +48,7 @@ if (!import.meta.env.SSR) { } ``` -Since [`Theme.enhanceApp`](/guide/custom-theme#theme-interface) can be async, you can conditionally import and register Vue plugins that access browser APIs on import: +Since [`Theme.enhanceApp`](./custom-theme#theme-interface) can be async, you can conditionally import and register Vue plugins that access browser APIs on import: ```js // .vitepress/theme/index.js diff --git a/docs/reference/runtime-api.md b/docs/reference/runtime-api.md index e4839d0c..03bb6160 100644 --- a/docs/reference/runtime-api.md +++ b/docs/reference/runtime-api.md @@ -141,7 +141,7 @@ If you are using or demoing components that are not SSR-friendly (for example, c ``` -- Related: [SSR Compatibility](/guide/ssr-compat) +- Related: [SSR Compatibility](../guide/ssr-compat) ## `$frontmatter` diff --git a/src/node/contentLoader.ts b/src/node/contentLoader.ts index 048fc216..98c5585d 100644 --- a/src/node/contentLoader.ts +++ b/src/node/contentLoader.ts @@ -9,19 +9,41 @@ import { createMarkdownRenderer, type MarkdownRenderer } from './markdown' export interface ContentOptions { /** * Include src? - * default: false + * @default false */ includeSrc?: boolean + /** * Render src to HTML and include in data? - * default: false + * @default false */ render?: boolean + /** - * Whether to parse and include excerpt (rendered as HTML) - * default: false + * If `boolean`, whether to parse and include excerpt? (rendered as HTML) + * + * If `function`, control how the excerpt is extracted from the content. + * + * If `string`, define a custom separator to be used for extracting the + * excerpt. Default separator is `---` if `excerpt` is `true`. + * + * @see https://github.com/jonschlinkert/gray-matter#optionsexcerpt + * @see https://github.com/jonschlinkert/gray-matter#optionsexcerpt_separator + * + * @default false */ - excerpt?: boolean + excerpt?: + | boolean + | (( + file: { + data: { [key: string]: any } + content: string + excerpt?: string + }, + options?: any + ) => void) + | string + /** * Transform the data. Note the data will be inlined as JSON in the client * bundle if imported from components or markdown files. @@ -110,9 +132,13 @@ export function createContentLoader( raw.push(cached.data) } else { const src = fs.readFileSync(file, 'utf-8') - const { data: frontmatter, excerpt } = matter(src, { - excerpt: true - }) + const { data: frontmatter, excerpt } = matter( + src, + // @ts-expect-error gray-matter types are wrong + typeof renderExcerpt === 'string' + ? { excerpt_separator: renderExcerpt } + : { excerpt: renderExcerpt } + ) const url = '/' + normalizePath(path.relative(config.srcDir, file)).replace(