@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 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
- # 创建项目,默认配置会写入 config.js,后续可自行编辑
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 templateDir = path.join(packageRoot, 'template');
17
- const viteConfigPath = path.join(templateDir, 'vite.config.js');
18
- const runtimeDependencies = {
19
- '@traptitech/markdown-it-katex': '^3.6.0',
20
- '@vitejs/plugin-vue': '^5.2.3',
21
- 'gray-matter': '^4.0.3',
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(templateDir, 'config.js'), path.join(fullPath, 'config.js'));
125
- await fs.cp(path.join(templateDir, 'content'), path.join(fullPath, 'content'), { recursive: true });
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
- - 修改 `template/` 后,需要用模板本身或新生成项目执行一次 `page-withu build`。
13
- - 不要引入大型 UI 框架,样式优先维护在 `template/src/styles/main.css`。
14
- - Markdown 能力优先在 `template/vite.config.js` 中集中处理,避免分散到多个组件。
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 # CLI 入口
22
- ├── docs/ # 使用、部署、开发文档
23
- ├── template/
24
- │ ├── config.js # 新项目默认配置
25
- │ ├── content/ # 新项目默认内容
26
- ├── src/ # 包内 Vue 运行时源码
27
- └── vite.config.js # 包内 Markdown 渲染与静态路由生成
28
- ├── package.json # page-withu CLI 与运行时依赖配置
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
- - 必要时修改 `template/config.js` 的默认字段。
74
- - 必要时修改模板中读取配置的 Vue 组件。
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
- ## 开发 Template
92
+ ## 开发包内运行时与默认内容
90
93
 
91
- 模板应用位于:
94
+ 包内 Vue 运行时应用位于:
92
95
 
93
96
  ```text
94
- template/
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 template
101
- node ../bin/page-withu.js serve
109
+ cd src/defaults
110
+ node ../../bin/page-withu.js serve
102
111
  ```
103
112
 
104
113
  常见开发位置:
105
114
 
106
- - `template/src/app.vue`:主页面路由、布局、浏览器标签页标题和 favicon。
107
- - `template/src/components/blog-list.vue`:博客列表、分页、搜索、标签和时间线。
108
- - `template/src/components/blog-detail.vue`:文章详情、ToC、图片放大、Mermaid 渲染。
109
- - `template/src/styles/main.css`:全局样式、亮暗模式、响应式布局、Markdown 样式。
110
- - `template/vite.config.js`:Markdown 插件、callout、代码高亮、静态 `.html` 路由生成。
111
- - `template/config.js`:站点默认配置。
112
- - `template/content/`:默认内容样例。
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 template
125
- node ../bin/page-withu.js build
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
- - `template/`
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
@@ -10,6 +10,8 @@
10
10
  export default {
11
11
  // 站点标题,会显示在页面顶部
12
12
  title: "PageWithU",
13
+ // 浏览器标签页图标,本地路径相对项目根目录,例如 "./content/logo.svg"
14
+ favicon: "/src/assets/bulb.svg",
13
15
  // 作者名,会显示在页脚版权信息中
14
16
  author: "Your Name",
15
17
  // 版权年份,默认使用当前年份
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x-withu/page-withu",
3
- "version": "1.1.1",
4
- "description": "A lightweight, elegant personal homepage generator.",
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
- "homepage",
28
- "blog",
29
- "generator",
39
+ "static site generator",
40
+ "lightweight",
30
41
  "vue",
31
42
  "vite"
32
43
  ],
@@ -4,7 +4,7 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>WithU</title>
7
+ <title>PageWithU</title>
8
8
  <link rel="icon" type="image/svg" href="/src/assets/bulb.svg">
9
9
  <script>
