clarity-theme 0.1.0 → 0.1.2

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.
Files changed (154) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +70 -4
  3. package/README.zh-CN.md +67 -4
  4. package/nuxt.config.ts +217 -214
  5. package/package.json +35 -37
  6. package/{app → src}/app.config.ts +110 -110
  7. package/{app → src}/app.vue +5 -5
  8. package/{app → src}/components/blog/BlogAside.vue +25 -25
  9. package/{app → src}/components/blog/BlogFooter.vue +25 -25
  10. package/{app → src}/components/blog/BlogHeader.global.vue +48 -48
  11. package/{app → src}/components/blog/BlogPanel.vue +43 -43
  12. package/{app → src}/components/blog/BlogSidebar.vue +53 -53
  13. package/{app → src}/components/blog/BlogWidget.vue +42 -42
  14. package/{app → src}/components/blog/Mask.vue +25 -25
  15. package/{app → src}/components/blog/SkipToContent.vue +9 -9
  16. package/{app → src}/components/blog/ThemeToggle.vue +22 -22
  17. package/{app → src}/components/content/Badge.vue +45 -45
  18. package/{app → src}/components/content/Blur.vue +14 -14
  19. package/{app → src}/components/content/CardList.vue +8 -8
  20. package/{app → src}/components/content/Chat.vue +41 -41
  21. package/{app → src}/components/content/Copy.vue +71 -71
  22. package/{app → src}/components/content/EmojiClock.vue +49 -49
  23. package/{app → src}/components/content/FeedCard.vue +96 -96
  24. package/{app → src}/components/content/FeedGroup.vue +61 -61
  25. package/{app → src}/components/content/Folding.vue +19 -19
  26. package/{app → src}/components/content/Key.vue +143 -143
  27. package/{app → src}/components/content/LinkBanner.vue +27 -27
  28. package/{app → src}/components/content/LinkCard.vue +28 -28
  29. package/{app → src}/components/content/MdTitle.vue +8 -8
  30. package/{app → src}/components/content/Mermaid.vue +99 -99
  31. package/{app → src}/components/content/MusicScore.vue +63 -63
  32. package/{app → src}/components/content/Pic.vue +49 -49
  33. package/{app → src}/components/content/Poetry.vue +27 -27
  34. package/{app → src}/components/content/ProseA.vue +24 -24
  35. package/{app → src}/components/content/ProseCode.vue +16 -16
  36. package/{app → src}/components/content/ProsePre.vue +117 -117
  37. package/{app → src}/components/content/ProseTable.vue +23 -23
  38. package/{app → src}/components/content/Quote.vue +21 -21
  39. package/{app → src}/components/content/Tab.vue +32 -32
  40. package/{app → src}/components/content/Timeline.vue +33 -33
  41. package/{app → src}/components/content/Tip.vue +40 -40
  42. package/{app → src}/components/content/VideoEmbed.vue +112 -112
  43. package/{app → src}/components/partial/Button.vue +36 -36
  44. package/{app → src}/components/partial/DlGroup.vue +31 -31
  45. package/{app → src}/components/partial/Dropdown.vue +21 -21
  46. package/{app → src}/components/partial/Error.vue +39 -39
  47. package/{app → src}/components/partial/Expand.vue +26 -26
  48. package/{app → src}/components/partial/IconNavList.vue +24 -24
  49. package/{app → src}/components/partial/Pagination.vue +62 -62
  50. package/{app → src}/components/partial/RadioGroup.vue +30 -30
  51. package/{app → src}/components/partial/Secret.vue +11 -11
  52. package/{app → src}/components/partial/Slider.vue +48 -48
  53. package/{app → src}/components/partial/Toggle.vue +17 -17
  54. package/{app → src}/components/popover/Lightbox.vue +44 -44
  55. package/{app → src}/components/popover/Search.vue +143 -143
  56. package/{app → src}/components/popover/SearchItem.vue +28 -28
  57. package/{app → src}/components/post/Archive.vue +40 -40
  58. package/{app → src}/components/post/Article.vue +49 -49
  59. package/{app → src}/components/post/Comment.vue +119 -119
  60. package/{app → src}/components/post/Excerpt.vue +40 -40
  61. package/{app → src}/components/post/OrderToggle.vue +70 -70
  62. package/{app → src}/components/post/PostFooter.vue +52 -52
  63. package/{app → src}/components/post/PostHeader.vue +66 -66
  64. package/{app → src}/components/post/PostSurround.vue +51 -51
  65. package/{app → src}/components/post/Slide.vue +85 -85
  66. package/{app → src}/components/util/Date.vue +62 -62
  67. package/{app → src}/components/util/HydrateSafe.vue +10 -10
  68. package/{app → src}/components/util/Img.vue +38 -38
  69. package/{app → src}/components/util/Link.vue +11 -11
  70. package/{app → src}/components/widget/BlogLog.vue +23 -23
  71. package/{app → src}/components/widget/BlogStats.vue +38 -38
  72. package/{app → src}/components/widget/BlogTech.vue +57 -57
  73. package/{app → src}/components/widget/Empty.vue +3 -3
  74. package/{app → src}/components/widget/Toc.vue +67 -67
  75. package/{app → src}/composables/useArticle.ts +115 -115
  76. package/{app → src}/composables/useAvoid.ts +87 -87
  77. package/{app → src}/composables/useCopy.ts +21 -21
  78. package/{app → src}/composables/usePagination.ts +70 -70
  79. package/{app → src}/composables/useShiki.ts +129 -129
  80. package/{app → src}/composables/useToc.ts +57 -57
  81. package/{app → src}/composables/useWidgets.ts +56 -56
  82. package/{config → src/config}/app.ts +62 -62
  83. package/{config → src/config}/content.ts +75 -75
  84. package/{config → src/config}/define.ts +3 -3
  85. package/src/config/feeds.empty.ts +6 -0
  86. package/{config → src/config}/index.ts +1 -1
  87. package/{config → src/config}/schema.ts +89 -89
  88. package/{app → src}/error.vue +31 -31
  89. package/{img → src/img}/index.ts +2 -2
  90. package/{app → src}/layouts/default.vue +25 -25
  91. package/{modules → src/modules}/clarity-config/index.ts +168 -162
  92. package/src/modules/clarity-source-layout/index.ts +78 -0
  93. package/{app → src}/pages/[...slug].vue +59 -59
  94. package/{app → src}/pages/archive.vue +129 -129
  95. package/{app → src}/pages/index.vue +76 -76
  96. package/{app → src}/pages/link.vue +62 -62
  97. package/{app → src}/pages/preview.vue +46 -46
  98. package/{app → src}/plugins/easter-egg.ts +23 -23
  99. package/{app → src}/plugins/init.ts +6 -6
  100. package/{app → src}/plugins/tippy.ts +22 -22
  101. package/{public → src/public}/assets/atom.css +274 -274
  102. package/{public → src/public}/assets/atom.xsl +109 -109
  103. package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.ts +58 -58
  104. package/{remark-plugins → src/remark-plugins}/remark-code-component.ts +41 -41
  105. package/{server → src/server}/api/stats.get.ts +84 -83
  106. package/{server → src/server}/routes/atom.xml.get.ts +85 -85
  107. package/{server → src/server}/routes/subscriptions.opml.get.ts +64 -64
  108. package/{shared → src/shared}/utils/clarity.ts +24 -24
  109. package/{shared → src/shared}/utils/icon.ts +218 -218
  110. package/{shared → src/shared}/utils/link.ts +52 -52
  111. package/{shared → src/shared}/utils/str.ts +88 -88
  112. package/{shared → src/shared}/utils/time.ts +115 -115
  113. package/{app → src}/shiki.config.ts +8 -8
  114. package/{app → src}/stores/layout.ts +34 -34
  115. package/{app → src}/stores/search.ts +37 -37
  116. package/{app → src}/types/article.ts +16 -16
  117. package/src/types/feed.ts +1 -0
  118. package/{app → src}/types/index.ts +14 -14
  119. package/{app → src}/types/nav.ts +11 -11
  120. package/{app → src}/utils/anim.ts +37 -37
  121. package/{app → src}/utils/img.ts +83 -83
  122. package/app/types/feed.ts +0 -1
  123. package/config/feeds.empty.ts +0 -4
  124. package/{app → src}/assets/css/_variable.scss +0 -0
  125. package/{app → src}/assets/css/animation.scss +0 -0
  126. package/{app → src}/assets/css/article.scss +0 -0
  127. package/{app → src}/assets/css/color.scss +0 -0
  128. package/{app → src}/assets/css/font.scss +0 -0
  129. package/{app → src}/assets/css/main.scss +0 -0
  130. package/{app → src}/assets/css/reusable.scss +0 -0
  131. package/{app → src}/assets/icons/solar-rewind-back-bold-duotone.svg +0 -0
  132. package/{app → src}/assets/icons/solar-rewind-forward-bold-duotone.svg +0 -0
  133. package/{app → src}/components/content/Alert.vue +0 -0
  134. package/{config → src/config}/content.d.mts +0 -0
  135. package/{config → src/config}/content.mjs +0 -0
  136. package/{config → src/config}/define.mjs +0 -0
  137. package/{config → src/config}/feed.ts +0 -0
  138. package/{config → src/config}/index.d.mts +0 -0
  139. package/{config → src/config}/index.mjs +0 -0
  140. package/{config → src/config}/public.ts +1 -1
  141. package/{config → src/config}/schema.d.mts +0 -0
  142. package/{config → src/config}/schema.mjs +0 -0
  143. package/{config → src/config}/server.ts +1 -1
  144. /package/{img → src/img}/index.d.mts +0 -0
  145. /package/{img → src/img}/index.mjs +0 -0
  146. /package/{modules → src/modules}/clarity-config/anti-mirror-client.ts +0 -0
  147. /package/{public → src/public}/fonts/AlimamaFangYuanTi.woff2 +0 -0
  148. /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.d.mts +0 -0
  149. /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.mjs +0 -0
  150. /package/{remark-plugins → src/remark-plugins}/remark-code-component.d.mts +0 -0
  151. /package/{remark-plugins → src/remark-plugins}/remark-code-component.mjs +0 -0
  152. /package/{server → src/server}/utils/clarity.ts +0 -0
  153. /package/{shared → src/shared}/types/content.ts +0 -0
  154. /package/{app → src}/types/modal.ts +0 -0
