feat(theme): add meta config support for collection, close #781 (#796)

This commit is contained in:
pengzhanbo 2025-12-13 15:45:55 +08:00 committed by GitHub
parent f7bc044147
commit 6e601f9f0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 415 additions and 30 deletions

View File

@ -229,6 +229,96 @@ permalink: /guide/a1b2c3d4/
---
```
In the collection, the `meta` option allows you to set the display method of article metadata,
This setting will directly affect the display of metadata on both the **article list page** and the **article content page**:
::: code-tabs#config
@tab .vuepress/config.ts
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
collections: [
{
type: 'doc',
dir: 'guide',
title: 'Guide',
// [!code hl:11]
meta: {
tags: true, // Whether to display labels
/**
* Whether to display the creation time, or set the time format
* - 'short': Display as `2022-01-01`, default
* - 'long': Display as `2022-01-01 00:00:00`
*/
createTime: true, // boolean | 'short' | 'long'
readingTime: true, // Whether to display the reading time estimate
wordCount: true, // Whether to display the word count
}
}
]
})
})
```
@tab .vuepress/plume.config.ts
``` ts
import { defineThemeConfig } from 'vuepress-theme-plume'
export default defineThemeConfig({
collections: [
{
type: 'doc',
dir: 'guide',
title: 'Guide',
// [!code hl:11]
meta: {
tags: true, // Whether to display labels
/**
* Whether to display the creation time, or set the time format
* - 'short': Display as `2022-01-01`, default
* - 'long': Display as `2022-01-01 00:00:00`
*/
createTime: true, // boolean | 'short' | 'long'
readingTime: true, // Whether to display the reading time estimate
wordCount: true, // Whether to display the word count
}
}
]
})
```
:::
In markdown, configure article metadata through frontmatter:
```md
---
title: Article Title
createTime: 2024/01/01 00:00:00
tags:
- tag1
- tag2
---
```
`title` and `createTime` are automatically generated when files are created and support manual modification.
### Available Properties
| Property | Type | Default | Description |
| ---------- | ---------- | ------------ | ------------- |
| title | `string` | File name | Article title |
| createTime | `string` | Current time | Creation time |
| tags | `string[]` | `[]` | Article tags |
Also supports all fields from [common frontmatter configuration](../../config/frontmatter/basic.md).
## Sidebar Configuration
Provides flexible sidebar navigation configuration options:

View File

@ -744,7 +744,75 @@ Automatically switches to `top` layout on narrow-screen devices to ensure displa
## Article Metadata
Configure article metadata through frontmatter:
## 文章元数据
In the collection, the `meta` option allows you to set the display method of article metadata,
This setting will directly affect the display of metadata on both the **article list page** and the **article content page**:
::: code-tabs#config
@tab .vuepress/config.ts
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
collections: [
{
type: 'post',
dir: 'blog',
title: '博客',
// [!code hl:11]
meta: {
tags: true, // Whether to display labels
/**
* Whether to display the creation time, or set the time format
* - 'short': Display as `2022-01-01`, default
* - 'long': Display as `2022-01-01 00:00:00`
*/
createTime: true, // boolean | 'short' | 'long'
readingTime: true, // Whether to display the reading time estimate
wordCount: true, // Whether to display the word count
}
}
]
})
})
```
@tab .vuepress/plume.config.ts
``` ts
import { defineThemeConfig } from 'vuepress-theme-plume'
export default defineThemeConfig({
collections: [
{
type: 'post',
dir: 'blog',
title: '博客',
// [!code hl:11]
meta: {
tags: true, // Whether to display labels
/**
* Whether to display the creation time, or set the time format
* - 'short': Display as `2022-01-01`, default
* - 'long': Display as `2022-01-01 00:00:00`
*/
createTime: true, // boolean | 'short' | 'long'
readingTime: true, // Whether to display the reading time estimate
wordCount: true, // Whether to display the word count
}
}
]
})
```
:::
In markdown, configure article metadata through frontmatter:
```md
---
@ -760,16 +828,16 @@ tags:
### Available Properties
| Property | Type | Default | Description |
| ---------- | ------------------- | ------- | ----------------------------------------- |
| title | `string` | File name | Article title |
| createTime | `string` | Current time | Creation time |
| tags | `string[]` | `[]` | Article tags |
| sticky | `boolean \| number` | false | Sticky flag, higher numbers sort first |
| draft | `boolean` | false | Draft mode, hidden after build |
| cover | `string` | `''` | Cover image path |
| coverStyle | `PostCoverStyle` | `null` | Cover style configuration |
| excerpt | `boolean \| string` | '' | Excerpt content, supports auto-extraction |
| Property | Type | Default | Description |
| ---------- | ------------------- | ------------ | ----------------------------------------- |
| title | `string` | File name | Article title |
| createTime | `string` | Current time | Creation time |
| tags | `string[]` | `[]` | Article tags |
| sticky | `boolean \| number` | false | Sticky flag, higher numbers sort first |
| draft | `boolean` | false | Draft mode, hidden after build |
| cover | `string` | `''` | Cover image path |
| coverStyle | `PostCoverStyle` | `null` | Cover style configuration |
| excerpt | `boolean \| string` | '' | Excerpt content, supports auto-extraction |
Also supports all fields from [common frontmatter configuration](../../config/frontmatter/basic.md).

