@movk/nuxt-docs 1.4.1 → 1.5.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.
package/app/app.config.ts CHANGED
@@ -34,7 +34,10 @@ export default defineAppConfig({
34
34
  }
35
35
  }
36
36
  },
37
- vercelAnalytics: false,
37
+ vercelAnalytics: {
38
+ enable: false,
39
+ debug: false
40
+ },
38
41
  header: {
39
42
  avatar: 'https://docs.mhaibaraai.cn/avatar.png',
40
43
  title: 'Movk Nuxt Docs',
package/app/app.vue CHANGED
@@ -45,8 +45,8 @@ provide('navigation', rootNavigation)
45
45
  <template>
46
46
  <UApp :toaster="appConfig.toaster">
47
47
  <NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
48
- <Analytics v-if="appConfig.vercelAnalytics" />
49
- <SpeedInsights v-if="appConfig.vercelAnalytics" />
48
+ <Analytics v-if="appConfig.vercelAnalytics" :debug="appConfig.vercelAnalytics?.debug" />
49
+ <SpeedInsights v-if="appConfig.vercelAnalytics" :debug="appConfig.vercelAnalytics?.debug" />
50
50
 
51
51
  <div :class="{ root: route.path.startsWith('/docs/') }">
52
52
  <template v-if="!route.path.startsWith('/examples')">
@@ -13,7 +13,7 @@ const items = [
13
13
  label: 'Copy Markdown link',
14
14
  icon: 'i-lucide-link',
15
15
  onSelect() {
16
- if (vercelAnalytics) track ('Page Action', { action: 'Copy Markdown Link' })
16
+ if (vercelAnalytics?.debug) track ('Page Action', { action: 'Copy Markdown Link' })
17
17
  copy(mdPath.value)
18
18
  toast.add({
19
19
  title: 'Copied to clipboard',
@@ -27,7 +27,7 @@ const items = [
27
27
  target: '_blank',
28
28
  to: `/raw${route.path}.md`,
29
29
  onSelect() {
30
- if (vercelAnalytics) track('Page Action', { action: 'View as Markdown' })
30
+ if (vercelAnalytics?.debug) track('Page Action', { action: 'View as Markdown' })
31
31
  }
32
32
  },
33
33
  {
@@ -36,7 +36,7 @@ const items = [
36
36
  target: '_blank',
37
37
  to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`,
38
38
  onSelect() {
39
- if (vercelAnalytics) track('Page Action', { action: 'Open in ChatGPT' })
39
+ if (vercelAnalytics?.debug) track('Page Action', { action: 'Open in ChatGPT' })
40
40
  }
41
41
  },
42
42
  {
@@ -45,13 +45,13 @@ const items = [
45
45
  target: '_blank',
46
46
  to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`,
47
47
  onSelect() {
48
- if (vercelAnalytics) track('Page Action', { action: 'Open in Claude' })
48
+ if (vercelAnalytics?.debug) track('Page Action', { action: 'Open in Claude' })
49
49
  }
50
50
  }
51
51
  ]
52
52
 
53
53
  async function copyPage() {
54
- if (vercelAnalytics) track('Page Action', { action: 'Copy Page Content' })
54
+ if (vercelAnalytics?.debug) track('Page Action', { action: 'Copy Page Content' })
55
55
  copy(await $fetch<string>(`/raw${route.path}.md`))
56
56
  }
57
57
  </script>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import { motion } from 'motion-v'
3
+ import type { VariantType } from 'motion-v'
4
+
5
+ const props = defineProps<{
6
+ open: boolean
7
+ }>()
8
+
9
+ const variants: { [k: string]: VariantType | ((custom: unknown) => VariantType) } = {
10
+ normal: {
11
+ rotate: 0,
12
+ y: 0,
13
+ opacity: 1
14
+ },
15
+ close: (custom: unknown) => {
16
+ const c = custom as number
17
+ return {
18
+ rotate: c === 1 ? 45 : c === 3 ? -45 : 0,
19
+ y: c === 1 ? 6 : c === 3 ? -6 : 0,
20
+ opacity: c === 2 ? 0 : 1,
21
+ transition: {
22
+ type: 'spring',
23
+ stiffness: 260,
24
+ damping: 20
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ const state = computed(() => props.open ? 'close' : 'normal')
31
+ </script>
32
+
33
+ <template>
34
+ <UButton
35
+ size="sm"
36
+ variant="ghost"
37
+ color="neutral"
38
+ square
39
+ >
40
+ <svg
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ class="size-5"
43
+ viewBox="0 0 24 24"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ stroke-width="2"
47
+ stroke-linecap="round"
48
+ stroke-linejoin="round"
49
+ >
50
+ <motion.line
51
+ x1="4"
52
+ y1="6"
53
+ x2="20"
54
+ y2="6"
55
+ :variants="variants"
56
+ :animate="state"
57
+ :custom="1"
58
+ class="outline-none"
59
+ />
60
+ <motion.line
61
+ x1="4"
62
+ y1="12"
63
+ x2="20"
64
+ y2="12"
65
+ :variants="variants"
66
+ :animate="state"
67
+ :custom="2"
68
+ class="outline-none"
69
+ />
70
+ <motion.line
71
+ x1="4"
72
+ y1="18"
73
+ x2="20"
74
+ y2="18"
75
+ :variants="variants"
76
+ :animate="state"
77
+ :custom="3"
78
+ class="outline-none"
79
+ />
80
+ </svg>
81
+ </UButton>
82
+ </template>
@@ -49,6 +49,14 @@ const links = computed<ButtonProps[]>(() => github && github.url
49
49
  </template>
50
50
  </template>
51
51
 
52
+ <template #toggle="{ open, toggle, ui }">
53
+ <HeaderToggleButton
54
+ :open="open"
55
+ :class="ui.toggle({ toggleSide: 'right' })"
56
+ @click="toggle"
57
+ />
58
+ </template>
59
+
52
60
  <template #body>
53
61
  <HeaderBody />
54
62
  </template>
@@ -13,7 +13,7 @@ const { track } = useAnalytics()
13
13
  const open = ref(false)
14
14
 
15
15
  watch(open, (isOpen) => {
16
- if (isOpen && appConfig.vercelAnalytics) {
16
+ if (isOpen && appConfig.vercelAnalytics?.debug) {
17
17
  track('Theme Picker Opened')
18
18
  }
19
19
  })
@@ -29,7 +29,7 @@ const neutral = computed({
29
29
  set(option) {
30
30
  appConfig.ui.colors.neutral = option
31
31
  window.localStorage.setItem(`${site.name}-ui-neutral`, appConfig.ui.colors.neutral)
32
- if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'neutral', value: option })
32
+ if (appConfig.vercelAnalytics?.debug) track('Theme Changed', { setting: 'neutral', value: option })
33
33
  }
34
34
  })
35
35
 
@@ -43,7 +43,7 @@ const primary = computed({
43
43
  appConfig.ui.colors.primary = option
44
44
  window.localStorage.setItem(`${site.name}-ui-primary`, appConfig.ui.colors.primary)
45
45
  setBlackAsPrimary(false)
46
- if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'primary', value: option })
46
+ if (appConfig.vercelAnalytics?.debug) track('Theme Changed', { setting: 'primary', value: option })
47
47
  }
48
48
  })
49
49
 
@@ -55,7 +55,7 @@ const radius = computed({
55
55
  set(option) {
56
56
  appConfig.theme.radius = option
57
57
  window.localStorage.setItem(`${site.name}-ui-radius`, String(appConfig.theme.radius))
58
- if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'radius', value: option })
58
+ if (appConfig.vercelAnalytics?.debug) track('Theme Changed', { setting: 'radius', value: option })
59
59
  }
60
60
  })
61
61
 
@@ -70,14 +70,14 @@ const mode = computed({
70
70
  },
71
71
  set(option) {
72
72
  colorMode.preference = option
73
- if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'color mode', value: option })
73
+ if (appConfig.vercelAnalytics?.debug) track('Theme Changed', { setting: 'color mode', value: option })
74
74
  }
75
75
  })
76
76
 
77
77
  function setBlackAsPrimary(value: boolean) {
78
78
  appConfig.theme.blackAsPrimary = value
79
79
  window.localStorage.setItem(`${site.name}-ui-black-as-primary`, String(value))
80
- if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'black as primary', value })
80
+ if (appConfig.vercelAnalytics?.debug) track('Theme Changed', { setting: 'black as primary', value })
81
81
  }