@@ -1,63 +1,67 @@
1
1
  import type { Nuxt } from '@nuxt/schema'
2
2
  import type { ClarityConfig } from '../../config/schema'
3
+ import type { ClarityServerConfig } from '../../config/server'
3
4
  import { existsSync } from 'node:fs'
4
- import { dirname, relative, resolve } from 'node:path'
5
- import { fileURLToPath } from 'node:url'
5
+ import { dirname, relative, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
6
7
  import { createJiti } from 'jiti'
7
8
  import {
8
9
  addImportsDir,
9
10
  addTypeTemplate,
10
11
  defineNuxtModule,
11
- updateAppConfig,
12
- useLogger,
13
- } from 'nuxt/kit'
14
- import nuxtPkg from 'nuxt/package.json'
15
- import { minify } from 'oxc-minify'
16
- import vuePkg from 'vue/package.json'
12
+ updateAppConfig,
13
+ useLogger,
14
+ } from 'nuxt/kit'
15
+ import nuxtPkg from 'nuxt/package.json'
16
+ import { minify } from 'oxc-minify'
17
+ import vuePkg from 'vue/package.json'
17
18
  import { toPublicClarityConfig } from '../../config/public'
18
- import { toServerClarityConfig } from '../../config/server'
19
19
  import { clarityConfigSchema } from '../../config/schema'
