@xingwangzhe/stalux 1.2.0 → 1.3.0
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 +55 -23
- package/package.json +19 -20
- package/scripts/cli.mjs +45 -43
- package/src/components/stalux/layout/listPage.astro +3 -2
- package/src/config.ts +30 -30
- package/src/index.ts +179 -155
- package/src/internal/components-plugin.ts +24 -24
- package/src/internal/dev-toolbar.ts +8 -8
- package/src/internal/font-subset.ts +372 -0
- package/src/internal/override.ts +57 -57
- package/src/internal/pagefind.ts +23 -22
- package/src/layouts/Stalux.astro +10 -0
- package/src/pages/404.astro +1 -0
- package/src/pages/about.astro +1 -0
- package/src/pages/archives.astro +1 -1
- package/src/pages/categories/[category].astro +1 -0
- package/src/pages/categories/index.astro +1 -0
- package/src/pages/index.astro +1 -1
- package/src/pages/links.astro +7 -6
- package/src/pages/posts/[post].astro +1 -0
- package/src/pages/tags/[tag].astro +1 -0
- package/src/pages/tags/index.astro +1 -0
- package/src/pages/words.astro +1 -1
- package/src/schemas/collections.ts +85 -84
- package/template/astro.config.mjs +3 -3
- package/template/package.json +5 -5
- package/template/stalux/posts/hello-stalux.md +1 -1
- package/template/stalux/words/_template.md +1 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-route font subsetting engine.
|
|
3
|
+
*
|
|
4
|
+
* Scans each content file individually, extracts its unique character set,
|
|
5
|
+
* and generates a minimal WOFF2 subset per route. Also generates a "common"
|
|
6
|
+
* subset for UI text (navs, i18n, layout).
|
|
7
|
+
*
|
|
8
|
+
* Output layout (under public/fonts/):
|
|
9
|
+
* fonts/
|
|
10
|
+
* common.woff2 — UI text subset (ASCII, nav, i18n)
|
|
11
|
+
* post-{abbrlink}.woff2 — per-post subset
|
|
12
|
+
* about.woff2 — about page subset
|
|
13
|
+
* words.woff2 — combined words subset
|
|
14
|
+
* manifest.json — route → subset file mapping
|
|
15
|
+
* subset.css — global @font-face declarations
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
20
|
+
import { resolve, basename, extname, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import type { AstroIntegrationLogger } from "astro";
|
|
23
|
+
// subset-font 是 WASM 模块,需要静态导入(Vite module runner 在构建钩子中不兼容动态 import)
|
|
24
|
+
import subsetFontFn from "subset-font";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Configuration
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Content root directory (relative to project root) */
|
|
31
|
+
const CONTENT_ROOT = "stalux";
|
|
32
|
+
|
|
33
|
+
/** Full font input path */
|
|
34
|
+
const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
|
|
35
|
+
|
|
36
|
+
/** Output directory for generated subsets */
|
|
37
|
+
const FONT_OUT_DIR = "public/fonts";
|
|
38
|
+
|
|
39
|
+
/** Characters always included in common subset */
|
|
40
|
+
const ALWAYS_INCLUDE_CHARS = new Set(
|
|
41
|
+
// ASCII printable
|
|
42
|
+
[...Array(0x7e - 0x20 + 1)]
|
|
43
|
+
.map((_, i) => String.fromCodePoint(0x20 + i))
|
|
44
|
+
.join("")
|
|
45
|
+
// Basic CJK punctuation
|
|
46
|
+
.concat(
|
|
47
|
+
"\u3000\u3001\u3002\u3008\u3009\u300a\u300b\u300c\u300d\u3010\u3011" +
|
|
48
|
+
"\uff01\uff08\uff09\uff0c\uff0e\uff1a\uff1b\uff1f\uff5e\u2013\u2014" +
|
|
49
|
+
"\u2018\u2019\u201c\u201d\u2026\uff09\uff08\u00b7",
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Types
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
interface RouteCharSet {
|
|
58
|
+
route: string;
|
|
59
|
+
chars: string;
|
|
60
|
+
hash: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface FontManifest {
|
|
64
|
+
common: string; // filename of common subset
|
|
65
|
+
routes: Record<string, string>; // route → subset filename
|
|
66
|
+
/** Per-post mapping: abbrlink → subset filename */
|
|
67
|
+
posts: Record<string, string>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Character extraction
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function extractChars(text: string): string {
|
|
75
|
+
const set = new Set<string>();
|
|
76
|
+
for (const ch of text) set.add(ch);
|
|
77
|
+
return [...set].sort().join("");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hashChars(chars: string): string {
|
|
81
|
+
return createHash("md5").update(chars).digest("hex").slice(0, 12);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Content scanning
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
interface ContentFile {
|
|
89
|
+
route: string;
|
|
90
|
+
filePath: string;
|
|
91
|
+
abbrlink?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function scanContent(projectRoot: string): ContentFile[] {
|
|
95
|
+
const files: ContentFile[] = [];
|
|
96
|
+
const contentRoot = resolve(projectRoot, CONTENT_ROOT);
|
|
97
|
+
|
|
98
|
+
// Posts
|
|
99
|
+
const postsDir = join(contentRoot, "posts");
|
|
100
|
+
if (existsSync(postsDir)) {
|
|
101
|
+
for (const f of readdirSync(postsDir)) {
|
|
102
|
+
if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
|
|
103
|
+
if (f.startsWith("_")) continue; // skip template files
|
|
104
|
+
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
105
|
+
// Extract abbrlink from frontmatter
|
|
106
|
+
const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
|
|
107
|
+
const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
|
|
108
|
+
files.push({
|
|
109
|
+
route: `posts/${abbrlink}`,
|
|
110
|
+
filePath: join(postsDir, f),
|
|
111
|
+
abbrlink,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// About
|
|
117
|
+
const aboutDir = join(contentRoot, "about");
|
|
118
|
+
if (existsSync(aboutDir)) {
|
|
119
|
+
for (const f of readdirSync(aboutDir)) {
|
|
120
|
+
if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
|
|
121
|
+
if (f.startsWith("_")) continue;
|
|
122
|
+
files.push({
|
|
123
|
+
route: "about",
|
|
124
|
+
filePath: join(aboutDir, f),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Words - aggregate all words files' characters into one
|
|
130
|
+
const wordsDir = join(contentRoot, "words");
|
|
131
|
+
if (existsSync(wordsDir)) {
|
|
132
|
+
const allWordsContent: string[] = [];
|
|
133
|
+
for (const f of readdirSync(wordsDir)) {
|
|
134
|
+
if (!f.endsWith(".md")) continue;
|
|
135
|
+
if (f.startsWith("_")) continue;
|
|
136
|
+
try {
|
|
137
|
+
allWordsContent.push(readFileSync(join(wordsDir, f), "utf-8"));
|
|
138
|
+
} catch {
|
|
139
|
+
/* skip */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (allWordsContent.length > 0) {
|
|
143
|
+
files.push({
|
|
144
|
+
route: "words",
|
|
145
|
+
filePath: join(wordsDir, "words-combined.md"),
|
|
146
|
+
// We'll handle this virtual file specially in char extraction
|
|
147
|
+
});
|
|
148
|
+
// Store combined content in a map for later extraction
|
|
149
|
+
wordCharsCache.set("words", extractChars(allWordsContent.join("\n")));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return files;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Cache for virtual route char sets
|
|
157
|
+
const wordCharsCache = new Map<string, string>();
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Common subset: extract chars from UI source files
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
function collectCommonChars(projectRoot: string): string {
|
|
164
|
+
const chars = new Set(ALWAYS_INCLUDE_CHARS);
|
|
165
|
+
|
|
166
|
+
const scanPatterns = [
|
|
167
|
+
"src/i18n/*.json",
|
|
168
|
+
"src/components/stalux/**/*.astro",
|
|
169
|
+
"src/layouts/**/*.astro",
|
|
170
|
+
"src/pages/**/*.astro",
|
|
171
|
+
"src/pages/**/*.ts",
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// Since we can't use glob here easily, scan specific known files
|
|
175
|
+
const srcDir = resolve(projectRoot, "src");
|
|
176
|
+
const scanDirs = ["i18n", "components/stalux", "layouts", "pages"];
|
|
177
|
+
|
|
178
|
+
function scanDir(dir: string) {
|
|
179
|
+
if (!existsSync(dir)) return;
|
|
180
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
181
|
+
const fullPath = join(dir, entry.name);
|
|
182
|
+
if (entry.isDirectory()) {
|
|
183
|
+
scanDir(fullPath);
|
|
184
|
+
} else if (
|
|
185
|
+
entry.name.endsWith(".astro") ||
|
|
186
|
+
entry.name.endsWith(".ts") ||
|
|
187
|
+
entry.name.endsWith(".json")
|
|
188
|
+
) {
|
|
189
|
+
try {
|
|
190
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
191
|
+
// Only extract string literals and text content (skip code)
|
|
192
|
+
for (const ch of content) chars.add(ch);
|
|
193
|
+
} catch {
|
|
194
|
+
/* skip */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
scanDir(srcDir);
|
|
201
|
+
|
|
202
|
+
return [...chars].sort().join("");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Deduplication: merge files with identical char sets
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
function deduplicate(routes: RouteCharSet[]): Map<string, string[]> {
|
|
210
|
+
// hash → [route1, route2, ...]
|
|
211
|
+
const groups = new Map<string, string[]>();
|
|
212
|
+
for (const r of routes) {
|
|
213
|
+
const list = groups.get(r.hash) ?? [];
|
|
214
|
+
list.push(r.route);
|
|
215
|
+
groups.set(r.hash, list);
|
|
216
|
+
}
|
|
217
|
+
return groups;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Main entry
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Run font subsetting during build.
|
|
226
|
+
* Called from astro:build:start hook.
|
|
227
|
+
*/
|
|
228
|
+
export async function runFontSubsetting(
|
|
229
|
+
projectRoot: string,
|
|
230
|
+
logger: AstroIntegrationLogger,
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
logger.info("Font subsetting: scanning content...");
|
|
233
|
+
|
|
234
|
+
// 1. Locate font file
|
|
235
|
+
const fontPath = resolve(projectRoot, FONT_INPUT);
|
|
236
|
+
if (!existsSync(fontPath)) {
|
|
237
|
+
logger.warn(`Font not found at ${fontPath}, skipping subsetting`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const fontBuffer = readFileSync(fontPath);
|
|
241
|
+
|
|
242
|
+
// 2. Scan content files
|
|
243
|
+
const contentFiles = scanContent(projectRoot);
|
|
244
|
+
logger.info(`Font subsetting: found ${contentFiles.length} content files`);
|
|
245
|
+
|
|
246
|
+
// 3. Collect common chars (UI, nav, i18n)
|
|
247
|
+
const commonChars = collectCommonChars(projectRoot);
|
|
248
|
+
const commonHash = hashChars(commonChars);
|
|
249
|
+
|
|
250
|
+
// 4. Extract per-route char sets
|
|
251
|
+
const routeSets: RouteCharSet[] = [];
|
|
252
|
+
for (const file of contentFiles) {
|
|
253
|
+
let content: string;
|
|
254
|
+
if (file.route === "words" && wordCharsCache.has("words")) {
|
|
255
|
+
content = wordCharsCache.get("words")!;
|
|
256
|
+
} else {
|
|
257
|
+
content = readFileSync(file.filePath, "utf-8");
|
|
258
|
+
}
|
|
259
|
+
const chars = extractChars(content);
|
|
260
|
+
routeSets.push({
|
|
261
|
+
route: file.route,
|
|
262
|
+
chars,
|
|
263
|
+
hash: hashChars(chars),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 5. Deduplicate and generate subsets
|
|
268
|
+
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
269
|
+
mkdirSync(outDir, { recursive: true });
|
|
270
|
+
|
|
271
|
+
// Dynamic import subset-font (ESM only)
|
|
272
|
+
let subsetFont = subsetFontFn;
|
|
273
|
+
|
|
274
|
+
// Generate common subset
|
|
275
|
+
const commonFilename = `common-${commonHash}.woff2`;
|
|
276
|
+
const commonOutPath = join(outDir, commonFilename);
|
|
277
|
+
if (!existsSync(commonOutPath)) {
|
|
278
|
+
const data = await subsetFont(fontBuffer, commonChars, { targetFormat: "woff2" });
|
|
279
|
+
writeFileSync(commonOutPath, data);
|
|
280
|
+
logger.info(` common subset: ${(data.length / 1024).toFixed(1)} KB → ${commonFilename}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Generate common CSS
|
|
284
|
+
const commonCSS = `/* Auto-generated common font subset */
|
|
285
|
+
@font-face {
|
|
286
|
+
font-family: "LXGW WenKai Subset";
|
|
287
|
+
src: url("/fonts/${commonFilename}") format("woff2");
|
|
288
|
+
font-display: swap;
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
writeFileSync(join(outDir, "common.css"), commonCSS);
|
|
292
|
+
|
|
293
|
+
// Generate per-route subsets (deduplicated) and their CSS
|
|
294
|
+
const groups = deduplicate(routeSets);
|
|
295
|
+
const routeMap: Record<string, string> = {};
|
|
296
|
+
const postMap: Record<string, string> = {};
|
|
297
|
+
|
|
298
|
+
let subsetCount = 0;
|
|
299
|
+
// Track which subsets we've already generated CSS for
|
|
300
|
+
const cssGenerated = new Set<string>();
|
|
301
|
+
|
|
302
|
+
for (const [hash, routes] of groups) {
|
|
303
|
+
const filename = `subset-${hash}.woff2`;
|
|
304
|
+
const outPath = join(outDir, filename);
|
|
305
|
+
if (!existsSync(outPath)) {
|
|
306
|
+
// Find the route chars for this hash
|
|
307
|
+
const route = routeSets.find((r) => r.hash === hash);
|
|
308
|
+
const chars = route?.chars ?? "";
|
|
309
|
+
// Only subset characters NOT in common (already covered by common.css)
|
|
310
|
+
const uniqueChars = chars
|
|
311
|
+
.split("")
|
|
312
|
+
.filter((ch) => !commonChars.includes(ch))
|
|
313
|
+
.join("");
|
|
314
|
+
if (uniqueChars.length > 0) {
|
|
315
|
+
const data = await subsetFont(fontBuffer, uniqueChars, { targetFormat: "woff2" });
|
|
316
|
+
writeFileSync(outPath, data);
|
|
317
|
+
subsetCount++;
|
|
318
|
+
logger.info(
|
|
319
|
+
` subset ${subsetCount}: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Generate per-route CSS (one per unique hash, references all routes with this hash)
|
|
325
|
+
if (!cssGenerated.has(hash)) {
|
|
326
|
+
cssGenerated.add(hash);
|
|
327
|
+
for (const route of routes) {
|
|
328
|
+
const cssContent = `/* Auto-generated font subset for route: ${route} */
|
|
329
|
+
@font-face {
|
|
330
|
+
font-family: "LXGW WenKai Subset";
|
|
331
|
+
src: url("/fonts/${filename}") format("woff2");
|
|
332
|
+
font-display: swap;
|
|
333
|
+
}
|
|
334
|
+
`;
|
|
335
|
+
// Use first route's name as the CSS filename (they share the same subset)
|
|
336
|
+
const cssName = routes.length === 1 ? route.replace("/", "-") : `group-${hash}`;
|
|
337
|
+
const cssFilename = `subset-${cssName}.css`;
|
|
338
|
+
writeFileSync(join(outDir, cssFilename), cssContent);
|
|
339
|
+
routeMap[route] = cssFilename;
|
|
340
|
+
|
|
341
|
+
if (route.startsWith("posts/")) {
|
|
342
|
+
const abbrlink = route.replace("posts/", "");
|
|
343
|
+
postMap[abbrlink] = cssFilename;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
// CSS already generated, just map
|
|
348
|
+
for (const route of routes) {
|
|
349
|
+
const cssFilename =
|
|
350
|
+
routes.length === 1
|
|
351
|
+
? `subset-${route.replace("/", "-")}.css`
|
|
352
|
+
: `subset-group-${hash}.css`;
|
|
353
|
+
routeMap[route] = cssFilename;
|
|
354
|
+
if (route.startsWith("posts/")) {
|
|
355
|
+
postMap[route.replace("posts/", "")] = cssFilename;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 6. Generate manifest
|
|
362
|
+
const manifest: FontManifest = {
|
|
363
|
+
common: commonFilename,
|
|
364
|
+
routes: routeMap,
|
|
365
|
+
posts: postMap,
|
|
366
|
+
};
|
|
367
|
+
writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
368
|
+
|
|
369
|
+
logger.info(
|
|
370
|
+
`Font subsetting done: ${subsetCount} route subsets + common (${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → variable)`,
|
|
371
|
+
);
|
|
372
|
+
}
|
package/src/internal/override.ts
CHANGED
|
@@ -14,68 +14,68 @@ import type { ComponentOverrideMap } from "../config";
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
16
|
export const STALUX_COMPONENTS = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
// Layout
|
|
18
|
+
Head: "layouts/head.astro",
|
|
19
|
+
Footer: "layouts/footer.astro",
|
|
20
|
+
Navs: "layouts/navs.astro",
|
|
21
|
+
ListPage: "layouts/listPage.astro",
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// Common
|
|
24
|
+
Author: "common/author.astro",
|
|
25
|
+
Date: "common/date.astro",
|
|
26
|
+
Search: "common/search.astro",
|
|
27
|
+
TextType: "common/textType.astro",
|
|
28
|
+
VerCount: "common/verCount.astro",
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// Home
|
|
31
|
+
SocialMedia: "links/socialMedia.astro",
|
|
32
|
+
ToArchive: "archives/toArchive.astro",
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
// Posts
|
|
35
|
+
PostLeft: "posts/postLeft.astro",
|
|
36
|
+
PostContent: "posts/postContent.astro",
|
|
37
|
+
PostRight: "posts/postRight.astro",
|
|
38
|
+
PostNavigation: "posts/postNavigation.astro",
|
|
39
|
+
PostCC: "posts/postCC.astro",
|
|
40
|
+
WalineComment: "posts/WalineComment.astro",
|
|
41
|
+
PostToc: "posts/postToc.astro",
|
|
42
|
+
PostAuthor: "posts/postAuthor.astro",
|
|
43
|
+
PostCard: "posts/postCard.astro",
|
|
44
|
+
PostList: "posts/postList.astro",
|
|
45
|
+
PostTaxonomy: "posts/PostTaxonomy.astro",
|
|
46
|
+
PhotoSwipeStyle: "posts/PhotoSwipeStyle.astro",
|
|
47
|
+
RandomPost: "posts/randomPost.astro",
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Archives
|
|
50
|
+
ArchivesList: "archives/archivesList.astro",
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// Categories
|
|
53
|
+
CategoriesCard: "categories/categoriesCard.astro",
|
|
54
|
+
CategoriesList: "categories/categoriesList.astro",
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// Tags
|
|
57
|
+
TagHeader: "tags/tagHeader.astro",
|
|
58
|
+
TagsCloud: "tags/tagsCloud.astro",
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// Words
|
|
61
|
+
WordCard: "words/wordCard.astro",
|
|
62
|
+
WordList: "words/wordList.astro",
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// Links
|
|
65
|
+
LinkCard: "links/linkCard.astro",
|
|
66
|
+
LinkList: "links/linkList.astro",
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
// Footer
|
|
69
|
+
FooterBadges: "footer/FooterBadges.astro",
|
|
70
|
+
FooterBeian: "footer/FooterBeian.astro",
|
|
71
|
+
FooterCopyright: "footer/FooterCopyright.astro",
|
|
72
|
+
FooterStats: "footer/FooterStats.astro",
|
|
73
|
+
FooterThemeInfo: "footer/FooterThemeInfo.astro",
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
// Analytics
|
|
76
|
+
Clarity: "analytics/clarity.astro",
|
|
77
|
+
Google: "analytics/google.astro",
|
|
78
|
+
Umami: "analytics/umami.astro",
|
|
79
79
|
} as const;
|
|
80
80
|
|
|
81
81
|
/** 组件键名联合类型 */
|
|
@@ -90,10 +90,10 @@ export type StaluxComponentKey = keyof typeof STALUX_COMPONENTS;
|
|
|
90
90
|
* @returns 组件的绝对路径
|
|
91
91
|
*/
|
|
92
92
|
export function resolveComponentPath(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
key: StaluxComponentKey,
|
|
94
|
+
overrides: ComponentOverrideMap = {},
|
|
95
|
+
componentsDir: string,
|
|
96
96
|
): string {
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
if (overrides[key]) return overrides[key]!;
|
|
98
|
+
return componentsDir + "/stalux/" + STALUX_COMPONENTS[key];
|
|
99
99
|
}
|
package/src/internal/pagefind.ts
CHANGED
|
@@ -7,34 +7,35 @@
|
|
|
7
7
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
10
11
|
import type { AstroIntegrationLogger } from "astro";
|
|
11
12
|
|
|
12
13
|
export async function pagefind(dir: URL, logger: AstroIntegrationLogger): Promise<void> {
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const outDir = fileURLToPath(dir);
|
|
15
|
+
logger.info("Running Pagefind indexer...");
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
try {
|
|
18
|
+
const { createIndex } = await import("pagefind");
|
|
19
|
+
const { index, errors } = await createIndex();
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
if (!index) {
|
|
22
|
+
logger.error("Pagefind failed to create index");
|
|
23
|
+
errors?.forEach((e) => logger.error(e));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const { page_count } = await index.addDirectory({ path: outDir });
|
|
28
|
+
if (page_count === 0) {
|
|
29
|
+
logger.warn("Pagefind: no pages indexed");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const { outputPath } = await index.writeFiles({
|
|
34
|
+
outputPath: path.join(outDir, "pagefind"),
|
|
35
|
+
});
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
logger.info(`Pagefind indexed ${page_count} pages → ${outputPath}`);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.error(`Pagefind indexing failed: ${error}`);
|
|
40
|
+
}
|
|
40
41
|
}
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -18,6 +18,8 @@ interface Props {
|
|
|
18
18
|
categories?: string[];
|
|
19
19
|
author?: string;
|
|
20
20
|
noindex?: boolean;
|
|
21
|
+
/** Font subset ID for per-route font optimization */
|
|
22
|
+
fontSubsetId?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const configEntries = await getCollection("config");
|
|
@@ -26,6 +28,11 @@ const authorConfig = getAuthorData(configEntries);
|
|
|
26
28
|
const promoteConfig = getPromoteData(configEntries);
|
|
27
29
|
const htmlLang = siteConfig.lang || "zh-CN";
|
|
28
30
|
|
|
31
|
+
// Resolve font subset CSS URL
|
|
32
|
+
const fontSubsetUrl = props.fontSubsetId
|
|
33
|
+
? `/fonts/subset-${props.fontSubsetId}.css`
|
|
34
|
+
: undefined;
|
|
35
|
+
|
|
29
36
|
const bgModules = import.meta.glob<{ default: { src: string } }>(
|
|
30
37
|
"../assets/background/pattern-*.min.svg",
|
|
31
38
|
{ eager: true },
|
|
@@ -61,6 +68,9 @@ const llmPromote = llmPromoteRaw
|
|
|
61
68
|
author={props.author}
|
|
62
69
|
noindex={props.noindex}
|
|
63
70
|
/>
|
|
71
|
+
<!-- Per-route font subset (generated at build time) -->
|
|
72
|
+
<link rel="stylesheet" href="/fonts/common.css" />
|
|
73
|
+
{fontSubsetUrl && <link rel="stylesheet" href={fontSubsetUrl} />}
|
|
64
74
|
<script define:vars={{ bgUrls, randomIndex }}>
|
|
65
75
|
window.__STALUX_BG_URLS__ = bgUrls;
|
|
66
76
|
window.__STALUX_BG_INDEX__ = randomIndex;
|
package/src/pages/404.astro
CHANGED
package/src/pages/about.astro
CHANGED
package/src/pages/archives.astro
CHANGED
|
@@ -12,7 +12,7 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
|
|
|
12
12
|
const posts = await getCollection("posts", ({ data }) => !data.draft);
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
<Stalux title={t("archives.title") + " | " + siteConfig.title} description={t("archives.description", { title: siteConfig.title })}>
|
|
15
|
+
<Stalux title={t("archives.title") + " | " + siteConfig.title} description={t("archives.description", { title: siteConfig.title })} fontSubsetId="archives">
|
|
16
16
|
<div style="height: 3em;"></div>
|
|
17
17
|
<Navs />
|
|
18
18
|
<h1 class={styles.h1}>{t("archives.heading")}</h1>
|
|
@@ -27,6 +27,7 @@ const props = Astro.props as Props;
|
|
|
27
27
|
<Stalux
|
|
28
28
|
title={t("taxonomy.category") + ": " + props.category + " | " + siteConfig.title}
|
|
29
29
|
description={t("taxonomy.browseCategory", { title: siteConfig.title, category: props.category, count: props.count })}
|
|
30
|
+
fontSubsetId="categories"
|
|
30
31
|
>
|
|
31
32
|
<div style="height: 3em;"></div>
|
|
32
33
|
<Navs />
|
|
@@ -14,6 +14,7 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
|
|
|
14
14
|
<Stalux
|
|
15
15
|
title={t("categories.title") + " | " + siteConfig.title}
|
|
16
16
|
description={t("categories.description", { title: siteConfig.title })}
|
|
17
|
+
fontSubsetId="categories"
|
|
17
18
|
>
|
|
18
19
|
<div style="height: 3em;"></div>
|
|
19
20
|
<Navs />
|
package/src/pages/index.astro
CHANGED
|
@@ -15,7 +15,7 @@ import ToArchive from "@components/stalux/archives/toArchive.astro";
|
|
|
15
15
|
import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, getCommentData, getFooterData, getPromoteData, getTypetextsData, getMediaLinksData } from "@utils/config-utils";
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
<Stalux title={siteTitle} description={siteDescription}>
|
|
18
|
+
<Stalux title={siteTitle} description={siteDescription} fontSubsetId="home">
|
|
19
19
|
<div class={styles.homeBody}>
|
|
20
20
|
<h1 class={styles.homeTitle}>{siteTitle}</h1>
|
|
21
21
|
<div class={styles.homeNavs}>
|
package/src/pages/links.astro
CHANGED
|
@@ -13,11 +13,12 @@ const linksTitle = linksConfig.title;
|
|
|
13
13
|
const linksDescription = linksConfig.description;
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
<ListPage
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
<ListPage
|
|
17
|
+
title={siteTitle}
|
|
18
|
+
description={siteDescription}
|
|
19
|
+
heading={linksTitle}
|
|
20
|
+
subheading={linksDescription}
|
|
21
|
+
fontSubsetId="links"
|
|
22
|
+
>
|
|
22
23
|
<LinkList />
|
|
23
24
|
</ListPage>
|