valaxy 0.28.0-beta.7 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+ import { useFrontmatter } from '../../composables/common'
5
+ import { useScreenSize } from '../../composables/helper/useScreenSize'
6
+ import { useLayout } from '../../composables/layout'
7
+ import { useSiteConfig, useThemeConfig } from '../../config'
8
+ import ValaxySvgLogo from '../ValaxySvgLogo.vue'
9
+
10
+ const show = ref(true)
11
+ const expanded = ref<Record<string, boolean>>({
12
+ breakpoints: true,
13
+ route: false,
14
+ frontmatter: false,
15
+ config: false,
16
+ })
17
+
18
+ function toggleSection(key: string) {
19
+ expanded.value[key] = !expanded.value[key]
20
+ }
21
+
22
+ // Breakpoints
23
+ const screenSize = useScreenSize()
24
+ const breakpoints = computed(() => [
25
+ { label: 'xs', value: screenSize.isXs.value },
26
+ { label: 'sm', value: screenSize.isSm.value },
27
+ { label: 'md', value: screenSize.isMd.value },
28
+ { label: 'lg', value: screenSize.isLg.value },
29
+ { label: 'xl', value: screenSize.isXl.value },
30
+ { label: '2xl', value: screenSize.is2xl.value },
31
+ ])
32
+
33
+ // Route
34
+ const route = useRoute()
35
+ const routeInfo = computed(() => ({
36
+ path: route.path,
37
+ name: route.name as string,
38
+ layout: route.meta?.layout || 'default',
39
+ query: Object.keys(route.query).length ? route.query : undefined,
40
+ params: Object.keys(route.params).length ? route.params : undefined,
41
+ }))
42
+
43
+ // Frontmatter
44
+ const frontmatter = useFrontmatter()
45
+ const fmSummary = computed(() => {
46
+ const fm = frontmatter.value
47
+ if (!fm || !Object.keys(fm).length)
48
+ return null
49
+ return fm
50
+ })
51
+
52
+ // Config
53
+ const siteConfig = useSiteConfig()
54
+ const themeConfig = useThemeConfig()
55
+ const layout = useLayout()
56
+
57
+ const configSummary = computed(() => ({
58
+ theme: siteConfig.value.lang ? undefined : undefined,
59
+ lang: siteConfig.value.lang,
60
+ title: siteConfig.value.title,
61
+ url: siteConfig.value.url,
62
+ layout: layout.value,
63
+ }))
64
+ </script>
65
+
66
+ <template>
67
+ <div
68
+ v-if="show"
69
+ class="valaxy-debug fixed bottom-4 left-2 z-9999 max-h-[80vh] w-72 overflow-y-auto rounded-lg bg-black/80 p-3 text-xs text-white shadow-lg backdrop-blur-sm"
70
+ @click.stop
71
+ >
72
+ <!-- Header -->
73
+ <div class="mb-2 flex items-center justify-between border-b border-white/20 pb-2">
74
+ <span class="flex items-center gap-1 font-bold text-cyan-400">
75
+ <ValaxySvgLogo class="size-4" />
76
+ Valaxy Debug
77
+ </span>
78
+ <button
79
+ class="rounded px-1 text-white/60 transition hover:bg-white/20 hover:text-white"
80
+ title="Close"
81
+ @click="show = false"
82
+ >
83
+
84
+ </button>
85
+ </div>
86
+
87
+ <!-- Breakpoints -->
88
+ <div class="mb-1">
89
+ <button
90
+ class="w-full rounded px-1 py-0.5 text-left font-bold text-emerald-400 transition hover:bg-white/10"
91
+ @click="toggleSection('breakpoints')"
92
+ >
93
+ {{ expanded.breakpoints ? '▾' : '▸' }} Breakpoints
94
+ </button>
95
+ <div v-if="expanded.breakpoints" class="mt-1 flex flex-wrap gap-1 pl-3">
96
+ <span
97
+ v-for="bp in breakpoints"
98
+ :key="bp.label"
99
+ class="rounded px-1.5 py-0.5"
100
+ :class="bp.value ? 'bg-emerald-500/30 text-emerald-300' : 'bg-white/5 text-white/40'"
101
+ >
102
+ {{ bp.label }}
103
+ </span>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- Route -->
108
+ <div class="mb-1">
109
+ <button
110
+ class="w-full rounded px-1 py-0.5 text-left font-bold text-blue-400 transition hover:bg-white/10"
111
+ @click="toggleSection('route')"
112
+ >
113
+ {{ expanded.route ? '▾' : '▸' }} Route
114
+ </button>
115
+ <div v-if="expanded.route" class="mt-1 space-y-0.5 pl-3">
116
+ <div><span class="text-white/50">path:</span> {{ routeInfo.path }}</div>
117
+ <div><span class="text-white/50">name:</span> {{ routeInfo.name }}</div>
118
+ <div><span class="text-white/50">layout:</span> {{ routeInfo.layout }}</div>
119
+ <div v-if="routeInfo.query">
120
+ <span class="text-white/50">query:</span> {{ JSON.stringify(routeInfo.query) }}
121
+ </div>
122
+ <div v-if="routeInfo.params">
123
+ <span class="text-white/50">params:</span> {{ JSON.stringify(routeInfo.params) }}
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Frontmatter -->
129
+ <div class="mb-1">
130
+ <button
131
+ class="w-full rounded px-1 py-0.5 text-left font-bold text-amber-400 transition hover:bg-white/10"
132
+ @click="toggleSection('frontmatter')"
133
+ >
134
+ {{ expanded.frontmatter ? '▾' : '▸' }} Frontmatter
135
+ </button>
136
+ <div v-if="expanded.frontmatter" class="mt-1 pl-3">
137
+ <pre v-if="fmSummary" class="max-h-48 overflow-auto whitespace-pre-wrap break-all rounded bg-white/5 p-1.5 text-white/80">{{ JSON.stringify(fmSummary, null, 2) }}</pre>
138
+ <span v-else class="text-white/40">No frontmatter</span>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Config -->
143
+ <div class="mb-1">
144
+ <button
145
+ class="w-full rounded px-1 py-0.5 text-left font-bold text-purple-400 transition hover:bg-white/10"
146
+ @click="toggleSection('config')"
147
+ >
148
+ {{ expanded.config ? '▾' : '▸' }} Config
149
+ </button>
150
+ <div v-if="expanded.config" class="mt-1 space-y-2 pl-3">
151
+ <div>
152
+ <div class="mb-0.5 text-white/50">
153
+ Site Config
154
+ </div>
155
+ <pre class="max-h-48 overflow-auto whitespace-pre-wrap break-all rounded bg-white/5 p-1.5 text-white/80">{{ JSON.stringify(configSummary, null, 2) }}</pre>
156
+ </div>
157
+ <div>
158
+ <div class="mb-0.5 text-white/50">
159
+ Theme Config
160
+ </div>
161
+ <pre class="max-h-48 overflow-auto whitespace-pre-wrap break-all rounded bg-white/5 p-1.5 text-white/80">{{ JSON.stringify(themeConfig, null, 2) }}</pre>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- Collapsed toggle button -->
168
+ <button
169
+ v-if="!show"
170
+ class="fixed bottom-4 left-2 z-9999 rounded-lg bg-black/60 px-2 py-1 text-xs text-white/60 shadow-lg backdrop-blur-sm transition hover:bg-black/80 hover:text-white"
171
+ title="Open Valaxy Debug"
172
+ @click="show = true"
173
+ >
174
+ <ValaxySvgLogo class="size-4" />
175
+ </button>
176
+ </template>
@@ -0,0 +1,53 @@
1
+ <script lang="ts" setup>
2
+ import { useData } from '../composables'
3
+
4
+ const { page } = useData()
5
+
6
+ const isDev = import.meta.env.DEV
7
+
8
+ /**
9
+ * Open the current page source file in editor (dev mode only)
10
+ * Uses Vite's built-in `/__open-in-editor` endpoint
11
+ */
12
+ function openInEditor() {
13
+ const filePath = page.value?.filePath
14
+ if (filePath) {
15
+ fetch(`${window.location.origin}/__open-in-editor?file=${encodeURIComponent(filePath)}`)
16
+ .catch((err) => {
17
+ console.error('[valaxy] Failed to open in editor:', err)
18
+ })
19
+ }
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <button
25
+ v-if="isDev"
26
+ class="valaxy-open-in-editor"
27
+ title="Open in Editor"
28
+ @click="openInEditor"
29
+ >
30
+ <slot>
31
+ <div i-ri-code-s-slash-line />
32
+ </slot>
33
+ </button>
34
+ </template>
35
+
36
+ <style>
37
+ .valaxy-open-in-editor {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ cursor: pointer;
41
+ opacity: 0.4;
42
+ transition: opacity 0.2s;
43
+ background: none;
44
+ border: none;
45
+ padding: 0;
46
+ color: inherit;
47
+ font-size: inherit;
48
+ }
49
+
50
+ .valaxy-open-in-editor:hover {
51
+ opacity: 1;
52
+ }
53
+ </style>
@@ -1,12 +1,8 @@
1
1
  import { definePerson, defineWebPage, defineWebSite, useSchemaOrg } from '@unhead/schema-org/vue'
2
2
  import { useSeoMeta } from '@unhead/vue'
3
-
4
- // TODO: add docs to override ValaxyApp
5
3
  import { computed } from 'vue'
6
4
  import { useI18n } from 'vue-i18n'
7
-
8
5
  import { useRoute } from 'vue-router'
9
-
10
6
  import { useFrontmatter, useLocale, useValaxyHead, useValaxyI18n } from '../../composables'
11
7
  import { useTimezone } from '../../composables/global'
12
8
  // https://github.com/vueuse/head
@@ -16,7 +12,6 @@ import { useSiteConfig } from '../../config'
16
12
 
17
13
  export function useValaxyApp() {
18
14
  const siteConfig = useSiteConfig()
19
- // todo, allow user config
20
15
  const fm = useFrontmatter()
21
16
 
22
17
  const { locale } = useI18n()
@@ -32,7 +27,6 @@ export function useValaxyApp() {
32
27
  }
33
28
 
34
29
  // seo
35
- // todo: get first image url from markdown
36
30
  const siteUrl = computed(() => fm.value.url || siteConfig.value.url)
37
31
  const description = computed(() => $tO(fm.value.excerpt) || $tO(fm.value.description) || $t(siteConfig.value.description))
38
32
 
@@ -43,7 +37,7 @@ export function useValaxyApp() {
43
37
  ogLocaleAlternate: computed(() => siteConfig.value.languages.filter(l => l !== locale.value)),
44
38
  ogSiteName: computed(() => $t(siteConfig.value.title)),
45
39
  ogTitle: computed(() => $tO(fm.value.title) || $t(siteConfig.value.title)),
46
- ogImage: computed(() => fm.value.ogImage || fm.value.cover || siteConfig.value.favicon),
40
+ ogImage: computed(() => fm.value.ogImage || fm.value.cover || fm.value.firstImage || siteConfig.value.favicon),
47
41
  ogType: 'website',
48
42
  ogUrl: siteUrl,
49
43
  })
@@ -1,47 +1,11 @@
1
1
  import type { MaybeRef } from 'vue'
2
2
  import type { Post } from '../../types'
3
+ import type { CategoryList } from './category-utils'
3
4
  import { computed, unref } from 'vue'
4
5
  import { useSiteStore } from '../stores'
5
6
 
6
- /**
7
- * 基础分类
8
- */
9
- export interface BaseCategory {
10
- /**
11
- * 分类下的文章数量
12
- */
13
- total: number
14
- }
15
-
16
- /**
17
- * @en
18
- * Category list
19
- *
20
- * @zh
21
- * 分类列表
22
- */
23
- export interface CategoryList {
24
- /**
25
- * category name
26
- */
27
- name: string
28
- /**
29
- * total posts
30
- */
31
- total: number
32
- children: Map<string, Post | CategoryList>
33
- }
34
- export type Category = CategoryList
35
- export type Categories = Map<string, Post | CategoryList>
36
-
37
- /**
38
- * For theme development, you can use this function to determine whether the category is a category list.
39
- * @todo write unit test
40
- * @param category
41
- */
42
- export function isCategoryList(category: any): category is CategoryList {
43
- return category.children
44
- }
7
+ export type { BaseCategory, Categories, Category, CategoryList } from './category-utils'
8
+ export { isCategoryList, removeItemFromCategory } from './category-utils'
45
9
 
46
10
  /**
47
11
  * get categories from posts
@@ -158,20 +122,3 @@ export function useCategories(category?: MaybeRef<string>, posts: Post[] = []) {
158
122
  }
159
123
  })
160
124
  }
161
-
162
- /**
163
- * remove item from category
164
- * @param categoryList
165
- * @param categoryName
166
- */
167
- export function removeItemFromCategory(categoryList: CategoryList, categoryName: string) {
168
- if (isCategoryList(categoryList)) {
169
- const categoryArr = categoryName.split('/')
170
- categoryList.children.delete(categoryArr[0])
171
- }
172
- }
173
-
174
- /**
175
- * @deprecated use `useCategories` instead
176
- */
177
- export const useCategory = useCategories
@@ -0,0 +1,50 @@
1
+ import type { Post } from '../../types'
2
+
3
+ /**
4
+ * 基础分类
5
+ */
6
+ export interface BaseCategory {
7
+ /**
8
+ * 分类下的文章数量
9
+ */
10
+ total: number
11
+ }
12
+
13
+ /**
14
+ * @en
15
+ * Category list
16
+ *
17
+ * @zh
18
+ * 分类列表
19
+ */
20
+ export interface CategoryList {
21
+ /**
22
+ * category name
23
+ */
24
+ name: string
25
+ /**
26
+ * total posts
27
+ */
28
+ total: number
29
+ children: Map<string, Post | CategoryList>
30
+ }
31
+ export type Category = CategoryList
32
+ export type Categories = Map<string, Post | CategoryList>
33
+
34
+ /**
35
+ * For theme development, you can use this function to determine whether the category is a category list.
36
+ * @param category
37
+ */
38
+ export function isCategoryList(category: unknown): category is CategoryList {
39
+ return !!category && typeof category === 'object' && 'children' in category && category.children instanceof Map
40
+ }
41
+
42
+ /**
43
+ * remove item from category
44
+ * @param categoryList
45
+ * @param categoryName
46
+ */
47
+ export function removeItemFromCategory(categoryList: CategoryList, categoryName: string) {
48
+ const categoryArr = categoryName.split('/')
49
+ categoryList.children.delete(categoryArr[0])
50
+ }
@@ -4,6 +4,7 @@ export * from './app'
4
4
  export * from './back'
5
5
  // for classify
6
6
  export * from './categories'
7
+ export * from './category-utils'
7
8
  export * from './collections'
8
9
 
9
10
  // common
@@ -49,7 +49,7 @@ export function useActiveAnchor(
49
49
 
50
50
  /**
51
51
  * 长目录自动滚动
52
- * @TODO add e2e test
52
+ * @see e2e/theme-yun/outline.spec.ts
53
53
  */
54
54
  const checkActiveLinkInViewport = () => {
55
55
  const activeLink = prevActiveLink
@@ -51,8 +51,3 @@ export function useTags() {
51
51
  return tagMap
52
52
  })
53
53
  }
54
-
55
- /**
56
- * @deprecated use `useTags` instead
57
- */
58
- export const useTag = useTags
@@ -1,5 +1,6 @@
1
1
  import type { ValaxySSGContext } from '../setups'
2
2
 
3
+ import { defineAsyncComponent } from 'vue'
3
4
  import AppLink from '../components/AppLink.vue'
4
5
  import ValaxyTranslate from '../components/builtin/ValaxyTranslate.vue'
5
6
 
@@ -10,4 +11,10 @@ import ValaxyTranslate from '../components/builtin/ValaxyTranslate.vue'
10
11
  export function registerGlobalComponents(ctx: ValaxySSGContext) {
11
12
  ctx.app.component('AppLink', AppLink)
12
13
  ctx.app.component('VT', ValaxyTranslate)
14
+
15
+ // DEV-only: register ValaxyDebug component (tree-shaken in production)
16
+ if (import.meta.env.DEV) {
17
+ const ValaxyDebug = defineAsyncComponent(() => import('../components/.exclude/ValaxyDebug.vue'))
18
+ ctx.app.component('ValaxyDebug', ValaxyDebug)
19
+ }
13
20
  }
@@ -14,6 +14,7 @@ import type { PageDataPayload } from '../../types'
14
14
  import type { ValaxySSGContext } from '../setups'
15
15
  import { ensureSuffix } from '@antfu/utils'
16
16
  import { useStorage } from '@vueuse/core'
17
+ import { nextTick, watch } from 'vue'
17
18
  import { createI18n } from 'vue-i18n'
18
19
 
19
20
  // @ts-expect-error virtual
@@ -52,11 +53,35 @@ export const i18n = createI18n({
52
53
  })
53
54
 
54
55
  export async function install({ app, router }: ValaxySSGContext, config: ComputedRef<ValaxyConfig<DefaultTheme.Config>>) {
55
- const locale = useStorage('valaxy-locale', config?.value.siteConfig.lang || 'en')
56
- i18n.global.locale.value = locale.value
56
+ const defaultLang = config?.value.siteConfig.lang || 'en'
57
+
58
+ // During SSR/SSG build **and** the initial client hydration pass we must
59
+ // keep the locale at `defaultLang` so that the rendered HTML matches on
60
+ // both sides — no hydration mismatch for any i18n-dependent attribute
61
+ // (title, class, text content, etc.).
62
+ //
63
+ // The stored user preference is restored **after** hydration is complete
64
+ // (router.isReady + nextTick) so Vue can patch the DOM normally.
65
+ i18n.global.locale.value = defaultLang
57
66
 
58
67
  app.use(i18n)
59
- router.isReady().then(() => {
68
+
69
+ router.isReady().then(async () => {
70
+ // Wait for the hydration to finish before restoring the stored locale.
71
+ await nextTick()
72
+
73
+ const storedLocale = useStorage('valaxy-locale', defaultLang)
74
+
75
+ // Apply the stored locale (if different from default)
76
+ if (storedLocale.value && storedLocale.value !== i18n.global.locale.value)
77
+ i18n.global.locale.value = storedLocale.value
78
+
79
+ // Keep i18n locale in sync when the stored value changes later
80
+ watch(storedLocale, (val) => {
81
+ if (val)
82
+ i18n.global.locale.value = val
83
+ })
84
+
60
85
  handleHMR(router)
61
86
  })
62
87
  }
@@ -1,7 +1,7 @@
1
1
  import 'node:process';
2
2
  import 'yargs';
3
3
  import 'yargs/helpers';
4
- export { c as cli, I as registerDevCommand, W as run, Z as startValaxyDev } from '../../shared/valaxy.DAkHYbg0.mjs';
4
+ export { c as cli, I as registerDevCommand, W as run, Z as startValaxyDev } from '../../shared/valaxy.DFTOADvQ.mjs';
5
5
  import 'node:os';
6
6
  import 'node:path';
7
7
  import 'consola';
@@ -29,6 +29,7 @@ import 'feed';
29
29
  import 'markdown-it';
30
30
  import 'table';
31
31
  import 'hookable';
32
+ import 'node:fs';
32
33
  import 'node:child_process';
33
34
  import 'node:v8';
34
35
  import 'vite-ssg-sitemap';
@@ -57,7 +58,6 @@ import 'p-map';
57
58
  import 'node:buffer';
58
59
  import 'minisearch';
59
60
  import 'lru-cache';
60
- import 'node:fs';
61
61
  import 'jiti';
62
62
  import 'unocss';
63
63
  import 'pascalcase';
@@ -1,7 +1,7 @@
1
1
  import { ViteSSGOptions } from 'vite-ssg';
2
2
  import * as vite from 'vite';
3
3
  import { UserConfig, InlineConfig, ViteDevServer, PluginOption, Plugin } from 'vite';
4
- import { D as DefaultTheme, R as RuntimeConfig, a as RedirectItem, V as ValaxyConfig, P as PartialDeep, b as ValaxyAddon, S as SiteConfig, U as UserSiteConfig } from '../shared/valaxy.JIuR8V4d.mjs';
4
+ import { S as SiteConfig, D as DefaultTheme, R as RuntimeConfig, a as RedirectItem, V as ValaxyConfig, P as PartialDeep, b as ValaxyAddon, U as UserSiteConfig } from '../shared/valaxy.6MW2qn5T.mjs';
5
5
  import Vue from '@vitejs/plugin-vue';
6
6
  import { Hookable } from 'hookable';
7
7
  import { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
@@ -445,6 +445,14 @@ interface ValaxyHooks {
445
445
  'md:afterRender': (ctx: MdAfterRenderContext) => HookResult;
446
446
  'build:before': () => HookResult;
447
447
  'build:after': () => HookResult;
448
+ /**
449
+ * Called to compute statistics (word count, reading time) for a markdown route.
450
+ * Default implementation uses `presetStatistics`; addons/themes can hook to override.
451
+ */
452
+ 'statistics': (ctx: {
453
+ route: EditableTreeNode;
454
+ options: SiteConfig['statistics'];
455
+ }) => HookResult;
448
456
  /**
449
457
  * @experimental
450
458
  * Called before content loaders start fetching.
@@ -768,6 +776,12 @@ interface ValaxyExtendConfig {
768
776
  * @default true
769
777
  */
770
778
  katex: boolean;
779
+ /**
780
+ * @description:en-US Auto-extract the first image from markdown content for Open Graph fallback
781
+ * @description:zh-CN 自动从 Markdown 内容中提取第一张图片,作为 Open Graph 的回退图片
782
+ * @default true
783
+ */
784
+ extractFirstImage: boolean;
771
785
  };
772
786
  /**
773
787
  * Enable MathJax3 math rendering (aligned with VitePress `markdown.math`).
@@ -1,4 +1,4 @@
1
- export { A as ALL_ROUTE, E as EXCERPT_SEPARATOR, G as GLOBAL_STATE, P as PATHNAME_PROTOCOL_RE, V as ViteValaxyPlugins, b as build, c as cli, a as createServer, d as createValaxyPlugin, e as customElements, f as defaultSiteConfig, g as defaultValaxyConfig, h as defaultViteConfig, i as defineAddon, j as defineConfig, k as defineSiteConfig, l as defineTheme, m as defineValaxyAddon, n as defineValaxyConfig, o as defineValaxyTheme, p as encryptContent, q as generateClientRedirects, r as getGitTimestamp, s as getIndexHtml, t as getServerInfoText, u as isExternal, v as isInstalledGlobally, w as isKatexEnabled, x as isKatexPluginNeeded, y as isMathJaxEnabled, z as isPath, B as loadConfigFromFile, C as mergeValaxyConfig, D as mergeViteConfigs, F as postProcessForSSG, H as processValaxyOptions, I as registerDevCommand, J as resolveAddonsConfig, K as resolveImportPath, L as resolveImportUrl, M as resolveOptions, N as resolveSiteConfig, O as resolveSiteConfigFromRoot, Q as resolveThemeConfigFromRoot, R as resolveThemeValaxyConfig, S as resolveUserThemeConfig, T as resolveValaxyConfig, U as resolveValaxyConfigFromRoot, W as run, X as ssgBuild, Y as ssgBuildLegacy, Z as startValaxyDev, _ as toAtFS, $ as transformObject, a0 as version } from '../shared/valaxy.DAkHYbg0.mjs';
1
+ export { A as ALL_ROUTE, E as EXCERPT_SEPARATOR, G as GLOBAL_STATE, P as PATHNAME_PROTOCOL_RE, V as ViteValaxyPlugins, b as build, c as cli, a as createServer, d as createValaxyPlugin, e as customElements, f as defaultSiteConfig, g as defaultValaxyConfig, h as defaultViteConfig, i as defineAddon, j as defineConfig, k as defineSiteConfig, l as defineTheme, m as defineValaxyAddon, n as defineValaxyConfig, o as defineValaxyTheme, p as encryptContent, q as generateClientRedirects, r as getGitTimestamp, s as getIndexHtml, t as getServerInfoText, u as isExternal, v as isInstalledGlobally, w as isKatexEnabled, x as isKatexPluginNeeded, y as isMathJaxEnabled, z as isPath, B as loadConfigFromFile, C as mergeValaxyConfig, D as mergeViteConfigs, F as postProcessForSSG, H as processValaxyOptions, I as registerDevCommand, J as resolveAddonsConfig, K as resolveImportPath, L as resolveImportUrl, M as resolveOptions, N as resolveSiteConfig, O as resolveSiteConfigFromRoot, Q as resolveThemeConfigFromRoot, R as resolveThemeValaxyConfig, S as resolveUserThemeConfig, T as resolveValaxyConfig, U as resolveValaxyConfigFromRoot, W as run, X as ssgBuild, Y as ssgBuildLegacy, Z as startValaxyDev, _ as toAtFS, $ as transformObject, a0 as version } from '../shared/valaxy.DFTOADvQ.mjs';
2
2
  import 'node:path';
3
3
  import 'fs-extra';
4
4
  import 'consola/utils';
@@ -29,6 +29,7 @@ import 'feed';
29
29
  import 'markdown-it';
30
30
  import 'table';
31
31
  import 'hookable';
32
+ import 'node:fs';
32
33
  import 'node:child_process';
33
34
  import 'node:v8';
34
35
  import 'vite-ssg-sitemap';
@@ -57,7 +58,6 @@ import 'p-map';
57
58
  import 'node:buffer';
58
59
  import 'minisearch';
59
60
  import 'lru-cache';
60
- import 'node:fs';
61
61
  import 'jiti';
62
62
  import 'unocss';
63
63
  import 'pascalcase';
@@ -167,6 +167,18 @@ interface PageFrontMatter extends BaseFrontMatter {
167
167
  * @description 封面图片
168
168
  */
169
169
  cover: string;
170
+ /**
171
+ * @description:en-US Open Graph image for SEO
172
+ * @description:zh-CN Open Graph 图片,用于 SEO
173
+ */
174
+ ogImage: string;
175
+ /**
176
+ * @protected
177
+ * @tutorial ⚠️ DO NOT SET MANUALLY (auto-extracted from markdown content)
178
+ * @description:en-US First image URL extracted from markdown content
179
+ * @description:zh-CN 从 Markdown 内容中自动提取的第一张图片 URL
180
+ */
181
+ firstImage: string;
170
182
  /**
171
183
  * display toc
172
184
  * @description 是否显示目录
@@ -638,10 +650,6 @@ interface SiteConfig {
638
650
  * @zh 是否启用
639
651
  */
640
652
  enable: boolean;
641
- /**
642
- * @deprecated will be deprecated, use search.provider instead
643
- */
644
- type?: SiteConfig['search']['provider'];
645
653
  /**
646
654
  * Search Type
647
655
  * - algolia: Algolia Search
@@ -28,7 +28,8 @@ import { Feed } from 'feed';
28
28
  import MarkdownIt from 'markdown-it';
29
29
  import { table, getBorderCharacters } from 'table';
30
30
  import { createHooks } from 'hookable';
31
- import { execFileSync, exec } from 'node:child_process';
31
+ import { existsSync, readFileSync } from 'node:fs';
32
+ import { execFileSync, execSync, exec } from 'node:child_process';
32
33
  import v8 from 'node:v8';
33
34
  import generateSitemap from 'vite-ssg-sitemap';
34
35
  import { createMarkdownItAsync, MarkdownItAsync } from 'markdown-it-async';
@@ -56,14 +57,13 @@ import pMap from 'p-map';
56
57
  import { Buffer as Buffer$1 } from 'node:buffer';
57
58
  import MiniSearch from 'minisearch';
58
59
  import { LRUCache } from 'lru-cache';
59
- import { existsSync, readFileSync } from 'node:fs';
60
60
  import { createJiti } from 'jiti';
61
61
  import { transformerDirectives, transformerVariantGroup, presetWind4, presetAttributify, presetIcons, presetTypography } from 'unocss';
62
62
  import pascalCase from 'pascalcase';
63
63
  import { convert } from 'html-to-text';
64
64
  import VueRouter from 'vue-router/vite';
65
65
  import { renderSSRHead } from '@unhead/vue/server';
66
- import { intro, confirm, select, outro } from '@clack/prompts';
66
+ import { intro, confirm, isCancel, cancel, select, outro } from '@clack/prompts';
67
67
  import net from 'node:net';
68
68
  import * as readline from 'node:readline';
69
69
  import qrcode from 'qrcode';
@@ -878,7 +878,8 @@ const defaultValaxyConfig = {
878
878
  }
879
879
  },
880
880
  features: {
881
- katex: true
881
+ katex: true,
882
+ extractFirstImage: true
882
883
  },
883
884
  math: false,
884
885
  cdn: {
@@ -931,8 +932,9 @@ async function resolveValaxyConfigFromRoot(root, options) {
931
932
  }
932
933
  const mergeValaxyConfig = createDefu((obj, key, value) => {
933
934
  if (isFunction(obj[key]) && isFunction(value)) {
935
+ const original = obj[key];
934
936
  obj[key] = function(...args) {
935
- obj[key].call(this, ...args);
937
+ original.call(this, ...args);
936
938
  value.call(this, ...args);
937
939
  };
938
940
  return true;
@@ -1719,7 +1721,7 @@ async function setupMarkdownPlugins(md, options, base = "/") {
1719
1721
  return md;
1720
1722
  }
1721
1723
 
1722
- const version = "0.28.0-beta.7";
1724
+ const version = "0.28.1";
1723
1725
 
1724
1726
  const GLOBAL_STATE = {
1725
1727
  valaxyApp: void 0,
@@ -3107,12 +3109,68 @@ function clearHtmlTags(str) {
3107
3109
  return str.replace(/<[^>]*>/g, "").replace(/\s+/g, " ");
3108
3110
  }
3109
3111
 
3112
+ function count(content) {
3113
+ const cn = (content.match(/[\u4E00-\u9FA5]/g) || []).length;
3114
+ const en = (content.replace(/[\u4E00-\u9FA5]/g, "").match(/[\w\u0392-\u03C9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3040-\u309F\uAC00-\uD7AF\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+/g) || []).length;
3115
+ return {
3116
+ cn,
3117
+ en
3118
+ };
3119
+ }
3120
+ function readTime({ cn, en }, options) {
3121
+ const readingTime = cn / (options.speed.cn || 300) + en / (options.speed.en || 100);
3122
+ return readingTime < 1 ? 1 : Math.ceil(readingTime);
3123
+ }
3124
+ function wordCount({ cn, en }) {
3125
+ const num = cn + en;
3126
+ if (num < 1e3)
3127
+ return num.toString();
3128
+ return `${Math.round(num / 100) / 10}k`;
3129
+ }
3130
+ function statistics(content, options) {
3131
+ const countData = count(content);
3132
+ return {
3133
+ countData,
3134
+ wordCount: wordCount(countData),
3135
+ readingTime: readTime(countData, options.readTime)
3136
+ };
3137
+ }
3138
+ function presetStatistics({
3139
+ route,
3140
+ options
3141
+ }) {
3142
+ const absolutePath = route.components.get("default") || "";
3143
+ if (existsSync(absolutePath)) {
3144
+ const file = readFileSync(absolutePath, "utf-8");
3145
+ const { wordCount: wordCount2, readingTime } = statistics(file, {
3146
+ readTime: {
3147
+ ...options.readTime,
3148
+ speed: {
3149
+ cn: options.readTime?.speed?.cn || 300,
3150
+ en: options.readTime?.speed?.en || 100
3151
+ }
3152
+ }
3153
+ });
3154
+ const { frontmatter } = route.meta;
3155
+ if (frontmatter && typeof frontmatter === "object" && !Array.isArray(frontmatter)) {
3156
+ const fm = frontmatter;
3157
+ if (!fm.wordCount)
3158
+ fm.wordCount = wordCount2;
3159
+ if (!fm.readingTime)
3160
+ fm.readingTime = readingTime;
3161
+ }
3162
+ }
3163
+ }
3164
+
3110
3165
  const buildHooks = [
3111
3166
  "build:before",
3112
3167
  "build:after"
3113
3168
  ];
3114
3169
  function createValaxyNode(options) {
3115
3170
  const hooks = createHooks();
3171
+ hooks.hook("statistics", ({ route, options: statsOptions }) => {
3172
+ presetStatistics({ route, options: statsOptions });
3173
+ });
3116
3174
  if (typeof options.config.hooks === "object") {
3117
3175
  Object.keys(options.config.hooks).forEach((name) => {
3118
3176
  const hookName = name;
@@ -3394,10 +3452,24 @@ function inferDescription(frontmatter) {
3394
3452
  return description;
3395
3453
  return head && getHeadMetaContent(head, "description") || "";
3396
3454
  }
3455
+ function extractFirstImage(code) {
3456
+ const mdImageMatch = code.match(/!\[.*?\]\((.+?)\)/);
3457
+ if (mdImageMatch)
3458
+ return mdImageMatch[1];
3459
+ const htmlImageMatch = code.match(/<img\s[^>]*?src=["'](.+?)["']/);
3460
+ if (htmlImageMatch)
3461
+ return htmlImageMatch[1];
3462
+ return void 0;
3463
+ }
3397
3464
  async function generatePageData(code, id, options) {
3398
3465
  const fileInfo = Valaxy.state.idMap.get(id);
3399
3466
  const relativePath = path.relative(options.userRoot, id);
3400
3467
  const fm = JSON.parse(JSON.stringify(fileInfo?.frontmatter));
3468
+ if (options.config.features?.extractFirstImage !== false && !fm.ogImage && !fm.cover) {
3469
+ const firstImage = extractFirstImage(code);
3470
+ if (firstImage)
3471
+ fm.firstImage = firstImage;
3472
+ }
3401
3473
  const pageData = {
3402
3474
  title: fm.title || fileInfo?.title || "",
3403
3475
  titleTemplate: fm.titleTemplate,
@@ -3408,7 +3480,8 @@ async function generatePageData(code, id, options) {
3408
3480
  relativePath,
3409
3481
  filePath: id
3410
3482
  };
3411
- pageData.lastUpdated = await getGitTimestamp(id);
3483
+ if (options.config.siteConfig.lastUpdated)
3484
+ pageData.lastUpdated = await getGitTimestamp(id);
3412
3485
  return pageData;
3413
3486
  }
3414
3487
 
@@ -4285,59 +4358,6 @@ async function createValaxyPlugin(options, serverOptions = {}) {
4285
4358
  ];
4286
4359
  }
4287
4360
 
4288
- function count(content) {
4289
- const cn = (content.match(/[\u4E00-\u9FA5]/g) || []).length;
4290
- const en = (content.replace(/[\u4E00-\u9FA5]/g, "").match(/[\w\u0392-\u03C9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3040-\u309F\uAC00-\uD7AF\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+/g) || []).length;
4291
- return {
4292
- cn,
4293
- en
4294
- };
4295
- }
4296
- function readTime({ cn, en }, options) {
4297
- const readingTime = cn / (options.speed.cn || 300) + en / (options.speed.en || 100);
4298
- return readingTime < 1 ? 1 : Math.ceil(readingTime);
4299
- }
4300
- function wordCount({ cn, en }) {
4301
- const num = cn + en;
4302
- if (num < 1e3)
4303
- return num.toString();
4304
- return `${Math.round(num / 100) / 10}k`;
4305
- }
4306
- function statistics(content, options) {
4307
- const countData = count(content);
4308
- return {
4309
- countData,
4310
- wordCount: wordCount(countData),
4311
- readingTime: readTime(countData, options.readTime)
4312
- };
4313
- }
4314
- function presetStatistics({
4315
- route,
4316
- options
4317
- }) {
4318
- const absolutePath = route.components.get("default") || "";
4319
- if (existsSync(absolutePath)) {
4320
- const file = readFileSync(absolutePath, "utf-8");
4321
- const { wordCount: wordCount2, readingTime } = statistics(file, {
4322
- readTime: {
4323
- ...options.readTime,
4324
- speed: {
4325
- cn: options.readTime?.speed?.cn || 300,
4326
- en: options.readTime?.speed?.en || 100
4327
- }
4328
- }
4329
- });
4330
- const { frontmatter } = route.meta;
4331
- if (frontmatter && typeof frontmatter === "object" && !Array.isArray(frontmatter)) {
4332
- const fm = frontmatter;
4333
- if (!fm.wordCount)
4334
- fm.wordCount = wordCount2;
4335
- if (!fm.readingTime)
4336
- fm.readingTime = readingTime;
4337
- }
4338
- }
4339
- }
4340
-
4341
4361
  async function getExcerptByType(excerpt = "", type = "html", mdIt) {
4342
4362
  switch (type) {
4343
4363
  case "ai":
@@ -4416,7 +4436,7 @@ async function createRouterPlugin(valaxyApp) {
4416
4436
  const md = await fs.readFile(path, "utf-8");
4417
4437
  const { data, excerpt, content } = matter(md, matterOptions);
4418
4438
  const mdFm = data;
4419
- const lastUpdated = options.config.siteConfig.lastUpdated;
4439
+ const lastUpdated = valaxyConfig.siteConfig.lastUpdated;
4420
4440
  delete mdFm.password;
4421
4441
  if (mdFm.gallery_password) {
4422
4442
  delete mdFm.gallery_password;
@@ -4454,8 +4474,7 @@ async function createRouterPlugin(valaxyApp) {
4454
4474
  ];
4455
4475
  const routerFM = {
4456
4476
  ...mdFm,
4457
- // 主题有新的字段需要主动设置
4458
- // @TODO 添加文档和配置项,或者反过来允许用户自行优化
4477
+ // Normalize tags: ensure always an array for consistent consumption
4459
4478
  tags: typeof mdFm.tags === "string" ? [mdFm.tags] : mdFm.tags,
4460
4479
  // set default updated to date if not present
4461
4480
  updated: mdFm.updated ?? mdFm.date
@@ -4480,7 +4499,7 @@ async function createRouterPlugin(valaxyApp) {
4480
4499
  });
4481
4500
  }
4482
4501
  if (valaxyConfig.siteConfig.statistics.enable) {
4483
- presetStatistics({
4502
+ await valaxyApp.hooks.callHook("statistics", {
4484
4503
  options: valaxyConfig.siteConfig.statistics,
4485
4504
  route
4486
4505
  });
@@ -5401,17 +5420,21 @@ async function getPosts(params, options) {
5401
5420
  return { data, content, excerpt, path: i };
5402
5421
  });
5403
5422
  const rawPosts = await Promise.all(readFilePromises);
5423
+ const draftPosts = [];
5404
5424
  const filteredPosts = rawPosts.filter((p) => {
5405
5425
  const { data } = p;
5406
5426
  if (data.password)
5407
5427
  return false;
5408
5428
  if (data.draft) {
5429
+ draftPosts.push(p.path);
5409
5430
  return false;
5410
5431
  }
5411
5432
  if (data.hide)
5412
5433
  return false;
5413
5434
  return true;
5414
5435
  });
5436
+ if (draftPosts.length)
5437
+ consola.log(`[rss] Skipped ${draftPosts.length} draft post(s): ${draftPosts.join(", ")}`);
5415
5438
  const posts = [];
5416
5439
  for (const rawPost of filteredPosts) {
5417
5440
  const { data, path, content, excerpt } = rawPost;
@@ -6080,58 +6103,185 @@ function registerCleanCommand(cli) {
6080
6103
  );
6081
6104
  }
6082
6105
 
6083
- function registerDebugCommand(cli) {
6084
- cli.command("debug", "Debug your blog", async () => {
6085
- console.log();
6086
- consola.log(" Operating System:", colors.green(os.platform()));
6087
- consola.log(" Node.JS Version:", colors.green(process.version));
6088
- consola.log(" Valaxy Version:", colors.cyan(`v${version}`));
6106
+ function getPnpmVersion() {
6107
+ try {
6108
+ return execSync("pnpm --version", { encoding: "utf-8" }).trim();
6109
+ } catch {
6110
+ return "not found";
6111
+ }
6112
+ }
6113
+ async function collectDebugInfo() {
6114
+ const info = {
6115
+ os: os.platform(),
6116
+ arch: os.arch(),
6117
+ node: process.version,
6118
+ pnpm: getPnpmVersion(),
6119
+ valaxy: version
6120
+ };
6121
+ try {
6122
+ const options = await resolveOptions({ userRoot: process.cwd() });
6123
+ info.userRoot = options.userRoot;
6124
+ info.theme = options.theme;
6125
+ info.themeVersion = options.config.themeConfig?.pkg?.version;
6126
+ info.addons = options.addons.filter((a) => a.enable).map((a) => ({
6127
+ name: a.name,
6128
+ version: a.pkg?.version || "unknown",
6129
+ global: a.global
6130
+ }));
6131
+ info.pages = options.pages.length;
6132
+ } catch {
6133
+ }
6134
+ return info;
6135
+ }
6136
+ function printFancy(info) {
6137
+ const lines = [];
6138
+ lines.push(`${colors.bold(colors.cyan("Environment"))}`);
6139
+ lines.push(` OS: ${colors.green(`${info.os} ${info.arch}`)}`);
6140
+ lines.push(` Node.js: ${colors.green(info.node)}`);
6141
+ lines.push(` Package Manager: ${colors.green(`pnpm ${info.pnpm}`)}`);
6142
+ lines.push(` Valaxy: ${colors.cyan(`v${info.valaxy}`)}`);
6143
+ if (info.theme) {
6144
+ lines.push("");
6145
+ lines.push(`${colors.bold(colors.cyan("Project"))}`);
6146
+ lines.push(` Root: ${colors.dim(info.userRoot)}`);
6147
+ lines.push(` Theme: ${colors.green(info.theme)} ${colors.blue(`v${info.themeVersion || "unknown"}`)}`);
6148
+ if (info.addons && info.addons.length > 0) {
6149
+ lines.push(` Addons:`);
6150
+ info.addons.forEach((addon, i) => {
6151
+ const prefix = i === info.addons.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
6152
+ const globalTag = addon.global ? colors.cyan(" (global)") : "";
6153
+ lines.push(` ${prefix} ${colors.yellow(addon.name)} ${colors.blue(`v${addon.version}`)}${globalTag}`);
6154
+ });
6155
+ } else {
6156
+ lines.push(` Addons: ${colors.dim("none")}`);
6157
+ }
6158
+ lines.push(` Pages: ${colors.green(String(info.pages))}`);
6159
+ }
6160
+ consola.box({
6161
+ title: "\u{1F30C} Valaxy Debug Info",
6162
+ message: lines.join("\n"),
6163
+ style: {
6164
+ borderColor: "cyan"
6165
+ }
6089
6166
  });
6090
6167
  }
6168
+ function printPlain(info) {
6169
+ const lines = [];
6170
+ lines.push("## Environment");
6171
+ lines.push(`- OS: ${info.os} ${info.arch}`);
6172
+ lines.push(`- Node: ${info.node}`);
6173
+ lines.push(`- Package Manager: pnpm ${info.pnpm}`);
6174
+ lines.push(`- Valaxy: v${info.valaxy}`);
6175
+ if (info.theme) {
6176
+ lines.push("");
6177
+ lines.push("## Project");
6178
+ lines.push(`- Root: ${info.userRoot}`);
6179
+ lines.push(`- Theme: ${info.theme} (v${info.themeVersion || "unknown"})`);
6180
+ if (info.addons && info.addons.length > 0) {
6181
+ const addonStr = info.addons.map((a) => `${a.name} (v${a.version})${a.global ? " [global]" : ""}`).join(", ");
6182
+ lines.push(`- Addons: ${addonStr}`);
6183
+ } else {
6184
+ lines.push("- Addons: none");
6185
+ }
6186
+ lines.push(`- Pages: ${info.pages}`);
6187
+ }
6188
+ console.log(lines.join("\n"));
6189
+ }
6190
+ function registerDebugCommand(cli) {
6191
+ cli.command(
6192
+ "debug",
6193
+ "Display debug information for your Valaxy project",
6194
+ (args) => args.option("plain", {
6195
+ type: "boolean",
6196
+ default: false,
6197
+ describe: "Output plain text without colors (for pasting into issues)"
6198
+ }),
6199
+ async (args) => {
6200
+ const info = await collectDebugInfo();
6201
+ if (args.plain)
6202
+ printPlain(info);
6203
+ else
6204
+ printFancy(info);
6205
+ }
6206
+ );
6207
+ }
6091
6208
 
6092
6209
  function registerDeployCommand(cli) {
6093
- cli.command("deploy", "deploy your blog to the cloud", async () => {
6094
- intro("Deploying Your Blog");
6095
- const shouldBuild = await confirm({
6096
- message: "Do you want to build your blog before deploying?"
6097
- });
6098
- if (shouldBuild) {
6099
- await execBuild({ ssg: true, ssgEngine: "valaxy", root: process.cwd(), output: "dist", log: "info" });
6100
- }
6101
- const deployType = await select({
6102
- message: "Where do you want to deploy?",
6103
- options: [
6104
- { label: "GitHub Pages", value: "gh-pages", hint: "You need install `gh-pages` dependencies." },
6105
- { label: "Your Own Server", value: "server" }
6106
- ]
6107
- });
6108
- if (deployType === "gh-pages") {
6109
- let isGhPagesInstalled = false;
6110
- try {
6111
- await import('gh-pages');
6112
- isGhPagesInstalled = true;
6113
- } catch (e) {
6114
- console.error(e);
6115
- const installGhPages = await confirm({
6116
- message: "Do you want to install `gh-pages` now?"
6117
- });
6118
- if (installGhPages) {
6119
- await import('@antfu/install-pkg').then((i) => i.installPackage("gh-pages", { dev: true }));
6210
+ cli.command(
6211
+ "deploy [root]",
6212
+ "deploy your blog to the cloud",
6213
+ (args) => commonOptions(args).option("type", {
6214
+ type: "string",
6215
+ choices: ["gh-pages", "remote"],
6216
+ describe: "deploy type, overrides `deploy.type` in config"
6217
+ }).option("output", {
6218
+ alias: "o",
6219
+ type: "string",
6220
+ default: "dist",
6221
+ describe: "output dir"
6222
+ }).strict().help(),
6223
+ async ({ root, type, output }) => {
6224
+ intro("Deploying Your Blog");
6225
+ const shouldBuild = await confirm({
6226
+ message: "Do you want to build your blog before deploying?"
6227
+ });
6228
+ if (isCancel(shouldBuild)) {
6229
+ cancel("Operation cancelled.");
6230
+ process.exit(0);
6231
+ }
6232
+ if (shouldBuild) {
6233
+ await execBuild({ ssg: true, ssgEngine: "valaxy", root, output, log: "info" });
6234
+ }
6235
+ const options = await resolveOptions({ userRoot: root }, "build");
6236
+ const configDeployType = type ?? options.config.deploy?.type;
6237
+ const deployType = configDeployType ?? await select({
6238
+ message: "Where do you want to deploy?",
6239
+ options: [
6240
+ { label: "GitHub Pages", value: "gh-pages", hint: "You need install `gh-pages` dependencies." },
6241
+ { label: "Your Own Server", value: "remote" }
6242
+ ]
6243
+ });
6244
+ if (isCancel(deployType)) {
6245
+ cancel("Operation cancelled.");
6246
+ process.exit(0);
6247
+ }
6248
+ if (deployType === "gh-pages") {
6249
+ let isGhPagesInstalled = false;
6250
+ try {
6251
+ await import('gh-pages');
6120
6252
  isGhPagesInstalled = true;
6121
- } else {
6122
- outro("Please install `gh-pages` before deploying to GitHub Pages.");
6253
+ } catch {
6254
+ const installGhPages = await confirm({
6255
+ message: "Do you want to install `gh-pages` now?"
6256
+ });
6257
+ if (isCancel(installGhPages)) {
6258
+ cancel("Operation cancelled.");
6259
+ process.exit(0);
6260
+ }
6261
+ if (installGhPages) {
6262
+ try {
6263
+ await import('@antfu/install-pkg').then((i) => i.installPackage("gh-pages", { dev: true }));
6264
+ isGhPagesInstalled = true;
6265
+ } catch (e) {
6266
+ consola.error("Failed to install `gh-pages`:", e);
6267
+ }
6268
+ } else {
6269
+ outro("Please install `gh-pages` before deploying to GitHub Pages.");
6270
+ }
6123
6271
  }
6124
- }
6125
- if (isGhPagesInstalled) {
6126
- const { publish } = await import('gh-pages');
6127
- await publish("dist", {
6128
- branch: "gh-pages",
6129
- message: "chore: deploy by valaxy"
6130
- });
6131
- outro("Done!");
6272
+ if (isGhPagesInstalled) {
6273
+ const { publish } = await import('gh-pages');
6274
+ await publish(output, {
6275
+ branch: "gh-pages",
6276
+ message: "chore: deploy by valaxy"
6277
+ });
6278
+ outro("Done!");
6279
+ }
6280
+ } else if (deployType === "remote") {
6281
+ outro("Remote deployment is not yet implemented.");
6132
6282
  }
6133
6283
  }
6134
- });
6284
+ );
6135
6285
  }
6136
6286
 
6137
6287
  async function findFreePort(start) {
@@ -1,5 +1,5 @@
1
- import { c as Post } from '../shared/valaxy.JIuR8V4d.mjs';
2
- export { A as Album, B as BaseFrontMatter, D as DefaultTheme, E as ExcerptType, F as FuseListItem, d as Page, e as PageFrontMatter, P as PartialDeep, f as Photo, g as Pkg, h as PostFrontMatter, a as RedirectItem, i as RedirectRule, R as RuntimeConfig, S as SiteConfig, j as SocialLink, U as UserSiteConfig, k as UserValaxyConfig, b as ValaxyAddon, V as ValaxyConfig } from '../shared/valaxy.JIuR8V4d.mjs';
1
+ import { c as Post } from '../shared/valaxy.6MW2qn5T.mjs';
2
+ export { A as Album, B as BaseFrontMatter, D as DefaultTheme, E as ExcerptType, F as FuseListItem, d as Page, e as PageFrontMatter, P as PartialDeep, f as Photo, g as Pkg, h as PostFrontMatter, a as RedirectItem, i as RedirectRule, R as RuntimeConfig, S as SiteConfig, j as SocialLink, U as UserSiteConfig, k as UserValaxyConfig, b as ValaxyAddon, V as ValaxyConfig } from '../shared/valaxy.6MW2qn5T.mjs';
3
3
  import { Header } from '@valaxyjs/utils';
4
4
  import '@vueuse/integrations/useFuse';
5
5
  import 'medium-zoom';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "valaxy",
3
3
  "type": "module",
4
- "version": "0.28.0-beta.7",
4
+ "version": "0.28.1",
5
5
  "description": "📄 Vite & Vue powered static blog generator.",
6
6
  "author": {
7
7
  "email": "me@yunyoujun.cn",
@@ -57,7 +57,7 @@
57
57
  "types"
58
58
  ],
59
59
  "engines": {
60
- "node": "^14.18.0 || >=16.0.0"
60
+ "node": "^18.0.0 || >=20.0.0"
61
61
  },
62
62
  "dependencies": {
63
63
  "@antfu/install-pkg": "^1.1.0",
@@ -95,7 +95,7 @@
95
95
  "jiti": "^2.6.1",
96
96
  "js-base64": "^3.7.8",
97
97
  "js-yaml": "^4.1.1",
98
- "katex": "^0.16.40",
98
+ "katex": "^0.16.42",
99
99
  "lru-cache": "^11.2.7",
100
100
  "markdown-it": "^14.1.1",
101
101
  "markdown-it-anchor": "^9.2.0",
@@ -105,7 +105,7 @@
105
105
  "markdown-it-emoji": "^3.0.0",
106
106
  "markdown-it-footnote": "^4.0.0",
107
107
  "markdown-it-image-figures": "^2.1.1",
108
- "markdown-it-table-of-contents": "^1.1.0",
108
+ "markdown-it-table-of-contents": "^1.2.0",
109
109
  "markdown-it-task-lists": "^2.1.1",
110
110
  "medium-zoom": "^1.1.0",
111
111
  "mermaid": "^11.13.0",
@@ -129,9 +129,8 @@
129
129
  "unplugin-vue-components": "28.0.0",
130
130
  "unplugin-vue-markdown": "^30.0.0",
131
131
  "vanilla-lazyload": "^19.1.3",
132
- "vite": "^8.0.1",
133
- "vite-dev-rpc": "^1.1.0",
134
- "vite-plugin-vue-devtools": "^8.1.0",
132
+ "vite": "^8.0.2",
133
+ "vite-plugin-vue-devtools": "^8.1.1",
135
134
  "vite-plugin-vue-layouts-next": "^2.1.0",
136
135
  "vite-ssg": "^28.3.0",
137
136
  "vite-ssg-sitemap": "^0.10.0",
@@ -140,8 +139,8 @@
140
139
  "vue-i18n": "^11.3.0",
141
140
  "vue-router": "^5.0.4",
142
141
  "yargs": "^18.0.0",
143
- "@valaxyjs/utils": "0.28.0-beta.7",
144
- "@valaxyjs/devtools": "0.28.0-beta.7"
142
+ "@valaxyjs/devtools": "0.28.1",
143
+ "@valaxyjs/utils": "0.28.1"
145
144
  },
146
145
  "devDependencies": {
147
146
  "@mdit-vue/plugin-component": "^3.0.2",
package/types/config.ts CHANGED
@@ -172,10 +172,6 @@ export interface SiteConfig {
172
172
  * @zh 是否启用
173
173
  */
174
174
  enable: boolean
175
- /**
176
- * @deprecated will be deprecated, use search.provider instead
177
- */
178
- type?: SiteConfig['search']['provider']
179
175
  /**
180
176
  * Search Type
181
177
  * - algolia: Algolia Search
@@ -109,6 +109,18 @@ export interface PageFrontMatter extends BaseFrontMatter {
109
109
  * @description 封面图片
110
110
  */
111
111
  cover: string
112
+ /**
113
+ * @description:en-US Open Graph image for SEO
114
+ * @description:zh-CN Open Graph 图片,用于 SEO
115
+ */
116
+ ogImage: string
117
+ /**
118
+ * @protected
119
+ * @tutorial ⚠️ DO NOT SET MANUALLY (auto-extracted from markdown content)
120
+ * @description:en-US First image URL extracted from markdown content
121
+ * @description:zh-CN 从 Markdown 内容中自动提取的第一张图片 URL
122
+ */
123
+ firstImage: string
112
124
  /**
113
125
  * display toc
114
126
  * @description 是否显示目录