View File

@ -228,6 +228,97 @@ permalink: /guide/a1b2c3d4/
---
```
## 文章元数据
在集合中通过 `meta` 选项,可以设置文章元数据的显示方式:
::: code-tabs#config
@tab .vuepress/config.ts
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
collections: [
{
type: 'doc',
dir: 'guide',
title: '指南',
// [!code hl:11]
meta: {
tags: true, // 是否显示标签
/**
* 是否显示创建时间,或设置时间格式
* - 'short': 显示为 `2022-01-01`,默认
* - 'long': 显示为 `2022-01-01 00:00:00`
*/
createTime: true, // boolean | 'short' | 'long'
readingTime: true, // 是否显示阅读时间估算
wordCount: true, // 是否显示字数统计
}
}
]
})
})
```
@tab .vuepress/plume.config.ts
``` ts
import { defineThemeConfig } from 'vuepress-theme-plume'
export default defineThemeConfig({
collections: [
{
type: 'doc',
dir: 'guide',
title: '指南',
// [!code hl:11]
meta: {
tags: true, // 是否显示标签
/**
* 是否显示创建时间,或设置时间格式
* - 'short': 显示为 `2022-01-01`,默认
* - 'long': 显示为 `2022-01-01 00:00:00`
*/
createTime: true, // boolean | 'short' | 'long'
readingTime: true, // 是否显示阅读时间估算
wordCount: true, // 是否显示字数统计
}
}
]
})
```
:::
在 markdown 中,通过 frontmatter 配置文章元数据:
```md
---
title: 文章标题
createTime: 2024/01/01 00:00:00
tags:
- tag1
- tag2
---
```
`title``createTime` 在文件创建时自动生成,支持手动修改。
### 可用属性
| 属性 | 类型 | 默认值 | 说明 |
| ---------- | ------------------- | -------- | -------------------------- |
| title | `string` | 文件名 | 文章标题 |
| createTime | `string` | 当前时间 | 创建时间 |
| tags | `string[]` | `[]` | 文章标签 |
同时支持[通用 frontmatter 配置](../../config/frontmatter/basic.md)中的所有字段。
## 侧边栏配置
提供灵活的侧边栏导航配置选项:

View File

@ -738,7 +738,73 @@ interface PostCoverStyle {
## 文章元数据
通过 frontmatter 配置文章元数据:
在集合中通过 `meta` 选项,可以设置文章元数据的显示方式,
该设置将直接影响 **文章列表页****文章内容页** 的元数据显示:
::: code-tabs#config
@tab .vuepress/config.ts
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
collections: [
{
type: 'post',
dir: 'blog',
title: '博客',
// [!code hl:11]
meta: {
tags: true, // 是否显示标签
/**
* 是否显示创建时间,或设置时间格式
* - 'short': 显示为 `2022-01-01`,默认
* - 'long': 显示为 `2022-01-01 00:00:00`
*/
createTime: true, // boolean | 'short' | 'long'
readingTime: true, // 是否显示阅读时间估算
wordCount: true, // 是否显示字数统计
}
}
]
})
})
```
@tab .vuepress/plume.config.ts
``` ts
import { defineThemeConfig } from 'vuepress-theme-plume'
export default defineThemeConfig({
collections: [
{
type: 'post',
dir: 'blog',
title: '博客',
// [!code hl:11]
meta: {
tags: true, // 是否显示标签
/**
* 是否显示创建时间,或设置时间格式
* - 'short': 显示为 `2022-01-01`,默认
* - 'long': 显示为 `2022-01-01 00:00:00`
*/
createTime: true, // boolean | 'short' | 'long'
readingTime: true, // 是否显示阅读时间估算
wordCount: true, // 是否显示字数统计
}
}
]
})
```
:::
在 markdown 中,通过 frontmatter 配置文章元数据:
```md
---

