Post

Post VS Page

FrontMatter ​

TIP

More configuration options can be found in:

PostFrontmatter Types
post.ts
ts
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 is a descendant of page, so the front matter in pages are supported by posts.

For example:

md
---
title: Title
hide: true
---
  • title: Title of the article.
  • hide: Adding hide in the header allows you to hide the article temporarily. (The article will still be rendered)
    • true / all: When set to true or all, the article will be rendered, and you can view it by visiting the link directly. It will not be displayed in article cards or archives.
    • index: When set to index, it will be hidden only in the front page. It will still be displayed in archives. (You can use this for some notes unnecessary for the front page, but good for the archive for reference sometimes)

Image and Vue Component Covers ​

Yun supports both image covers and interactive Vue covers in post cards and article headers. A component cover does not require an image. It can draw its entire scene with inline SVG, CSS or Canvas.

A cover drawn with Vue and SVG ​

This example draws the sky, clouds, orbits and constellation with inline SVG. Click the button to draw the constellation and start the motion; click again to pause. No generated image, image URL or animation dependency is used.

Hello, Valaxy! Your words, a new constellation.
  1. Create components/covers/HelloValaxyCover.vue in your blog. The default blog scaffold already includes it. Copy the complete component below to use it in an existing blog.
  2. Select the component in a post’s frontmatter. cover is optional:
yaml
---
title: Hello, Valaxy!
coverComponent: HelloValaxyCover
coverProps:
  subtitle: Your words, a new constellation.
---
Complete Vue + SVG component
HelloValaxyCover.vue
vue
<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 is the PascalCase component name derived from its filename, not a path or template string.
  • Use only serializable values in coverProps: strings, numbers, booleans, arrays and plain objects.
  • The container supplies context (card or page) and optional src (the original cover URL). These reserved props override values in coverProps. A purely graphical component can ignore src.
  • Components load on demand and participate in static generation. The SVG remains visible before JavaScript starts; interaction starts after hydration.
  • A component cover owns its interactive area. The title and the rest of the post card still open the article.

Components can also live in src/components/covers/ or a theme/addon’s components/covers/. Matching names follow the normal root override order. Use unique filenames within each cover directory tree.

Reuse the component in Markdown ​

Markdown supports the same component with independent props and state:

md
<HelloValaxyCover subtitle="The same component, inside your story." />

The example defaults to context="body" when used directly. Each card, header and body instance keeps its own toggle state. Yun integrates component covers; other themes can use the core <ValaxyCover :src="cover" :component="coverComponent" :component-props="coverProps" /> renderer. Vue components in article content work across themes; see Using Vue in Markdown.

Animation and rendering ​

The example uses Vue state and CSS transitions/keyframes to animate SVG. It needs only Vue. Its animation starts on click, pauses outside the viewport or in a hidden tab, and respects prefers-reduced-motion while keeping the toggle functional.

Use useId() for SVG gradient/mask IDs so several instances can appear on one page. Keep initial coordinates deterministic; do not call Math.random() or read window during setup. Put browser APIs such as IntersectionObserver in onMounted, and disconnect them on unmount. Canvas/WebGL libraries that need the DOM can be wrapped in <ClientOnly> with an SVG fallback.

Motion is optional. Yun already provides @vueuse/motion; alternatively, install motion-v in your blog (pnpm add motion-v) for its <motion.path> API and SVG path drawing. These are separate libraries; their APIs are not interchangeable. See Motion for Vue: SVG line drawing. Import the library in your cover component, and preserve the same visibility, reduced-motion and SSR behavior.

Optional images and social previews ​

For an image-only cover, use cover: /images/my-cover.jpg. For a component cover, the same optional field supplies an image fallback if the component is missing or fails. Without it, there is no image fallback. A separate ogImage can be provided for social sharing:

yaml
coverComponent: HelloValaxyCover
# Both are optional; use your own image files if needed.
# cover: /images/cover-fallback.jpg
# ogImage: /images/share-preview.png

