How to write a theme?
TIP
Valaxy is fully compatible with the Vite/Vue ecosystem, so you can freely use third-party Vite/Vue plugins when writing themes.
Valaxy themes don’t need pre-compilation; you can directly publish the source files.
Work in progress…
As the author of Valaxy, I can easily implement my own themes. However, this also means I may have difficulty understanding the real needs of theme developers.
Therefore, if you have any questions about developing themes, please visit the QQ Channel "Yun Le Fang" or start a Discussion to communicate with me. I will provide as much help as possible and write documentation for common issues.
By the way, since there aren’t many themes yet, theme authors can discover some personal rewards from YunYouJun here.
Theme Examples
- valaxy-theme-starter: Valaxy theme development template
- valaxy-theme-yun: valaxy-theme-yun, a more complete theme example
- valaxy-theme-press: valaxy-theme-press, the current documentation theme example
创建主题模板
# Use valaxy-theme-starter template
pnpm create valaxy
# choose Theme在动手之前,我们先来了解一下一个 Valaxy 主题的基础结构,它与正常的用户目录结构也十分相似。
以 valaxy-theme-yun 为例:
尽管它们看起来很多,但是大部分都是可选的,你可以根据主题的需求按需编写。
App.vue: Theme entry file for mounting global theme componentsREADME.md: Theme documentation (undoubtedly essential 😛)client: Client-side helper functions exposed by the theme to usersindex.ts: Entry file for theme’s client-side helper functions
components: Theme componentsValaxyMain.vue: Theme’s article rendering componentYunSidebar.vue: Theme’s sidebar componentYunSponsor.vue: Theme’s sponsor componentYunWaline.vue: Third-party comment Waline adapter component
composables: Helper Composition APIconfig.ts: Theme configuration filehelper.ts: Theme helper functionsindex.ts: Theme Composition API entry filepost.ts: Theme’s post-related helper functions
docs: Theme documentation (organize and present with your favorite structure!)For customization and Dogfooding purposes, Valaxy’s documentation is built using itself with a documentation theme valaxy-theme-press. If you just want a simple and lightweight documentation site, Vitepress is a good choice. (valaxy-theme-starter may include this example template in the future.)
en-US: English documentationzh-CN: Chinese documentation
features: Theme signature features, functions that don’t depend on Vue Composition API (different fromcomposables)fireworks.ts: Fireworks click effect
layouts: Theme layouts (extend more layouts)default.vue: Default layouthome.vue: Home page layoutlayout.vue: Post list layoutpost.vue: Post layout (posts inpages/posts/folder default topostlayout)tags.vue: Tags layout
locales: Theme multi-language supporten.yml: English language filezh-CN.yml: Chinese language file
node_modules: Theme dependencies (do not commit to repository)node: Theme’s Node-side logicpackage.json: Theme information and dependenciespages: Theme’s default pages (extend more pages)index.vue: Home pagepage: Regular page[page].vue: Post list page, dynamic route, e.g.,/page/2
setup: Theme entry file (can register Vue plugins, etc.)main.ts: Main entry filedefineAppSetup
stores: Theme state managementapp.ts: Global state management file
styles: Theme stylesindex.ts: Theme styles entry file
tsconfig.json: 主题的 TypeScript 配置types: 主题的类型声明index.d.ts: 主题的类型声明入口文件
unocss.config.ts: 主题的 unocss 配置utils: 主题的工具函数valaxy.config.ts: 主题的配置文件
APIs
我们提供了一个扩展函数,以供你快速扩展页面信息。
你也可以直接扩展 vue-router/vite 插件中的 extendRoute。
https://github.com/posva/unplugin-vue-router/issues/43#issuecomment-1433140464 (now part of vue-router)
import { defineTheme } from 'valaxy'
export default defineTheme({
router: {
extendRoute(route) {
// want to get component absolute paths?
// const path = route.components.get('default')
console.log(route)
},
},
extendMd(ctx) {
console.log(ctx.path)
},
})import type { EditableTreeNode } from 'vue-router/unplugin'
// provided by valaxy, just as a tip
export interface ValaxyConfig {
vue?: Parameters<typeof Vue>[0]
components?: Parameters<typeof Components>[0]
unocss?: UnoCSSConfig
pages?: Parameters<typeof Pages>[0]
extendMd?: (ctx: {
route: EditableTreeNode
data: Readonly<Record<string, any>>
excerpt?: string
path: string
}) => void
}TIP
data 解析自 Markdown frontmatter,为原始数据(不可变),将会被合并至 route.meta.frontmatter 中。
Client
Toggle Dark
The following variables are stored in global state, which you can get through useAppStore.
isDark: Whether dark mode is enabledthemeColor: Theme color (followsisDark)toggleDark: Toggle dark modetoggleDarkWithTransition: Toggle dark mode with transition
Default Theme Config.valaxyDarkOptions
import type { UseDarkOptions } from '@vueuse/core'
// eslint-disable-next-line ts/no-namespace
export namespace DefaultTheme {
export interface Config {
valaxyDarkOptions?: {
/**
* Options for `useDark`
* disableTransition default is `true`
* Its options are not computed, init when loaded.
* @see https://vueuse.org/core/useDark
* @url https://paco.me/writing/disable-theme-transitions
*
* @zh `useDark` 的选项
* disableTransition 默认为 `true`,不会进行渐变过渡,这是 VueUse 的默认行为
*/
useDarkOptions?: UseDarkOptions
/**
* Enable circle transition when toggling dark mode
* Then use `toggleDarkWithTransition` instead of `toggleDark`
* @zh 启用圆形过渡切换暗黑模式
*/
circleTransition?: boolean
/**
* Theme color
* @zh 主题色
*/
themeColor?: {
/**
* Theme color for light mode
* @zh 亮色主题色
*/
light?: string
/**
* Theme color for dark mode
* @zh 暗色主题色
*/
dark?: string
}
}
/**
* Custom header levels of outline in the aside component.
*
* @default 2
*/
outline?: number | [number, number] | 'deep' | false
}
}Node
Hooks
开始编写
App.vue
你的入口文件
譬如我想要为主题添加一个全局的 Loading 页面。
你可以从 valaxy 导入全局状态 useAppStore,记录 showLoading 来实现。
你也可以使用你自己的全局状态管理。参见 全局状态管理。
<script lang="ts" setup>
import { useHead } from '@unhead/vue'
import { useAppStore } from 'valaxy'
import { onMounted } from 'vue'
// ...
const app = useAppStore()
onMounted(() => {
app.showLoading = false
})
</script>
<template>
<!-- ... -->
<!-- 添加 Loading 组件,components/YunLoading.vue -->
<!-- https://github.com/YunYouJun/valaxy/blob/main/packages/valaxy-theme-yun/components/YunLoading.vue -->
<Transition name="fade">
<YunLoading v-if="app.showLoading" />
</Transition>
</template>TIP
- You can completely override the root component through the
ValaxyApp.vuecomponent to achieve deeper customization needs. (Completely customized by you, no longer default handling such as mountingrouter-view, etc.)
ValaxyMain
你需要自定义一个 ValaxyMain 组件来决定主题的文章渲染部分。
你可以从
ValaxyMain的props中获取frontmatter与pageData。
<script lang="ts" setup>
import type { PageData, Post } from 'valaxy'
defineProps<{
frontmatter: Post
data?: PageData
}>()
</script>
<template>
<main>
<slot name="main-content">
<ValaxyMd :frontmatter="frontmatter">
<slot name="main-content-md" />
<slot />
</ValaxyMd>
</slot>
</main>
</template>样式
引入默认样式
Valaxy 提供了一些默认样式,你需要在主题中自行引入。
例如,新建 valaxy-theme-yun/setup/main.ts:
import { defineAppSetup, scrollTo } from 'valaxy'
import { nextTick } from 'vue'
// 引入 valaxy 公共样式
import 'valaxy/client/styles/common/index.scss'
// 你也可以按需引入
// common
import 'valaxy/client/styles/common/code.scss'
import 'valaxy/client/styles/common/hamburger.scss'
import 'valaxy/client/styles/common/transition.scss'
// Markdown Style
import 'valaxy/client/styles/common/markdown.scss'
export default defineAppSetup((ctx) => {
const { router, isClient } = ctx
if (!isClient)
return
router.afterEach((to, from) => {
if (to.path !== from.path)
return
nextTick(() => {
scrollTo(document.body, to.hash, {
smooth: true,
})
})
})
})Markdown 样式
Markdown 样式是主题呈现文章样式的部分,需要由主题自定义。
你可以参考 valaxy-theme-press 自定义 Markdown 主题的方式,见 styles/markdown.scss。
如果你想先使用常见的默认样式(后续再进行定制),你可以直接使用 star-markdown-css。 使用方式可参见 valaxy-theme-yun/styles
NProgress 进度条
内置了基础的 nprogress 样式,你可以通过覆盖 nprogress 的默认样式进行定制:
#nprogress {
pointer-events: none;
.bar {
background: var(--va-c-primary);
opacity: 0.75;
position: fixed;
z-index: 1024;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
}功能
API
你还可以使用 Valaxy 内置的 API 以快速实现相关功能。
获取用户的 Valaxy Config
你可以通过内置的 useValaxyConfig 获取用户的 Valaxy 配置。
TIP
这部分配置与用户的 valaxy.config.ts 中的配置相对应,但它仅在客户端使用,因此并不包含 Node 端相关配置(如 vite 等)。
import { useSiteConfig, useValaxyConfig } from 'valaxy'
import { useThemeConfig } from 'valaxy-theme-custom'
const config = useValaxyConfig()
// site.config.ts or config.value.siteConfig
const siteConfig = useSiteConfig()
// theme.config.ts or config.value.themeConfig
const themeConfig = useThemeConfig()提供 Typed useThemeConfig
你可以提供一个主题的 useThemeConfig 函数,以便自己/用户获得带有类型约束的配置。
// custom your theme type
import type { YunTheme } from '../types'
import { useValaxyConfig } from 'valaxy'
/**
* getThemeConfig
*/
export function useThemeConfig<ThemeConfig = YunTheme.Config>() {
const config = useValaxyConfig<ThemeConfig>()
return computed(() => config!.value.themeConfig)
}<script lang="ts" setup>
import { useThemeConfig } from 'valaxy-theme-custom'
const themeConfig = useThemeConfig()
</script>获取文章列表
获取文章列表有两种方式。
usePostList: 获取文章列表(不推荐)
import { usePostList } from 'valaxy'
const postList = usePostList()useSiteStore: 获取全局站点信息(推荐)
const site = useSiteStore()
// site.postList以上两者之间的区别是,usePostList 是一个基础函数,每次调用都会获取所有文章并重新过滤一次,而 useSiteStore 则会先调用 usePostList 并将获取的文章列表缓存在全局的状态中,以供你后续调用。
(此外,useSiteStore 还实现了保存文章时(如标题)热更新列表信息的功能。)
valaxy/packages/valaxy-theme-yun/components/YunPostList.vue 是一个使用
useSiteStore展示文章列表的示例。 分页功能可参考 valaxy-theme-yun/pages/page/[page].vue 与 valaxy-theme-yun/components/YunPostList.vue。
获取文章分类与标签
在你获取文章列表后,site.postList 中的每篇文章都具有 categories(分类) 与 tags(标签) 属性。
你还可以通过 useCategories 与 useTags 获取所有分类、标签,其中便包含了与文章的对应关系。
import { useCategories, useTags } from 'valaxy'
const categories = useCategories()
const tags = useTags()- valaxy/packages/valaxy-theme-yun/layouts/categories.vue 是一个使用
useCategories展示文章分类的示例。 - valaxy/packages/valaxy-theme-yun/layouts/tags.vue 是一个使用
useTags展示文章标签的示例。(useYunTags是主题对useTags的封裝。)
useTags中的tags为一个对象,其键为标签名,值为对应的文章列表。useCategories可传入参数category(useCategories('aaa')) 以获取指定分类的文章列表。
获取 Front-matter
你可以通过 useFrontmatter 获取当前页面的 Front-matter。
譬如:
<script lang="ts" setup>
import { useFrontmatter } from 'valaxy'
const fm = useFrontmatter()
</script>
<template>
<h1>{{ fm.title }}</h1>
</template>全局状态管理
你可以借助 Pinia (Valaxy 内置)建立自己的全局状态,并在随后使用它,
import { acceptHMRUpdate, defineStore } from 'pinia'
// custom your theme name
export const useYunAppStore = defineStore('yun-app', () => {
// global cache for yun
return {}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useYunAppStore, import.meta.hot))// where you want to use
// components/YunExample.vue
import { useYunAppStore } from '../stores/app'
const yun = useYunAppStore()上一篇/下一篇
文章底部通常存在切换上一篇/下一篇的导航。
你可以利用 siteStore.postList 自行实现,也可以使用 Valaxy 提供的 usePrevNext。
import { usePrevNext } from 'valaxy'
const [prev, next] = usePrevNext()
// prev/next type is PostFrontMatter
// prev.title prev.path目录
如果你想要快速实现一个目录,Valaxy 提供了一个内置钩子函数 useOutline。
你可以用它快速获取文章页的目录信息 headers 与对应点击事件 handleClick,如:
<script setup lang="ts">
import { useOutline } from 'valaxy'
const { headers, handleClick } = useOutline()
</script>
<template>
<nav aria-labelledby="doc-outline-aria-label">
<span id="doc-outline-aria-label" class="visually-hidden">
Table of Contents
</span>
<PressOutlineItem
class="va-toc relative z-1 css-i18n-toc"
:headers="headers"
:on-click="handleClick"
root
/>
</nav>
</template>引用静态资源
当主题需要内置一些静态资源(如:图片等),你可以通过相对引用的方式实现。(这在 scss 样式文件中也适用)
譬如 assets 与 components 处于同一目录下时:
├── components
│ └── ValaxyLogo.vue
└── assets
└── images
└── valaxy-logo.png<script lang="ts" setup>
import valaxyLogoPng from '../assets/images/valaxy-logo.png'
</script>
<template>
<img max-w="50" m="auto" :src="valaxyLogoPng" alt="Valaxy Logo" z="1">
</template>
<style scoped>
.test-image {
background-image: url('../assets/images/valaxy-logo.png');
}
</style>Third Party Plugin
Implement Comments
As a blog, users typically have commenting needs.
Due to the variety of comment systems, theme developers like Hexo often need to repeatedly implement multiple comment systems on the theme side. This is obviously tedious.
Valaxy decided to centrally provide various packaged comment components and helper functions through plugins.
For example, theme developers can use valaxy-addon-waline to quickly integrate the Waline comment system. Users can use the same configuration to roam between different themes.
For integration, see valaxy-addon-waline.
Performance Optimization
Add Dep Pre-bundling optimizeDeps
To improve the loading performance of subsequent pages, Vite bundles ESM dependencies with many internal modules into a single module. If your theme depends on some large ESM packages, you can pre-build these dependencies by adding the optimizeDeps option.
dayjshas been pre-built by default, you don’t need to add it again.
import { defineTheme } from 'valaxy'
export default defineTheme({
vite: {
optimizeDeps: {
include: ['lodash-es'],
},
}
})Remind Users with Special Needs to Install Third-party Plugins
If your theme adapts to multiple addons, but not all users need to install them. Such as comment plugins:
valaxy-addon-walinevalaxy-addon-twikoo
When a user hasn’t actively installed the corresponding addon (i.e., the addon doesn’t exist), it will default to redirecting to an empty function.
Therefore, if a plugin is not required, please remind users who want to use this feature to install the corresponding plugin in the theme documentation.
To Be Continued.