10
10
  (function () {
@@ -41,12 +41,18 @@
41
41
 
42
42
 
43
43
  <footer>
44
- <p>&copy; {{ userConfig.year }} {{ userConfig.author }}</p>
45
- <div class="footer-links">
46
- <template v-for="(link, index) in userConfig.footerLinks" :key="link.label">
47
- <a :href="link.url">{{ link.label }}</a>
48
- <span v-if="index < userConfig.footerLinks.length - 1" class="separator">&middot;</span>
49
- </template>
44
+ <div class="footer-content">
45
+ <div class="footer-meta">
46
+ <p>&copy; {{ 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">&middot;</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/blog-list.vue'
61
- import BlogDetail from './components/blog-detail.vue'
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: 'domains', label: 'Domains' },
71
- { page: 'blog', label: 'Blog' },
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 || 'WithU'
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 = favicon === defaultFavicon || !favicon.startsWith('/') ? favicon : withBasePath(favicon)
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
- <header class="blog-article-header">
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
- </header>
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
- <a v-for="item in toc" :key="item.id" :href="`#${item.id}`"
39
- :class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]" @click="tocOpen = false">
40
- {{ item.title }}
41
- </a>
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: 16px;
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
- font-size: 1.45rem;
177
- font-weight: 750;
178
- color: var(--color-heading);
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-heading);
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: 12px;
271
+ gap: 24px;
272
+ min-width: 0;
194
273
  }
195
274
 
196
275
  /* ===== Navigation Bar ===== */
197
276
  .nav-bar {
198
277
  display: flex;
199
- gap: 4px;
200
- padding: 4px;
201
- border: 1px solid var(--color-card-border);
202
- border-radius: 999px;
203
- background: var(--color-card-bg);
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: 5px 12px;
288
+ padding: 6px 0;
208
289
  font-size: 0.9rem;
209
- font-weight: 500;
210
- color: var(--color-subtext);
290
+ font-weight: 650;
291
+ color: var(--color-subtext-light, #8a9ba8);
211
292
  text-decoration: none;
212
- border-radius: 999px;
213
- transition: color 0.2s ease, background-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:hover {
218
- color: var(--color-text);
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-link);
224
- background: var(--color-link-active-bg);
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: 38px;
425
- height: 38px;
426
- border: 1px solid var(--color-card-border);
543
+ width: 34px;
544
+ height: 34px;
545
+ border: none;
427
546
  border-radius: 50%;
428
- background: var(--color-card-bg);
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.3s ease, border-color 0.3s ease, transform 0.15s ease;
434
- box-shadow: 0 1px 4px var(--color-card-active-shadow);
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
- border-color: var(--color-card-hover-border);
439
- box-shadow: 0 2px 8px var(--color-card-hover-shadow);
440
- transform: scale(1.05);
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.95);
573
+ transform: scale(0.92);
445
574
  }
446
575
 
447
576
  .theme-toggle svg {
448
- width: 19px;
449
- height: 19px;
577
+ width: 18px;
578
+ height: 18px;
450
579
  fill: none;
451
- stroke: var(--color-subtext);
580
+ stroke: currentColor;
452
581
  stroke-width: 2;
453
582
  stroke-linecap: round;
454
583
  stroke-linejoin: round;
455
- transition: stroke 0.3s ease;
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 0 16px 16px;
1176
+ padding: 0 4px 16px 16px;
1011
1177
  border-left: 1px solid var(--color-footer-border);
1012
- overflow-y: auto;
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
- align-items: flex-start;
1209
- flex-direction: column;
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
- width: 100%;
1214
- justify-content: space-between;
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
- font-size: 1.3rem;
1457
+ max-width: 120px;
1458
+ font-size: 0.9rem;
1231
1459
  }
1232
1460
 
1233
1461
  .nav-item {
1234
- padding: 5px 10px;
1235
- font-size: 0.84rem;
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 runtimeRoot = dirname(fileURLToPath(import.meta.url))
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 packageRuntimeRoot = process.env.PAGE_WITHU_RUNTIME_ROOT
11
- const dependencyRoot = packageRuntimeRoot ? resolve(packageRuntimeRoot, 'node_modules') : resolve(runtimeRoot, 'node_modules')
12
- const runtimeRequire = packageRuntimeRoot ? createRequire(resolve(packageRuntimeRoot, 'package.json')) : createRequire(import.meta.url)
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: runtimeRoot,
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: [runtimeRoot, projectRoot, dependencyRoot],
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