Valaxy does not automatically capture Vue components during a build. Use the export below to save a static social image. Social previews use ogImage, then cover, then the first article image or the site’s favicon. Neither image field is required to render the SVG cover. Image-based components can use withBase(src) for subpath deployments.

Export a social image from the same SVG ​

Click Export PNG below the live example to download a 1200 × 630 PNG. The sky, constellation, clouds and text come from the displayed SVG. CSS animation is frozen at its current frame; HTML buttons are excluded and the toggle state stays unchanged.

Place the downloaded hello-valaxy-og.png in your blog’s public/images/ directory and set:

yaml
ogImage: /images/hello-valaxy-og.png

Export again after changing the component or its text. No image generation service, upload or additional build dependency is needed.

To add your own export control, pass a mounted SVG element to <ValaxySvgExport>. The example cover exposes it with defineExpose({ svg }):

vue
<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" />
</template>

Alternatively, import { svgToPng } from 'valaxy' and call await svgToPng(svg, { width: 1200, height: 630 }) to get a PNG Blob for your own download flow. Resizing follows the SVG’s viewBox and preserveAspectRatio; this example slightly crops the top and bottom edges.

Export supports self-contained SVG. Embed image resources as data URLs and prefer system fonts. HTML/foreignObject, SMIL animation and automatic external font embedding are unsupported. Trigger export after mounting in a browser, never during SSR setup.

Troubleshooting component covers ​

In development, unknown component names show a hint in the cover area and console pointing to components/covers/. Loading or rendering failures identify the component and retain the original error in the console. An optional image cover remains as a fallback. These developer diagnostics are hidden in production.

Excerpt ​

You can insert <!--more--> to generate an excerpt. You can set the excerpt rendering type by setting excerpt_type.

  • excerpt: Custom excerpt (higher priority than <!-- more -->)
  • excerpt_type: The rendering type for the excerpt in the preview list (Used with <!-- more -->)
    • md: Display as original markdown
    • html: Display as HTML
    • text: Display as text (removing HTML tags)
md
---
title: 'excerpt_type: text'
excerpt_type: text
---

## Header

![yun-bg](https://cdn.yunyoujun.cn/img/bg/stars-timing-0-blur-30px.jpg)

<!-- more -->

Main Content
md
---
title: 'excerpt_type: md'
excerpt_type: md
---

## Header

![yun-bg](https://cdn.yunyoujun.cn/img/bg/stars-timing-0-blur-30px.jpg)

<!-- more -->

Main Content
md
---
title: 'excerpt_type: html'
excerpt_type: html
---

## Header

![yun-bg](https://cdn.yunyoujun.cn/img/bg/stars-timing-0-blur-30px.jpg)

<!-- more -->

Main Content
md
---
title: 'custom excerpt'
excerpt: This is a custom excerpt.
---

## Header

![yun-bg](https://cdn.yunyoujun.cn/img/bg/stars-timing-0-blur-30px.jpg)

Main Content

You will get excerpt:

md
HEADER yun-bg
md
## Header ![yun-bg](https://cdn.yunyoujun.cn/img/bg/stars-timing-0-blur-30px.jpg)
md
<!-- Rendered HTML -->
md
This is a custom excerpt.

Insert ​

Components ​

  • To insert existing public components in the article, please refer to Components.
  • To insert custom components in the article, please refer to Custom Components.

Scripts ​

You can use useScriptTag directly, encapsulate it as a component, or add it directly to the article.

vue
<script lang="ts" setup>
useScriptTag('https://static.codepen.io/assets/embed/ei.js')
</script>

Force Standard ​

Since Valaxy supports parsing Vue component rendering, when you enter <CustomComponent></CustomComponent>, it will parse the CustomComponent.vue component in the components directory and render it.

When you don’t want it to be rendered, be sure to wrap it in backticks, like:

md
`<CustomComponent></CustomComponent>`

To Be Continued.

Contributors