View File

@ -2,6 +2,7 @@
import type { PostsCoverStyle, ThemePostsItem } from '../../../shared/index.js'
import VPLink from '@theme/VPLink.vue'
import { isMobile as _isMobile } from '@vuepress/helper/client'
import { getReadingTimeLocale, useReadingTimeLocaleConfig } from '@vuepress/plugin-reading-time/client'
import { computed, onMounted, ref } from 'vue'
import { withBase } from 'vuepress/client'
import { useData, useInternalLink, useTagColors } from '../../composables/index.js'
@ -24,9 +25,26 @@ const { collection } = useData<'page', 'post'>()
const colors = useTagColors()
const { categories: categoriesLink, tags: tagsLink } = useInternalLink()
const createTime = computed(() => post.createTime?.split(/\s|T/)[0].replace(/\//g, '-'))
const metaConfig = computed(() => collection.value?.meta ?? {})
const createTime = computed(() => {
if (!post.createTime || metaConfig.value.createTime === false)
return ''
const format = metaConfig.value.createTime === true ? 'short' : metaConfig.value.createTime ?? 'short'
return (format !== 'short' ? post.createTime : post.createTime?.split(/\s|T/)[0]).replace(/\//g, '-')
})
const categoryList = computed(() => post.categoryList ?? [])
const readingTimeLocale = useReadingTimeLocaleConfig()
const readingTime = computed(() => {
const fallback = { time: '', words: '' }
if (!post.readingTime)
return fallback
const res = readingTimeLocale.value ? getReadingTimeLocale(post.readingTime, readingTimeLocale.value) : fallback
res.time = res.time.replace(/^\D+/, '')
return res
})
const sticky = computed(() => {
if (typeof post.sticky === 'boolean') {
return post.sticky
@ -122,6 +140,11 @@ const coverStyles = computed(() => {
<span v-if="i !== categoryList.length - 1">/</span>
</template>
</div>
<div v-if="readingTime.time && (metaConfig.readingTime !== false || metaConfig.wordCount !== false)" class="reading-time">
<span class="vpi-books icon" />
<span v-if="metaConfig.wordCount !== false">{{ readingTime.words }}</span>
<span v-if="metaConfig.readingTime !== false">{{ readingTime.time }}</span>
</div>
<div v-if="tags.length" class="tag-list">
<span class="icon vpi-tag" />
<template v-for="tag in tags" :key="tag.name">
@ -316,7 +339,7 @@ const coverStyles = computed(() => {
.post-item-content .post-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
gap: 0 16px;
align-items: center;
justify-content: flex-start;
font-size: 14px;
@ -327,19 +350,14 @@ const coverStyles = computed(() => {
.post-item-content .post-meta > div {
display: flex;
gap: 0 6px;
align-items: center;
justify-content: flex-start;
}
.post-item-content .post-meta .tag-list {
display: flex;
align-items: center;
}
.post-item-content .post-meta .tag-list .tag {
display: inline-block;
padding: 3px 5px;
margin-right: 6px;
font-size: 12px;
line-height: 1;
color: var(--vp-tag-color);
@ -355,7 +373,6 @@ const coverStyles = computed(() => {
.post-item-content .post-meta .icon {
width: 14px;
height: 14px;
margin-right: 0.3rem;
color: var(--vp-c-text-3);
transition: color var(--vp-t-color);
}
@ -370,11 +387,11 @@ const coverStyles = computed(() => {
margin: 0.5rem 0;
}
.excerpt.vp-doc :deep(p:first-of-type) {
.excerpt.vp-doc :deep(:first-of-type) {
margin-top: 0;
}
.excerpt.vp-doc :deep(p:last-of-type) {
.excerpt.vp-doc :deep(:last-of-type) {
margin-bottom: 0;
}
@ -392,4 +409,10 @@ const coverStyles = computed(() => {
margin: 16px 0;
}
}
@media (max-width: 419px) {
.excerpt.vp-doc :deep(.hint-container) {
margin: 16px 0;
}
}
</style>

View File

@ -12,17 +12,26 @@ const readingTime = useReadingTimeLocale()
const { tags: tagsLink } = useInternalLink()
const { isPosts } = usePostsPageData()
const metaConfig = computed(() => collection.value?.meta ?? {})
const createTime = computed(() => {
if (matter.value.createTime === false || metaConfig.value.createTime === false)
return ''
const format = metaConfig.value.createTime === true ? 'short' : metaConfig.value.createTime ?? 'short'
const show = theme.value.createTime ?? true
if (!show || (show === 'only-posts' && !isPosts.value))
return ''
if (matter.value.createTime)
return matter.value.createTime.split(/\s|T/)[0].replace(/\//g, '-')
const createTime = matter.value.createTime
if (createTime)
return (format !== 'short' ? createTime : createTime.split(/\s|T/)[0]).replace(/\//g, '-')
return ''
})
const tags = computed(() => {
if (metaConfig.value.tags === false)
return []
const tagTheme = collection.value?.tagsTheme ?? 'colored'
if (matter.value.tags) {
return matter.value.tags.slice(0, 4).map(tag => ({
@ -45,7 +54,7 @@ const badge = computed(() => {
const hasDocMetaSlot = inject<Ref<boolean>>('doc-meta-slot-exists', ref(false))
const hasMeta = computed(() =>
readingTime.value.time
(readingTime.value.time && (metaConfig.value.readingTime !== false || metaConfig.value.wordCount !== false))
|| tags.value.length
|| createTime.value
|| hasDocMetaSlot.value,
@ -66,10 +75,10 @@ const hasMeta = computed(() =>
<div v-if="hasMeta" class="vp-doc-meta">
<slot name="doc-meta-before" />
<p v-if="readingTime.time && matter.readingTime !== false" class="reading-time">
<p v-if="readingTime.time && matter.readingTime !== false && (metaConfig.readingTime !== false || metaConfig.wordCount !== false)" class="reading-time">
<span class="vpi-books icon" />
<span>{{ readingTime.words }}</span>
<span>{{ readingTime.time }}</span>
<span v-if="metaConfig.wordCount !== false">{{ readingTime.words }}</span>
<span v-if="metaConfig.readingTime !== false">{{ readingTime.time }}</span>
</p>
<p v-if="tags.length > 0">

View File

@ -48,6 +48,7 @@ function processPostData(
excerpt: '',
cover: page.frontmatter.cover,
coverStyle: page.frontmatter.coverStyle,
readingTime: page.data.readingTime,
}
if (typeof data.cover === 'object') {

View File

@ -50,6 +50,36 @@ export interface ThemeBaseCollection {
* frontmatter
*/
autoFrontmatter?: AutoFrontmatterOptions | false
/**
*
* -
* -
*/
meta?: {
/**
*
* @default true
*/
tags?: boolean
/**
*
* @default true
*/
readingTime?: boolean
/**
*
* @default true
*/
wordCount?: boolean
/**
*
* - `short`: 2022-01-01
* - `long`: 2022-01-01 00:00:00
* @default 'short'
*/
createTime?: 'short' | 'long' | boolean
}
}
/**

View File

@ -1,3 +1,5 @@
import type { ReadingTime } from '@vuepress/plugin-reading-time'
export interface PostsCategoryItem {
/**
* ID
@ -100,6 +102,11 @@ export interface ThemePostsItem {
* 稿
*/
draft?: boolean
/**
*
*/
readingTime?: ReadingTime
}
/**

View File

@ -5,7 +5,7 @@ export interface ThemePostFrontmatter extends ThemePageFrontmatter {
/**
*
*/
createTime?: string
createTime?: string | false
/**
*