@xingwangzhe/stalux 1.26.2 → 1.27.1

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.
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+
3
+ // scripts/cli.ts
4
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ function timestamp() {
7
+ return new Date().toISOString().replace("T", " ").slice(0, 19);
8
+ }
9
+ function getConfigYamls() {
10
+ const year = new Date().getFullYear();
11
+ const now = new Date().toISOString();
12
+ return {
13
+ "site.yml": `id: site
14
+ lang: en
15
+ title: "My Blog"
16
+ description: "A blog built with Stalux theme"
17
+ url: "https://example.com"
18
+ timezone: "Asia/Shanghai"
19
+ `,
20
+ "author.yml": `id: author
21
+ name: "Your Name"
22
+ avatar: "/avatar.png"
23
+ bio: "Blogger & Developer"
24
+ `,
25
+ "navs.yml": `id: navs
26
+ items:
27
+ - title: "Home"
28
+ link: "/"
29
+ icon: "house"
30
+ - title: "Archives"
31
+ link: "/archives"
32
+ icon: "archive"
33
+ - title: "Tags"
34
+ link: "/tags"
35
+ icon: "tag"
36
+ - title: "Categories"
37
+ link: "/categories"
38
+ icon: "folder"
39
+ - title: "Words"
40
+ link: "/words"
41
+ icon: "quote"
42
+ - title: "Links"
43
+ link: "/links"
44
+ icon: "link"
45
+ `,
46
+ "head.yml": `id: head
47
+ # googleAnalyticsId: "G-XXXXXXXXXX"
48
+ # bingClarityId: "xxxxxxxxxx"
49
+ # umami:
50
+ # id: ""
51
+ # url: ""
52
+ # anyhead: ""
53
+ `,
54
+ "footer.yml": `id: footer
55
+ buildtime: "${now}"
56
+ copyright:
57
+ enabled: true
58
+ startYear: ${year}
59
+ customText: ""
60
+ theme:
61
+ showPoweredBy: true
62
+ showThemeInfo: true
63
+ beian:
64
+ icp:
65
+ enabled: false
66
+ number: ""
67
+ security:
68
+ enabled: false
69
+ text: ""
70
+ number: ""
71
+ badges:
72
+ - label: "Powered by"
73
+ message: "Astro"
74
+ color: "orange"
75
+ style: "flat-square"
76
+ href: "https://astro.build/"
77
+ custom: |
78
+ <div id="custom-footer-hook"></div>
79
+ `,
80
+ "media-links.yml": `id: media-links
81
+ items:
82
+ - icon: "github"
83
+ link: "https://github.com/yourname"
84
+ `,
85
+ "links.yml": `id: links
86
+ title: "Links"
87
+ description: "Friends & Resources"
88
+ sites:
89
+ - name: "Astro"
90
+ description: "The web framework for content-driven websites"
91
+ link: "https://astro.build"
92
+ icon: "https://astro.build/favicon.svg"
93
+ `,
94
+ "comment.yml": `id: comment
95
+ enabled: false
96
+ `,
97
+ "promote.yml": `id: promote
98
+ export_md: false
99
+ # llm_promote: |
100
+ `,
101
+ "ai-discovery.yml": `id: ai-discovery
102
+ conformance: "disabled"
103
+ `,
104
+ "typetexts.yml": `id: typetexts
105
+ items:
106
+ - "Free for free, not free for charge!"
107
+ - "Where's the any key?"
108
+ - "Press F12?"
109
+ - "Hello World!"
110
+ `
111
+ };
112
+ }
113
+ function getExamplePost() {
114
+ const date = timestamp();
115
+ return `---
116
+ title: Hello Stalux!
117
+ abbrlink: hello-stalux
118
+ date: "${date}"
119
+ tags: [Stalux, Getting Started]
120
+ categories: [Blog]
121
+ desc: Welcome to Stalux!
122
+ cc: CC-BY-NC-SA-4.0
123
+ ---
124
+
125
+ Welcome to **Stalux**!
126
+
127
+ This is your first post. Edit or delete it, then start writing your own content.
128
+
129
+ ## Features
130
+
131
+ - \uD83C\uDF19 Dark mode with elegant glassmorphism design
132
+ - \uD83D\uDD24 Per-route font subsetting — each page loads only needed characters
133
+ - \uD83D\uDD0D Full-text search (Pagefind)
134
+ - \uD83D\uDCE1 RSS & Atom feeds
135
+ - \uD83D\uDCAC Comment system (Waline)
136
+ - \uD83D\uDCCA Mermaid diagrams
137
+ - \uD83D\uDCD0 Math formula rendering
138
+ - \uD83D\uDDBC️ PhotoSwipe lightbox
139
+
140
+ For more details, visit [Stalux documentation](https://stalux.needhelp.icu).
141
+ `;
142
+ }
143
+ function getAboutMd() {
144
+ return `---
145
+ title: About Me
146
+ description: About this blog and the author
147
+ ---
148
+
149
+ Hello! Welcome to my blog.
150
+
151
+ This is my personal space where I share technology, life, and thoughts.
152
+
153
+ ## About This Site
154
+
155
+ - Built with [Astro](https://astro.build) + [Stalux](https://stalux.needhelp.icu)
156
+ - Content licensed under CC-BY-NC-SA-4.0
157
+ - Source code hosted on GitHub
158
+ `;
159
+ }
160
+ function getWordsTemplate() {
161
+ const date = timestamp();
162
+ return `---
163
+ source: "Author Name"
164
+ link: "https://example.com"
165
+ sourceDate: ""
166
+ date: "${date}"
167
+ ---
168
+
169
+ > A quote or short note...
170
+ `;
171
+ }
172
+ function getWordsExample() {
173
+ const date = timestamp();
174
+ return `---
175
+ source: "Albert Einstein"
176
+ link: "https://example.com"
177
+ sourceDate: ""
178
+ date: "${date}"
179
+ ---
180
+
181
+ > Imagination is more important than knowledge.
182
+ `;
183
+ }
184
+ function writeIfMissing(filePath, label, content) {
185
+ if (existsSync(filePath)) {
186
+ console.log(` ⏭ ${label} (exists, skipped)`);
187
+ return;
188
+ }
189
+ mkdirSync(dirname(filePath), { recursive: true });
190
+ writeFileSync(filePath, content);
191
+ console.log(` ✅ ${label}`);
192
+ }
193
+ function main([command, targetArg = "."] = process.argv.slice(2)) {
194
+ if (!command || command === "--help" || command === "-h") {
195
+ printHelp();
196
+ return;
197
+ }
198
+ if (command !== "init") {
199
+ console.error(`❌ Unknown command: ${command}`);
200
+ printHelp();
201
+ process.exitCode = 1;
202
+ return;
203
+ }
204
+ const targetPath = resolve(process.cwd(), targetArg);
205
+ const contentRoot = join(targetPath, "stalux");
206
+ console.log(`\uD83D\uDCE6 Initializing Stalux content in ${targetPath}...
207
+ `);
208
+ for (const dir of ["config", "posts", "about", "words"]) {
209
+ mkdirSync(join(contentRoot, dir), { recursive: true });
210
+ }
211
+ const files = {
212
+ ...Object.fromEntries(Object.entries(getConfigYamls()).map(([name, content]) => [`config/${name}`, content])),
213
+ "posts/hello-stalux.md": getExamplePost(),
214
+ "about/index.md": getAboutMd(),
215
+ "words/_template.md": getWordsTemplate(),
216
+ "words/einstein-imagination.md": getWordsExample()
217
+ };
218
+ for (const [file, content] of Object.entries(files)) {
219
+ writeIfMissing(join(contentRoot, file), `stalux/${file}`, content);
220
+ }
221
+ console.log(`
222
+ ✅ Done! Stalux content initialized.
223
+ `);
224
+ printNextSteps();
225
+ }
226
+ function printHelp() {
227
+ console.log(`
228
+ Stalux — Modern Astro Blog Theme
229
+
230
+ Usage:
231
+ stalux init Initialize Stalux content (configs, posts, pages)
232
+ stalux init my-blog Initialize in a subdirectory
233
+
234
+ What it does:
235
+ - Creates stalux/config/*.yml (all configuration files)
236
+ - Creates stalux/posts/hello-stalux.md (example post)
237
+ - Creates stalux/about/index.md (about page)
238
+ - Creates stalux/words/ (example quotes)
239
+ - Does NOT overwrite existing files
240
+
241
+ Prerequisites:
242
+ 1. Create an Astro project: bun create astro
243
+ 2. Install stalux: bun add @xingwangzhe/stalux
244
+ 3. Run init: bunx stalux init
245
+ `);
246
+ }
247
+ function printNextSteps() {
248
+ console.log(" \uD83D\uDCDD Next Steps:");
249
+ console.log("");
250
+ console.log(" 1. Add stalux to your astro.config.ts:");
251
+ console.log(' import stalux from "@xingwangzhe/stalux";');
252
+ console.log(' integrations: [stalux({ contentDir: "stalux" })],');
253
+ console.log("");
254
+ console.log(" 2. Add content collections to src/content.config.ts:");
255
+ console.log(' import { defineCollections } from "@xingwangzhe/stalux/schemas/collections";');
256
+ console.log(' export const collections = defineCollections({ contentDir: "stalux" });');
257
+ console.log("");
258
+ console.log(" 3. Start writing content in stalux/posts/");
259
+ console.log(" 4. Run: bun run dev");
260
+ console.log("");
261
+ console.log(" \uD83D\uDCD6 Docs: https://stalux.needhelp.icu");
262
+ console.log("");
263
+ }
264
+ main();
package/README.md CHANGED
@@ -23,7 +23,7 @@ bun add @xingwangzhe/stalux # Install theme (all dependencies included)
23
23
  bunx stalux init # Generate stalux/ content directory
