@x-withu/page-withu 1.1.1 → 1.1.3
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 +3 -4
- package/bin/page-withu.js +8 -63
- package/docs/develop.md +42 -30
- package/docs/setup.md +2 -0
- package/package.json +21 -10
- package/{template → src/app}/index.html +1 -1
- package/{template → src/app}/src/app.vue +26 -12
- package/{template/src/components/blog-detail.vue → src/app/src/components/blog_detail.vue} +8 -6
- package/{template → src/app}/src/styles/main.css +300 -48
- package/{template → src}/vite.config.js +56 -9
- /package/{template → src/app}/src/assets/bulb.svg +0 -0
- /package/{template/src/components/blog-list.vue → src/app/src/components/blog_list.vue} +0 -0
- /package/{template → src/app}/src/components/site_card.vue +0 -0
- /package/{template → src/app}/src/components/theme_toggle.vue +0 -0
- /package/{template → src/app}/src/scripts/main.js +0 -0
- /package/{template → src/defaults}/config.js +0 -0
- /package/{template → src/defaults}/content/blog/hello-world.md +0 -0
- /package/{template → src/defaults}/content/domains.md +0 -0
- /package/{template → src/defaults}/content/index.md +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<strong>
|
|
5
|
-
|
|
5
|
+
一款轻量的个人主页生成器
|
|
6
6
|
</strong>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
@@ -41,15 +41,14 @@
|
|
|
41
41
|
# 安装 page-withu
|
|
42
42
|
npm install -g @x-withu/page-withu
|
|
43
43
|
|
|
44
|
-
#
|
|
44
|
+
# 创建项目
|
|
45
45
|
page-withu new my-homepage
|
|
46
|
-
|
|
47
46
|
cd my-homepage
|
|
48
47
|
|
|
49
48
|
# 本地预览
|
|
50
49
|
page-withu serve
|
|
51
50
|
|
|
52
|
-
#
|
|
51
|
+
# 生成静态网站
|
|
53
52
|
page-withu build
|
|
54
53
|
```
|
|
55
54
|
|
package/bin/page-withu.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
3
|
import fs from 'node:fs/promises';
|
|
5
4
|
import { constants as fsConstants } from 'node:fs';
|
|
6
5
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
6
|
import path from 'node:path';
|
|
9
7
|
import { fileURLToPath } from 'node:url';
|
|
10
8
|
|
|
@@ -13,33 +11,18 @@ const pkg = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.
|
|
|
13
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
12
|
const __dirname = path.dirname(__filename);
|
|
15
13
|
const packageRoot = path.resolve(__dirname, '..');
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'highlight.js': '^11.11.1',
|
|
23
|
-
'katex': '^0.16.45',
|
|
24
|
-
'markdown-it': '^14.1.0',
|
|
25
|
-
'markdown-it-emoji': '^3.0.0',
|
|
26
|
-
'markdown-it-footnote': '^4.0.0',
|
|
27
|
-
'mermaid': '^11.15.0',
|
|
28
|
-
'vite': '^6.3.1',
|
|
29
|
-
'vue': '^3.5.13',
|
|
30
|
-
};
|
|
31
|
-
const runtimeKey = createHash('sha256').update(JSON.stringify(runtimeDependencies)).digest('hex').slice(0, 12);
|
|
32
|
-
const runtimeDir = path.join(homedir(), '.page-withu', 'runtime', `${pkg.version}-${runtimeKey}`);
|
|
14
|
+
const sourceDir = path.join(packageRoot, 'src');
|
|
15
|
+
const defaultsDir = path.join(sourceDir, 'defaults');
|
|
16
|
+
const viteConfigPath = path.join(sourceDir, 'vite.config.js');
|
|
17
|
+
const packageRequire = createRequire(import.meta.url);
|
|
18
|
+
const vitePackageDir = path.dirname(packageRequire.resolve('vite/package.json'));
|
|
19
|
+
const viteBin = path.join(vitePackageDir, 'bin/vite.js');
|
|
33
20
|
|
|
34
21
|
const cyan = (text) => `\x1b[36m${text}\x1b[0m`;
|
|
35
22
|
const green = (text) => `\x1b[32m${text}\x1b[0m`;
|
|
36
23
|
const red = (text) => `\x1b[31m${text}\x1b[0m`;
|
|
37
24
|
const yellow = (text) => `\x1b[33m${text}\x1b[0m`;
|
|
38
25
|
|
|
39
|
-
function npmCommand() {
|
|
40
|
-
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
26
|
async function exists(filePath) {
|
|
44
27
|
try {
|
|
45
28
|
await fs.access(filePath, fsConstants.F_OK);
|
|
@@ -64,39 +47,6 @@ function run(command, args, options = {}) {
|
|
|
64
47
|
});
|
|
65
48
|
}
|
|
66
49
|
|
|
67
|
-
async function ensureRuntime() {
|
|
68
|
-
const marker = path.join(runtimeDir, '.ready');
|
|
69
|
-
if (await exists(marker)) return runtimeDir;
|
|
70
|
-
|
|
71
|
-
process.stdout.write('Preparing Page With U runtime...\n');
|
|
72
|
-
try {
|
|
73
|
-
await fs.mkdir(runtimeDir, { recursive: true });
|
|
74
|
-
await fs.writeFile(path.join(runtimeDir, 'package.json'), `${JSON.stringify({
|
|
75
|
-
private: true,
|
|
76
|
-
type: 'module',
|
|
77
|
-
dependencies: runtimeDependencies,
|
|
78
|
-
allowScripts: {
|
|
79
|
-
esbuild: true,
|
|
80
|
-
},
|
|
81
|
-
}, null, 2)}\n`);
|
|
82
|
-
await run(npmCommand(), ['install', '--fund=false', '--audit=false'], {
|
|
83
|
-
cwd: runtimeDir,
|
|
84
|
-
stdio: 'ignore',
|
|
85
|
-
});
|
|
86
|
-
await fs.writeFile(marker, new Date().toISOString());
|
|
87
|
-
process.stdout.write(`${green('Page With U runtime is ready.')}\n`);
|
|
88
|
-
return runtimeDir;
|
|
89
|
-
} catch (err) {
|
|
90
|
-
process.stderr.write(`${red('Failed to prepare Page With U runtime.')}\n`);
|
|
91
|
-
throw err;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function resolveRuntimeFile(runtimeRoot, specifier) {
|
|
96
|
-
const runtimeRequire = createRequire(path.join(runtimeRoot, 'package.json'));
|
|
97
|
-
return runtimeRequire.resolve(specifier);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
50
|
async function detectProject(projectDir) {
|
|
101
51
|
const required = ['config.js', 'content/index.md', 'content/domains.md'];
|
|
102
52
|
const missing = [];
|
|
@@ -121,8 +71,8 @@ async function createProject(projectName = 'my-homepage') {
|
|
|
121
71
|
|
|
122
72
|
try {
|
|
123
73
|
await fs.mkdir(fullPath, { recursive: true });
|
|
124
|
-
await fs.copyFile(path.join(
|
|
125
|
-
await fs.cp(path.join(
|
|
74
|
+
await fs.copyFile(path.join(defaultsDir, 'config.js'), path.join(fullPath, 'config.js'));
|
|
75
|
+
await fs.cp(path.join(defaultsDir, 'content'), path.join(fullPath, 'content'), { recursive: true });
|
|
126
76
|
await fs.mkdir(path.join(fullPath, 'content/blog'), { recursive: true });
|
|
127
77
|
|
|
128
78
|
process.stdout.write(`${green(`Successfully created PageWithU project in ${targetDir}!`)}\n`);
|
|
@@ -146,9 +96,6 @@ async function runVite(command, args) {
|
|
|
146
96
|
process.exit(1);
|
|
147
97
|
}
|
|
148
98
|
|
|
149
|
-
const runtimeRoot = await ensureRuntime();
|
|
150
|
-
const vitePackageDir = path.dirname(resolveRuntimeFile(runtimeRoot, 'vite/package.json'));
|
|
151
|
-
const viteBin = path.join(vitePackageDir, 'bin/vite.js');
|
|
152
99
|
const viteArgs = command === 'serve'
|
|
153
100
|
? ['--config', viteConfigPath, ...args]
|
|
154
101
|
: ['build', '--config', viteConfigPath, ...args];
|
|
@@ -158,9 +105,7 @@ async function runVite(command, args) {
|
|
|
158
105
|
cwd: projectRoot,
|
|
159
106
|
env: {
|
|
160
107
|
...process.env,
|
|
161
|
-
NODE_PATH: path.join(runtimeRoot, 'node_modules'),
|
|
162
108
|
PAGE_WITHU_PROJECT_ROOT: projectRoot,
|
|
163
|
-
PAGE_WITHU_RUNTIME_ROOT: runtimeRoot,
|
|
164
109
|
},
|
|
165
110
|
});
|
|
166
111
|
} catch (err) {
|
package/docs/develop.md
CHANGED
|
@@ -9,23 +9,26 @@ Page With U 的目标是保持轻量、清晰、容易定制。开发时建议
|
|
|
9
9
|
- 优先修改已有文件,不要为了小功能引入复杂架构。
|
|
10
10
|
- 模板默认配置要适合大多数用户开箱即用。
|
|
11
11
|
- 文档中的用户流程要始终和 CLI 行为保持一致。
|
|
12
|
-
-
|
|
13
|
-
- 不要引入大型 UI 框架,样式优先维护在 `
|
|
14
|
-
- Markdown 能力优先在 `
|
|
12
|
+
- 修改包内运行时或默认内容后,需要用源码目录或新生成项目执行一次 `page-withu build`。
|
|
13
|
+
- 不要引入大型 UI 框架,样式优先维护在 `src/app/src/styles/main.css`。
|
|
14
|
+
- Markdown 能力优先在 `src/vite.config.js` 中集中处理,避免分散到多个组件。
|
|
15
15
|
|
|
16
16
|
项目结构:
|
|
17
17
|
|
|
18
18
|
```text
|
|
19
19
|
page-withu/
|
|
20
20
|
├── bin/
|
|
21
|
-
│ └── page-withu.js
|
|
22
|
-
├── docs/
|
|
23
|
-
├──
|
|
24
|
-
│ ├──
|
|
25
|
-
│ ├──
|
|
26
|
-
│
|
|
27
|
-
│
|
|
28
|
-
├──
|
|
21
|
+
│ └── page-withu.js # CLI 入口
|
|
22
|
+
├── docs/ # 使用、部署、开发文档
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── app/ # 包内 Vue 运行时应用
|
|
25
|
+
│ │ ├── index.html
|
|
26
|
+
│ │ └── src/
|
|
27
|
+
│ ├── defaults/ # 新项目默认配置和内容
|
|
28
|
+
│ │ ├── config.js
|
|
29
|
+
│ │ └── content/
|
|
30
|
+
│ └── vite.config.js # 包内 Markdown 渲染与静态路由生成
|
|
31
|
+
├── package.json # page-withu CLI 与运行时依赖配置
|
|
29
32
|
└── README.md
|
|
30
33
|
```
|
|
31
34
|
|
|
@@ -70,8 +73,8 @@ page-withu new my-homepage
|
|
|
70
73
|
调整 CLI 行为时,通常需要同步修改:
|
|
71
74
|
|
|
72
75
|
- `bin/page-withu.js` 中的 commander command。
|
|
73
|
-
- 必要时修改 `
|
|
74
|
-
-
|
|
76
|
+
- 必要时修改 `src/defaults/config.js` 的默认字段。
|
|
77
|
+
- 必要时修改包内运行时中读取配置的 Vue 组件。
|
|
75
78
|
- 必要时更新 `README.md` 和 `docs/setup.md`。
|
|
76
79
|
|
|
77
80
|
测试脚手架生成流程:
|
|
@@ -86,30 +89,36 @@ node /path/to/page-withu/bin/page-withu.js build
|
|
|
86
89
|
|
|
87
90
|
如果构建成功,说明内容型项目可以通过包内运行时正常生成。
|
|
88
91
|
|
|
89
|
-
##
|
|
92
|
+
## 开发包内运行时与默认内容
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
包内 Vue 运行时应用位于:
|
|
92
95
|
|
|
93
96
|
```text
|
|
94
|
-
|
|
97
|
+
src/app/
|
|
95
98
|
```
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
新项目默认配置和内容位于:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
src/defaults/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
启动运行时开发服务:
|
|
98
107
|
|
|
99
108
|
```bash
|
|
100
|
-
cd
|
|
101
|
-
node
|
|
109
|
+
cd src/defaults
|
|
110
|
+
node ../../bin/page-withu.js serve
|
|
102
111
|
```
|
|
103
112
|
|
|
104
113
|
常见开发位置:
|
|
105
114
|
|
|
106
|
-
- `
|
|
107
|
-
- `
|
|
108
|
-
- `
|
|
109
|
-
- `
|
|
110
|
-
- `
|
|
111
|
-
- `
|
|
112
|
-
- `
|
|
115
|
+
- `src/app/src/app.vue`:主页面路由、布局、浏览器标签页标题和 favicon。
|
|
116
|
+
- `src/app/src/components/blog_list.vue`:博客列表、分页、搜索、标签和时间线。
|
|
117
|
+
- `src/app/src/components/blog_detail.vue`:文章详情、ToC、图片放大、Mermaid 渲染。
|
|
118
|
+
- `src/app/src/styles/main.css`:全局样式、亮暗模式、响应式布局、Markdown 样式。
|
|
119
|
+
- `src/vite.config.js`:Markdown 插件、callout、代码高亮、静态 `.html` 路由生成。
|
|
120
|
+
- `src/defaults/config.js`:站点默认配置。
|
|
121
|
+
- `src/defaults/content/`:默认内容样例。
|
|
113
122
|
|
|
114
123
|
修改 Markdown 渲染逻辑后,建议检查这些能力:
|
|
115
124
|
|
|
@@ -118,11 +127,11 @@ node ../bin/page-withu.js serve
|
|
|
118
127
|
- Mermaid、LaTeX、脚注、Emoji 是否能正常渲染。
|
|
119
128
|
- callout 是否在亮色和暗色模式下都可读。
|
|
120
129
|
|
|
121
|
-
|
|
130
|
+
构建默认内容项目:
|
|
122
131
|
|
|
123
132
|
```bash
|
|
124
|
-
cd
|
|
125
|
-
node
|
|
133
|
+
cd src/defaults
|
|
134
|
+
node ../../bin/page-withu.js build
|
|
126
135
|
```
|
|
127
136
|
|
|
128
137
|
如果改动影响脚手架生成后的项目,还需要用 CLI 重新生成一个临时项目并构建。
|
|
@@ -150,7 +159,10 @@ npm run pack:check
|
|
|
150
159
|
确认包中至少包含:
|
|
151
160
|
|
|
152
161
|
- `bin/page-withu.js`
|
|
153
|
-
- `
|
|
162
|
+
- `src/app/`
|
|
163
|
+
- `src/defaults/config.js`
|
|
164
|
+
- `src/defaults/content/`
|
|
165
|
+
- `src/vite.config.js`
|
|
154
166
|
- `docs/`
|
|
155
167
|
- `package.json`
|
|
156
168
|
- `README.md`
|
package/docs/setup.md
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-withu/page-withu",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "A lightweight
|
|
3
|
+
"version": "1.1.3",
|
|
4
|
+
"description": "A lightweight personal homepage generator.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/Explorer-Dong/page-withu"
|
|
@@ -12,21 +12,32 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
15
|
-
"template/config.js",
|
|
16
|
-
"template/content/",
|
|
17
|
-
"template/src/",
|
|
18
|
-
"template/index.html",
|
|
19
|
-
"template/vite.config.js",
|
|
20
15
|
"docs/",
|
|
16
|
+
"src/app/",
|
|
17
|
+
"src/defaults/config.js",
|
|
18
|
+
"src/defaults/content/",
|
|
19
|
+
"src/vite.config.js",
|
|
21
20
|
"README.md"
|
|
22
21
|
],
|
|
23
22
|
"scripts": {
|
|
24
23
|
"pack:check": "npm pack --dry-run"
|
|
25
24
|
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@traptitech/markdown-it-katex": "^3.6.0",
|
|
27
|
+
"@vitejs/plugin-vue": "^5.2.3",
|
|
28
|
+
"gray-matter": "^4.0.3",
|
|
29
|
+
"highlight.js": "^11.11.1",
|
|
30
|
+
"katex": "^0.16.45",
|
|
31
|
+
"markdown-it": "^14.1.0",
|
|
32
|
+
"markdown-it-emoji": "^3.0.0",
|
|
33
|
+
"markdown-it-footnote": "^4.0.0",
|
|
34
|
+
"mermaid": "^11.15.0",
|
|
35
|
+
"vite": "^6.3.1",
|
|
36
|
+
"vue": "^3.5.13"
|
|
37
|
+
},
|
|
26
38
|
"keywords": [
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"generator",
|
|
39
|
+
"static site generator",
|
|
40
|
+
"lightweight",
|
|
30
41
|
"vue",
|
|
31
42
|
"vite"
|
|
32
43
|
],
|
|
@@ -41,12 +41,18 @@
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
<footer>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
<div class="footer-content">
|
|
45
|
+
<div class="footer-meta">
|
|
46
|
+
<p>© {{ userConfig.year }} {{ userConfig.author }}</p>
|
|
47
|
+
<div class="footer-links">
|
|
48
|
+
<template v-for="(link, index) in userConfig.footerLinks" :key="link.label">
|
|
49
|
+
<a :href="link.url">{{ link.label }}</a>
|
|
50
|
+
<span v-if="index < userConfig.footerLinks.length - 1" class="separator">·</span>
|
|
51
|
+
</template>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<p class="made-with-page-withu">Made with <a href="https://github.com/Explorer-Dong/page-withu" target="_blank"
|
|
55
|
+
rel="noreferrer">PageWithU</a></p>
|
|
50
56
|
</div>
|
|
51
57
|
</footer>
|
|
52
58
|
</main>
|
|
@@ -57,8 +63,8 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
57
63
|
import userConfig from '@page-withu/user-config'
|
|
58
64
|
import ThemeToggle from './components/theme_toggle.vue'
|
|
59
65
|
import SiteCard from './components/site_card.vue'
|
|
60
|
-
import BlogList from './components/
|
|
61
|
-
import BlogDetail from './components/
|
|
66
|
+
import BlogList from './components/blog_list.vue'
|
|
67
|
+
import BlogDetail from './components/blog_detail.vue'
|
|
62
68
|
import { frontmatter as domainsFrontmatter } from '@page-withu/user-content/domains.md'
|
|
63
69
|
import { sections as aboutSections } from '@page-withu/user-content/index.md'
|
|
64
70
|
import defaultFavicon from './assets/bulb.svg'
|
|
@@ -67,14 +73,22 @@ const blogModules = import.meta.glob('@page-withu/user-content/blog/*.md', { eag
|
|
|
67
73
|
const validPages = ['about', 'domains', 'blog']
|
|
68
74
|
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
|
|
69
75
|
const navItems = [
|
|
70
|
-
{ page: '
|
|
71
|
-
{ page: '
|
|
76
|
+
{ page: 'about', label: '主页' },
|
|
77
|
+
{ page: 'domains', label: '项目' },
|
|
78
|
+
{ page: 'blog', label: '博客' },
|
|
72
79
|
]
|
|
73
80
|
|
|
74
81
|
const domainsTitle = domainsFrontmatter.title || 'Domains'
|
|
75
82
|
const sites = domainsFrontmatter.sites || []
|
|
76
|
-
const tabTitle = userConfig.tabTitle || userConfig.title || '
|
|
83
|
+
const tabTitle = userConfig.tabTitle || userConfig.title || 'PageWithU'
|
|
77
84
|
const favicon = userConfig.favicon === '/src/assets/bulb.svg' ? defaultFavicon : userConfig.favicon || defaultFavicon
|
|
85
|
+
const externalUrlPattern = /^[a-z][a-z\d+.-]*:/i
|
|
86
|
+
|
|
87
|
+
function resolveFaviconHref(value) {
|
|
88
|
+
if (value === defaultFavicon || externalUrlPattern.test(value) || value.startsWith('//')) return value
|
|
89
|
+
const localPath = value.replace(/^\.\//, '')
|
|
90
|
+
return withBasePath(localPath.startsWith('/') ? localPath : `/${localPath}`)
|
|
91
|
+
}
|
|
78
92
|
|
|
79
93
|
function applyDocumentMeta() {
|
|
80
94
|
document.title = tabTitle
|
|
@@ -84,7 +98,7 @@ function applyDocumentMeta() {
|
|
|
84
98
|
icon.rel = 'icon'
|
|
85
99
|
document.head.appendChild(icon)
|
|
86
100
|
}
|
|
87
|
-
icon.href =
|
|
101
|
+
icon.href = resolveFaviconHref(favicon)
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
applyDocumentMeta()
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
<a class="blog-back" :href="blogHref" @click.prevent="$emit('back')">← Back to all posts</a>
|
|
4
4
|
<div :class="['blog-article-layout', { 'without-toc': !toc.length }]">
|
|
5
5
|
<div class="blog-article-main">
|
|
6
|
-
<
|
|
6
|
+
<div class="blog-article-header">
|
|
7
7
|
<h1 class="markdown-body">{{ post.title }}</h1>
|
|
8
8
|
<div class="blog-meta">
|
|
9
9
|
<time :datetime="post.date">{{ formatDate(post.date) }}</time>
|
|
10
10
|
<span v-for="tag in post.tags" :key="tag" class="tag">{{ tag }}</span>
|
|
11
11
|
</div>
|
|
12
|
-
</
|
|
12
|
+
</div>
|
|
13
13
|
|
|
14
14
|
<div ref="contentRef" class="markdown-body" @click="openImage" v-html="post.html"></div>
|
|
15
15
|
|
|
@@ -35,10 +35,12 @@
|
|
|
35
35
|
<button class="blog-toc-close" type="button" aria-label="Close table of contents"
|
|
36
36
|
@click="tocOpen = false">×</button>
|
|
37
37
|
</div>
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
<div class="blog-toc-links">
|
|
39
|
+
<a v-for="item in toc" :key="item.id" :href="`#${item.id}`"
|
|
40
|
+
:class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]" @click="tocOpen = false">
|
|
41
|
+
{{ item.title }}
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
42
44
|
</aside>
|
|
43
45
|
</div>
|
|
44
46
|
|
|
@@ -77,6 +77,49 @@
|
|
|
77
77
|
--color-tag-bg: rgba(110, 168, 254, 0.15);
|
|
78
78
|
--color-tag-text: #6ea8fe;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
:root:not([data-theme="light"]) main > header {
|
|
82
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
83
|
+
background: rgba(18, 18, 18, 0.78);
|
|
84
|
+
box-shadow: 0 14px 38px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
:root:not([data-theme="light"]) .site-title,
|
|
88
|
+
:root:not([data-theme="light"]) .site-title:hover {
|
|
89
|
+
color: #fff;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:root:not([data-theme="light"]) .nav-item {
|
|
93
|
+
color: rgba(255, 255, 255, 0.68);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
:root:not([data-theme="light"]) .nav-item:hover,
|
|
97
|
+
:root:not([data-theme="light"]) .nav-item.active {
|
|
98
|
+
color: #fff;
|
|
99
|
+
text-shadow: 0 0 18px rgba(255, 255, 255, 0.28);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
:root:not([data-theme="light"]) .theme-toggle {
|
|
103
|
+
color: rgba(255, 255, 255, 0.68);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
:root:not([data-theme="light"]) .theme-toggle:hover {
|
|
107
|
+
background: rgba(255, 255, 255, 0.1);
|
|
108
|
+
color: #fff;
|
|
109
|
+
text-shadow: 0 0 18px rgba(255, 255, 255, 0.28);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
:root:not([data-theme="light"]) .theme-toggle .icon-sun {
|
|
113
|
+
display: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
:root:not([data-theme="light"]) .theme-toggle .icon-moon {
|
|
117
|
+
display: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
:root:not([data-theme="light"]) .theme-toggle .icon-system {
|
|
121
|
+
display: block;
|
|
122
|
+
}
|
|
80
123
|
}
|
|
81
124
|
|
|
82
125
|
/* Explicit dark mode override */
|
|
@@ -165,63 +208,109 @@ main {
|
|
|
165
208
|
}
|
|
166
209
|
|
|
167
210
|
/* ===== Header ===== */
|
|
168
|
-
header {
|
|
211
|
+
main > header {
|
|
212
|
+
position: sticky;
|
|
213
|
+
top: 16px;
|
|
214
|
+
z-index: 80;
|
|
169
215
|
display: flex;
|
|
170
216
|
align-items: center;
|
|
171
217
|
justify-content: space-between;
|
|
172
|
-
gap:
|
|
218
|
+
gap: 24px;
|
|
219
|
+
width: min(100%, 720px);
|
|
220
|
+
margin: 0 auto;
|
|
221
|
+
padding: 6px 8px 6px 20px;
|
|
222
|
+
border: 1px solid var(--color-card-border);
|
|
223
|
+
border-radius: 24px;
|
|
224
|
+
background: rgba(255, 255, 255, 0.85);
|
|
225
|
+
box-shadow: 0 14px 38px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
|
226
|
+
backdrop-filter: blur(18px) saturate(1.35);
|
|
227
|
+
-webkit-backdrop-filter: blur(18px) saturate(1.35);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
[data-theme="dark"] main > header {
|
|
231
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
232
|
+
background: rgba(18, 18, 18, 0.78);
|
|
233
|
+
box-shadow: 0 14px 38px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main > header::before {
|
|
237
|
+
content: none;
|
|
173
238
|
}
|
|
174
239
|
|
|
175
240
|
.site-title {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
241
|
+
flex: 0 0 auto;
|
|
242
|
+
max-width: 180px;
|
|
243
|
+
overflow: hidden;
|
|
244
|
+
color: var(--color-text);
|
|
245
|
+
text-overflow: ellipsis;
|
|
246
|
+
white-space: nowrap;
|
|
247
|
+
font-size: 0.95rem;
|
|
248
|
+
font-weight: 800;
|
|
179
249
|
text-decoration: none;
|
|
180
250
|
margin: 0;
|
|
181
251
|
padding: 0;
|
|
182
252
|
background: none;
|
|
183
253
|
}
|
|
184
254
|
|
|
255
|
+
[data-theme="dark"] .site-title {
|
|
256
|
+
color: #fff;
|
|
257
|
+
}
|
|
258
|
+
|
|
185
259
|
.site-title:hover {
|
|
186
|
-
color: var(--color-
|
|
260
|
+
color: var(--color-link);
|
|
187
261
|
background: none;
|
|
188
262
|
}
|
|
189
263
|
|
|
264
|
+
[data-theme="dark"] .site-title:hover {
|
|
265
|
+
color: #fff;
|
|
266
|
+
}
|
|
267
|
+
|
|
190
268
|
.header-actions {
|
|
191
269
|
display: flex;
|
|
192
270
|
align-items: center;
|
|
193
|
-
gap:
|
|
271
|
+
gap: 24px;
|
|
272
|
+
min-width: 0;
|
|
194
273
|
}
|
|
195
274
|
|
|
196
275
|
/* ===== Navigation Bar ===== */
|
|
197
276
|
.nav-bar {
|
|
198
277
|
display: flex;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
278
|
+
align-items: center;
|
|
279
|
+
gap: 24px;
|
|
280
|
+
min-width: 0;
|
|
281
|
+
padding: 0;
|
|
282
|
+
border: none;
|
|
283
|
+
border-radius: 0;
|
|
284
|
+
background: transparent;
|
|
204
285
|
}
|
|
205
286
|
|
|
206
287
|
.nav-item {
|
|
207
|
-
padding:
|
|
288
|
+
padding: 6px 0;
|
|
208
289
|
font-size: 0.9rem;
|
|
209
|
-
font-weight:
|
|
210
|
-
color: var(--color-subtext);
|
|
290
|
+
font-weight: 650;
|
|
291
|
+
color: var(--color-subtext-light, #8a9ba8);
|
|
211
292
|
text-decoration: none;
|
|
212
|
-
border-radius:
|
|
213
|
-
transition: color 0.2s ease,
|
|
293
|
+
border-radius: 0;
|
|
294
|
+
transition: color 0.2s ease, text-shadow 0.2s ease;
|
|
214
295
|
background: none;
|
|
296
|
+
white-space: nowrap;
|
|
215
297
|
}
|
|
216
298
|
|
|
217
|
-
.nav-item
|
|
218
|
-
color:
|
|
219
|
-
background: var(--color-link-hover-bg);
|
|
299
|
+
[data-theme="dark"] .nav-item {
|
|
300
|
+
color: rgba(255, 255, 255, 0.68);
|
|
220
301
|
}
|
|
221
302
|
|
|
303
|
+
.nav-item:hover,
|
|
222
304
|
.nav-item.active {
|
|
223
|
-
color: var(--color-
|
|
224
|
-
background:
|
|
305
|
+
color: var(--color-text);
|
|
306
|
+
background: none;
|
|
307
|
+
text-shadow: none;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
[data-theme="dark"] .nav-item:hover,
|
|
311
|
+
[data-theme="dark"] .nav-item.active {
|
|
312
|
+
color: #fff;
|
|
313
|
+
text-shadow: 0 0 18px rgba(255, 255, 255, 0.28);
|
|
225
314
|
}
|
|
226
315
|
|
|
227
316
|
/* ===== Section Titles ===== */
|
|
@@ -398,6 +487,17 @@ footer p {
|
|
|
398
487
|
margin-bottom: 6px;
|
|
399
488
|
}
|
|
400
489
|
|
|
490
|
+
.footer-content {
|
|
491
|
+
display: flex;
|
|
492
|
+
align-items: center;
|
|
493
|
+
justify-content: space-between;
|
|
494
|
+
gap: 16px;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.footer-meta {
|
|
498
|
+
min-width: 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
401
501
|
.footer-links {
|
|
402
502
|
display: flex;
|
|
403
503
|
align-items: center;
|
|
@@ -419,40 +519,73 @@ footer p {
|
|
|
419
519
|
color: var(--color-separator);
|
|
420
520
|
}
|
|
421
521
|
|
|
522
|
+
.made-with-page-withu {
|
|
523
|
+
flex-shrink: 0;
|
|
524
|
+
margin: 0;
|
|
525
|
+
color: var(--color-footer-text);
|
|
526
|
+
font-size: 0.85rem;
|
|
527
|
+
text-align: right;
|
|
528
|
+
white-space: nowrap;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.made-with-page-withu a {
|
|
532
|
+
color: var(--color-footer-link);
|
|
533
|
+
font-weight: 600;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.made-with-page-withu a:hover {
|
|
537
|
+
color: var(--color-footer-link-hover);
|
|
538
|
+
background: var(--color-footer-link-hover-bg);
|
|
539
|
+
}
|
|
540
|
+
|
|
422
541
|
/* ===== Theme Toggle ===== */
|
|
423
542
|
.theme-toggle {
|
|
424
|
-
width:
|
|
425
|
-
height:
|
|
426
|
-
border:
|
|
543
|
+
width: 34px;
|
|
544
|
+
height: 34px;
|
|
545
|
+
border: none;
|
|
427
546
|
border-radius: 50%;
|
|
428
|
-
background:
|
|
547
|
+
background: transparent;
|
|
429
548
|
cursor: pointer;
|
|
430
549
|
display: flex;
|
|
431
550
|
align-items: center;
|
|
432
551
|
justify-content: center;
|
|
433
|
-
transition: background-color 0.
|
|
434
|
-
|
|
552
|
+
transition: background-color 0.2s ease, transform 0.15s ease, color 0.2s ease;
|
|
553
|
+
color: var(--color-subtext);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
[data-theme="dark"] .theme-toggle {
|
|
557
|
+
color: rgba(255, 255, 255, 0.68);
|
|
435
558
|
}
|
|
436
559
|
|
|
437
560
|
.theme-toggle:hover {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
561
|
+
background: var(--color-link-hover-bg);
|
|
562
|
+
color: var(--color-text);
|
|
563
|
+
text-shadow: none;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
[data-theme="dark"] .theme-toggle:hover {
|
|
567
|
+
background: rgba(255, 255, 255, 0.1);
|
|
568
|
+
color: #fff;
|
|
569
|
+
text-shadow: 0 0 18px rgba(255, 255, 255, 0.28);
|
|
441
570
|
}
|
|
442
571
|
|
|
443
572
|
.theme-toggle:active {
|
|
444
|
-
transform: scale(0.
|
|
573
|
+
transform: scale(0.92);
|
|
445
574
|
}
|
|
446
575
|
|
|
447
576
|
.theme-toggle svg {
|
|
448
|
-
width:
|
|
449
|
-
height:
|
|
577
|
+
width: 18px;
|
|
578
|
+
height: 18px;
|
|
450
579
|
fill: none;
|
|
451
|
-
stroke:
|
|
580
|
+
stroke: currentColor;
|
|
452
581
|
stroke-width: 2;
|
|
453
582
|
stroke-linecap: round;
|
|
454
583
|
stroke-linejoin: round;
|
|
455
|
-
transition:
|
|
584
|
+
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.theme-toggle:hover svg {
|
|
588
|
+
transform: rotate(15deg);
|
|
456
589
|
}
|
|
457
590
|
|
|
458
591
|
.theme-toggle .icon-sun {
|
|
@@ -553,6 +686,39 @@ footer p {
|
|
|
553
686
|
padding: 16px;
|
|
554
687
|
overflow-x: auto;
|
|
555
688
|
margin-bottom: 1em;
|
|
689
|
+
scrollbar-width: thin;
|
|
690
|
+
scrollbar-color: transparent transparent;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.markdown-body pre:hover {
|
|
694
|
+
scrollbar-color: color-mix(in srgb, var(--color-subtext) 45%, transparent) transparent;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.markdown-body pre::-webkit-scrollbar {
|
|
698
|
+
height: 4px;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.markdown-body pre::-webkit-scrollbar-button {
|
|
702
|
+
display: none;
|
|
703
|
+
width: 0;
|
|
704
|
+
height: 0;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.markdown-body pre::-webkit-scrollbar-track {
|
|
708
|
+
background: transparent;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.markdown-body pre::-webkit-scrollbar-thumb {
|
|
712
|
+
background: transparent;
|
|
713
|
+
border-radius: 999px;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.markdown-body pre:hover::-webkit-scrollbar-thumb {
|
|
717
|
+
background: color-mix(in srgb, var(--color-subtext) 42%, transparent);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.markdown-body pre::-webkit-scrollbar-thumb:hover {
|
|
721
|
+
background: color-mix(in srgb, var(--color-subtext) 62%, transparent);
|
|
556
722
|
}
|
|
557
723
|
|
|
558
724
|
.markdown-body pre code {
|
|
@@ -1007,12 +1173,53 @@ footer p {
|
|
|
1007
1173
|
flex-direction: column;
|
|
1008
1174
|
gap: 8px;
|
|
1009
1175
|
max-height: calc(100vh - 48px);
|
|
1010
|
-
padding: 0
|
|
1176
|
+
padding: 0 4px 16px 16px;
|
|
1011
1177
|
border-left: 1px solid var(--color-footer-border);
|
|
1012
|
-
overflow
|
|
1178
|
+
overflow: hidden;
|
|
1013
1179
|
font-size: 0.84rem;
|
|
1014
1180
|
}
|
|
1015
1181
|
|
|
1182
|
+
.blog-toc-links {
|
|
1183
|
+
display: flex;
|
|
1184
|
+
flex-direction: column;
|
|
1185
|
+
gap: 8px;
|
|
1186
|
+
min-height: 0;
|
|
1187
|
+
overflow-y: auto;
|
|
1188
|
+
scrollbar-width: thin;
|
|
1189
|
+
scrollbar-color: transparent transparent;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.blog-toc-links:hover {
|
|
1193
|
+
scrollbar-color: color-mix(in srgb, var(--color-subtext) 45%, transparent) transparent;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.blog-toc-links::-webkit-scrollbar {
|
|
1197
|
+
width: 4px;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
.blog-toc-links::-webkit-scrollbar-button {
|
|
1201
|
+
display: none;
|
|
1202
|
+
width: 0;
|
|
1203
|
+
height: 0;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
.blog-toc-links::-webkit-scrollbar-track {
|
|
1207
|
+
background: transparent;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.blog-toc-links::-webkit-scrollbar-thumb {
|
|
1211
|
+
background: transparent;
|
|
1212
|
+
border-radius: 999px;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.blog-toc-links:hover::-webkit-scrollbar-thumb {
|
|
1216
|
+
background: color-mix(in srgb, var(--color-subtext) 42%, transparent);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
.blog-toc-links::-webkit-scrollbar-thumb:hover {
|
|
1220
|
+
background: color-mix(in srgb, var(--color-subtext) 62%, transparent);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1016
1223
|
.blog-toc-header {
|
|
1017
1224
|
display: flex;
|
|
1018
1225
|
align-items: center;
|
|
@@ -1204,14 +1411,34 @@ footer p {
|
|
|
1204
1411
|
gap: 28px;
|
|
1205
1412
|
}
|
|
1206
1413
|
|
|
1207
|
-
header {
|
|
1208
|
-
|
|
1209
|
-
|
|
1414
|
+
main > header {
|
|
1415
|
+
top: 12px;
|
|
1416
|
+
gap: 18px;
|
|
1417
|
+
width: 100%;
|
|
1418
|
+
padding: 8px 10px 8px 18px;
|
|
1210
1419
|
}
|
|
1211
1420
|
|
|
1212
1421
|
.header-actions {
|
|
1213
|
-
|
|
1214
|
-
|
|
1422
|
+
flex: 1;
|
|
1423
|
+
width: auto;
|
|
1424
|
+
justify-content: flex-end;
|
|
1425
|
+
gap: 18px;
|
|
1426
|
+
min-width: 0;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.nav-bar {
|
|
1430
|
+
gap: 18px;
|
|
1431
|
+
overflow-x: auto;
|
|
1432
|
+
scrollbar-width: none;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.nav-bar::-webkit-scrollbar {
|
|
1436
|
+
display: none;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.theme-toggle {
|
|
1440
|
+
width: 38px;
|
|
1441
|
+
height: 38px;
|
|
1215
1442
|
}
|
|
1216
1443
|
}
|
|
1217
1444
|
|
|
@@ -1227,12 +1454,13 @@ footer p {
|
|
|
1227
1454
|
}
|
|
1228
1455
|
|
|
1229
1456
|
.site-title {
|
|
1230
|
-
|
|
1457
|
+
max-width: 120px;
|
|
1458
|
+
font-size: 0.9rem;
|
|
1231
1459
|
}
|
|
1232
1460
|
|
|
1233
1461
|
.nav-item {
|
|
1234
|
-
padding:
|
|
1235
|
-
font-size: 0.
|
|
1462
|
+
padding: 7px 0;
|
|
1463
|
+
font-size: 0.86rem;
|
|
1236
1464
|
}
|
|
1237
1465
|
|
|
1238
1466
|
.about-section {
|
|
@@ -1322,6 +1550,7 @@ footer p {
|
|
|
1322
1550
|
box-shadow: var(--shadow-card);
|
|
1323
1551
|
transform: translate(-50%, -50%);
|
|
1324
1552
|
display: none;
|
|
1553
|
+
overflow: hidden;
|
|
1325
1554
|
}
|
|
1326
1555
|
|
|
1327
1556
|
.blog-toc.open {
|
|
@@ -1329,12 +1558,11 @@ footer p {
|
|
|
1329
1558
|
}
|
|
1330
1559
|
|
|
1331
1560
|
.blog-toc-header {
|
|
1332
|
-
position: sticky;
|
|
1333
|
-
top: -16px;
|
|
1334
1561
|
margin: -16px -16px 4px;
|
|
1335
1562
|
padding: 16px;
|
|
1336
1563
|
background: var(--color-card-bg);
|
|
1337
1564
|
border-bottom: 1px solid var(--color-footer-border);
|
|
1565
|
+
flex-shrink: 0;
|
|
1338
1566
|
}
|
|
1339
1567
|
|
|
1340
1568
|
.blog-toc-close {
|
|
@@ -1377,3 +1605,27 @@ footer p {
|
|
|
1377
1605
|
height: 36px;
|
|
1378
1606
|
}
|
|
1379
1607
|
}
|
|
1608
|
+
|
|
1609
|
+
@media (max-width: 520px) {
|
|
1610
|
+
main > header {
|
|
1611
|
+
padding-left: 14px;
|
|
1612
|
+
gap: 14px;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.site-title {
|
|
1616
|
+
max-width: 80px;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.header-actions {
|
|
1620
|
+
gap: 14px;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.nav-bar {
|
|
1624
|
+
gap: 14px;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
.theme-toggle {
|
|
1628
|
+
width: 32px;
|
|
1629
|
+
height: 32px;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
|
-
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const sourceRoot = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const appRoot = resolve(sourceRoot, 'app')
|
|
9
10
|
const projectRoot = resolve(process.env.PAGE_WITHU_PROJECT_ROOT || process.cwd())
|
|
10
|
-
const
|
|
11
|
-
const dependencyRoot =
|
|
12
|
-
const runtimeRequire =
|
|
11
|
+
const packageRoot = resolve(sourceRoot, '..')
|
|
12
|
+
const dependencyRoot = resolve(packageRoot, 'node_modules')
|
|
13
|
+
const runtimeRequire = createRequire(import.meta.url)
|
|
13
14
|
const loadRuntimeModule = async (specifier) => import(pathToFileURL(runtimeRequire.resolve(specifier)).href)
|
|
14
15
|
const runtimeModuleAlias = Object.fromEntries([
|
|
15
16
|
'vue',
|
|
@@ -22,6 +23,18 @@ const userContentDir = resolve(projectRoot, 'content')
|
|
|
22
23
|
const cacheKey = createHash('sha256').update(projectRoot).digest('hex').slice(0, 12)
|
|
23
24
|
const cacheDir = resolve(tmpdir(), `page-withu-${cacheKey}`)
|
|
24
25
|
const basePath = process.env.BASE_PATH || '/'
|
|
26
|
+
const externalUrlPattern = /^[a-z][a-z\d+.-]*:/i
|
|
27
|
+
|
|
28
|
+
function resolveProjectAssetPath(path) {
|
|
29
|
+
if (!path || path === '/src/assets/bulb.svg' || externalUrlPattern.test(path) || path.startsWith('//') || path.startsWith('/')) return null
|
|
30
|
+
const relativePath = path.replace(/^\.\//, '')
|
|
31
|
+
return {
|
|
32
|
+
relativePath,
|
|
33
|
+
sourcePath: join(projectRoot, relativePath),
|
|
34
|
+
outputPath: join(resolve(projectRoot, 'dist'), relativePath),
|
|
35
|
+
href: withBasePath(`/${relativePath}`),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
25
38
|
|
|
26
39
|
function slugify(text) {
|
|
27
40
|
return text
|
|
@@ -159,6 +172,27 @@ function markdown(deps) {
|
|
|
159
172
|
}
|
|
160
173
|
}
|
|
161
174
|
|
|
175
|
+
function projectAssets(userConfig) {
|
|
176
|
+
return {
|
|
177
|
+
name: 'project-assets',
|
|
178
|
+
configureServer(server) {
|
|
179
|
+
const favicon = resolveProjectAssetPath(userConfig.favicon)
|
|
180
|
+
if (!favicon) return
|
|
181
|
+
|
|
182
|
+
server.middlewares.use((req, res, next) => {
|
|
183
|
+
const pathname = decodeURI((req.url || '').split('?')[0])
|
|
184
|
+
if (pathname !== `/${favicon.relativePath}` || !existsSync(favicon.sourcePath)) {
|
|
185
|
+
next()
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (favicon.sourcePath.endsWith('.svg')) res.setHeader('Content-Type', 'image/svg+xml')
|
|
190
|
+
res.end(readFileSync(favicon.sourcePath))
|
|
191
|
+
})
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
162
196
|
function staticHtmlRoutes(userConfig) {
|
|
163
197
|
return {
|
|
164
198
|
name: 'static-html-routes',
|
|
@@ -178,7 +212,7 @@ function staticHtmlRoutes(userConfig) {
|
|
|
178
212
|
|
|
179
213
|
const pageSize = userConfig.pagination?.pageSize || 5
|
|
180
214
|
const totalPages = Math.max(1, Math.ceil(blogPosts.length / pageSize))
|
|
181
|
-
const routes = ['domains.html', 'blog.html']
|
|
215
|
+
const routes = ['index.html', 'domains.html', 'blog.html']
|
|
182
216
|
for (let page = 2; page <= totalPages; page += 1) routes.push(`blog/page/${page}.html`)
|
|
183
217
|
for (const slug of blogPosts) routes.push(`blog/${slug}.html`)
|
|
184
218
|
|
|
@@ -187,6 +221,19 @@ function staticHtmlRoutes(userConfig) {
|
|
|
187
221
|
mkdirSync(dirname(file), { recursive: true })
|
|
188
222
|
writeFileSync(file, index)
|
|
189
223
|
}
|
|
224
|
+
|
|
225
|
+
const favicon = resolveProjectAssetPath(userConfig.favicon)
|
|
226
|
+
if (favicon && existsSync(favicon.sourcePath)) {
|
|
227
|
+
mkdirSync(dirname(favicon.outputPath), { recursive: true })
|
|
228
|
+
copyFileSync(favicon.sourcePath, favicon.outputPath)
|
|
229
|
+
|
|
230
|
+
for (const route of routes) {
|
|
231
|
+
const file = join(dist, route)
|
|
232
|
+
if (!existsSync(file)) continue
|
|
233
|
+
const html = readFileSync(file, 'utf8').replace(/<link rel="icon"[^>]*href="[^"]*"[^>]*>/, `<link rel="icon" href="${favicon.href}">`)
|
|
234
|
+
writeFileSync(file, html)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
190
237
|
},
|
|
191
238
|
}
|
|
192
239
|
}
|
|
@@ -214,7 +261,7 @@ export default defineConfig(async () => {
|
|
|
214
261
|
const userConfig = (await import(`${pathToFileURL(userConfigFile).href}?t=${Date.now()}`)).default
|
|
215
262
|
|
|
216
263
|
return {
|
|
217
|
-
root:
|
|
264
|
+
root: appRoot,
|
|
218
265
|
base: basePath,
|
|
219
266
|
cacheDir,
|
|
220
267
|
resolve: {
|
|
@@ -224,11 +271,11 @@ export default defineConfig(async () => {
|
|
|
224
271
|
'@page-withu/user-content': userContentDir,
|
|
225
272
|
},
|
|
226
273
|
},
|
|
227
|
-
plugins: [vueModule.default(), markdown(deps), staticHtmlRoutes(userConfig)],
|
|
274
|
+
plugins: [vueModule.default(), markdown(deps), projectAssets(userConfig), staticHtmlRoutes(userConfig)],
|
|
228
275
|
server: {
|
|
229
276
|
port: 5500,
|
|
230
277
|
fs: {
|
|
231
|
-
allow: [
|
|
278
|
+
allow: [appRoot, projectRoot, dependencyRoot],
|
|
232
279
|
},
|
|
233
280
|
},
|
|
234
281
|
build: {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|