20
+ import { toServerClarityConfig } from '../../config/server'
20
21
  import handleMirror from './anti-mirror-client'
21
-
22
- const moduleDir = dirname(fileURLToPath(import.meta.url))
23
- const themeDir = resolve(moduleDir, '../..')
24
-
22
+
23
+ const moduleDir = dirname(fileURLToPath(import.meta.url))
24
+ /** Theme 运行时源码根(src/):模块位于 src/modules/clarity-config */
25
+ const themeSrcDir = resolve(moduleDir, '../..')
26
+ /** Theme npm 包根(nuxt.config.ts / package.json 所在层) */
27
+ const themePkgDir = resolve(moduleDir, '../../..')
28
+
25
29
  /** app/app.config.ts 的 clarity 键只允许 UI 覆盖(站点级字段属于 clarity.config.ts) */
26
30
  const uiConfigKeys = new Set(['component', 'footer', 'header', 'link', 'nav', 'pagination', 'themes'])
27
-
28
- export interface ModuleOptions {
29
- /** 消费项目中 clarity 配置文件路径(相对 rootDir) */
30
- configFile?: string
31
- }
32
-
33
- export default defineNuxtModule<ModuleOptions>({
34
- meta: {
35
- name: 'clarity-config',
36
- configKey: 'clarityConfig',
37
- },
38
-
39
- async setup(options, nuxt) {
40
- const logger = useLogger('clarity-config')
41
- const rootDir = nuxt.options.rootDir
42
-
43
- const configPath = resolve(rootDir, options.configFile ?? findConfigFile(rootDir))
44
- const feedsPath = findFeedsFile(rootDir, themeDir)
45
-
46
- // ---- 依赖注入层:Theme 内部不感知消费项目的文件布局 ----
47
- Object.assign(nuxt.options.alias, {
48
- '#clarity/config': configPath,
49
- '#clarity/feeds': feedsPath.resolved,
50
- })
51
-
31
+
32
+ export interface ModuleOptions {
33
+ /** 消费项目中 clarity 配置文件路径(相对 rootDir) */
34
+ configFile?: string
35
+ }
36
+
37
+ export default defineNuxtModule<ModuleOptions>({
38
+ meta: {
39
+ name: 'clarity-config',
40
+ configKey: 'clarityConfig',
41
+ },
42
+
43
+ async setup(options, nuxt) {
44
+ const logger = useLogger('clarity-config')
45
+ const rootDir = nuxt.options.rootDir
46
+
47
+ const configPath = resolve(rootDir, options.configFile ?? findConfigFile(rootDir))
48
+ const feedsPath = findFeedsFile(rootDir, themeSrcDir)
49
+
50
+ // ---- 依赖注入层:Theme 内部不感知消费项目的文件布局 ----
51
+ Object.assign(nuxt.options.alias, {
52
+ '#clarity/config': configPath,
53
+ '#clarity/feeds': feedsPath.resolved,
54
+ })
55
+
52
56
  // @pinia/nuxt 不会自动扫描 Layer 的 stores 目录,需显式注册
53
- addImportsDir(resolve(themeDir, 'app/stores'))
57
+ addImportsDir(resolve(themeSrcDir, 'stores'))
54
58
 
55
59
  // ---- AppConfig 类型化:消费者 defineAppConfig({ clarity: ... }) 获得完整类型提示 ----
56
60
  // 注意:类型文件生成于 buildDir/types/ 下,相对路径必须以其为基准计算,
57
61
  // 否则导入解析失败会因 skipLibCheck 静默退化为 any(审计发现 #9)。
58
62
  const appConfigTypeDir = resolve(nuxt.options.buildDir, 'types')
59
- const appConfigTypePath = relative(appConfigTypeDir, resolve(themeDir, 'config/app.ts')).replaceAll('\\', '/')
60
- const globalsTypePath = relative(appConfigTypeDir, resolve(themeDir, 'app/types/index.ts')).replaceAll('\\', '/')
63
+ const appConfigTypePath = relative(appConfigTypeDir, resolve(themeSrcDir, 'config/app.ts')).replaceAll('\\', '/')
64
+ const globalsTypePath = relative(appConfigTypeDir, resolve(themeSrcDir, 'types/index.ts')).replaceAll('\\', '/')
61
65
  addTypeTemplate({
62
66
  filename: 'types/clarity-app-config.d.ts',
63
67
  getContents: () => [
@@ -79,27 +83,27 @@ export default defineNuxtModule<ModuleOptions>({
79
83
  '',
80
84
  ].join('\n'),
81
85
  })
82
-
83
- // @bikariya/shiki 运行时固定导入 ~/shiki.config,
84
- // 消费项目未提供时回退到 Theme 内置配置;
85
- // 需置于 '~' 之前以保证 Vite 别名前缀优先匹配
86
- if (!existsSync(resolve(nuxt.options.srcDir, 'shiki.config.ts'))) {
87
- nuxt.options.alias = {
88
- '~/shiki.config': resolve(themeDir, 'app/shiki.config.ts'),
89
- ...nuxt.options.alias,
90
- }
91
- }
92
-
93
- if (!existsSync(configPath)) {
94
- throw new Error(
95
- `[clarity-config] 未找到 ${configPath}。`
96
- + '请在消费项目根目录创建 clarity.config.ts(可参考 playground/clarity.config.ts)。',
97
- )
98
- }
99
- if (!feedsPath.found) {
100
- logger.warn('未找到 feeds.ts,友链页面与 OPML 订阅将输出空数据。')
101
- }
102
-
86
+
87
+ // @bikariya/shiki 运行时固定导入 ~/shiki.config,
88
+ // 消费项目未提供时回退到 Theme 内置配置;
89
+ // 需置于 '~' 之前以保证 Vite 别名前缀优先匹配
90
+ if (!existsSync(resolve(nuxt.options.srcDir, 'shiki.config.ts'))) {
91
+ nuxt.options.alias = {
92
+ '~/shiki.config': resolve(themeSrcDir, 'shiki.config.ts'),
93
+ ...nuxt.options.alias,
94
+ }
95
+ }
96
+
97
+ if (!existsSync(configPath)) {
98
+ throw new Error(
99
+ `[clarity-config] 未找到 ${configPath}。`
100
+ + '请在消费项目根目录创建 clarity.config.ts(可参考 playground/clarity.config.ts)。',
101
+ )
102
+ }
103
+ if (!feedsPath.found) {
104
+ logger.warn('未找到 feeds.ts,友链页面与 OPML 订阅将输出空数据。')
105
+ }
106
+
103
107
  // ---- 读取并校验站点配置(错误在构建开始前暴露) ----
104
108
  const jiti = createJiti(import.meta.url, { moduleCache: false, interopDefault: true })
105
109
  const configModule = await importConfigFile(jiti, configPath)
@@ -108,31 +112,33 @@ export default defineNuxtModule<ModuleOptions>({
108
112
 
109
113
  // ---- 边界审计:app/app.config.ts 中不应出现站点级字段(其优先级更高,会覆盖站点配置注入) ----
110
114
  await warnNonUiAppConfigOverrides(nuxt, jiti, logger)
111
-
115
+
112
116
  // ---- Site Config → App Config 桥梁 ----
113
- // 注意 Nuxt appConfig 合并优先级:消费项目 app.config > Theme app.config > 模块注入,
114
- // 因此站点派生默认值(header.logo 等)不放入 Theme 的 app.config.ts,
115
- // 由这里注入后仍可被消费项目覆盖。
117
+ // 注意 Nuxt appConfig 合并优先级:消费项目 app.config > Theme app.config > 模块注入,
118
+ // 因此站点派生默认值(header.logo 等)不放入 Theme 的 app.config.ts,
119
+ // 由这里注入后仍可被消费项目覆盖。
116
120
  updateAppConfig({
117
- clarity: {
118
- ...toPublicClarityConfig(config),
119
- header: {
120
- logo: site.author.avatar ?? '',
121
- subtitle: site.subtitle ?? '',
122
- },
123
- footer: {
124
- copyright: site.copyright?.name
125
- ? `© ${new Date().getFullYear()} ${site.author.name} · ${site.copyright.name}`
126
- : `© ${new Date().getFullYear()} ${site.author.name}`,
127
- },
128
- },
121
+ clarity: {
122
+ ...toPublicClarityConfig(config),
123
+ header: {
124
+ logo: site.author.avatar ?? '',
125
+ subtitle: site.subtitle ?? '',
126
+ },
127
+ footer: {
128
+ copyright: site.copyright?.name
129
+ ? `© ${new Date().getFullYear()} ${site.author.name} · ${site.copyright.name}`
130
+ : `© ${new Date().getFullYear()} ${site.author.name}`,
131
+ },
132
+ },
129
133
  })
130
134
 
131
135
  // ---- 服务端专用配置:注入 Nitro 私有 runtimeConfig(不进入客户端 bundle)----
132
136
  // Atom/OPML/stats 处理器与 feature 路由守卫从这里读取完整配置。
133
- nuxt.options.runtimeConfig.clarity = toServerClarityConfig(config)
134
-
135
- // ---- SEO / site / robots / llms ----
137
+ // 注意:Nuxt 会按消费者实际值推导 runtimeConfig 类型(可选字段可能被推断为必填),
138
+ // 此处以 Theme 声明的 ClarityServerConfig 为准,避免与消费者无关的推断形状冲突。
139
+ ;(nuxt.options.runtimeConfig as { clarity?: ClarityServerConfig }).clarity = toServerClarityConfig(config)
140
+
141
+ // ---- SEO / site / robots / llms ----
136
142
  nuxt.options.site = {
137
143
  ...nuxt.options.site,
138
144
  name: site.title,
@@ -153,61 +159,61 @@ export default defineNuxtModule<ModuleOptions>({
153
159
  title: site.title,
154
160
  description: site.description,
155
161
  }
156
-
157
- // ---- Head 元数据 ----
158
- const head = nuxt.options.app.head
159
- head.meta ??= []
160
- head.meta.push({ name: 'author', content: [site.author.name, site.author.email].filter(Boolean).join(', ') })
161
- head.link ??= []
162
- head.link.push({ rel: 'icon', href: site.favicon })
163
- if (features.atom) {
164
- head.link.push({ rel: 'alternate', type: 'application/atom+xml', href: '/atom.xml' })
165
- }
166
- const twikooPreload = integrations.twikoo?.preload ?? integrations.twikoo?.envId
167
- if (twikooPreload) {
168
- head.link.push({ rel: 'preconnect', href: twikooPreload })
169
- }
170
- head.script ??= []
171
- head.script.push(...integrations.scripts as any[])
172
- head.titleTemplate = `%s %separator ${site.title}`
173
-
174
- // ---- 构建信息(供 BlogTech 等 Widget 使用) ----
175
- const consumerPkg = await loadJson(resolve(rootDir, 'package.json'), jiti)
176
- const themePkg = await loadJson(resolve(themeDir, 'package.json'), jiti)
177
- nuxt.options.runtimeConfig.public.clarity = {
178
- theme: 'Clarity',
179
- themeVersion: String(themePkg?.version ?? ''),
180
- themeHomepage: String(themePkg?.homepage ?? ''),
181
- siteVersion: String(consumerPkg?.version ?? ''),
182
- sitePackageManager: String(consumerPkg?.packageManager ?? ''),
183
- nuxtVersion: nuxtPkg.version,
184
- vueVersion: vuePkg.version,
185
- }
186
-
187
- // ---- 路由规则 ----
162
+
163
+ // ---- Head 元数据 ----
164
+ const head = nuxt.options.app.head
165
+ head.meta ??= []
166
+ head.meta.push({ name: 'author', content: [site.author.name, site.author.email].filter(Boolean).join(', ') })
167
+ head.link ??= []
168
+ head.link.push({ rel: 'icon', href: site.favicon })
169
+ if (features.atom) {
170
+ head.link.push({ rel: 'alternate', type: 'application/atom+xml', href: '/atom.xml' })
171
+ }
172
+ const twikooPreload = integrations.twikoo?.preload ?? integrations.twikoo?.envId
173
+ if (twikooPreload) {
174
+ head.link.push({ rel: 'preconnect', href: twikooPreload })
175
+ }
176
+ head.script ??= []
177
+ head.script.push(...integrations.scripts as any[])
178
+ head.titleTemplate = `%s %separator ${site.title}`
179
+
180
+ // ---- 构建信息(供 BlogTech 等 Widget 使用) ----
181
+ const consumerPkg = await loadJson(resolve(rootDir, 'package.json'), jiti)
182
+ const themePkg = await loadJson(resolve(themePkgDir, 'package.json'), jiti)
183
+ nuxt.options.runtimeConfig.public.clarity = {
184
+ theme: 'Clarity',
185
+ themeVersion: String(themePkg?.version ?? ''),
186
+ themeHomepage: String(themePkg?.homepage ?? ''),
187
+ siteVersion: String(consumerPkg?.version ?? ''),
188
+ sitePackageManager: String(consumerPkg?.packageManager ?? ''),
189
+ nuxtVersion: nuxtPkg.version,
190
+ vueVersion: vuePkg.version,
191
+ }
192
+
193
+ // ---- 路由规则 ----
188
194
  const routeRules = nuxt.options.routeRules ??= {}
189
- if (features.stats) {
190
- routeRules['/api/stats'] = { prerender: true, headers: { 'Content-Type': 'application/json' } }
191
- }
192
- if (features.atom) {
193
- routeRules['/atom.xml'] = { prerender: true, headers: { 'Content-Type': 'application/xml' } }
194
- }
195
- if (features.opml) {
196
- routeRules['/subscriptions.opml'] = { prerender: true, headers: { 'Content-Type': 'application/xml' } }
197
- }
198
- routeRules['/favicon.ico'] = { redirect: { to: site.favicon } }
199
-
200
- // ---- 自定义链接与 /posts 前缀处理 ----
201
- nuxt.hook('content:file:afterParse', (ctx) => {
202
- const { permalink, path } = ctx.content as Record<string, string | undefined>
203
- if (permalink) {
204
- ctx.content.path = permalink
205
- }
206
- else if (article.hidePostPrefix && path?.startsWith('/posts/')) {
207
- ctx.content.path = path.slice('/posts'.length)
208
- }
209
- })
210
-
195
+ if (features.stats) {
196
+ routeRules['/api/stats'] = { prerender: true, headers: { 'Content-Type': 'application/json' } }
197
+ }
198
+ if (features.atom) {
199
+ routeRules['/atom.xml'] = { prerender: true, headers: { 'Content-Type': 'application/xml' } }
200
+ }
201
+ if (features.opml) {
202
+ routeRules['/subscriptions.opml'] = { prerender: true, headers: { 'Content-Type': 'application/xml' } }
203
+ }
204
+ routeRules['/favicon.ico'] = { redirect: { to: site.favicon } }
205
+
206
+ // ---- 自定义链接与 /posts 前缀处理 ----
207
+ nuxt.hook('content:file:afterParse', (ctx) => {
208
+ const { permalink, path } = ctx.content as Record<string, string | undefined>
209
+ if (permalink) {
210
+ ctx.content.path = permalink
211
+ }
212
+ else if (article.hidePostPrefix && path?.startsWith('/posts/')) {
213
+ ctx.content.path = path.slice('/posts'.length)
214
+ }
215
+ })
216
+
211
217
  // ---- 可选功能:anti-mirror ----
212
218
  // 黑名单完全由站点配置提供;Theme 不携带任何上游默认域名(审计发现 #5)。
213
219
  if (features.antiMirror !== false) {
@@ -286,7 +292,7 @@ async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof c
286
292
  }
287
293
  }
288
294
  }
289
-
295
+
290
296
  async function injectAntiMirror(nuxt: Nuxt, blacklist: string[], target: string) {
291
297
  const source = `(${handleMirror.toString()})(${JSON.stringify(blacklist.map(btoa))},${JSON.stringify(btoa(target))})`
292
298
  const { code, errors } = await minify('clarity-anti-mirror.iife.js', source)
@@ -295,30 +301,30 @@ async function injectAntiMirror(nuxt: Nuxt, blacklist: string[], target: string)
295
301
  }
296
302
  ;(nuxt.options.app.head.script ??= []).push({ innerHTML: code })
297
303
  }
298
-
299
- function findConfigFile(rootDir: string) {
300
- for (const name of ['clarity.config.ts', 'clarity.config.mjs', 'clarity.config.js']) {
301
- if (existsSync(resolve(rootDir, name))) {
302
- return name
303
- }
304
- }
305
- return 'clarity.config.ts'
306
- }
307
-
308
- function findFeedsFile(rootDir: string, themeDir: string) {
309
- for (const name of ['feeds.ts', 'feeds.mjs', 'feeds.js']) {
310
- if (existsSync(resolve(rootDir, name))) {
311
- return { found: true, resolved: resolve(rootDir, name) }
312
- }
313
- }
314
- return { found: false, resolved: resolve(themeDir, 'config/feeds.empty.ts') }
315
- }
316
-
317
- async function loadJson(path: string, jiti: ReturnType<typeof createJiti>) {
318
- try {
304
+
305
+ function findConfigFile(rootDir: string) {
306
+ for (const name of ['clarity.config.ts', 'clarity.config.mjs', 'clarity.config.js']) {
307
+ if (existsSync(resolve(rootDir, name))) {
308
+ return name
309
+ }
310
+ }
311
+ return 'clarity.config.ts'
312
+ }
313
+
314
+ function findFeedsFile(rootDir: string, themeSrcDir: string) {
315
+ for (const name of ['feeds.ts', 'feeds.mjs', 'feeds.js']) {
316
+ if (existsSync(resolve(rootDir, name))) {
317
+ return { found: true, resolved: resolve(rootDir, name) }
318
+ }
319
+ }
320
+ return { found: false, resolved: resolve(themeSrcDir, 'config/feeds.empty.ts') }
321
+ }
322
+
323
+ async function loadJson(path: string, jiti: ReturnType<typeof createJiti>) {
324
+ try {
319
325
  return ((await jiti.import(path)) as { default?: Record<string, unknown> } | undefined)?.default
320
- }
321
- catch {
322
- return undefined
323
- }
324
- }
326
+ }
327
+ catch {
328
+ return undefined
329
+ }
330
+ }
@@ -0,0 +1,78 @@
1
+ import type { Nuxt } from '@nuxt/schema'
2
+ import { dirname, relative, resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { defineNuxtModule, getLayerDirectories } from 'nuxt/kit'
5
+
6
+ const moduleDir = dirname(fileURLToPath(import.meta.url))
7
+ const themeSrcDir = resolve(moduleDir, '../..')
8
+ const themePkgDir = resolve(moduleDir, '../../..')
9
+
10
+ /**
11
+ * Clarity keeps all runtime source under `src/`. These values must only apply
12
+ * to the theme layer; declaring them statically in `nuxt.config.ts` would let
13
+ * c12 merge them into consumers which do not explicitly override them.
14
+ */
15
+ export default defineNuxtModule({
16
+ meta: {
17
+ name: 'clarity-source-layout',
18
+ },
19
+
20
+ setup(_options, nuxt) {
21
+ const layer = findThemeLayer(nuxt)
22
+ if (!layer) {
23
+ return
24
+ }
25
+
26
+ const sourceDir = withTrailingSlash(themeSrcDir)
27
+ const directories = {
28
+ layouts: withTrailingSlash(resolve(themeSrcDir, 'layouts')),
29
+ middleware: withTrailingSlash(resolve(themeSrcDir, 'middleware')),
30
+ modules: withTrailingSlash(resolve(themeSrcDir, 'modules')),
31
+ pages: withTrailingSlash(resolve(themeSrcDir, 'pages')),
32
+ plugins: withTrailingSlash(resolve(themeSrcDir, 'plugins')),
33
+ public: withTrailingSlash(resolve(themeSrcDir, 'public')),
34
+ server: withTrailingSlash(resolve(themeSrcDir, 'server')),
35
+ shared: withTrailingSlash(resolve(themeSrcDir, 'shared')),
36
+ }
37
+
38
+ layer.config.srcDir = sourceDir
39
+ layer.config.serverDir = directories.server
40
+ layer.config.dir = {
41
+ ...layer.config.dir,
42
+ app: sourceDir,
43
+ modules: directories.modules,
44
+ public: directories.public,
45
+ shared: directories.shared,
46
+ }
47
+
48
+ // `getLayerDirectories()` caches a plain metadata object per layer before
49
+ // modules are installed. Nuxt exposes no invalidation API, so update the
50
+ // cached paths in place as well as `layer.config`.
51
+ const cached = getLayerDirectories(nuxt).find(directory => isSamePath(directory.root, themePkgDir))
52
+ if (cached) {
53
+ Object.assign(cached, {
54
+ app: sourceDir,
55
+ appLayouts: directories.layouts,
56
+ appMiddleware: directories.middleware,
57
+ appPages: directories.pages,
58
+ appPlugins: directories.plugins,
59
+ modules: directories.modules,
60
+ public: directories.public,
61
+ server: directories.server,
62
+ shared: directories.shared,
63
+ })
64
+ }
65
+ },
66
+ })
67
+
68
+ function findThemeLayer(nuxt: Nuxt) {
69
+ return nuxt.options._layers.find(layer => isSamePath(layer.config.rootDir ?? layer.cwd, themePkgDir))
70
+ }
71
+
72
+ function isSamePath(left: string, right: string) {
73
+ return left === right || relative(resolve(left), resolve(right)) === ''
74
+ }
75
+
76
+ function withTrailingSlash(path: string) {
77
+ return `${path.replace(/[\\/]$/, '').replaceAll('\\', '/')}/`
78
+ }
@@ -1,59 +1,59 @@
1
- <script setup lang="ts">
2
- const route = useRoute()
3
-
4
- const { data: post } = await useAsyncData(
5
- `content:${route.path}`,
6
- () => queryCollection('content').path(route.path).first(),
7
- )
8
-
9
- const excerpt = computed(() => post.value?.description || '')
10
- const asideWidgetNames = computed<WidgetName[]>(() => {
11
- if (!post.value)
12
- return ['blog-log']
13
- return (post.value.meta?.aside as WidgetName[] | undefined) ?? ['toc']
14
- })
15
- const { widgets } = useWidgets(asideWidgetNames)
16
-
17
- if (post.value) {
18
- useSeoMeta({
19
- title: post.value.title,
20
- ogType: 'article',
21
- ogImage: post.value.image,
22
- description: post.value.description,
23
- })
24
- }
25
- else {
26
- const event = useRequestEvent()
27
- event && setResponseStatus(event, 404)
28
- route.meta.title = '404'
29
- }
30
- </script>
31
-
32
- <template>
33
- <template #aside>
34
- <!-- 更换页面时相同 key 的组件不会更新 -->
35
- <component :is="widget.comp" v-for="widget in widgets" :key="widget.name" />
36
- </template>
37
-
38
- <template v-if="post">
39
- <PostHeader v-bind="post" />
40
- <PostExcerpt v-if="excerpt" :excerpt />
41
- <!-- 使用 float-in 动画会导致搜索跳转不准确 -->
42
- <ContentRenderer
43
- class="article"
44
- :class="getPostTypeClassName(post?.type, { prefix: 'md' })"
45
- :value="post"
46
- tag="article"
47
- />
48
-
49
- <PostFooter v-bind="post" />
50
- <PostSurround />
51
- <PostComment />
52
- </template>
53
-
54
- <ZError
55
- v-else
56
- icon="line-md:document-delete-twotone"
57
- title="内容为空或页面不存在"
58
- />
59
- </template>
1
+ <script setup lang="ts">
2
+ const route = useRoute()
3
+
4
+ const { data: post } = await useAsyncData(
5
+ `content:${route.path}`,
6
+ () => queryCollection('content').path(route.path).first(),
7
+ )
8
+
9
+ const excerpt = computed(() => post.value?.description || '')
10
+ const asideWidgetNames = computed<WidgetName[]>(() => {
11
+ if (!post.value)
12
+ return ['blog-log']
13
+ return (post.value.meta?.aside as WidgetName[] | undefined) ?? ['toc']
14
+ })
15
+ const { widgets } = useWidgets(asideWidgetNames)
16
+
17
+ if (post.value) {
18
+ useSeoMeta({
19
+ title: post.value.title,
20
+ ogType: 'article',
21
+ ogImage: post.value.image,
22
+ description: post.value.description,
23
+ })
24
+ }
25
+ else {
26
+ const event = useRequestEvent()
27
+ event && setResponseStatus(event, 404)
28
+ route.meta.title = '404'
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <template #aside>
34
+ <!-- 更换页面时相同 key 的组件不会更新 -->
35
+ <component :is="widget.comp" v-for="widget in widgets" :key="widget.name" />
36
+ </template>
37
+
38
+ <template v-if="post">
39
+ <PostHeader v-bind="post" />
40
+ <PostExcerpt v-if="excerpt" :excerpt />
41
+ <!-- 使用 float-in 动画会导致搜索跳转不准确 -->
42
+ <ContentRenderer
43
+ class="article"
44
+ :class="getPostTypeClassName(post?.type, { prefix: 'md' })"
45
+ :value="post"
46
+ tag="article"
47
+ />
48
+
49
+ <PostFooter v-bind="post" />
50
+ <PostSurround />
51
+ <PostComment />
52
+ </template>
53
+
54
+ <ZError
55
+ v-else
56
+ icon="line-md:document-delete-twotone"
57
+ title="内容为空或页面不存在"
58
+ />
59
+ </template>