文章
FrontMatter
提示
更多配置项可参见:
- 文章(Post)配置:PostFrontmatter (文章配置包含页面配置)
- 页面(Page)配置:PageFrontmatter (可参见页面配置 | Valaxy)
PostFrontmatter Types
import type { PageFrontMatter } from './page'
export type ExcerptType = 'md' | 'html' | 'text' | 'ai'
export interface PostFrontMatter extends PageFrontMatter {
/**
* @description:en-US Custom post title class in post list
* @description:zh-CN 文章列表中 自定义标题样式
*/
postTitleClass: string
/**
* @description:en-US Post Card Type, can be bilibili/yuque/... (need theme support)
* @description:zh-CN 卡片类型,可以是 bilibili/yuque/... (需主题支持)
*/
type: 'bilibili' | 'yuque' | string
/**
* @en override url, and jump directly
* @zh 覆盖 post url,直接跳转
*/
url: string
/**
* @description:en-US custom excerpt, `excerpt_type` will be invalid
* @description 手动指定摘要,此时 `excerpt_type` 将会无效
*/
excerpt: string
/**
* @description 摘要类型
* @default 'html'
* render type of excerpt
* - md: render as raw markdown
* - html: render as html
* - text: render as text
*/
excerpt_type: 'md' | 'text' | 'html' | 'ai'
/**
* @description:en-US Category, if it is an array, it represents multiple folders in order
* @description:zh-CN 分类,若为数组,则按顺序代表多层文件夹
*/
categories: string | string[]
/**
* @description:en-US Tags, can have multiple
* @description:zh-CN 标签,可以有多个
*/
tags: string[]
/**
* @description:en-US Whether to display the previous and next navigation
* @description:zh-CN 是否显示前一篇、后一篇导航
*/
nav: boolean
/**
* @description:en-US Pin to top, the larger the number, the closer to the front
* @description:zh-CN 置顶,数字越大越靠前
*/
top: number
/**
* @description:en-US Whether it is a draft, it will only be displayed during development
* @description:zh-CN 是否为草稿,将仅在开发时被展示
*/
draft: boolean
/**
* hide in index
* - true/`all`: hide in index & archive
* - `index`: hide in index
* @description 是否隐藏
*/
hide: 'index' | boolean
/**
* @en
* when the post is updated more than 30 days ago, show a warning
* default 30 days, you can set `time_warning` in frontmatter to change it
*
* @zh
* 当文章更新时间超过 30 天时,显示一个警告
* 默认 30 天,你可以在 frontmatter 中设置 `time_warning` (数字)来修改,单位 ms
* @example 3600000
*/
time_warning: boolean | number
/**
* @protected
* @see https://valaxy.site/guide/config/#%E9%98%85%E8%AF%BB%E7%BB%9F%E8%AE%A1
* @tutorial ⚠️ DO NOT SET MANUALLY (generated by `site.config.ts` -> `statistics.enable: true`)
* You can use `statistics.readTime.speed` to change the speed of reading time.
* @description:en-US Reading time
* @description:zh-CN 阅读时间
*/
readingTime: number
/**
* @protected
* @see https://valaxy.site/guide/config/#%E9%98%85%E8%AF%BB%E7%BB%9F%E8%AE%A1
* @tutorial ⚠️ DO NOT SET MANUALLY (generated by `site.config.ts` -> `statistics.enable: true`)
* You need enable `statistics` in site config to use this feature.
* @description:en-US Word count
* @description:zh-CN 字数统计
*/
wordCount: string
}文章(post)继承自页面(page),因此页面中的 Front Matter 通用被文章支持。
单篇文章支持的配置项。
譬如:
---
title: Title
hide: true
---title: 文章标题hide: 你可以在文章头部添加 hide 属性,来临时隐藏某篇文章。(该文章仍然会被渲染)true/all: 当设置为true或all时,该文章仍然会被渲染,你可以直接访问链接进行查看。但不会被显示在展示的文章卡片与归档中。index: 设置为index时,将只在首页隐藏,归档中仍然展示。(譬如放一些没有必要放在首页的笔记,并在归档中方便自己查看。)
图片与 Vue 组件封面
Yun 的文章卡片和文章头部都支持图片封面与可交互的 Vue 组件封面。组件封面不需要图片,可以完全使用内联 SVG、CSS 或 Canvas 绘制。
用 Vue 与 SVG 绘制封面
下面的天空、云层、轨道和星座全部由内联 SVG 绘制。点击按钮会连起星座并启动动画,再次点击暂停。不使用生成图片、图片地址或额外动画依赖。
- 在博客中创建
components/covers/HelloValaxyCover.vue。默认博客脚手架已包含该组件;已有博客可复制下面的完整代码。 - 在文章 Front Matter 中指定组件即可,
cover图片可省略:
---
title: Hello, Valaxy!
coverComponent: HelloValaxyCover
coverProps:
subtitle: 让文字,连成新的星座。
---完整 Vue + SVG 组件代码
<script setup lang="ts">
import { onMounted, onUnmounted, shallowRef, useId, useTemplateRef } from 'vue'
withDefaults(defineProps<{
context?: 'card' | 'page' | 'body'
subtitle?: string
}>(), {
context: 'body',
subtitle: 'Your words, a new constellation.',
})
const id = useId()
const root = shallowRef<HTMLElement>()
const svg = useTemplateRef<SVGSVGElement>('svg')
defineExpose({ svg })
const illuminated = shallowRef(false)
const inView = shallowRef(false)
const pageVisible = shallowRef(true)
// Fixed coordinates keep the server render and the first client render identical.
const stars = Array.from({ length: 36 }, (_, i) => ({
x: (i * 137 + 41) % 960,
y: (i * 73 + 29) % 410,
r: i % 4 === 0 ? 2 : 1,
}))
const constellation = [[530, 110], [570, 165], [615, 228], [660, 290], [707, 215], [754, 150], [794, 88]]
let observer: IntersectionObserver | undefined
function updateVisibility() {
pageVisible.value = document.visibilityState !== 'hidden'
}
onMounted(() => {
updateVisibility()
document.addEventListener('visibilitychange', updateVisibility)
if (typeof IntersectionObserver !== 'undefined') {
observer = new IntersectionObserver(([entry]) => {
inView.value = entry.isIntersecting
})
if (root.value)
observer.observe(root.value)
}
else {
inView.value = true
}
})
onUnmounted(() => {
observer?.disconnect()
document.removeEventListener('visibilitychange', updateVisibility)
})
</script>
<template>
<div
ref="root" class="hello-valaxy-cover" :data-context="context"
:class="{ illuminated, playing: illuminated && inView && pageVisible, compact: context === 'card' }"
>
<svg ref="svg" class="hello-valaxy-sky" viewBox="0 0 960 540" preserveAspectRatio="xMinYMid slice" aria-hidden="true" focusable="false">
<defs>
<linearGradient :id="`${id}-sky`" x2="0.8" y2="1">
<stop stop-color="#102751" />
<stop offset="0.65" stop-color="#225d93" />
<stop offset="1" stop-color="#7cc9da" />
</linearGradient>
<radialGradient :id="`${id}-halo`">
<stop stop-color="#95e5ed" stop-opacity="0.3" />
<stop offset="1" stop-color="#95e5ed" stop-opacity="0" />
</radialGradient>
<linearGradient :id="`${id}-caption`">
<stop stop-color="#091b3b" stop-opacity="0.65" />
<stop offset="0.8" stop-color="#091b3b" stop-opacity="0" />
</linearGradient>
</defs>
<path d="M0 0H960V540H0Z" :fill="`url(#${id}-sky)`" />
<circle cx="670" cy="210" r="250" :fill="`url(#${id}-halo)`" />
<g fill="#daefff" opacity="0.6">
<circle v-for="(star, i) in stars" :key="i" :cx="star.x" :cy="star.y" :r="star.r" />
</g>
<g class="hello-valaxy-orbits" fill="none" stroke="#b8e5f3">
<circle cx="660" cy="208" r="158" opacity="0.2" />
<ellipse cx="660" cy="208" rx="212" ry="66" transform="rotate(-28 660 208)" opacity="0.25" />
<g class="hello-valaxy-orbit hello-valaxy-motion">
<circle cx="818" cy="208" r="5" fill="#b5f1ee" stroke="none" />
<circle cx="818" cy="208" r="11" opacity="0.3" />
</g>
</g>
<g fill="none" stroke="#c4f6ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M530 110 570 165 615 228 660 290 707 215 754 150 794 88" opacity="0.18" />
<path class="hello-valaxy-constellation" d="M530 110 570 165 615 228 660 290 707 215 754 150 794 88" pathLength="1" />
</g>
<g class="hello-valaxy-stars" fill="#e0fcff">
<g v-for="([x, y], i) in constellation" :key="i">
<circle class="hello-valaxy-glow" :cx="x" :cy="y" r="12" />
<path :d="`M${x - 6} ${y}h12M${x} ${y - 6}v12`" stroke="currentColor" />
<circle :cx="x" :cy="y" r="3" />
</g>
</g>
<g class="hello-valaxy-clouds hello-valaxy-motion">
<path d="M-40 460Q30 366 110 420Q148 340 242 408Q304 369 374 433Q440 347 528 408Q605 350 690 425Q790 335 886 410Q954 385 1000 443V570H-40Z" fill="#6faecb" opacity="0.35" />
<path d="M-40 494Q60 416 148 474Q223 403 326 473Q405 428 473 487Q565 391 660 466Q748 412 835 474Q920 404 1000 478V570H-40Z" fill="#b4e4e9" opacity="0.4" />
</g>
<path d="M0 0H960V540H0Z" :fill="`url(#${id}-caption)`" />
<g fill="#f4fbff">
<text x="60" y="342" :style="{ fontFamily: 'Georgia, serif', fontSize: context === 'card' ? '74px' : '64px', fontWeight: 800 }">Hello, Valaxy!</text>
<text x="60" y="388" :style="{ fontFamily: 'sans-serif', fontSize: context === 'card' ? '28px' : '22px' }">{{ subtitle }}</text>
</g>
</svg>
<div class="hello-valaxy-caption">
<span class="hello-valaxy-description">Hello, Valaxy! {{ subtitle }}</span>
<button type="button" :aria-pressed="illuminated" @click="illuminated = !illuminated">
{{ illuminated ? 'Stars are shining' : 'Light up the stars' }}
</button>
</div>
</div>
</template>
<style scoped>
.hello-valaxy-cover {
position: relative;
isolation: isolate;
width: 100%;
height: auto;
min-height: 12rem;
aspect-ratio: 16 / 9;
overflow: hidden;
color: #f4fbff;
background: #102751;
border-radius: inherit;
}
.hello-valaxy-cover[data-context="card"],
.hello-valaxy-cover[data-context="page"] {
height: 100%;
}
.hello-valaxy-sky {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.hello-valaxy-caption {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
padding: clamp(1rem, 4vw, 2.5rem);
pointer-events: none;
}
.hello-valaxy-description {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.hello-valaxy-caption button {
padding: 0.4rem 0.8rem;
border: 1px solid rgb(220 245 255 / 0.65);
border-radius: 2rem;
color: inherit;
background: #153c64;
font: inherit;
font-size: 0.8rem;
cursor: pointer;
pointer-events: auto;
}
.hello-valaxy-caption button:focus-visible {
outline: 2px solid #c4f6ff;
outline-offset: 4px;
}
.hello-valaxy-constellation {
stroke-dasharray: 1;
stroke-dashoffset: 1;
transition: stroke-dashoffset 900ms ease;
}
.illuminated .hello-valaxy-constellation {
stroke-dashoffset: 0;
}
.hello-valaxy-glow {
opacity: 0;
transition: opacity 600ms ease;
}
.illuminated .hello-valaxy-glow {
opacity: 0.2;
}
.hello-valaxy-orbit {
transform-origin: 660px 208px;
animation: hello-valaxy-orbit 24s linear infinite;
}
.hello-valaxy-clouds {
animation: hello-valaxy-drift 12s ease-in-out infinite alternate;
}
.hello-valaxy-motion {
animation-play-state: paused;
}
.playing .hello-valaxy-motion {
animation-play-state: running;
}
.compact .hello-valaxy-caption {
padding: 1rem;
}
@keyframes hello-valaxy-orbit {
to { transform: rotate(360deg); }
}
@keyframes hello-valaxy-drift {
to { transform: translate(18px, -8px); }
}
@media (prefers-reduced-motion: reduce) {
.hello-valaxy-motion {
animation: none;
}
.hello-valaxy-constellation,
.hello-valaxy-glow {
transition: none;
}
}
</style>coverComponent是文件名对应的 PascalCase 组件名,不是文件路径或模板字符串。coverProps只使用可序列化的值,例如字符串、数字、布尔值、数组和普通对象。- 容器传入
context(卡片为card,文章头部为page)和可选的src(原始cover地址)。这两个保留属性优先于coverProps中的同名值;纯图形组件可以忽略src。 - 组件按需加载,并参与静态生成。JavaScript 启动前已有 SVG 画面,水合后即可交互。
- 卡片中的组件区域可以独立交互;标题及卡片其他区域仍可打开文章。
也支持 src/components/covers/,以及主题和插件的 components/covers/。同名组件遵循正常的根目录覆盖顺序;同一目录树内的文件应使用唯一组件名。
正文中复用组件
正文可以直接使用同一个组件,并传入独立的属性:
<HelloValaxyCover subtitle="在正文中,也能点亮星空。" />示例组件直接使用时默认 context="body",卡片、文章头部和正文实例各自保存开关状态。Yun 已接入组件封面;其他主题可以使用核心的 <ValaxyCover :src="cover" :component="coverComponent" :component-props="coverProps" /> 接入。正文 Vue 组件的支持不受主题限制,更多见 在 Markdown 中使用 Vue。
动画与渲染
示例通过 Vue 状态与 CSS 过渡、关键帧驱动 SVG,无需额外动画库。点击后开始动画,移出视口或切换到后台标签页时暂停;系统开启减少动态效果时保留开关功能,并停用运动和过渡。
SVG 渐变、遮罩等 ID 使用 useId(),避免同一页面多个实例相互影响。初始坐标应保持确定,不要在 setup 中调用 Math.random() 或读取 window。IntersectionObserver 等浏览器 API 放在 onMounted 中,并在卸载时清理。需要 DOM 的 Canvas/WebGL 库可以放在 <ClientOnly> 中,并提供 SVG 静态回退。
Motion 是可选项。Yun 已提供 @vueuse/motion;也可以在博客中执行 pnpm add motion-v,使用它的 <motion.path> 和 SVG 描边动画。这是两个不同的库,API 不能混用。参见 Motion for Vue:SVG 描边动画。在封面组件内导入动画库,并保留可见性、减少动态效果和 SSR 处理即可。
可选图片与分享预览
纯图片封面仍使用 cover: /images/my-cover.jpg。使用组件封面时,cover 可以提供组件缺失或运行失败后的图片回退;未设置时就没有图片回退。分享图片可以通过独立的 ogImage 指定:
coverComponent: HelloValaxyCover
# 两项都可省略;需要时使用自己的图片文件。
# cover: /images/cover-fallback.jpg
# ogImage: /images/share-preview.pngValaxy 不会在构建时自动截图 Vue 组件。你可以用下方的导出功能保存静态分享图。分享预览按 ogImage、cover、正文首图、站点 favicon 的顺序取值。SVG 组件封面的显示不依赖这两个图片字段。需要加载图片的组件可以用 withBase(src) 适配子路径部署。
从同一份 SVG 导出分享图
点击上方示例的「导出 PNG 分享图」即可下载 1200 × 630 的 PNG。背景、星座、云层和文字都来自正在展示的 SVG,CSS 动画会定格为导出时的画面;HTML 按钮不进入图片,也不会改变当前开关状态。
将下载的 hello-valaxy-og.png 放入博客的 public/images/,然后设置:
ogImage: /images/hello-valaxy-og.png修改组件或文案后重新导出即可。无需生图服务、上传或额外构建依赖。
自定义导出入口可以使用 <ValaxySvgExport>,传入已挂载的 SVG 元素。示例封面通过 defineExpose({ svg }) 暴露它:
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const cover = useTemplateRef<{ svg: SVGSVGElement | null }>('cover')
</script>
<template>
<HelloValaxyCover ref="cover" />
<ValaxySvgExport :svg="cover?.svg" filename="hello-valaxy-og.png" label="导出 PNG 分享图" />
</template>也可以 import { svgToPng } from 'valaxy',调用 await svgToPng(svg, { width: 1200, height: 630 }) 获取 PNG Blob,自行处理保存。尺寸变化遵循 SVG 的 viewBox 和 preserveAspectRatio;本例会略微裁切上下边缘。
导出针对自包含 SVG:图片资源需内嵌为 data URL,文字建议使用系统字体;不支持 HTML/foreignObject、SMIL 动画,也不会自动嵌入外部字体。请在浏览器挂载后触发导出,不能在 SSR setup 中调用。
排查组件封面
开发模式下,组件名不存在会在封面区域和控制台提示检查 components/covers/ 中的文件名;组件加载或渲染失败会显示组件名,并在控制台保留原始错误。设置了 cover 时仍保留图片回退。生产环境不展示这些开发提示。
摘要
你可以通过插入 <!-- more --> 的方式生成摘要(excerpt)。 可通过设置 excerpt_type 设置摘要渲染类型。
excerpt: 自定义摘要(优先级高于<!-- more -->)excerpt_type: 预览列表摘要的渲染类型(与<!-- more -->配合使用)md: 展示原始 Markdownhtml: 以 HTML 形式展示text: 以纯文本形式展示(去除 HTML 标签)
---
title: 'excerpt_type: text'
excerpt_type: text
---
## Header

<!-- more -->
Main Content---
title: 'excerpt_type: md'
excerpt_type: md
---
## Header

<!-- more -->
Main Content---
title: 'excerpt_type: html'
excerpt_type: html
---
## Header

<!-- more -->
Main Content---
title: 'custom excerpt'
excerpt: This is a custom excerpt.
---
## Header

Main ContentYou will get excerpt:
HEADER yun-bg## Header <!-- Rendered HTML -->This is a custom excerpt.插入
组件
脚本
可直接通过 useScriptTag 使用,封装为组件或直接添加在文章中。
<script lang="ts" setup>
useScriptTag('https://static.codepen.io/assets/embed/ei.js')
</script>强制规范
由于 Valaxy 支持解析 Vue 组件渲染,因此当您输入 <CustomComponent></CustomComponent> 时,它会解析 components 目录下的 CustomComponent.vue 组件并渲染。
当您不需要其被渲染时,请务必使用反引号包裹,如:
`<CustomComponent></CustomComponent>`To Be Continued.