valaxy 0.28.0-beta.7 → 0.28.0

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.DQ6HsU2J.mjs';
5
5
  import 'node:os';
6
6
  import 'node:path';
7
7
  import 'consola';
@@ -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 { 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.6MW2qn5T.mjs';
5
5
  import Vue from '@vitejs/plugin-vue';
6
6
  import { Hookable } from 'hookable';
7
7
  import { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
@@ -768,6 +768,12 @@ interface ValaxyExtendConfig {
768
768
  * @default true
769
769
  */
770
770
  katex: boolean;
771
+ /**
772
+ * @description:en-US Auto-extract the first image from markdown content for Open Graph fallback
773
+ * @description:zh-CN 自动从 Markdown 内容中提取第一张图片,作为 Open Graph 的回退图片
774
+ * @default true
775
+ */
776
+ extractFirstImage: boolean;
771
777
  };
772
778
  /**
773
779
  * 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.DQ6HsU2J.mjs';
2
2
  import 'node:path';
3
3
  import 'fs-extra';
4
4
  import 'consola/utils';
@@ -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,7 @@ 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 { execFileSync, execSync, exec } from 'node:child_process';
32
32
  import v8 from 'node:v8';
33
33
  import generateSitemap from 'vite-ssg-sitemap';
34
34
  import { createMarkdownItAsync, MarkdownItAsync } from 'markdown-it-async';
@@ -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.0";
1723
1725
 
1724
1726
  const GLOBAL_STATE = {
1725
1727
  valaxyApp: void 0,
@@ -3394,10 +3396,24 @@ function inferDescription(frontmatter) {
3394
3396
  return description;
3395
3397
  return head && getHeadMetaContent(head, "description") || "";
3396
3398
  }
3399
+ function extractFirstImage(code) {
3400
+ const mdImageMatch = code.match(/!\[.*?\]\((.+?)\)/);
3401
+ if (mdImageMatch)
3402
+ return mdImageMatch[1];
3403
+ const htmlImageMatch = code.match(/<img\s[^>]*?src=["'](.+?)["']/);
3404
+ if (htmlImageMatch)
3405
+ return htmlImageMatch[1];
3406
+ return void 0;
3407
+ }
3397
3408
  async function generatePageData(code, id, options) {
3398
3409
  const fileInfo = Valaxy.state.idMap.get(id);
3399
3410
  const relativePath = path.relative(options.userRoot, id);
3400
3411
  const fm = JSON.parse(JSON.stringify(fileInfo?.frontmatter));
3412
+ if (options.config.features?.extractFirstImage !== false && !fm.ogImage && !fm.cover) {
3413
+ const firstImage = extractFirstImage(code);
3414
+ if (firstImage)
3415
+ fm.firstImage = firstImage;
3416
+ }
3401
3417
  const pageData = {
3402
3418
  title: fm.title || fileInfo?.title || "",
3403
3419
  titleTemplate: fm.titleTemplate,
@@ -3408,7 +3424,8 @@ async function generatePageData(code, id, options) {
3408
3424
  relativePath,
3409
3425
  filePath: id
3410
3426
  };
3411
- pageData.lastUpdated = await getGitTimestamp(id);
3427
+ if (options.config.siteConfig.lastUpdated)
3428
+ pageData.lastUpdated = await getGitTimestamp(id);
3412
3429
  return pageData;
3413
3430
  }
3414
3431
 
@@ -5401,17 +5418,21 @@ async function getPosts(params, options) {
5401
5418
  return { data, content, excerpt, path: i };
5402
5419
  });
5403
5420
  const rawPosts = await Promise.all(readFilePromises);
5421
+ const draftPosts = [];
5404
5422
  const filteredPosts = rawPosts.filter((p) => {
5405
5423
  const { data } = p;
5406
5424
  if (data.password)
5407
5425
  return false;
5408
5426
  if (data.draft) {
5427
+ draftPosts.push(p.path);
5409
5428
  return false;
5410
5429
  }
5411
5430
  if (data.hide)
5412
5431
  return false;
5413
5432
  return true;
5414
5433
  });
5434
+ if (draftPosts.length)
5435
+ consola.log(`[rss] Skipped ${draftPosts.length} draft post(s): ${draftPosts.join(", ")}`);
5415
5436
  const posts = [];
5416
5437
  for (const rawPost of filteredPosts) {
5417
5438
  const { data, path, content, excerpt } = rawPost;
@@ -6080,14 +6101,108 @@ function registerCleanCommand(cli) {
6080
6101
  );
6081
6102
  }
6082
6103
 
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}`));
6104
+ function getPnpmVersion() {
6105
+ try {
6106
+ return execSync("pnpm --version", { encoding: "utf-8" }).trim();
6107
+ } catch {
6108
+ return "not found";
6109
+ }
6110
+ }
6111
+ async function collectDebugInfo() {
6112
+ const info = {
6113
+ os: os.platform(),
6114
+ arch: os.arch(),
6115
+ node: process.version,
6116
+ pnpm: getPnpmVersion(),
6117
+ valaxy: version
6118
+ };
6119
+ try {
6120
+ const options = await resolveOptions({ userRoot: process.cwd() });
6121
+ info.userRoot = options.userRoot;
6122
+ info.theme = options.theme;
6123
+ info.themeVersion = options.config.themeConfig?.pkg?.version;
6124
+ info.addons = options.addons.filter((a) => a.enable).map((a) => ({
6125
+ name: a.name,
6126
+ version: a.pkg?.version || "unknown",
6127
+ global: a.global
6128
+ }));
6129
+ info.pages = options.pages.length;
6130
+ } catch {
6131
+ }
6132
+ return info;
6133
+ }
6134
+ function printFancy(info) {
6135
+ const lines = [];
6136
+ lines.push(`${colors.bold(colors.cyan("Environment"))}`);
6137
+ lines.push(` OS: ${colors.green(`${info.os} ${info.arch}`)}`);
6138
+ lines.push(` Node.js: ${colors.green(info.node)}`);
6139
+ lines.push(` Package Manager: ${colors.green(`pnpm ${info.pnpm}`)}`);
6140
+ lines.push(` Valaxy: ${colors.cyan(`v${info.valaxy}`)}`);
6141
+ if (info.theme) {
6142
+ lines.push("");
6143
+ lines.push(`${colors.bold(colors.cyan("Project"))}`);
6144
+ lines.push(` Root: ${colors.dim(info.userRoot)}`);
6145
+ lines.push(` Theme: ${colors.green(info.theme)} ${colors.blue(`v${info.themeVersion || "unknown"}`)}`);
6146
+ if (info.addons && info.addons.length > 0) {
6147
+ lines.push(` Addons:`);
6148
+ info.addons.forEach((addon, i) => {
6149
+ const prefix = i === info.addons.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
6150
+ const globalTag = addon.global ? colors.cyan(" (global)") : "";
6151
+ lines.push(` ${prefix} ${colors.yellow(addon.name)} ${colors.blue(`v${addon.version}`)}${globalTag}`);
6152
+ });
6153
+ } else {
6154
+ lines.push(` Addons: ${colors.dim("none")}`);
6155
+ }
6156
+ lines.push(` Pages: ${colors.green(String(info.pages))}`);
6157
+ }
6158
+ consola.box({
6159
+ title: "\u{1F30C} Valaxy Debug Info",
6160
+ message: lines.join("\n"),
6161
+ style: {
6162
+ borderColor: "cyan"
6163
+ }
6089
6164
  });
6090
6165
  }
6166
+ function printPlain(info) {
6167
+ const lines = [];
6168
+ lines.push("## Environment");
6169
+ lines.push(`- OS: ${info.os} ${info.arch}`);
6170
+ lines.push(`- Node: ${info.node}`);
6171
+ lines.push(`- Package Manager: pnpm ${info.pnpm}`);
6172
+ lines.push(`- Valaxy: v${info.valaxy}`);
6173
+ if (info.theme) {
6174
+ lines.push("");
6175
+ lines.push("## Project");
6176
+ lines.push(`- Root: ${info.userRoot}`);
6177
+ lines.push(`- Theme: ${info.theme} (v${info.themeVersion || "unknown"})`);
6178
+ if (info.addons && info.addons.length > 0) {
6179
+ const addonStr = info.addons.map((a) => `${a.name} (v${a.version})${a.global ? " [global]" : ""}`).join(", ");
6180
+ lines.push(`- Addons: ${addonStr}`);
6181
+ } else {
6182
+ lines.push("- Addons: none");
6183
+ }
6184
+ lines.push(`- Pages: ${info.pages}`);
6185
+ }
6186
+ console.log(lines.join("\n"));
6187
+ }
6188
+ function registerDebugCommand(cli) {
6189
+ cli.command(
6190
+ "debug",
6191
+ "Display debug information for your Valaxy project",
6192
+ (args) => args.option("plain", {
6193
+ type: "boolean",
6194
+ default: false,
6195
+ describe: "Output plain text without colors (for pasting into issues)"
6196
+ }),
6197
+ async (args) => {
6198
+ const info = await collectDebugInfo();
6199
+ if (args.plain)
6200
+ printPlain(info);
6201
+ else
6202
+ printFancy(info);
6203
+ }
6204
+ );
6205
+ }
6091
6206
 
6092
6207
  function registerDeployCommand(cli) {
6093
6208
  cli.command("deploy", "deploy your blog to the cloud", async () => {
@@ -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.0",
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.0",
143
+ "@valaxyjs/utils": "0.28.0"
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 是否显示目录