24
24
  ```
25
25
 
26
- Then configure `astro.config.mjs`. All plugins are bundled into the Stalux integration by default — **no manual configuration is needed**:
26
+ Requires Node.js 26 or later. Then configure `astro.config.ts`. All plugins are bundled into the Stalux integration by default — **no manual configuration is needed**:
27
27
 
28
28
  - **Markdown**: Mermaid (MDAST detection + HAST/SVG rendering), math formulas (Temml → MathML), word count / feature flags, and PhotoSwipe image lightbox are injected into the default `satteri()` processor automatically (math / frontmatter / gfm / smart punctuation are enabled by default).
29
29
  - **Sitemap**: `@astrojs/sitemap` is bundled (`.md` source endpoints are filtered out by default).
@@ -88,7 +88,7 @@ bun run dev
88
88
  - 🤖 **LLM discovery files** (llms.txt / llms-full.txt)
89
89
  - 🤝 **WebMCP tools** for AI agents (W3C draft, pure front-end)
90
90
  - ⚡ **View transitions** for smooth navigation
91
- - 🌐 **i18n** (English / Chinese)
91
+ - 🌐 **i18n** (English / Simplified Chinese; missing locale keys fall back to English)
92
92
  - 🏷️ **Tags, categories, archives** pages
93
93
  - 🎨 **Component override system** (Starlight-style)
94
94
  - 🛠️ **Easy YAML configuration** — no coding required
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.26.2",
3
+ "version": "1.27.1",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -19,8 +19,11 @@
19
19
  "type": "git",
20
20
  "url": "git+https://github.com/xingwangzhe/stalux.git"
21
21
  },
22
+ "engines": {
23
+ "node": ">=26"
24
+ },
22
25
  "bin": {
23
- "stalux": "scripts/cli.mjs"
26
+ "stalux": ".release/cli.mjs"
24
27
  },
25
28
  "files": [
26
29
  "src/index.ts",
@@ -40,7 +43,7 @@
40
43
  "src/scripts/**/*.ts",
41
44
  "public/background/**",
42
45
  "src/assets/**/*",
43
- "scripts/cli.mjs",
46
+ ".release/cli.mjs",
44
47
  "!src/content.config.ts"
45
48
  ],
46
49
  "type": "module",
@@ -65,10 +68,13 @@
65
68
  },
66
69
  "scripts": {
67
70
  "dev": "astro dev",
68
- "build": "astro build && bun run verify:build",
71
+ "build": "astro build && node scripts/verify-build.ts",
69
72
  "preview": "astro preview",
70
73
  "astro": "astro",
71
- "init": "bun scripts/cli.mjs init",
74
+ "init": "node scripts/cli.ts init",
75
+ "node:check": "tsc6 --project tsconfig.node.json",
76
+ "build:cli": "bun build scripts/cli.ts --target=node --outfile .release/cli.mjs",
77
+ "prepack": "bun run build:cli",
72
78
  "check": "biome check .",
73
79
  "check:fix": "biome check --write .",
74
80
  "format": "biome format --write .",
@@ -80,11 +86,11 @@
80
86
  "test:run": "vitest run",
81
87
  "test:coverage": "vitest run --coverage",
82
88
  "knip": "knip",
83
- "verify:build": "bun scripts/verify-build.mjs",
84
- "validate": "bun run check && bun run astro:check && bun run knip && bun run test:coverage && bun run build",
89
+ "verify:build": "node scripts/verify-build.ts",
90
+ "validate": "bun run check && bun run node:check && bun run astro:check && bun run knip && bun run test:coverage && bun run build && bun run build:cli",
85
91
  "prepare": "git config core.hooksPath scripts/git-hooks",
86
92
  "dev:debug": "STALUX_DEBUG=1 astro dev --verbose",
87
- "build:debug": "STALUX_DEBUG=1 astro build --verbose && bun run verify:build"
93
+ "build:debug": "STALUX_DEBUG=1 astro build --verbose && node scripts/verify-build.ts"
88
94
  },
89
95
  "dependencies": {
90
96
  "@astrojs/markdown-satteri": "0.4.2",
@@ -112,6 +118,7 @@
112
118
  "devDependencies": {
113
119
  "@astrojs/check": "0.9.10",
114
120
  "@biomejs/biome": "2.5.14",
121
+ "@types/node": "^26.6.2",
115
122
  "@vitest/coverage-v8": "5.0.1",
116
123
  "astro": "^7.3.4",
117
124
  "knip": "^6.37.0",
@@ -68,7 +68,6 @@ const walineConfig = {
68
68
 
69
69
  <style is:global>
70
70
  :root {
71
- --waline-accent: var(--stalux-accent-color);
72
71
  --waline-accent-rgb: 100, 255, 218;
73
72
  }
74
73
 
@@ -18,22 +18,4 @@ import { getSidebarAnimation } from "@utils/view-transitions";
18
18
  <div class={style.sidebarContainer}>
19
19
  <RandomPost />
20
20
  </div>
21
-
22
- <style>
23
- :global(.stalux-root) {
24
- --sidebar-width: 280px;
25
- }
26
-
27
- @media (max-width: 1200px) {
28
- :global(.stalux-root) {
29
- --sidebar-width: 240px;
30
- }
31
- }
32
-
33
- @media (max-width: 768px) {
34
- :global(.stalux-root) {
35
- --sidebar-width: 0;
36
- }
37
- }
38
- </style>
39
21
  </aside>
@@ -18,7 +18,7 @@ const { t } = createTranslator((siteConfig.lang as string) || "zh-CN");
18
18
  ---
19
19
 
20
20
  {(props.prev || props.next) && (
21
- <nav class={style.postNavigation} aria-label="文章导航">
21
+ <nav class={style.postNavigation} aria-label={t("post.navigation")}>
22
22
  <div class={style.postNavigationInner}>
23
23
  {props.prev ? (
24
24
  <a
package/src/i18n/en.json CHANGED
@@ -24,6 +24,7 @@
24
24
  "backHome": "Back to Home"
25
25
  },
26
26
  "post": {
27
+ "navigation": "Post navigation",
27
28
  "publishTime": "Published: ",
28
29
  "updateTime": "Updated: ",
29
30
  "readingTime": "🕒 Reading time: ",
@@ -24,6 +24,7 @@
24
24
  "backHome": "返回首页"
25
25
  },
26
26
  "post": {
27
+ "navigation": "文章导航",
27
28
  "publishTime": "发布时间:",
28
29
  "updateTime": "更新时间:",
29
30
  "readingTime": "🕒 阅读时间:",
@@ -56,7 +56,6 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
56
56
  font-weight: bold;
57
57
  margin: 0;
58
58
  color: var(--white-90p);
59
- text-shadow: 0 2px 10px var(--shadow-medium);
60
59
  }
61
60
 
62
61
  .error-content p {
@@ -112,9 +112,6 @@ ul {
112
112
  --stalux-bg-filter: none;
113
113
  --stalux-text-shadow: #64ffda 0.1rem 0.1rem 0.2rem;
114
114
  --stalux-accent-color: #64ffda;
115
- --stalux-search-backdrop: rgba(0, 0, 0, 0.1);
116
- --stalux-search-box-bg: rgba(0, 0, 0, 0.1);
117
- --stalux-search-border: rgba(0, 0, 0, 0.2);
118
115
 
119
116
  /* 卡片和标签颜色 Token(可在这里统一调整主题色/透明度) */
120
117
  --card-bg: rgba(0, 0, 0, 0.2);
@@ -146,19 +143,13 @@ ul {
146
143
  --white-30p: rgba(255, 255, 255, 0.3);
147
144
  --white-20p: rgba(255, 255, 255, 0.2);
148
145
  --white-15p: rgba(255, 255, 255, 0.15);
149
- --white-12p: rgba(255, 255, 255, 0.12);
150
146
  --white-10p: rgba(255, 255, 255, 0.1);
151
- --white-8p: rgba(255, 255, 255, 0.08);
152
147
  --white-05p: rgba(255, 255, 255, 0.05);
153
148
 
154
- --black-full: #000000;
155
149
  --black-30p: rgba(0, 0, 0, 0.3);
156
- --black-28p: rgba(0, 0, 0, 0.28);
157
150
  --black-20p: rgba(0, 0, 0, 0.2);
158
151
  --black-15p: rgba(0, 0, 0, 0.15);
159
152
  --black-10p: rgba(0, 0, 0, 0.1);
160
- --black-05p: rgba(0, 0, 0, 0.05);
161
- --black-03p: rgba(0, 0, 0, 0.03);
162
153
 
163
154
  /* 强调色变体 */
164
155
  --accent-90p: rgba(100, 255, 218, 0.9);
@@ -170,7 +161,6 @@ ul {
170
161
  --accent-40p: rgba(100, 255, 218, 0.4);
171
162
  --accent-30p: rgba(100, 255, 218, 0.3);
172
163
  --accent-20p: rgba(100, 255, 218, 0.2);
173
- --accent-10p: rgba(100, 255, 218, 0.1);
174
164
 
175
165
  /* 常用过渡和动画 */
176
166
  --transition-fast: 0.18s ease;
@@ -181,7 +171,6 @@ ul {
181
171
  --transition-slowest: 0.6s ease;
182
172
 
183
173
  --easing-bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275);
184
- --easing-smooth: cubic-bezier(0.25, 0.9, 0.2, 1);
185
174
 
186
175
  /* 常用间距值 */
187
176
  --space-2xs: 0.25rem;
@@ -197,9 +186,7 @@ ul {
197
186
  --radius-sm: 4px;
198
187
  --radius-md: 6px;
199
188
  --radius-lg: 8px;
200
- --radius-xl: 10px;
201
189
  --radius-full: 9999px;
202
- --radius-circle: 50%;
203
190
 
204
191
  /* 常用阴影 */
205
192
  --shadow-sm: 0 2px 5px rgba(0, 0, 0, 0.1);
@@ -210,18 +197,12 @@ ul {
210
197
  --shadow-dark: rgba(0, 0, 0, 0.753);
211
198
 
212
199
  /* 语义化颜色 */
213
- --date-text-color: #e8f2ff;
214
- --updated-text-color: #fff7de;
215
200
  --muted-text: #bbb;
216
- --border-color-light: #e5e5e5;
217
201
 
218
202
  /* 字体族栈 */
219
203
  /* --font-body 由 Astro Fonts API 提供(--font-code 同理,见集成注入的 fonts 配置) */
220
- --font-body-serif: "Noto Serif CJK SC", "Source Han Serif SC", "STSong", Georgia, serif;
221
204
 
222
205
  /* 字体大小系统 */
223
- --font-size-title: 2.8rem;
224
- --font-size-h2: 2rem;
225
206
  --font-size-subtitle: 1.35rem;
226
207
  --font-size-base: 1.1rem;
227
208
  --font-size-small: 0.95rem;
@@ -232,7 +213,6 @@ ul {
232
213
  --border-thin: 1px;
233
214
  --border-medium: 2px;
234
215
  --border-thick: 3px;
235
- --border-thicker: 4px;
236
216
  }
237
217
  ::-webkit-scrollbar {
238
218
  width: 8px;
@@ -296,7 +296,6 @@
296
296
  --dot: 10px;
297
297
  font-size: 1rem;
298
298
  margin: 0 0 0.25rem 0;
299
- color: var(--stalux-muted);
300
299
  }
301
300
 
302
301
  .monthTitle::before {
@@ -55,8 +55,9 @@ nav {
55
55
  box-shadow: var(--shadow-md);
56
56
  }
57
57
 
58
+ /* 维持既有渲染:未定义的 --black-50p 使 hover 背景解析为透明 */
58
59
  .scrollBtn:hover {
59
- background: var(--black-50p);
60
+ background: transparent;
60
61
  }
61
62
 
62
63
  .scrollBtn[data-visible] {
@@ -96,10 +96,6 @@
96
96
 
97
97
  /* 小屏幕手机 */
98
98
  @media (max-width: 480px) {
99
- .postLayout {
100
- padding: 0;
101
- }
102
-
103
99
  .container {
104
100
  gap: 1.25rem;
105
101
  padding: 0;
@@ -1,11 +1,15 @@
1
+ .h1,
2
+ .description {
3
+ text-align: center;
4
+ }
5
+
1
6
  .h1 {
2
7
  margin-top: 1.5em;
3
- text-align: center;
4
8
  text-shadow: var(--stalux-text-shadow);
5
9
  font-size: 2.5em;
6
10
  }
11
+
7
12
  .description {
8
- text-align: center;
9
13
  margin-bottom: 1.5em;
10
14
  font-size: 1.2em;
11
15
  color: var(--white-60p);
@@ -1,12 +1,16 @@
1
+ .title,
2
+ .description {
3
+ text-align: center;
4
+ }
5
+
1
6
  .title {
2
7
  margin-top: 1.5em;
3
- text-align: center;
4
8
  justify-content: center;
5
9
  text-shadow: var(--stalux-text-shadow);
6
10
  font-size: 2.5em;
7
11
  }
12
+
8
13
  .description {
9
- text-align: center;
10
14
  margin-bottom: 1.5em;
11
15
  font-size: 1.2em;
12
16
  color: var(--white-60p);
@@ -1,12 +1,15 @@
1
+ .title,
2
+ .description {
3
+ text-align: center;
4
+ }
5
+
1
6
  .title {
2
7
  margin-top: 1.5em;
3
- text-align: center;
4
8
  text-shadow: var(--stalux-text-shadow);
5
9
  font-size: 2.5em;
6
10
  }
7
11
 
8
12
  .description {
9
- text-align: center;
10
13
  margin-bottom: 1.5em;
11
14
  font-size: 1.2em;
12
15
  color: var(--white-60p);
@@ -1,11 +1,15 @@
1
+ .title,
2
+ .description {
3
+ text-align: center;
4
+ }
5
+
1
6
  .title {
2
7
  margin-top: 1.5em;
3
- text-align: center;
4
8
  text-shadow: var(--stalux-text-shadow);
5
9
  font-size: 2.5em;
6
10
  }
11
+
7
12
  .description {
8
- text-align: center;
9
13
  margin-bottom: 1.5em;
10
14
  font-size: 1.2em;
11
15
  color: var(--white-60p);
@@ -1,12 +1,15 @@
1
+ .title,
2
+ .description {
3
+ text-align: center;
4
+ }
5
+
1
6
  .title {
2
7
  margin-top: 1.5em;
3
- text-align: center;
4
8
  text-shadow: var(--stalux-text-shadow);
5
9
  font-size: 2.5em;
6
10
  }
7
11
 
8
12
  .description {
9
- text-align: center;
10
13
  margin-bottom: 1.5em;
11
14
  font-size: 1.2em;
12
15
  color: var(--white-60p);
package/src/utils/i18n.ts CHANGED
@@ -12,7 +12,9 @@ export interface Translator {
12
12
  }
13
13
 
14
14
  export function createTranslator(lang: string): Translator {
15
- const dict = dicts[lang] || dicts["zh-CN"];
15
+ const normalizedLang = lang.toLowerCase();
16
+ const language = normalizedLang.startsWith("en") ? "en" : "zh-CN";
17
+ const dict = dicts[language] ?? dicts["zh-CN"] ?? en;
16
18
 
17
19
  function t(key: string, params?: Record<string, string | number>): string {
18
20
  const keys = key.split(".");
@@ -21,12 +23,12 @@ export function createTranslator(lang: string): Translator {
21
23
  if (text && typeof text === "object") {
22
24
  text = (text as Record<string, unknown>)[k];
23
25
  } else {
24
- return key;
26
+ return lookup(en, key) ?? key;
25
27
  }
26
28
  }
27
- if (typeof text !== "string") return key;
29
+ if (typeof text !== "string" || !text.trim()) text = lookup(en, key) ?? key;
28
30
 
29
- let result = text;
31
+ let result = text as string;
30
32
  if (params) {
31
33
  for (const [k, v] of Object.entries(params)) {
32
34
  result = result.replace(`{${k}}`, String(v));
@@ -35,7 +37,16 @@ export function createTranslator(lang: string): Translator {
35
37
  return result;
36
38
  }
37
39
 
38
- return { t, lang };
40
+ return { t, lang: language };
41
+ }
42
+
43
+ function lookup(dictionary: Record<string, unknown>, key: string): string | undefined {
44
+ let value: unknown = dictionary;
45
+ for (const part of key.split(".")) {
46
+ if (!value || typeof value !== "object") return;
47
+ value = (value as Record<string, unknown>)[part];
48
+ }
49
+ return typeof value === "string" && value.trim() ? value : undefined;
39
50
  }
40
51
 
41
52
  /**
package/scripts/cli.mjs DELETED
@@ -1,331 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Stalux CLI — 主题初始化工具
5
- *
6
- * 用法:
7
- * bunx stalux init # 在当前目录初始化内容模板
8
- * bunx stalux init my-blog # 在子目录初始化项目
9
- *
10
- * 说明:
11
- * - 只在 stalux/ 目录下生成内容配置文件和示例文章
12
- * - 不会覆盖已有的 package.json、astro.config.mjs、src/content.config.ts
13
- * - 静默运行,无交互式问答
14
- * - 静默运行,无交互式问答
15
- */
16
-
17
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
18
- import { join, resolve as pathResolve } from "node:path";
19
-
20
- // ---------------------------------------------------------------------------
21
- // 默认内容模板数据
22
- // ---------------------------------------------------------------------------
23
-
24
- function getConfigYamls() {
25
- const year = new Date().getFullYear();
26
- const now = new Date().toISOString();
27
- return {
28
- "site.yml": `id: site
29
- lang: en
30
- title: "My Blog"
31
- description: "A blog built with Stalux theme"
32
- url: "https://example.com"
33
- timezone: "Asia/Shanghai"
34
- `,
35
- "author.yml": `id: author
36
- name: "Your Name"
37
- avatar: "/avatar.png"
38
- bio: "Blogger & Developer"
39
- `,
40
- "navs.yml": `id: navs
41
- items:
42
- - title: "Home"
43
- link: "/"
44
- icon: "house"
45
- - title: "Archives"
46
- link: "/archives"
47
- icon: "archive"
48
- - title: "Tags"
49
- link: "/tags"
50
- icon: "tag"
51
- - title: "Categories"
52
- link: "/categories"
53
- icon: "folder"
54
- - title: "Words"
55
- link: "/words"
56
- icon: "quote"
57
- - title: "Links"
58
- link: "/links"
59
- icon: "link"
60
- `,
61
- "head.yml": `id: head
62
- # googleAnalyticsId: "G-XXXXXXXXXX"
63
- # bingClarityId: "xxxxxxxxxx"
64
- # umami:
65
- # id: ""
66
- # url: ""
67
- # anyhead: ""
68
- `,
69
- "footer.yml": `id: footer
70
- buildtime: "${now}"
71
- copyright:
72
- enabled: true
73
- startYear: ${year}
74
- customText: ""
75
- theme:
76
- showPoweredBy: true
77
- showThemeInfo: true
78
- beian:
79
- icp:
80
- enabled: false
81
- number: ""
82
- security:
83
- enabled: false
84
- text: ""
85
- number: ""
86
- badges:
87
- - label: "Powered by"
88
- message: "Astro"
89
- color: "orange"
90
- style: "flat-square"
91
- href: "https://astro.build/"
92
- custom: |
93
- <div id="custom-footer-hook"></div>
94
- `,
95
- "media-links.yml": `id: media-links
96
- items:
97
- - icon: "github"
98
- link: "https://github.com/yourname"
99
- `,
100
- "links.yml": `id: links
101
- title: "Links"
102
- description: "Friends & Resources"
103
- sites:
104
- - name: "Astro"
105
- description: "The web framework for content-driven websites"
106
- link: "https://astro.build"
107
- icon: "https://astro.build/favicon.svg"
108
- `,
109
- "comment.yml": `id: comment
110
- enabled: false
111
- `,
112
- "promote.yml": `id: promote
113
- export_md: false
114
- # llm_promote: |
115
- `,
116
- "ai-discovery.yml": `id: ai-discovery
117
- conformance: "disabled"
118
- `,
119
- "typetexts.yml": `id: typetexts
120
- items:
121
- - "Free for free, not free for charge!"
122
- - "Where's the any key?"
123
- - "Press F12?"
124
- - "Hello World!"
125
- `,
126
- };
127
- }
128
-
129
- function getExamplePost() {
130
- const date = new Date().toISOString().replace("T", " ").slice(0, 19);
131
- return `---
132
- title: Hello Stalux!
133
- abbrlink: hello-stalux
134
- date: "${date}"
135
- tags: [Stalux, Getting Started]
136
- categories: [Blog]
137
- desc: Welcome to Stalux!
138
- cc: CC-BY-NC-SA-4.0
139
- ---
140
-
141
- Welcome to **Stalux**!
142
-
143
- This is your first post. Edit or delete it, then start writing your own content.
144
-
145
- ## Features
146
-
147
- - 🌙 Dark mode with elegant glassmorphism design
148
- - 🔤 Per-route font subsetting — each page loads only needed characters
149
- - 🔍 Full-text search (Pagefind)
150
- - 📡 RSS & Atom feeds
151
- - 💬 Comment system (Waline)
152
- - 📊 Mermaid diagrams
153
- - 📐 Math formula rendering
154
- - 🖼️ PhotoSwipe lightbox
155
-
156
- For more details, visit [Stalux documentation](https://stalux.needhelp.icu).
157
- `;
158
- }
159
-
160
- function getAboutMd() {
161
- return `---
162
- title: About Me
163
- description: About this blog and the author
164
- ---
165
-
166
- Hello! Welcome to my blog.
167
-
168
- This is my personal space where I share technology, life, and thoughts.
169
-
170
- ## About This Site
171
-
172
- - Built with [Astro](https://astro.build) + [Stalux](https://stalux.needhelp.icu)
173
- - Content licensed under CC-BY-NC-SA-4.0
174
- - Source code hosted on GitHub
175
- `;
176
- }
177
-
178
- function getWordsTemplate() {
179
- const date = new Date().toISOString().replace("T", " ").slice(0, 19);
180
- return `---
181
- source: "Author Name"
182
- link: "https://example.com"
183
- sourceDate: ""
184
- date: "${date}"
185
- ---
186
-
187
- > A quote or short note...
188
- `;
189
- }
190
-
191
- function getWordsExample() {
192
- const date = new Date().toISOString().replace("T", " ").slice(0, 19);
193
- return `---
194
- source: "Albert Einstein"
195
- link: "https://example.com"
196
- sourceDate: ""
197
- date: "${date}"
198
- ---
199
-
200
- > Imagination is more important than knowledge.
201
- `;
202
- }
203
-
204
- // ---------------------------------------------------------------------------
205
- // 主流程
206
- // ---------------------------------------------------------------------------
207
-
208
- function main() {
209
- const args = process.argv.slice(2);
210
-
211
- if (args[0] === "--help" || args[0] === "-h" || !args[0]) {
212
- printHelp();
213
- process.exit(0);
214
- }
215
-
216
- if (args[0] !== "init") {
217
- console.error(`❌ Unknown command: ${args[0]}`);
218
- printHelp();
219
- process.exit(1);
220
- }
221
-
222
- const targetArg = args[1] || ".";
223
- const targetPath = pathResolve(process.cwd(), targetArg);
224
-
225
- console.log(`📦 Initializing Stalux content in ${targetPath}...`);
226
- console.log("");
227
-
228
- // 创建 stalux/ 目录结构
229
- const contentRoot = join(targetPath, "stalux");
230
- const dirs = ["config", "posts", "about", "words"];
231
- for (const dir of dirs) {
232
- mkdirSync(join(contentRoot, dir), { recursive: true });
233
- }
234
-
235
- // 生成 config YAML 文件(不覆盖已有)
236
- const configs = getConfigYamls();
237
- for (const [file, content] of Object.entries(configs)) {
238
- const filePath = join(contentRoot, "config", file);
239
- if (existsSync(filePath)) {
240
- console.log(` ⏭ stalux/config/${file} (exists, skipped)`);
241
- continue;
242
- }
243
- writeFileSync(filePath, content);
244
- console.log(` ✅ stalux/config/${file}`);
245
- }
246
-
247
- // 生成示例文章
248
- const postPath = join(contentRoot, "posts", "hello-stalux.md");
249
- if (!existsSync(postPath)) {
250
- writeFileSync(postPath, getExamplePost());
251
- console.log(` ✅ stalux/posts/hello-stalux.md`);
252
- } else {
253
- console.log(` ⏭ stalux/posts/hello-stalux.md (exists, skipped)`);
254
- }
255
-
256
- // 生成 about 页面
257
- const aboutPath = join(contentRoot, "about", "index.md");
258
- if (!existsSync(aboutPath)) {
259
- writeFileSync(aboutPath, getAboutMd());
260
- console.log(` ✅ stalux/about/index.md`);
261
- } else {
262
- console.log(` ⏭ stalux/about/index.md (exists, skipped)`);
263
- }
264
-
265
- // 生成 words
266
- const wordsDir = join(contentRoot, "words");
267
- const wordsTemplatePath = join(wordsDir, "_template.md");
268
- if (!existsSync(wordsTemplatePath)) {
269
- writeFileSync(wordsTemplatePath, getWordsTemplate());
270
- console.log(` ✅ stalux/words/_template.md`);
271
- } else {
272
- console.log(` ⏭ stalux/words/_template.md (exists, skipped)`);
273
- }
274
-
275
- const einsteinPath = join(wordsDir, "einstein-imagination.md");
276
- if (!existsSync(einsteinPath)) {
277
- writeFileSync(einsteinPath, getWordsExample());
278
- console.log(` ✅ stalux/words/einstein-imagination.md`);
279
- } else {
280
- console.log(` ⏭ stalux/words/einstein-imagination.md (exists, skipped)`);
281
- }
282
-
283
- console.log("");
284
- console.log(" ✅ Done! Stalux content initialized.");
285
- console.log("");
286
- printNextSteps();
287
- }
288
-
289
- function printHelp() {
290
- console.log(`
291
- Stalux — Modern Astro Blog Theme
292
-
293
- Usage:
294
- stalux init Initialize Stalux content (configs, posts, pages)
295
- stalux init my-blog Initialize in a subdirectory
296
-
297
- What it does:
298
- - Creates stalux/config/*.yml (all configuration files)
299
- - Creates stalux/posts/hello-stalux.md (example post)
300
- - Creates stalux/about/index.md (about page)
301
- - Creates stalux/words/ (example quotes)
302
- - Does NOT overwrite existing files
303
-
304
- Prerequisites:
305
- 1. Create an Astro project: bun create astro
306
- 2. Install stalux: bun add @xingwangzhe/stalux
307
- 3. Run init: bunx stalux init
308
- `);
309
- }
310
-
311
- function printNextSteps() {
312
- console.log(" 📝 Next Steps:");
313
- console.log("");
314
- console.log(" 1. Add stalux to your astro.config.mjs:");
315
- console.log(' import stalux from "@xingwangzhe/stalux";');
316
- console.log(' integrations: [stalux({ contentDir: "stalux" })],');
317
- console.log("");
318
- console.log(" 2. Add content collections to src/content.config.ts:");
319
- console.log(
320
- ' import { defineCollections } from "@xingwangzhe/stalux/schemas/collections";',
321
- );
322
- console.log(' export const collections = defineCollections({ contentDir: "stalux" });');
323
- console.log("");
324
- console.log(" 3. Start writing content in stalux/posts/");
325
- console.log(" 4. Run: bun run dev");
326
- console.log("");
327
- console.log(" 📖 Docs: https://stalux.needhelp.icu");
328
- console.log("");
329
- }
330
-
331
- main();