82
82
 
83
83
  const fonts = ['Public Sans', 'DM Sans', 'Geist', 'Inter', 'Poppins', 'Outfit', 'Raleway']
@@ -88,7 +88,7 @@ const font = computed({
88
88
  set(option) {
89
89
  appConfig.theme.font = option
90
90
  window.localStorage.setItem(`${site.name}-ui-font`, appConfig.theme.font)
91
- if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'font', value: option })
91
+ if (appConfig.vercelAnalytics?.debug) track('Theme Changed', { setting: 'font', value: option })
92
92
  }
93
93
  })
94
94
 
@@ -113,7 +113,7 @@ const icon = computed({
113
113
  appConfig.theme.icons = option
114
114
  appConfig.ui.icons = themeIcons[option as keyof typeof themeIcons] as any
115
115
  window.localStorage.setItem(`${site.name}-ui-icons`, appConfig.theme.icons)
116
- if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'icons', value: option })
116
+ if (appConfig.vercelAnalytics?.debug) track('Theme Changed', { setting: 'icons', value: option })
117
117
  }
118
118
  })
119
119
 
@@ -130,7 +130,7 @@ const hasAppConfigChanges = computed(() => {
130
130
  })
131
131
 
132
132
  function exportCSS() {
133
- if (appConfig.vercelAnalytics) track('Theme Exported', { type: 'css' })
133
+ if (appConfig.vercelAnalytics?.debug) track('Theme Exported', { type: 'css' })
134
134
 
135
135
  const lines = [
136
136
  '@import "tailwindcss";',
@@ -161,7 +161,7 @@ function exportCSS() {
161
161
  }
162
162
 
163
163
  function exportAppConfig() {
164
- if (appConfig.vercelAnalytics) track('Theme Exported', { type: 'appConfig' })
164
+ if (appConfig.vercelAnalytics?.debug) track('Theme Exported', { type: 'appConfig' })
165
165
 
166
166
  const config: Record<string, any> = {}
167
167
 
@@ -192,7 +192,7 @@ function exportAppConfig() {
192
192
  }
193
193
 
194
194
  function resetTheme() {
195
- if (appConfig.vercelAnalytics) track('Theme Reset')
195
+ if (appConfig.vercelAnalytics?.debug) track('Theme Reset')
196
196
 
197
197
  // Reset without triggering individual tracking events
198
198
  appConfig.ui.colors.primary = 'green'
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from '@nuxtjs/mdc/config'
2
+ import { transformerColorHighlight } from 'shiki-transformer-color-highlight'
3
+ import { transformerIconHighlight } from './utils/shiki-transformer-icon-highlight'
4
+
5
+ export default defineConfig({
6
+ shiki: {
7
+ transformers: [
8
+ transformerColorHighlight(),
9
+ transformerIconHighlight()
10
+ ]
11
+ }
12
+ })
@@ -2,7 +2,10 @@ import type { ButtonProps } from '@nuxt/ui'
2
2
 
3
3
  declare module 'nuxt/schema' {
4
4
  interface AppConfig {
5
- vercelAnalytics: boolean
5
+ vercelAnalytics: {
6
+ enable: boolean
7
+ debug: boolean
8
+ }
6
9
  seo: {
7
10
  titleTemplate: string
8
11
  title: string
@@ -38,12 +41,12 @@ declare module 'nuxt/schema' {
38
41
  suffix: string
39
42
  per_page: number
40
43
  until: string
41
- author?: string
44
+ author: string
42
45
  /**
43
46
  * 日期格式化配置
44
47
  * @example { locale: 'zh-CN', options: { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' } }
45
48
  */
46
- dateFormat?: {
49
+ dateFormat: {
47
50
  locale?: string
48
51
  options?: Intl.DateTimeFormatOptions
49
52
  }
@@ -0,0 +1,90 @@
1
+ /** copy from https://github.com/nuxt/ui/blob/v4/docs/app/utils/shiki-transformer-icon-highlight.ts */
2
+ import type { ShikiTransformer } from 'shiki'
3
+
4
+ export interface TransformerIconHighlightOptions {
5
+ /**
6
+ * Custom function to render the icon HTML
7
+ * @default Uses Iconify API with mask mode
8
+ */
9
+ htmlIcon?: (icon: string) => string
10
+ }
11
+
12
+ // Common icon collections to validate against (sorted by length descending for proper matching)
13
+ const ICON_COLLECTIONS = [
14
+ 'simple-icons',
15
+ 'vscode-icons',
16
+ 'tabler',
17
+ 'lucide',
18
+ 'logos',
19
+ 'ph'
20
+ ]
21
+
22
+ function parseIconName(text: string): { collection: string, name: string, format: 'i' | 'colon' } | null {
23
+ // Strip quotes if present (single, double, or backticks)
24
+ let cleanText = text
25
+ if (/^['"`].*['"`]$/.test(text)) {
26
+ cleanText = text.slice(1, -1)
27
+ }
28
+
29
+ // Try i-{collection}-{name} format
30
+ if (cleanText.startsWith('i-')) {
31
+ const rest = cleanText.slice(2)
32
+ for (const collection of ICON_COLLECTIONS) {
33
+ if (rest.startsWith(`${collection}-`)) {
34
+ const name = rest.slice(collection.length + 1)
35
+ if (name && /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(name)) {
36
+ return { collection, name, format: 'i' }
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ // Try {collection}:{name} format
43
+ const colonIndex = cleanText.indexOf(':')
44
+ if (colonIndex > 0) {
45
+ const collection = cleanText.slice(0, colonIndex)
46
+ const name = cleanText.slice(colonIndex + 1)
47
+ if (ICON_COLLECTIONS.includes(collection) && name && /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(name)) {
48
+ return { collection, name, format: 'colon' }
49
+ }
50
+ }
51
+
52
+ return null
53
+ }
54
+
55
+ export function transformerIconHighlight(options: TransformerIconHighlightOptions = {}): ShikiTransformer {
56
+ const { htmlIcon } = options
57
+
58
+ return {
59
+ name: 'shiki-transformer-icon-highlight',
60
+ span(hast, _line, _col, _lineElement, token) {
61
+ const text = token.content
62
+
63
+ // Try to parse as an icon
64
+ const parsed = parseIconName(text)
65
+ if (!parsed) return
66
+
67
+ const iconIdentifier = `${parsed.collection}:${parsed.name}`
68
+ // Add color=black for mask-image to work properly (mask uses luminance)
69
+ const iconUrl = `https://api.iconify.design/${iconIdentifier}.svg?color=%23000`
70
+
71
+ // Create the icon element as a proper HAST element
72
+ const iconElement = htmlIcon
73
+ ? { type: 'raw' as const, value: htmlIcon(iconIdentifier) }
74
+ : {
75
+ type: 'element' as const,
76
+ tagName: 'i',
77
+ properties: {
78
+ class: 'shiki-icon-highlight',
79
+ style: `--shiki-icon-url: url('${iconUrl}')`
80
+ },
81
+ children: []
82
+ }
83
+
84
+ // Prepend the icon to the span content
85
+ if (hast.children) {
86
+ hast.children.unshift(iconElement)
87
+ }
88
+ }
89
+ }
90
+ }
package/content.config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defineCollection, defineContentConfig, property, z } from '@nuxt/content'
1
+ import { defineCollection, defineContentConfig, z } from '@nuxt/content'
2
2
  import { useNuxt } from '@nuxt/kit'
3
3
  import { asSeoCollection } from '@nuxtjs/seo/content'
4
4
  import { joinURL } from 'ufo'
@@ -6,6 +6,26 @@ import { joinURL } from 'ufo'
6
6
  const { options } = useNuxt()
7
7
  const cwd = joinURL(options.rootDir, 'content')
8
8
 
9
+ const Avatar = z.object({
10
+ src: z.string(),
11
+ alt: z.string().optional()
12
+ })
13
+
14
+ const Button = z.object({
15
+ label: z.string(),
16
+ icon: z.string().optional(),
17
+ avatar: Avatar.optional(),
18
+ leadingIcon: z.string().optional(),
19
+ trailingIcon: z.string().optional(),
20
+ to: z.string().optional(),
21
+ target: z.enum(['_blank', '_self']).optional(),
22
+ color: z.enum(['primary', 'neutral', 'success', 'warning', 'error', 'info']).optional(),
23
+ size: z.enum(['xs', 'sm', 'md', 'lg', 'xl']).optional(),
24
+ variant: z.enum(['solid', 'outline', 'subtle', 'soft', 'ghost', 'link']).optional(),
25
+ id: z.string().optional(),
26
+ class: z.string().optional()
27
+ })
28
+
9
29
  export default defineContentConfig({
10
30
  collections: {
11
31
  landing: defineCollection(asSeoCollection({
@@ -22,7 +42,7 @@ export default defineContentConfig({
22
42
  include: 'docs/**/*'
23
43
  }],
24
44
  schema: z.object({
25
- links: z.array(property(z.object({})).inherit('@nuxt/ui/components/Button.vue')),
45
+ links: z.array(Button),
26
46
  category: z.string().optional(),
27
47
  navigation: z.object({
28
48
  title: z.string().optional()
package/modules/css.ts CHANGED
@@ -21,7 +21,23 @@ export default defineNuxtModule({
21
21
 
22
22
  @source "${contentDir.replace(/\\/g, '/')}/**/*";
23
23
  @source "${layerDir.replace(/\\/g, '/')}/**/*";
24
- @source "../../app.config.ts";`
24
+ @source "../../app.config.ts";
25
+
26
+ /* Shiki icon highlight transformer styles */
27
+ .shiki-icon-highlight {
28
+ display: inline-block;
29
+ width: 1.25em;
30
+ height: 1.25em;
31
+ vertical-align: -0.25em;
32
+ margin-right: 0.125em;
33
+ background-color: var(--ui-text-highlighted);
34
+ -webkit-mask-repeat: no-repeat;
35
+ mask-repeat: no-repeat;
36
+ -webkit-mask-size: 100% 100%;
37
+ mask-size: 100% 100%;
38
+ -webkit-mask-image: var(--shiki-icon-url);
39
+ mask-image: var(--shiki-icon-url);
40
+ }`
25
41
  }
26
42
  })
27
43
 
package/nuxt.config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createResolver } from '@nuxt/kit'
2
+ import { defineNuxtConfig } from 'nuxt/config'
2
3
  import pkg from './package.json'
3
4
 
4
5
  const { resolve } = createResolver(import.meta.url)
@@ -18,6 +19,13 @@ export default defineNuxtConfig({
18
19
  'motion-v/nuxt',
19
20
  'nuxt-llms'
20
21
  ],
22
+ app: {
23
+ rootAttrs: {
24
+ 'data-vaul-drawer-wrapper': '',
25
+ 'class': 'bg-default'
26
+ }
27
+ },
28
+ // @ts-ignore - content 配置的类型定义在运行时才能正确解析
21
29
  content: {
22
30
  build: {
23
31
  markdown: {
@@ -32,6 +40,11 @@ export default defineNuxtConfig({
32
40
  noApiRoute: false
33
41
  }
34
42
  },
43
+ ui: {
44
+ experimental: {
45
+ componentDetection: true
46
+ }
47
+ },
35
48
  runtimeConfig: {
36
49
  public: {
37
50
  version: pkg.version
@@ -45,7 +58,13 @@ export default defineNuxtConfig({
45
58
  : {}
46
59
  },
47
60
  experimental: {
48
- typescriptPlugin: true
61
+ typescriptPlugin: true,
62
+ asyncContext: true,
63
+ defaults: {
64
+ nuxtLink: {
65
+ externalRelAttribute: 'noopener'
66
+ }
67
+ }
49
68
  },
50
69
  compatibilityDate: 'latest',
51
70
  nitro: {
@@ -65,12 +84,6 @@ export default defineNuxtConfig({
65
84
  'tailwind-variants',
66
85
  'tailwindcss/colors'
67
86
  ]
68
- },
69
- resolve: {
70
- alias: {
71
- extend: 'extend/index.js',
72
- debug: 'debug/src/browser.js'
73
- }
74
87
  }
75
88
  },
76
89
  fonts: {
@@ -87,6 +100,10 @@ export default defineNuxtConfig({
87
100
  icon: {
88
101
  provider: 'iconify'
89
102
  },
103
+ image: {
104
+ format: ['webp', 'jpeg', 'jpg', 'png', 'svg'],
105
+ provider: 'ipx'
106
+ },
90
107
  linkChecker: {
91
108
  report: {
92
109
  publish: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@movk/nuxt-docs",
3
3
  "type": "module",
4
- "version": "1.4.1",
4
+ "version": "1.5.0",
5
5
  "private": false,
6
6
  "description": "An elegant documentation theme for Nuxt, powered by Nuxt UI and Nuxt Content.",
7
7
  "author": "YiXuan <mhaibaraai@gmail.com>",
@@ -27,11 +27,11 @@
27
27
  "README.md"
28
28
  ],
29
29
  "dependencies": {
30
- "@iconify-json/lucide": "^1.2.81",
30
+ "@iconify-json/lucide": "^1.2.82",
31
31
  "@iconify-json/simple-icons": "^1.2.63",
32
32
  "@iconify-json/vscode-icons": "^1.2.37",
33
- "@movk/core": "^1.0.2",
34
- "@nuxt/content": "^3.9.0",
33
+ "@movk/core": "^1.0.3",
34
+ "@nuxt/content": "^3.10.0",
35
35
  "@nuxt/image": "^2.0.0",
36
36
  "@nuxt/kit": "^4.2.2",
37
37
  "@nuxt/ui": "^4.3.0",
@@ -45,13 +45,14 @@
45
45
  "exsolve": "^1.0.8",
46
46
  "git-url-parse": "^16.1.0",
47
47
  "motion-v": "^1.7.4",
48
- "nuxt-component-meta": "^0.15.0",
48
+ "nuxt-component-meta": "^0.16.0",
49
49
  "nuxt-llms": "^0.1.3",
50
50
  "ohash": "^2.0.11",
51
51
  "pathe": "^2.0.3",
52
52
  "pkg-types": "^2.3.0",
53
53
  "prettier": "^3.7.4",
54
54
  "scule": "^1.3.0",
55
+ "shiki-transformer-color-highlight": "^1.0.0",
55
56
  "tailwindcss": "^4.1.18",
56
57
  "ufo": "^1.6.1"
57
58
  }
@@ -16,6 +16,14 @@ export default defineCachedEventHandler(async (event) => {
16
16
  }
17
17
 
18
18
  const { github } = useAppConfig()
19
+
20
+ if (!github || typeof github === 'boolean') {
21
+ throw createError({
22
+ statusCode: 500,
23
+ statusMessage: 'GitHub configuration is not available'
24
+ })
25
+ }
26
+
19
27
  const octokit = new Octokit({ auth: process.env.NUXT_GITHUB_TOKEN })
20
28
 
21
29
  const allCommits = await Promise.all(
@@ -14,6 +14,14 @@ export default defineCachedEventHandler(async (event) => {
14
14
  }
15
15
 
16
16
  const { github } = useAppConfig()
17
+
18
+ if (!github || typeof github === 'boolean') {
19
+ throw createError({
20
+ statusCode: 500,
21
+ statusMessage: 'GitHub configuration is not available'
22
+ })
23
+ }
24
+
17
25
  const octokit = new Octokit({ auth: process.env.NUXT_GITHUB_TOKEN })
18
26
 
19
27
  try {
@@ -30,6 +38,9 @@ export default defineCachedEventHandler(async (event) => {
30
38
  }
31
39
 
32
40
  const commit = commits[0]
41
+ if (!commit) {
42
+ return null
43
+ }
33
44
 
34
45
  // 获取提交者信息,处理 web-flow 场景(PR squash merge)
35
46
  let authorName = commit.commit.author?.name ?? ''
@@ -63,14 +74,13 @@ export default defineCachedEventHandler(async (event) => {
63
74
  const date = commit.commit.author?.date ?? ''
64
75
  const dateFormat = github.dateFormat ?? {}
65
76
  const locale = dateFormat.locale ?? 'zh-CN'
66
- const timeZone = dateFormat.timeZone ?? 'Asia/Shanghai'
67
77
  const formatOptions: Intl.DateTimeFormatOptions = dateFormat.options ?? {
68
78
  year: 'numeric',
69
79
  month: 'numeric',
70
80
  day: 'numeric',
71
81
  hour: '2-digit',
72
82
  minute: '2-digit',
73
- timeZone
83
+ timeZone: 'Asia/Shanghai'
74
84
  }
75
85
  const dateFormatted = date
76
86
  ? new Date(date).toLocaleDateString(locale, formatOptions)