Framework Maintainability Guide
Scope and Objective
This guide defines the extension-first engineering model for this VitePress framework.
The primary objective is to allow feature growth through configuration and registration APIs, not core rewrites.
Detailed Pages
Source-of-Truth Structure
- Runtime and APIs:
.vitepress/utils/vitepress/** - Theme components:
.vitepress/theme/components/** - Locale resources:
.vitepress/config/locale/** - Markdown plugins:
.vitepress/plugins/** - Plugin registration:
.vitepress/config/markdown-plugins.ts
All internal imports must use project aliases (@utils, @config, @components) to keep refactors stable.
Extension APIs (No Core Edits Required)
1. Typography Style Registry
API: @utils/vitepress/api/frontmatter/hero/HeroTypographyRegistryApi
import { heroTypographyRegistry } from "@utils/vitepress/api/frontmatter/hero";
heroTypographyRegistry.registerStyle({
type: "editorial-soft",
aliases: ["soft-editorial"],
motion: {
intensity: 0.9,
title: { x: 6, y: -4, scale: 1.03 },
text: { x: 8, y: 3, scale: 1.02 },
tagline: { x: 4, y: 6, scale: 1.01 },
image: { x: 5, y: -2, scale: 1.015 },
transitionDuration: 520,
transitionDelayStep: 36,
transitionEasing: "cubic-bezier(0.2, 0.9, 0.2, 1)",
},
});Then use hero.typography.type: editorial-soft in frontmatter.
No core typography runtime changes are required.
2. Navigation Dropdown Layout Registry
API: @utils/vitepress/api/navigation/NavDropdownLayoutRegistryApi
import { navDropdownLayoutRegistry } from "@utils/vitepress/api/navigation";
import VPNavLayoutEditorial from "@components/navigation/layouts/VPNavLayoutEditorial.vue";
navDropdownLayoutRegistry.registerLayout("editorial", VPNavLayoutEditorial);Then in nav config:
dropdown: {
layout: "editorial",
panels: [...]
}Alternative override (per-item):
dropdown: {
layoutComponent: "VPNavLayoutEditorial",
panels: [...]
}3. Floating Element Type Registry
API: @utils/vitepress/api/frontmatter/hero/FloatingElementRegistryApi
import { floatingElementRegistry } from "@utils/vitepress/api/frontmatter/hero";
floatingElementRegistry.registerType({
type: "keyword-chip",
renderAs: "badge",
className: "floating-keyword-chip",
});Frontmatter usage:
hero:
floating:
items:
- type: keyword-chip
text: Event APIFor fully custom UI, bind to a registered Vue component:
hero:
floating:
items:
- component: HeroFloatingCourseCard
componentProps:
title: KubeJS Course
provider: GitBookCreating a New Component
- Create the component file under
.vitepress/theme/components/<category>/. - Export it from the corresponding registry barrel under
.vitepress/utils/vitepress/componentRegistry/if it must be reusable globally. - Register it in
.vitepress/utils/vitepress/components.tsif Markdown/global component usage is required. - Add locale JSON files under:
.vitepress/config/locale/en-US/components/....vitepress/config/locale/zh-CN/components/...
- Add or update component ID mapping in
.vitepress/config/locale/component-id-mapping.json.
Minimal i18n component pattern:
<script setup lang="ts">
import { useSafeI18n } from "@utils/i18n/locale";
const { t } = useSafeI18n("my-component", {
title: "Default title",
});
</script>
<template>
<h2>{{ t.title }}</h2>
</template>i18n System Contract
useSafeI18nnow resolves locale reactively from VitePress language state.- Translation buckets are cached per
componentId@locale. - Locale switches update component text without reload.
- Missing keys automatically fall back to default translations passed in code.
- Component file-path mapping is resolved from
component-id-mapping.json.
This design is resilient for dynamic locale switches and avoids stale translation state from singleton non-reactive access.
Adding a New Markdown Plugin
- Add plugin implementation in
.vitepress/plugins/. - Wire it in
.vitepress/config/markdown-plugins.ts. - If plugin output needs a custom component, register the component via
components.ts. - Add localized usage examples in docs pages.
Theme Sync Standard (First Enter + Reload Safe)
Use this standard for every component that has theme-sensitive visuals (hero backgrounds, themed assets, icon sets, metadata-like visual blocks, nav cards).
- Do not read DOM theme classes directly in feature components.
- Do not use raw
useData().isDarkas the sole source for first-paint visuals. - Use
useThemeRuntime(isDark)and consumeeffectiveDark,themeReady, andversionfor visual decisions that must be stable on first enter, reload, and runtime toggle. - Inside hero descendants, use
useHeroTheme()and preferisDarkRef.valueplusresolveThemeValue(...). - For first-paint-sensitive hero parts, gate rendering with
themeReadyinVPHeroto avoid light/dark flash. - Never fallback from dark to light or light to dark automatically. Shared resolvers must follow
dark ?? valueandlight ?? value. - Component folders stay view-only. If theme sync needs observers, scheduling, or shared lifecycle, move that logic into
.vitepress/utils/vitepress/runtime/theme/**.
Reference APIs:
@utils/vitepress/runtime/theme/useThemeRuntime@utils/vitepress/runtime/theme/heroThemeContext@utils/vitepress/runtime/theme/themeValueResolver
Minimal pattern:
import { useData } from "vitepress";
import { useThemeRuntime } from "@utils/vitepress/runtime/theme";
const { isDark } = useData();
const { effectiveDark, themeReady, version } = useThemeRuntime(isDark);Resize Sync Standard
All resize-sensitive components must use the shared resize runtime instead of ad-hoc observers.
- Use
createElementResizeState(targetRef, onResize, { debounceMs }). - Re-observe once the target element exists (
if (targetRef.value) reobserve(targetRef.value)). - Do not create manual
ResizeObserverinstances unless the shared API cannot satisfy a special case. - Keep cleanup lifecycle in the shared runtime, not duplicated per component.
Reference API:
@utils/vitepress/runtime/viewport/elementResizeState
Minimal pattern:
import { createElementResizeState } from "@utils/vitepress/runtime/viewport";
const targetRef = ref<HTMLElement | null>(null);
const { reobserve } = createElementResizeState(
targetRef,
() => syncLayout(),
{ debounceMs: 80 },
);Day-to-Day Development Workflow
Use this order when changing framework behavior so contracts stay synchronized:
- Change the contract first.
Update schema, types, and normalization in
.vitepress/utils/vitepress/api/**. - Change shared runtime second.
Put stateful lifecycle, DOM observation, viewport sync, theme sync, or adaptive logic in
.vitepress/utils/vitepress/runtime/**. - Change view components third.
Keep
.vitepress/theme/components/**focused on rendering and light composition. - Update examples and docs immediately. If a new frontmatter key or extension point exists, add at least one markdown example and document the intended usage.
- Run the matching verification commands before merge.
Recommended command sequence from repo root:
yarn locale
yarn sidebar
yarn tags
yarn buildWhen config or frontmatter contracts changed, also run:
yarn sync-config
yarn frontmatterCode Placement Guide
Use these directories as the primary source of truth:
.vitepress/config/project-config.tsSite-level product settings, feature toggles, language list, search provider, deployment, social links..vitepress/config/lang/**Nav/theme/search locale modules..vitepress/config/shaders/**Built-in shader templates and shader registry..vitepress/config/markdown-plugins.tsMarkdown plugin composition and registration order..vitepress/plugins/**Markdown-it plugin implementations..vitepress/theme/components/**Vue rendering layer. Components should stay thin and consume normalized config/runtime state..vitepress/theme/styles/**Global style layers, variables, plugin styles, shared component CSS..vitepress/utils/vitepress/api/**Schemas, normalization, registries, contract types, extension APIs..vitepress/utils/vitepress/runtime/**Stateful domains such as theme sync, viewport sync, hero behavior, media/runtime observers..vitepress/utils/vitepress/componentRegistry/**Reusable export barrels for globally shared components..vitepress/utils/vitepress/components.tsFinal global component registration for markdown/runtime use.
Rule of thumb:
- If it parses or validates config, it belongs in
api. - If it owns lifecycle or DOM coordination, it belongs in
runtime. - If it just renders props/state, it belongs in
theme/components.
Runtime and Function Extension Playbook
When adding a new function, composable, service, or controller:
- Put pure contract helpers in
api, nottheme/components. - Put stateful controllers in
runtime, preferably as small class-based modules when lifecycle is non-trivial. - Export new public APIs from the nearest
index.tsbarrel. - Avoid direct DOM reads in feature components when the behavior is shared or timing-sensitive.
- Prefer one shared observer/runtime over repeated
MutationObserverorResizeObserverinstances inside many components.
Good examples already in the framework:
- Theme stabilization:
.vitepress/utils/vitepress/runtime/theme/** - Element resize runtime:
.vitepress/utils/vitepress/runtime/viewport/** - Hero nav adaptation:
.vitepress/utils/vitepress/runtime/hero/navAdaptiveState.ts
Component and Global Registration Playbook
When adding a new Vue component:
- Create it under the correct folder in
.vitepress/theme/components/<category>/. - If it should be imported by other framework code, export it from the matching barrel in
.vitepress/utils/vitepress/componentRegistry/**. - If markdown users should be able to write it directly, register it in
.vitepress/utils/vitepress/components.ts. - If it has UI text, add locale resources and keep component ID mapping synchronized.
For markdown-facing components, the registry chain should stay:
component file -> componentRegistry barrel -> components.ts -> markdown/runtime consumption
Configuration Extension Playbook
For new configuration or frontmatter fields:
- Add the type to
.vitepress/utils/vitepress/api/frontmatter/hero/HeroFrontmatterApi.tsor the relevant API module. - Normalize legacy and modern forms in the same API layer.
- Keep rendering components consuming normalized values only.
- If the field affects nav/search/theme behavior, update the matching runtime controller rather than duplicating logic in the component.
- Add a docs example page and update the maintainability/reference docs in the same change.
For site-level feature changes:
- Update
.vitepress/config/project-config.ts. - Update locale config under
.vitepress/config/lang/**if labels or search locales change. - Run
yarn sync-configandyarn frontmatterwhen the configuration must propagate into docs metadata or generated content.
Style Extension Playbook
Global styling is layered deliberately. Follow the import order in .vitepress/theme/styles/index.css:
- Config variables
- Base styles
- Plugin styles
- Shared component styles
Use the right style vehicle for the job:
- Scoped
<style>in a component: Use for component-local layout and visuals. - Global CSS under
.vitepress/theme/styles/**: Use for cross-component tokens, plugin skinning, layout primitives, and theme-wide selectors. - CSS variables via frontmatter/config: Use for runtime-themable values, especially hero/background/nav/search colors.
Do not introduce ad-hoc global selectors when a CSS variable contract or scoped rule is enough.
Hero Extension Playbook
Hero extension work should start from the contract layer, not the view layer.
1. Add a Typography Style
Register new styles with the typography registry:
import { heroTypographyRegistry } from "@utils/vitepress/api/frontmatter/hero";
heroTypographyRegistry.registerStyle({
type: "editorial-soft",
aliases: ["soft-editorial"],
motion: {
intensity: 0.9,
title: { x: 6, y: -4, scale: 1.03 },
text: { x: 8, y: 3, scale: 1.02 },
tagline: { x: 4, y: 6, scale: 1.01 },
image: { x: 5, y: -2, scale: 1.015 },
transitionDuration: 520,
transitionDelayStep: 36,
transitionEasing: "cubic-bezier(0.2, 0.9, 0.2, 1)",
},
});Use the registered type in frontmatter as hero.typography.type.
2. Add a Floating Element Type
Register custom floating item types in the floating registry:
import { floatingElementRegistry } from "@utils/vitepress/api/frontmatter/hero";
floatingElementRegistry.registerType({
type: "keyword-chip",
renderAs: "badge",
className: "floating-keyword-chip",
});If the type requires custom rendering, bind it to a component and document the required componentProps.
3. Add a Shader Template
Shader templates are registered in .vitepress/config/shaders/index.ts and shaped by .vitepress/config/shaders/templates/base-shader.ts.
Minimal pattern:
import { registerShaderTemplate } from "@config/shaders";
import { baseVertexShader, buildTemplate } from "@config/shaders/templates/base-shader";
registerShaderTemplate("aurora", buildTemplate({
key: "aurora",
vertex: baseVertexShader,
fragment: `...`,
defaultUniforms: {
uIntensity: 0.8,
},
}));If it should ship as a built-in preset, also add a dedicated file under .vitepress/config/shaders/ and include it in the default registry map.
4. Add a New Background Renderer Type
If the new feature is not just a new shader preset but a new background type:
- Extend
HeroBackgroundTypeand related contracts inHeroFrontmatterApi.ts. - Normalize the new config shape in the same API layer.
- Create the renderer component under
.vitepress/theme/components/hero/background/. - Wire the type-to-component mapping in
.vitepress/theme/components/hero/background/BackgroundLayer.vue. - Add a localized example page proving the new type works.
5. Extend Hero Nav/Search Visuals
Top-of-page nav and search visuals are driven by the hero.colors.* contract and resolved in .vitepress/utils/vitepress/runtime/hero/navAdaptiveState.ts.
If you need new hero-driven nav/search behavior:
- Add the typed color key to
HeroFrontmatterApi.ts. - Resolve it in
navAdaptiveState.ts. - Consume it through CSS variables rather than direct component branching.
Documentation and Verification Checklist
Every framework-facing extension should ship with:
- Type updates
- Runtime/component integration
- At least one markdown example
- Locale/doc updates in both languages when applicable
- Verification commands recorded in the PR or handoff
Minimum verification for framework work:
yarn localeyarn sidebaryarn tagsyarn build
Maintenance Rules
- Keep APIs small and class-based where lifecycle/state is non-trivial.
- Do not add compatibility re-export stubs for deprecated paths.
- Use alias imports only (
@...) for internal code. - Keep extension points registration-driven.
- Validate with:
pnpm -s tsc --noEmit