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.
- package/LICENSE +20 -20
- package/README.md +70 -4
- package/README.zh-CN.md +67 -4
- package/nuxt.config.ts +217 -214
- package/package.json +35 -37
- package/{app → src}/app.config.ts +110 -110
- package/{app → src}/app.vue +5 -5
- package/{app → src}/components/blog/BlogAside.vue +25 -25
- package/{app → src}/components/blog/BlogFooter.vue +25 -25
- package/{app → src}/components/blog/BlogHeader.global.vue +48 -48
- package/{app → src}/components/blog/BlogPanel.vue +43 -43
- package/{app → src}/components/blog/BlogSidebar.vue +53 -53
- package/{app → src}/components/blog/BlogWidget.vue +42 -42
- package/{app → src}/components/blog/Mask.vue +25 -25
- package/{app → src}/components/blog/SkipToContent.vue +9 -9
- package/{app → src}/components/blog/ThemeToggle.vue +22 -22
- package/{app → src}/components/content/Badge.vue +45 -45
- package/{app → src}/components/content/Blur.vue +14 -14
- package/{app → src}/components/content/CardList.vue +8 -8
- package/{app → src}/components/content/Chat.vue +41 -41
- package/{app → src}/components/content/Copy.vue +71 -71
- package/{app → src}/components/content/EmojiClock.vue +49 -49
- package/{app → src}/components/content/FeedCard.vue +96 -96
- package/{app → src}/components/content/FeedGroup.vue +61 -61
- package/{app → src}/components/content/Folding.vue +19 -19
- package/{app → src}/components/content/Key.vue +143 -143
- package/{app → src}/components/content/LinkBanner.vue +27 -27
- package/{app → src}/components/content/LinkCard.vue +28 -28
- package/{app → src}/components/content/MdTitle.vue +8 -8
- package/{app → src}/components/content/Mermaid.vue +99 -99
- package/{app → src}/components/content/MusicScore.vue +63 -63
- package/{app → src}/components/content/Pic.vue +49 -49
- package/{app → src}/components/content/Poetry.vue +27 -27
- package/{app → src}/components/content/ProseA.vue +24 -24
- package/{app → src}/components/content/ProseCode.vue +16 -16
- package/{app → src}/components/content/ProsePre.vue +117 -117
- package/{app → src}/components/content/ProseTable.vue +23 -23
- package/{app → src}/components/content/Quote.vue +21 -21
- package/{app → src}/components/content/Tab.vue +32 -32
- package/{app → src}/components/content/Timeline.vue +33 -33
- package/{app → src}/components/content/Tip.vue +40 -40
- package/{app → src}/components/content/VideoEmbed.vue +112 -112
- package/{app → src}/components/partial/Button.vue +36 -36
- package/{app → src}/components/partial/DlGroup.vue +31 -31
- package/{app → src}/components/partial/Dropdown.vue +21 -21
- package/{app → src}/components/partial/Error.vue +39 -39
- package/{app → src}/components/partial/Expand.vue +26 -26
- package/{app → src}/components/partial/IconNavList.vue +24 -24
- package/{app → src}/components/partial/Pagination.vue +62 -62
- package/{app → src}/components/partial/RadioGroup.vue +30 -30
- package/{app → src}/components/partial/Secret.vue +11 -11
- package/{app → src}/components/partial/Slider.vue +48 -48
- package/{app → src}/components/partial/Toggle.vue +17 -17
- package/{app → src}/components/popover/Lightbox.vue +44 -44
- package/{app → src}/components/popover/Search.vue +143 -143
- package/{app → src}/components/popover/SearchItem.vue +28 -28
- package/{app → src}/components/post/Archive.vue +40 -40
- package/{app → src}/components/post/Article.vue +49 -49
- package/{app → src}/components/post/Comment.vue +119 -119
- package/{app → src}/components/post/Excerpt.vue +40 -40
- package/{app → src}/components/post/OrderToggle.vue +70 -70
- package/{app → src}/components/post/PostFooter.vue +52 -52
- package/{app → src}/components/post/PostHeader.vue +66 -66
- package/{app → src}/components/post/PostSurround.vue +51 -51
- package/{app → src}/components/post/Slide.vue +85 -85
- package/{app → src}/components/util/Date.vue +62 -62
- package/{app → src}/components/util/HydrateSafe.vue +10 -10
- package/{app → src}/components/util/Img.vue +38 -38
- package/{app → src}/components/util/Link.vue +11 -11
- package/{app → src}/components/widget/BlogLog.vue +23 -23
- package/{app → src}/components/widget/BlogStats.vue +38 -38
- package/{app → src}/components/widget/BlogTech.vue +57 -57
- package/{app → src}/components/widget/Empty.vue +3 -3
- package/{app → src}/components/widget/Toc.vue +67 -67
- package/{app → src}/composables/useArticle.ts +115 -115
- package/{app → src}/composables/useAvoid.ts +87 -87
- package/{app → src}/composables/useCopy.ts +21 -21
- package/{app → src}/composables/usePagination.ts +70 -70
- package/{app → src}/composables/useShiki.ts +129 -129
- package/{app → src}/composables/useToc.ts +57 -57
- package/{app → src}/composables/useWidgets.ts +56 -56
- package/{config → src/config}/app.ts +62 -62
- package/{config → src/config}/content.ts +75 -75
- package/{config → src/config}/define.ts +3 -3
- package/src/config/feeds.empty.ts +6 -0
- package/{config → src/config}/index.ts +1 -1
- package/{config → src/config}/schema.ts +89 -89
- package/{app → src}/error.vue +31 -31
- package/{img → src/img}/index.ts +2 -2
- package/{app → src}/layouts/default.vue +25 -25
- package/{modules → src/modules}/clarity-config/index.ts +168 -162
- package/src/modules/clarity-source-layout/index.ts +78 -0
- package/{app → src}/pages/[...slug].vue +59 -59
- package/{app → src}/pages/archive.vue +129 -129
- package/{app → src}/pages/index.vue +76 -76
- package/{app → src}/pages/link.vue +62 -62
- package/{app → src}/pages/preview.vue +46 -46
- package/{app → src}/plugins/easter-egg.ts +23 -23
- package/{app → src}/plugins/init.ts +6 -6
- package/{app → src}/plugins/tippy.ts +22 -22
- package/{public → src/public}/assets/atom.css +274 -274
- package/{public → src/public}/assets/atom.xsl +109 -109
- package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.ts +58 -58
- package/{remark-plugins → src/remark-plugins}/remark-code-component.ts +41 -41
- package/{server → src/server}/api/stats.get.ts +84 -83
- package/{server → src/server}/routes/atom.xml.get.ts +85 -85
- package/{server → src/server}/routes/subscriptions.opml.get.ts +64 -64
- package/{shared → src/shared}/utils/clarity.ts +24 -24
- package/{shared → src/shared}/utils/icon.ts +218 -218
- package/{shared → src/shared}/utils/link.ts +52 -52
- package/{shared → src/shared}/utils/str.ts +88 -88
- package/{shared → src/shared}/utils/time.ts +115 -115
- package/{app → src}/shiki.config.ts +8 -8
- package/{app → src}/stores/layout.ts +34 -34
- package/{app → src}/stores/search.ts +37 -37
- package/{app → src}/types/article.ts +16 -16
- package/src/types/feed.ts +1 -0
- package/{app → src}/types/index.ts +14 -14
- package/{app → src}/types/nav.ts +11 -11
- package/{app → src}/utils/anim.ts +37 -37
- package/{app → src}/utils/img.ts +83 -83
- package/app/types/feed.ts +0 -1
- package/config/feeds.empty.ts +0 -4
- package/{app → src}/assets/css/_variable.scss +0 -0
- package/{app → src}/assets/css/animation.scss +0 -0
- package/{app → src}/assets/css/article.scss +0 -0
- package/{app → src}/assets/css/color.scss +0 -0
- package/{app → src}/assets/css/font.scss +0 -0
- package/{app → src}/assets/css/main.scss +0 -0
- package/{app → src}/assets/css/reusable.scss +0 -0
- package/{app → src}/assets/icons/solar-rewind-back-bold-duotone.svg +0 -0
- package/{app → src}/assets/icons/solar-rewind-forward-bold-duotone.svg +0 -0
- package/{app → src}/components/content/Alert.vue +0 -0
- package/{config → src/config}/content.d.mts +0 -0
- package/{config → src/config}/content.mjs +0 -0
- package/{config → src/config}/define.mjs +0 -0
- package/{config → src/config}/feed.ts +0 -0
- package/{config → src/config}/index.d.mts +0 -0
- package/{config → src/config}/index.mjs +0 -0
- package/{config → src/config}/public.ts +1 -1
- package/{config → src/config}/schema.d.mts +0 -0
- package/{config → src/config}/schema.mjs +0 -0
- package/{config → src/config}/server.ts +1 -1
- /package/{img → src/img}/index.d.mts +0 -0
- /package/{img → src/img}/index.mjs +0 -0
- /package/{modules → src/modules}/clarity-config/anti-mirror-client.ts +0 -0
- /package/{public → src/public}/fonts/AlimamaFangYuanTi.woff2 +0 -0
- /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.d.mts +0 -0
- /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.mjs +0 -0
- /package/{remark-plugins → src/remark-plugins}/remark-code-component.d.mts +0 -0
- /package/{remark-plugins → src/remark-plugins}/remark-code-component.mjs +0 -0
- /package/{server → src/server}/utils/clarity.ts +0 -0
- /package/{shared → src/shared}/types/content.ts +0 -0
- /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
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
60
|
-
const globalsTypePath = relative(appConfigTypeDir, resolve(
|
|
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(
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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>
|