clarity-theme 0.1.3 → 0.1.4
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/README.md +12 -2
- package/README.zh-CN.md +12 -2
- package/nuxt.config.ts +17 -1
- package/package.json +6 -2
- package/src/app.config.ts +8 -104
- package/src/assets/icons/zhilu.svg +8 -0
- package/src/blog.config.ts +71 -0
- package/src/components/blog/BlogFooter.vue +3 -3
- package/src/components/blog/BlogHeader.global.vue +9 -9
- package/src/components/blog/BlogSidebar.vue +3 -3
- package/src/components/blog/ThemeToggle.vue +2 -2
- package/src/components/content/Alert.vue +2 -2
- package/src/components/content/FeedCard.vue +2 -2
- package/src/components/content/Mermaid.vue +1 -1
- package/src/components/content/ProseCode.vue +4 -32
- package/src/components/content/ProsePre.vue +2 -2
- package/src/components/content/ProseTable.vue +1 -1
- package/src/components/popover/Search.vue +3 -3
- package/src/components/post/Comment.vue +3 -15
- package/src/components/post/Excerpt.vue +3 -3
- package/src/components/post/OrderToggle.vue +3 -3
- package/src/components/post/PostFooter.vue +3 -4
- package/src/components/post/PostHeader.vue +4 -4
- package/src/components/post/PostSurround.vue +2 -2
- package/src/components/post/Slide.vue +2 -2
- package/src/components/util/Img.vue +1 -1
- package/src/components/widget/BlogLog.vue +9 -19
- package/src/components/widget/BlogStats.vue +4 -4
- package/src/components/widget/BlogTech.vue +16 -17
- package/src/components/widget/CommGroup.vue +43 -0
- package/src/composables/useArticle.ts +9 -9
- package/src/composables/usePagination.ts +2 -2
- package/src/composables/useWidgets.ts +2 -0
- package/src/config/app.ts +50 -4
- package/src/config/content.ts +6 -0
- package/src/config/feed.ts +2 -42
- package/src/config/pnpm-workspace.ts +19 -0
- package/src/config/ui.ts +111 -0
- package/src/generated/blog.config.mjs +3 -0
- package/src/generated/package-json.mjs +4 -0
- package/src/generated/pnpm-workspace.mjs +1 -0
- package/src/modules/anti-mirror/index.ts +33 -0
- package/src/modules/{clarity-config/anti-mirror-client.ts → anti-mirror/runtime/client.ts} +3 -3
- package/src/modules/clarity-config/index.ts +335 -77
- package/src/modules/clarity-source-layout/index.ts +13 -0
- package/src/pages/archive.vue +4 -4
- package/src/pages/index.vue +4 -3
- package/src/pages/link.vue +5 -5
- package/src/pages/preview.vue +2 -2
- package/src/server/api/stats.get.ts +9 -19
- package/src/server/routes/atom.xml.get.ts +48 -50
- package/src/server/routes/subscriptions.opml.get.ts +14 -31
- package/src/shared/utils/time.ts +13 -18
- package/src/types/article.ts +2 -2
- package/src/types/feed.ts +35 -1
- package/src/types/upstream-compat.d.ts +17 -0
- package/src/types/yaml-modules.d.ts +26 -0
- package/src/shared/types/content.ts +0 -30
- package/src/types/modal.ts +0 -16
- /package/src/assets/icons/{solar-rewind-back-bold-duotone.svg → SolarRewindBackBoldDuotone.svg} +0 -0
- /package/src/assets/icons/{solar-rewind-forward-bold-duotone.svg → SolarRewindForwardBoldDuotone.svg} +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineNuxtModule } from 'nuxt/kit'
|
|
2
|
+
import { minifySync } from 'oxc-minify'
|
|
3
|
+
import handleMirror from './runtime/client'
|
|
4
|
+
|
|
5
|
+
const blacklist = [
|
|
6
|
+
'dgjlx.com', // blog.revincx.icu
|
|
7
|
+
'dgvhqt.com', // blog.zhilu.cyou
|
|
8
|
+
'hcmsla.com', // thyuu.com
|
|
9
|
+
'wmlop.com', // xaoxuu.com
|
|
10
|
+
'yswjxs.com', // blog.zhilu.cyou
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export default defineNuxtModule({
|
|
14
|
+
meta: {
|
|
15
|
+
name: 'anti-mirror',
|
|
16
|
+
},
|
|
17
|
+
async setup(options, nuxt) {
|
|
18
|
+
// 生成的数据模块由 clarity-config 在 setup 阶段写入,静态 import 会在
|
|
19
|
+
// 模块加载阶段读到上一次构建的旧值,因此改为 setup 内动态 import。
|
|
20
|
+
const { default: blogConfig } = await import('../../generated/blog.config.mjs')
|
|
21
|
+
nuxt.options.app.head.script ??= []
|
|
22
|
+
nuxt.options.app.head.script.push({
|
|
23
|
+
innerHTML: toIifeString(handleMirror, blacklist.map(btoa), btoa(blogConfig.url)),
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function toIifeString<T extends unknown[]>(fn: (...args: T) => void, ...args: T) {
|
|
29
|
+
const fnString = fn.toString()
|
|
30
|
+
const argsString = JSON.stringify(args).slice(1, -1)
|
|
31
|
+
const minified = minifySync('', `(${fnString})(${argsString})`)
|
|
32
|
+
return minified.code
|
|
33
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export default (sourcesEncoded: string[], targetEncoded: string) => {
|
|
2
2
|
const sources = sourcesEncoded.map(atob)
|
|
3
|
-
const
|
|
3
|
+
const target = atob(targetEncoded)
|
|
4
4
|
const canonical = document.querySelector<HTMLLinkElement>('link[rel="canonical"]')
|
|
5
5
|
const isBadMirror = sources.some(domain => location.hostname.endsWith(domain))
|
|
6
6
|
if (isBadMirror) {
|
|
7
7
|
if (canonical)
|
|
8
|
-
canonical.href = canonical.href.replace(location.host,
|
|
9
|
-
location.host =
|
|
8
|
+
canonical.href = canonical.href.replace(location.host, target)
|
|
9
|
+
location.host = target
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Nuxt } from '@nuxt/schema'
|
|
2
2
|
import type { ClarityConfig } from '../../config/schema'
|
|
3
3
|
import type { ClarityServerConfig } from '../../config/server'
|
|
4
|
-
import { existsSync } from 'node:fs'
|
|
5
|
-
import { dirname, relative, resolve } from 'node:path'
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import { createJiti } from 'jiti'
|
|
8
8
|
import {
|
|
@@ -12,21 +12,18 @@ import {
|
|
|
12
12
|
updateAppConfig,
|
|
13
13
|
useLogger,
|
|
14
14
|
} from 'nuxt/kit'
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import vuePkg from 'vue/package.json'
|
|
15
|
+
import { parse as parseYaml } from 'yaml'
|
|
16
|
+
import { fallbackPnpmWorkspace } from '../../config/pnpm-workspace'
|
|
18
17
|
import { toPublicClarityConfig } from '../../config/public'
|
|
19
18
|
import { clarityConfigSchema } from '../../config/schema'
|
|
20
19
|
import { toServerClarityConfig } from '../../config/server'
|
|
21
|
-
import
|
|
20
|
+
import uiDefaults from '../../config/ui'
|
|
22
21
|
|
|
23
22
|
const moduleDir = dirname(fileURLToPath(import.meta.url))
|
|
24
23
|
/** Theme 运行时源码根(src/):模块位于 src/modules/clarity-config */
|
|
25
24
|
const themeSrcDir = resolve(moduleDir, '../..')
|
|
26
|
-
/** Theme npm 包根(nuxt.config.ts / package.json 所在层) */
|
|
27
|
-
const themePkgDir = resolve(moduleDir, '../../..')
|
|
28
25
|
|
|
29
|
-
/** app/app.config.ts
|
|
26
|
+
/** app/app.config.ts 中允许作为 UI 覆盖的顶层键(站点级字段属于 clarity.config.ts) */
|
|
30
27
|
const uiConfigKeys = new Set(['component', 'footer', 'header', 'link', 'nav', 'pagination', 'themes'])
|
|
31
28
|
|
|
32
29
|
export interface ModuleOptions {
|
|
@@ -46,17 +43,29 @@ export default defineNuxtModule<ModuleOptions>({
|
|
|
46
43
|
|
|
47
44
|
const configPath = resolve(rootDir, options.configFile ?? findConfigFile(rootDir))
|
|
48
45
|
const feedsPath = findFeedsFile(rootDir, themeSrcDir)
|
|
46
|
+
const consumerBlogConfigPath = findExisting(rootDir, ['blog.config.ts', 'blog.config.mjs', 'blog.config.js'])
|
|
47
|
+
const consumerSrcFeedsPath = findExisting(nuxt.options.srcDir, ['feeds.ts', 'feeds.mjs', 'feeds.js'])
|
|
49
48
|
|
|
50
49
|
// ---- 依赖注入层:Theme 内部不感知消费项目的文件布局 ----
|
|
51
|
-
|
|
50
|
+
// 上游文件通过 `~~/blog.config` / `~/feeds` 读取站点数据;消费项目使用
|
|
51
|
+
// clarity.config.ts(或根目录 feeds.ts),这里以前缀更长的别名承接,
|
|
52
|
+
// 使上游同步文件无需改写即可在 Layer 中解析。
|
|
53
|
+
// 新键必须置于 '~' / '~~' 之前,保证 Vite / Nitro 前缀匹配优先命中。
|
|
54
|
+
nuxt.options.alias = {
|
|
55
|
+
...(consumerBlogConfigPath ? {} : { '~~/blog.config': resolve(themeSrcDir, 'blog.config.ts') }),
|
|
56
|
+
// 上游 server 路由通过 ~~/shared/utils/time 引用工具;消费项目没有
|
|
57
|
+
// shared 目录时指向 Theme 内置实现。
|
|
58
|
+
...(existsSync(resolve(rootDir, 'shared')) ? {} : { '~~/shared': resolve(themeSrcDir, 'shared') }),
|
|
59
|
+
...(consumerSrcFeedsPath ? {} : { '~/feeds': feedsPath.resolved }),
|
|
52
60
|
'#clarity/config': configPath,
|
|
53
61
|
'#clarity/feeds': feedsPath.resolved,
|
|
54
|
-
|
|
62
|
+
...nuxt.options.alias,
|
|
63
|
+
}
|
|
55
64
|
|
|
56
65
|
// @pinia/nuxt 不会自动扫描 Layer 的 stores 目录,需显式注册
|
|
57
66
|
addImportsDir(resolve(themeSrcDir, 'stores'))
|
|
58
67
|
|
|
59
|
-
// ---- AppConfig
|
|
68
|
+
// ---- AppConfig 类型化:上游扁平键 + 0.1.x 的 clarity 键都获得类型提示 ----
|
|
60
69
|
// 注意:类型文件生成于 buildDir/types/ 下,相对路径必须以其为基准计算,
|
|
61
70
|
// 否则导入解析失败会因 skipLibCheck 静默退化为 any(审计发现 #9)。
|
|
62
71
|
const appConfigTypeDir = resolve(nuxt.options.buildDir, 'types')
|
|
@@ -68,14 +77,23 @@ export default defineNuxtModule<ModuleOptions>({
|
|
|
68
77
|
// app/types/index.ts 的 Window.twikoo 等环境声明在 node_modules Layer 中
|
|
69
78
|
// 不会被 tsconfig include 命中(被 node_modules exclude 过滤),需显式导入
|
|
70
79
|
`import '${globalsTypePath}'`,
|
|
71
|
-
`import type { ClarityAppConfig, ClarityUiConfigInput } from '${appConfigTypePath}'`,
|
|
80
|
+
`import type { ClarityAppConfig, ClarityAppConfigArticle, ClarityUiConfigInput } from '${appConfigTypePath}'`,
|
|
72
81
|
'',
|
|
73
82
|
'declare module \'@nuxt/schema\' {',
|
|
74
|
-
' // 读取侧:
|
|
83
|
+
' // 读取侧:clarity 用 unknown 回落到注入值;UI 扁平键补充完整类型,',
|
|
84
|
+
' // 使上游组件在 JSON 字面量(空数组 / 宽化字符串)下仍获得正确类型。',
|
|
85
|
+
' // article 只声明 categories(Record 索引),order / types 保留 Resolved 字面量。',
|
|
75
86
|
' interface CustomAppConfig {',
|
|
87
|
+
' /** @deprecated 0.1.x 兼容键:UI 覆盖请直接使用顶层扁平键(上游形状) */',
|
|
76
88
|
' clarity?: unknown',
|
|
89
|
+
' article?: ClarityAppConfigArticle',
|
|
90
|
+
' component?: ClarityUiConfig[\'component\']',
|
|
91
|
+
' footer?: ClarityUiConfig[\'footer\']',
|
|
92
|
+
' header?: ClarityUiConfig[\'header\']',
|
|
93
|
+
' link?: ClarityUiConfig[\'link\']',
|
|
94
|
+
' nav?: ClarityUiConfig[\'nav\']',
|
|
95
|
+
' themes?: ClarityUiConfig[\'themes\']',
|
|
77
96
|
' }',
|
|
78
|
-
' // 输入侧:完整配置(Theme 内部)或 UI 部分覆盖(消费项目 app/app.config.ts)',
|
|
79
97
|
' interface AppConfigInput {',
|
|
80
98
|
' clarity?: ClarityAppConfig | ClarityUiConfigInput',
|
|
81
99
|
' }',
|
|
@@ -110,13 +128,163 @@ export default defineNuxtModule<ModuleOptions>({
|
|
|
110
128
|
const config = parseClarityConfig((configModule as { default?: unknown })?.default ?? configModule, configPath)
|
|
111
129
|
const { site, article, integrations, features } = config
|
|
112
130
|
|
|
113
|
-
// ----
|
|
114
|
-
|
|
131
|
+
// ---- 上游构建期数据模块:~~/package.json 与 ~~/pnpm-workspace.yaml ----
|
|
132
|
+
// 上游 BlogTech / atom.xml 通过这两个说明符读取消费项目数据;直接把别名
|
|
133
|
+
// 指到真实 JSON/YAML 文件时,Nitro prerenderer 会将其外部化(Node 无法
|
|
134
|
+
// 以 ESM 导入 JSON/YAML),导致含 BlogTech 的页面(首页)在预渲染时崩溃。
|
|
135
|
+
// 因此在 buildDir 生成等价 ES 模块,运行时别名与 TS 声明都指向生成物。
|
|
136
|
+
const consumerPkg = (await loadJson(resolve(rootDir, 'package.json'), jiti)) ?? {}
|
|
137
|
+
const pnpmWorkspace = loadPnpmWorkspace(rootDir)
|
|
138
|
+
// 写入 Theme 包内的固定路径:prepare / build / prerenderer 等多个
|
|
139
|
+
// Nuxt 实例可能使用不同 buildDir,包内路径在所有上下文中一致可用。
|
|
140
|
+
const buildInfoDir = resolve(themeSrcDir, 'generated')
|
|
141
|
+
mkdirSync(buildInfoDir, { recursive: true })
|
|
142
|
+
const packageJsonModulePath = join(buildInfoDir, 'package-json.mjs').replaceAll('\\', '/')
|
|
143
|
+
writeFileSync(packageJsonModulePath, [
|
|
144
|
+
`const packageJson = ${JSON.stringify({
|
|
145
|
+
name: String(consumerPkg.name ?? ''),
|
|
146
|
+
version: String(consumerPkg.version ?? ''),
|
|
147
|
+
packageManager: String(consumerPkg.packageManager ?? ''),
|
|
148
|
+
})}`,
|
|
149
|
+
'export default packageJson',
|
|
150
|
+
`export const packageManager = ${JSON.stringify(String(consumerPkg.packageManager ?? ''))}`,
|
|
151
|
+
`export const version = ${JSON.stringify(String(consumerPkg.version ?? ''))}`,
|
|
152
|
+
'',
|
|
153
|
+
].join('\n'))
|
|
154
|
+
const pnpmWorkspaceModulePath = join(buildInfoDir, 'pnpm-workspace.mjs').replaceAll('\\', '/')
|
|
155
|
+
writeFileSync(pnpmWorkspaceModulePath, `export default ${JSON.stringify(pnpmWorkspace)}\n`)
|
|
156
|
+
nuxt.options.alias = {
|
|
157
|
+
'~~/package.json': packageJsonModulePath,
|
|
158
|
+
'~~/pnpm-workspace.yaml': pnpmWorkspaceModulePath,
|
|
159
|
+
...nuxt.options.alias,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 上游形状的 blog.config 数据模块:anti-mirror 等 Layer 模块由 Nuxt 以
|
|
163
|
+
// jiti 加载,无法解析 #clarity/* 别名(src/blog.config.ts 运行时适配层
|
|
164
|
+
// 仅适用于 App / Nitro 构建上下文),因此这里把映射结果内联成真实模块。
|
|
165
|
+
const blogConfigData = toUpstreamBlogConfig(config)
|
|
166
|
+
const blogConfigModulePath = join(buildInfoDir, 'blog.config.mjs').replaceAll('\\', '/')
|
|
167
|
+
writeFileSync(blogConfigModulePath, [
|
|
168
|
+
`const blogConfig = ${JSON.stringify(blogConfigData)}`,
|
|
169
|
+
'export default blogConfig',
|
|
170
|
+
`export const myFeed = ${JSON.stringify(toMyFeedEntry(blogConfigData))}`,
|
|
171
|
+
'',
|
|
172
|
+
].join('\n'))
|
|
173
|
+
|
|
174
|
+
// 两个说明符的 TS 侧声明(app / server 类型工程的路径重定向目标)
|
|
175
|
+
const yamlDecl = addTypeTemplate({
|
|
176
|
+
filename: 'types/clarity-yaml-workspace.d.ts',
|
|
177
|
+
getContents: () => [
|
|
178
|
+
'declare const value: Record<string, unknown>',
|
|
179
|
+
'export default value',
|
|
180
|
+
'',
|
|
181
|
+
].join('\n'),
|
|
182
|
+
})
|
|
183
|
+
const packageJsonDecl = addTypeTemplate({
|
|
184
|
+
filename: 'types/clarity-package-json.d.ts',
|
|
185
|
+
getContents: () => [
|
|
186
|
+
'declare const value: { name: string, version: string, packageManager: string }',
|
|
187
|
+
'export default value',
|
|
188
|
+
'export const packageManager: string',
|
|
189
|
+
'export const version: string',
|
|
190
|
+
'',
|
|
191
|
+
].join('\n'),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// 上游类型兼容垫片(post.author 可选字段、*.yaml 导入声明)
|
|
195
|
+
// 需要在 app / shared / server 三个类型工程中同样生效。
|
|
196
|
+
const compatTypesPath = resolve(themeSrcDir, 'types/upstream-compat.d.ts')
|
|
197
|
+
const withLeadingDot = (path: string) => path.startsWith('.') ? path : `./${path}`
|
|
198
|
+
nuxt.hook('prepare:types', ({ references, sharedReferences, nodeReferences, tsConfig }) => {
|
|
199
|
+
references.push({ path: compatTypesPath })
|
|
200
|
+
sharedReferences.push({ path: compatTypesPath })
|
|
201
|
+
nodeReferences.push({ path: compatTypesPath })
|
|
202
|
+
// TS 的别名路径替换会去掉 .json/.yaml 扩展名导致解析失败,
|
|
203
|
+
// 重定向到上方声明模块。
|
|
204
|
+
if (tsConfig.compilerOptions?.paths) {
|
|
205
|
+
const packageJsonDeclPath = relative(nuxt.options.buildDir, resolve(nuxt.options.buildDir, packageJsonDecl.filename)).replaceAll('\\', '/')
|
|
206
|
+
tsConfig.compilerOptions.paths['~~/package.json'] = [withLeadingDot(packageJsonDeclPath)]
|
|
207
|
+
const yamlDeclPath = relative(nuxt.options.buildDir, resolve(nuxt.options.buildDir, yamlDecl.filename)).replaceAll('\\', '/')
|
|
208
|
+
tsConfig.compilerOptions.paths['~~/pnpm-workspace.yaml'] = [withLeadingDot(yamlDeclPath)]
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
nuxt.hook('nitro:prepare:types', ({ declarations }) => {
|
|
212
|
+
declarations.push(`/// <reference types="${compatTypesPath.replaceAll('\\', '/')}" />`)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// prerenderer 是独立的 Nitro 子构建(nitro:config 不会对其生效),
|
|
216
|
+
// 生成模块在其中必须内联:外部化为绝对 Windows 路径后 Node 无法
|
|
217
|
+
// 以 ESM 导入,含 BlogTech 的首页会在预渲染时挂起。
|
|
218
|
+
;(nuxt.hook as unknown as (event: string, cb: (config: Record<string, any>) => void) => void)('prerender:config', (prerendererConfig) => {
|
|
219
|
+
prerendererConfig.externals ??= {}
|
|
220
|
+
prerendererConfig.externals.inline ??= []
|
|
221
|
+
prerendererConfig.externals.inline.push(packageJsonModulePath, pnpmWorkspaceModulePath)
|
|
222
|
+
})
|
|
223
|
+
nuxt.hook('nitro:config', (nitroConfig) => {
|
|
224
|
+
nitroConfig.typescript ??= {}
|
|
225
|
+
nitroConfig.typescript.tsConfig ??= {}
|
|
226
|
+
nitroConfig.typescript.tsConfig.compilerOptions ??= {}
|
|
227
|
+
nitroConfig.typescript.tsConfig.compilerOptions.paths ??= {}
|
|
228
|
+
nitroConfig.typescript.tsConfig.compilerOptions.paths['~~/package.json'] = [withLeadingDot(packageJsonDecl.filename)]
|
|
229
|
+
nitroConfig.typescript.tsConfig.compilerOptions.paths['~~/pnpm-workspace.yaml'] = [withLeadingDot(yamlDecl.filename)]
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// ---- 消费项目 app/app.config.ts:读取 0.1.x `clarity` 键的 UI 覆盖 ----
|
|
233
|
+
const consumerAppConfig = await loadConsumerAppConfig(nuxt, jiti)
|
|
234
|
+
const clarityUi = pickUiOverrides(consumerAppConfig?.clarity)
|
|
235
|
+
await warnSiteLevelAppConfigOverrides(consumerAppConfig, logger)
|
|
236
|
+
|
|
237
|
+
// ---- 上游形状的扁平 AppConfig 注入(upstream parity 的关键)----
|
|
238
|
+
// blog-v3 组件一律通过 useAppConfig() 读取扁平键(title / nav / component.*)。
|
|
239
|
+
// 这里将 clarity.config.ts + UI 默认值 + 0.1.x clarity 覆盖注入为同一形状;
|
|
240
|
+
// 消费项目仍可在自己的 app.config.ts 中按上游习惯直接覆盖任意扁平键。
|
|
241
|
+
updateAppConfig({
|
|
242
|
+
// 站点数据(上游来自 blog.config.ts 展开进 app.config.ts)
|
|
243
|
+
title: site.title,
|
|
244
|
+
subtitle: site.subtitle ?? '',
|
|
245
|
+
description: site.description,
|
|
246
|
+
author: {
|
|
247
|
+
name: site.author.name,
|
|
248
|
+
avatar: site.author.avatar ?? '',
|
|
249
|
+
email: site.author.email ?? '',
|
|
250
|
+
homepage: site.author.homepage ?? '/',
|
|
251
|
+
},
|
|
252
|
+
copyright: site.copyright ?? { abbr: '', name: '', url: '' },
|
|
253
|
+
favicon: site.favicon,
|
|
254
|
+
language: site.language,
|
|
255
|
+
timeEstablished: site.established ?? '',
|
|
256
|
+
timeZone: site.timezone,
|
|
257
|
+
url: site.url,
|
|
258
|
+
defaultCategory: article.defaultCategory,
|
|
259
|
+
article: {
|
|
260
|
+
categories: article.categories,
|
|
261
|
+
types: article.types,
|
|
262
|
+
order: article.order,
|
|
263
|
+
useRandomPremalat: false,
|
|
264
|
+
hidePostPrefix: article.hidePostPrefix,
|
|
265
|
+
robotsNotIndex: article.robotsNotIndex,
|
|
266
|
+
},
|
|
267
|
+
feed: config.feed,
|
|
268
|
+
scripts: integrations.scripts,
|
|
269
|
+
stats: config.stats,
|
|
270
|
+
twikoo: integrations.twikoo ?? { envId: '', preload: '' },
|
|
271
|
+
|
|
272
|
+
// UI 配置:站点派生值覆盖结构默认值,0.1.x clarity 覆盖优先级最高
|
|
273
|
+
...mergeUiConfig(clarityUi, {
|
|
274
|
+
...uiDefaults,
|
|
275
|
+
header: {
|
|
276
|
+
...uiDefaults.header,
|
|
277
|
+
logo: site.author.avatar ?? site.favicon,
|
|
278
|
+
subtitle: site.subtitle ?? site.description,
|
|
279
|
+
},
|
|
280
|
+
footer: {
|
|
281
|
+
...uiDefaults.footer,
|
|
282
|
+
copyright: `© ${new Date().getFullYear()} ${site.author.name}`,
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
})
|
|
115
286
|
|
|
116
|
-
// ----
|
|
117
|
-
// 注意 Nuxt appConfig 合并优先级:消费项目 app.config > Theme app.config > 模块注入,
|
|
118
|
-
// 因此站点派生默认值(header.logo 等)不放入 Theme 的 app.config.ts,
|
|
119
|
-
// 由这里注入后仍可被消费项目覆盖。
|
|
287
|
+
// ---- 0.1.x 兼容:继续注入完整的 clarity 键(useClarityConfig() 读取侧)----
|
|
120
288
|
updateAppConfig({
|
|
121
289
|
clarity: {
|
|
122
290
|
...toPublicClarityConfig(config),
|
|
@@ -133,9 +301,8 @@ export default defineNuxtModule<ModuleOptions>({
|
|
|
133
301
|
})
|
|
134
302
|
|
|
135
303
|
// ---- 服务端专用配置:注入 Nitro 私有 runtimeConfig(不进入客户端 bundle)----
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
// 此处以 Theme 声明的 ClarityServerConfig 为准,避免与消费者无关的推断形状冲突。
|
|
304
|
+
// 保留 0.1.x 的 useClarityServerConfig() 兼容;上游 server 路由已恢复为
|
|
305
|
+
// 直接读取 `~~/blog.config`,不再依赖该通道。
|
|
139
306
|
;(nuxt.options.runtimeConfig as { clarity?: ClarityServerConfig }).clarity = toServerClarityConfig(config)
|
|
140
307
|
|
|
141
308
|
// ---- SEO / site / robots / llms ----
|
|
@@ -177,19 +344,6 @@ export default defineNuxtModule<ModuleOptions>({
|
|
|
177
344
|
head.script.push(...integrations.scripts as any[])
|
|
178
345
|
head.titleTemplate = `%s %separator ${site.title}`
|
|
179
346
|
|
|
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
347
|
// ---- 路由规则 ----
|
|
194
348
|
const routeRules = nuxt.options.routeRules ??= {}
|
|
195
349
|
if (features.stats) {
|
|
@@ -214,16 +368,11 @@ export default defineNuxtModule<ModuleOptions>({
|
|
|
214
368
|
}
|
|
215
369
|
})
|
|
216
370
|
|
|
217
|
-
// ----
|
|
218
|
-
//
|
|
371
|
+
// ---- features.antiMirror 已废弃 ----
|
|
372
|
+
// 上游 modules/anti-mirror 由 Nuxt 自动加载(Layer 的 src/modules 同样生效),
|
|
373
|
+
// 黑名单与跳转目标与上游保持一致;配置项仅为 0.1.x 兼容保留。
|
|
219
374
|
if (features.antiMirror !== false) {
|
|
220
|
-
|
|
221
|
-
if (blacklist.length === 0) {
|
|
222
|
-
logger.warn('features.antiMirror 已启用,但未配置 blacklist,已跳过反镜像脚本注入。')
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
await injectAntiMirror(nuxt, blacklist, site.url)
|
|
226
|
-
}
|
|
375
|
+
logger.warn('features.antiMirror 已废弃:上游 anti-mirror 模块始终启用,黑名单与上游保持一致。')
|
|
227
376
|
}
|
|
228
377
|
},
|
|
229
378
|
})
|
|
@@ -251,8 +400,8 @@ function parseClarityConfig(raw: unknown, configPath: string): ClarityConfig {
|
|
|
251
400
|
return result.data
|
|
252
401
|
}
|
|
253
402
|
|
|
254
|
-
/**
|
|
255
|
-
async function
|
|
403
|
+
/** 从消费项目 app/app.config.ts 读取导出对象(失败时交给 Nuxt 自身报错) */
|
|
404
|
+
async function loadConsumerAppConfig(nuxt: Nuxt, jiti: ReturnType<typeof createJiti>) {
|
|
256
405
|
const candidates = [
|
|
257
406
|
resolve(nuxt.options.srcDir, 'app.config.ts'),
|
|
258
407
|
resolve(nuxt.options.rootDir, 'app.config.ts'),
|
|
@@ -267,20 +416,10 @@ async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof c
|
|
|
267
416
|
globalScope.defineAppConfig ??= (config: unknown) => config
|
|
268
417
|
try {
|
|
269
418
|
const mod = await jiti.import(appConfigPath) as unknown
|
|
270
|
-
|
|
271
|
-
if (!clarity?.clarity || typeof clarity.clarity !== 'object') {
|
|
272
|
-
continue
|
|
273
|
-
}
|
|
274
|
-
const offenders = Object.keys(clarity.clarity).filter(key => !uiConfigKeys.has(key))
|
|
275
|
-
if (offenders.length) {
|
|
276
|
-
logger.warn(
|
|
277
|
-
`app/app.config.ts 的 clarity 中出现站点级字段 [${offenders.join(', ')}],`
|
|
278
|
-
+ '它们会覆盖 clarity.config.ts 的注入结果;请将站点配置迁移到 clarity.config.ts。',
|
|
279
|
-
)
|
|
280
|
-
}
|
|
419
|
+
return ((mod as { default?: unknown })?.default ?? mod) as Record<string, unknown> | undefined
|
|
281
420
|
}
|
|
282
421
|
catch {
|
|
283
|
-
|
|
422
|
+
return undefined
|
|
284
423
|
}
|
|
285
424
|
finally {
|
|
286
425
|
if (previousDefine === undefined) {
|
|
@@ -291,15 +430,134 @@ async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof c
|
|
|
291
430
|
}
|
|
292
431
|
}
|
|
293
432
|
}
|
|
433
|
+
return undefined
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** 站点级字段出现在 app.config.ts 会压过 clarity.config.ts 注入,需要提示迁移 */
|
|
437
|
+
async function warnSiteLevelAppConfigOverrides(appConfig: Record<string, unknown> | undefined, logger: ReturnType<typeof useLogger>) {
|
|
438
|
+
if (!appConfig) {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
const offenders = Object.keys(appConfig).filter(key => !uiConfigKeys.has(key) && key !== 'clarity')
|
|
442
|
+
if (appConfig.clarity && typeof appConfig.clarity === 'object') {
|
|
443
|
+
const clarityOffenders = Object.keys(appConfig.clarity as Record<string, unknown>).filter(key => !uiConfigKeys.has(key))
|
|
444
|
+
if (clarityOffenders.length) {
|
|
445
|
+
logger.warn(
|
|
446
|
+
`app/app.config.ts 的 clarity 中出现站点级字段 [${clarityOffenders.join(', ')}],`
|
|
447
|
+
+ '请将站点配置迁移到 clarity.config.ts。',
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (offenders.length) {
|
|
452
|
+
logger.warn(
|
|
453
|
+
`app/app.config.ts 中出现站点级字段 [${offenders.join(', ')}],`
|
|
454
|
+
+ '它们会覆盖 clarity.config.ts 的注入结果;请将站点配置迁移到 clarity.config.ts。',
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** 深度合并 UI 覆盖:对象递归合并、数组整体替换(覆盖语义) */
|
|
460
|
+
function mergeUiConfig(overrides: Record<string, unknown> | undefined, base: Record<string, unknown>) {
|
|
461
|
+
if (!overrides) {
|
|
462
|
+
return base
|
|
463
|
+
}
|
|
464
|
+
const result: Record<string, unknown> = { ...base }
|
|
465
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
466
|
+
const baseValue = result[key]
|
|
467
|
+
result[key]
|
|
468
|
+
= value && baseValue && typeof value === 'object' && typeof baseValue === 'object'
|
|
469
|
+
&& !Array.isArray(value) && !Array.isArray(baseValue)
|
|
470
|
+
? mergeUiConfig(value as Record<string, unknown>, baseValue as Record<string, unknown>)
|
|
471
|
+
: value
|
|
472
|
+
}
|
|
473
|
+
return result
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function pickUiOverrides(clarity: unknown) {
|
|
477
|
+
if (!clarity || typeof clarity !== 'object') {
|
|
478
|
+
return undefined
|
|
479
|
+
}
|
|
480
|
+
const picked = Object.fromEntries(
|
|
481
|
+
Object.entries(clarity as Record<string, unknown>).filter(([key]) => uiConfigKeys.has(key)),
|
|
482
|
+
)
|
|
483
|
+
return Object.keys(picked).length ? picked : undefined
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** clarity.config → 上游 blog.config 扁平形状(与 src/blog.config.ts 适配层保持一致) */
|
|
487
|
+
function toUpstreamBlogConfig(config: ClarityConfig) {
|
|
488
|
+
const { site, article, feed, stats, integrations } = config
|
|
489
|
+
return {
|
|
490
|
+
title: site.title,
|
|
491
|
+
subtitle: site.subtitle ?? '',
|
|
492
|
+
description: site.description,
|
|
493
|
+
author: {
|
|
494
|
+
name: site.author.name,
|
|
495
|
+
avatar: site.author.avatar ?? '',
|
|
496
|
+
email: site.author.email ?? '',
|
|
497
|
+
homepage: site.author.homepage ?? '/',
|
|
498
|
+
},
|
|
499
|
+
copyright: site.copyright ?? { abbr: '', name: '', url: '' },
|
|
500
|
+
favicon: site.favicon,
|
|
501
|
+
language: site.language,
|
|
502
|
+
timeEstablished: site.established ?? '',
|
|
503
|
+
timeZone: site.timezone,
|
|
504
|
+
url: site.url,
|
|
505
|
+
defaultCategory: article.defaultCategory,
|
|
506
|
+
article: {
|
|
507
|
+
categories: article.categories,
|
|
508
|
+
types: article.types,
|
|
509
|
+
order: article.order,
|
|
510
|
+
useRandomPremalat: false,
|
|
511
|
+
hidePostPrefix: article.hidePostPrefix,
|
|
512
|
+
robotsNotIndex: article.robotsNotIndex,
|
|
513
|
+
},
|
|
514
|
+
feed,
|
|
515
|
+
scripts: integrations.scripts,
|
|
516
|
+
stats,
|
|
517
|
+
twikoo: integrations.twikoo ?? { envId: '', preload: '' },
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function toMyFeedEntry(blogConfig: ReturnType<typeof toUpstreamBlogConfig>) {
|
|
522
|
+
return {
|
|
523
|
+
author: blogConfig.author.name,
|
|
524
|
+
sitenick: blogConfig.title,
|
|
525
|
+
title: blogConfig.title,
|
|
526
|
+
desc: blogConfig.subtitle || blogConfig.description,
|
|
527
|
+
link: blogConfig.url,
|
|
528
|
+
feed: new URL('/atom.xml', blogConfig.url).toString(),
|
|
529
|
+
icon: blogConfig.favicon,
|
|
530
|
+
avatar: blogConfig.author.avatar,
|
|
531
|
+
archs: ['Nuxt'],
|
|
532
|
+
date: blogConfig.timeEstablished,
|
|
533
|
+
comment: '这是我自己',
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** 读取消费项目 pnpm-workspace.yaml 的 catalogs;缺失或无 catalogs 时回退到 Theme 内置版本数据 */
|
|
538
|
+
function loadPnpmWorkspace(rootDir: string) {
|
|
539
|
+
const yamlPath = resolve(rootDir, 'pnpm-workspace.yaml')
|
|
540
|
+
if (existsSync(yamlPath)) {
|
|
541
|
+
try {
|
|
542
|
+
const parsed = parseYaml(readFileSync(yamlPath, 'utf8')) as Record<string, unknown>
|
|
543
|
+
if (parsed?.catalogs && typeof parsed.catalogs === 'object') {
|
|
544
|
+
return { catalogs: parsed.catalogs as Record<string, Record<string, string>> }
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
// 解析失败时回退到 Theme 内置数据
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return { catalogs: fallbackPnpmWorkspace.catalogs as Record<string, Record<string, string>> }
|
|
294
552
|
}
|
|
295
553
|
|
|
296
|
-
async function
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
554
|
+
async function loadJson(path: string, jiti: ReturnType<typeof createJiti>) {
|
|
555
|
+
try {
|
|
556
|
+
return ((await jiti.import(path)) as { default?: Record<string, unknown> } | undefined)?.default
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return undefined
|
|
301
560
|
}
|
|
302
|
-
;(nuxt.options.app.head.script ??= []).push({ innerHTML: code })
|
|
303
561
|
}
|
|
304
562
|
|
|
305
563
|
function findConfigFile(rootDir: string) {
|
|
@@ -311,20 +569,20 @@ function findConfigFile(rootDir: string) {
|
|
|
311
569
|
return 'clarity.config.ts'
|
|
312
570
|
}
|
|
313
571
|
|
|
314
|
-
function
|
|
315
|
-
for (const name of
|
|
316
|
-
|
|
317
|
-
|
|
572
|
+
function findExisting(dir: string, names: string[]) {
|
|
573
|
+
for (const name of names) {
|
|
574
|
+
const path = resolve(dir, name)
|
|
575
|
+
if (existsSync(path)) {
|
|
576
|
+
return path
|
|
318
577
|
}
|
|
319
578
|
}
|
|
320
|
-
return
|
|
579
|
+
return undefined
|
|
321
580
|
}
|
|
322
581
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
catch {
|
|
328
|
-
return undefined
|
|
582
|
+
function findFeedsFile(rootDir: string, themeSrcDir: string) {
|
|
583
|
+
const found = findExisting(rootDir, ['feeds.ts', 'feeds.mjs', 'feeds.js'])
|
|
584
|
+
if (found) {
|
|
585
|
+
return { found: true, resolved: found }
|
|
329
586
|
}
|
|
587
|
+
return { found: false, resolved: resolve(themeSrcDir, 'config/feeds.empty.ts') }
|
|
330
588
|
}
|
|
@@ -45,6 +45,19 @@ export default defineNuxtModule({
|
|
|
45
45
|
shared: directories.shared,
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Nuxt 的 LayerAliasingPlugin 会把 Layer 源码内的 ~ / ~~ 前缀按
|
|
49
|
+
// layer 自身目录改写(node_modules Layer 默认指向 Theme 包根)。
|
|
50
|
+
// 上游组件中的 ~~ 语义是「站点根目录」(~~/package.json、~/feeds 等),
|
|
51
|
+
// 这里把本 Layer 的别名显式指向消费项目目录;消费项目不存在的文件
|
|
52
|
+
// (blog.config、shared/、feeds)由 clarity-config 的精确别名兜底。
|
|
53
|
+
layer.config.alias = {
|
|
54
|
+
...layer.config.alias,
|
|
55
|
+
'~': nuxt.options.srcDir,
|
|
56
|
+
'@': nuxt.options.srcDir,
|
|
57
|
+
'~~': nuxt.options.rootDir,
|
|
58
|
+
'@@': nuxt.options.rootDir,
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
// `getLayerDirectories()` caches a plain metadata object per layer before
|
|
49
62
|
// modules are installed. Nuxt exposes no invalidation API, so update the
|
|
50
63
|
// cached paths in place as well as `layer.config`.
|
package/src/pages/archive.vue
CHANGED
|
@@ -4,12 +4,12 @@ import { groupBy } from 'es-toolkit/array'
|
|
|
4
4
|
import { sumBy } from 'es-toolkit/math'
|
|
5
5
|
import { mapValues } from 'es-toolkit/object'
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const appConfig = useAppConfig()
|
|
8
8
|
useSeoMeta({
|
|
9
9
|
title: '归档',
|
|
10
|
-
description: `${
|
|
10
|
+
description: `${appConfig.title}的所有文章归档。`,
|
|
11
11
|
})
|
|
12
|
-
const birthYear = computed(() =>
|
|
12
|
+
const birthYear = computed(() => appConfig.component.stats.birthYear)
|
|
13
13
|
const showTuning = ref(false)
|
|
14
14
|
const spacing = ref(0)
|
|
15
15
|
const column = ref(1)
|
|
@@ -36,7 +36,7 @@ const yearlyWordCount = computed(() =>
|
|
|
36
36
|
|
|
37
37
|
function getArticleYear(article: ArticleProps) {
|
|
38
38
|
try {
|
|
39
|
-
return toZonedTemporal(
|
|
39
|
+
return toZonedTemporal(article[sortOrder.value] as string).year.toString()
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
42
|
return ''
|
package/src/pages/index.vue
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { orderBy } from 'es-toolkit/array'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const appConfig = useAppConfig()
|
|
5
5
|
useSeoMeta({
|
|
6
|
-
description:
|
|
7
|
-
ogImage:
|
|
6
|
+
description: appConfig.description,
|
|
7
|
+
ogImage: appConfig.author.avatar,
|
|
8
8
|
})
|
|
9
9
|
|
|
10
10
|
const { data: listRaw } = await useAsyncData('posts:index', () => getArticleIndexOptions(), { default: () => [] })
|
|
@@ -34,6 +34,7 @@ const { data: previewCount } = useAsyncData(
|
|
|
34
34
|
<template #aside>
|
|
35
35
|
<WidgetBlogStats />
|
|
36
36
|
<WidgetBlogTech />
|
|
37
|
+
<WidgetCommGroup />
|
|
37
38
|
</template>
|
|
38
39
|
|
|
39
40
|
<BlogHeader class="mobile-only" to="/" tag="h1" />
|