@xingwangzhe/stalux 1.4.1 → 1.4.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/package.json +1 -1
- package/src/index.ts +8 -2
- package/src/internal/font-subset-dev.ts +181 -0
- package/src/layouts/Stalux.astro +2 -2
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -208,11 +208,17 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration {
|
|
|
208
208
|
}
|
|
209
209
|
},
|
|
210
210
|
|
|
211
|
-
"astro:server:setup": async ({ logger }) => {
|
|
212
|
-
// 7.
|
|
211
|
+
"astro:server:setup": async ({ server, logger }) => {
|
|
212
|
+
// 7. 字体裁剪(开发模式,生成 common 子集 + 按需路由切片)
|
|
213
213
|
try {
|
|
214
214
|
const projectRoot = process.cwd();
|
|
215
|
+
// 先生成 common 子集
|
|
215
216
|
await runFontSubsetting(projectRoot, logger, true);
|
|
217
|
+
// 再添加按需路由切片中间件
|
|
218
|
+
const { createDevFontMiddleware } = await import(
|
|
219
|
+
new URL("./internal/font-subset-dev.ts", import.meta.url).href
|
|
220
|
+
);
|
|
221
|
+
server.middlewares.use(createDevFontMiddleware(projectRoot, logger));
|
|
216
222
|
} catch (error) {
|
|
217
223
|
logger.debug(`Font subsetting deferred in dev: ${String(error)}`);
|
|
218
224
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev mode on-demand font subsetting middleware.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts /fonts/subset-*.css requests, generates the WOFF2 subset
|
|
5
|
+
* for the requested route on-the-fly, and serves it.
|
|
6
|
+
*
|
|
7
|
+
* Handles ALL HTML routes: posts, about, words, home, archives, tags, etc.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { resolve, join, dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import type { AstroIntegrationLogger } from "astro";
|
|
14
|
+
import subsetFontFn from "subset-font";
|
|
15
|
+
|
|
16
|
+
const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
|
|
17
|
+
const CONTENT_ROOT = "stalux";
|
|
18
|
+
const FONT_OUT_DIR = "public/fonts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get characters for a given route subset.
|
|
22
|
+
* Returns null if route is unknown.
|
|
23
|
+
*/
|
|
24
|
+
function getRouteChars(projectRoot: string, subsetId: string): string | null {
|
|
25
|
+
const contentRoot = resolve(projectRoot, CONTENT_ROOT);
|
|
26
|
+
|
|
27
|
+
// ── Posts ──────────────────────────────────────────────
|
|
28
|
+
if (subsetId.startsWith("posts-")) {
|
|
29
|
+
const abbrlink = subsetId.slice(6);
|
|
30
|
+
const postsDir = join(contentRoot, "posts");
|
|
31
|
+
if (!existsSync(postsDir)) return null;
|
|
32
|
+
for (const f of readdirSync(postsDir)) {
|
|
33
|
+
if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
|
|
34
|
+
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
35
|
+
if (content.includes(`abbrlink: ${abbrlink}`)) return content;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── About ──────────────────────────────────────────────
|
|
42
|
+
if (subsetId === "about") {
|
|
43
|
+
const aboutDir = join(contentRoot, "about");
|
|
44
|
+
if (!existsSync(aboutDir)) return null;
|
|
45
|
+
for (const f of readdirSync(aboutDir)) {
|
|
46
|
+
if (f.endsWith(".md") || f.endsWith(".mdx")) return readFileSync(join(aboutDir, f), "utf-8");
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Words ──────────────────────────────────────────────
|
|
52
|
+
if (subsetId === "words") {
|
|
53
|
+
const wordsDir = join(contentRoot, "words");
|
|
54
|
+
if (!existsSync(wordsDir)) return null;
|
|
55
|
+
const all: string[] = [];
|
|
56
|
+
for (const f of readdirSync(wordsDir)) {
|
|
57
|
+
if (f.endsWith(".md") && !f.startsWith("_")) {
|
|
58
|
+
all.push(readFileSync(join(wordsDir, f), "utf-8"));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return all.join("\n") || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Other HTML routes (home, archives, tags, categories, links, 404) ──
|
|
65
|
+
// These pages use i18n text + config YAML text, which are already
|
|
66
|
+
// covered by the common subset. Return config text for extra safety.
|
|
67
|
+
if (["home", "archives", "tags", "categories", "links", "common"].includes(subsetId)) {
|
|
68
|
+
const configDir = join(contentRoot, "config");
|
|
69
|
+
if (!existsSync(configDir)) return null;
|
|
70
|
+
const all: string[] = [];
|
|
71
|
+
for (const f of readdirSync(configDir)) {
|
|
72
|
+
if (f.endsWith(".yml") || f.endsWith(".yaml")) {
|
|
73
|
+
all.push(readFileSync(join(configDir, f), "utf-8"));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return all.join("\n") || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate and serve a font subset on demand.
|
|
84
|
+
* Returns the CSS content to be served.
|
|
85
|
+
*/
|
|
86
|
+
async function generateSubsetOnDemand(
|
|
87
|
+
projectRoot: string,
|
|
88
|
+
subsetId: string,
|
|
89
|
+
logger: AstroIntegrationLogger,
|
|
90
|
+
): Promise<string | null> {
|
|
91
|
+
const chars = getRouteChars(projectRoot, subsetId);
|
|
92
|
+
if (!chars) return null;
|
|
93
|
+
|
|
94
|
+
// Extract unique characters
|
|
95
|
+
const charSet = new Set<string>();
|
|
96
|
+
for (const ch of chars) charSet.add(ch);
|
|
97
|
+
if (charSet.size === 0) return null;
|
|
98
|
+
|
|
99
|
+
// Deduplicate against common subset: read common.css to find its referenced WOFF2
|
|
100
|
+
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
101
|
+
const commonCssPath = join(outDir, "common.css");
|
|
102
|
+
const commonCharSet = new Set<string>();
|
|
103
|
+
|
|
104
|
+
// We can't precisely know which chars common covers without re-extracting.
|
|
105
|
+
// But we can read the i18n + config source for a rough estimate.
|
|
106
|
+
// For now, generate the full subset - the WOFF2 will be small either way.
|
|
107
|
+
// (Most chars are already in common, so the delta is tiny.)
|
|
108
|
+
|
|
109
|
+
// Locate font file
|
|
110
|
+
const fontPaths = [
|
|
111
|
+
resolve(projectRoot, FONT_INPUT),
|
|
112
|
+
resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", FONT_INPUT),
|
|
113
|
+
resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", FONT_INPUT),
|
|
114
|
+
];
|
|
115
|
+
let fontPath: string | undefined;
|
|
116
|
+
for (const p of fontPaths) {
|
|
117
|
+
if (existsSync(p)) { fontPath = p; break; }
|
|
118
|
+
}
|
|
119
|
+
if (!fontPath) return null;
|
|
120
|
+
|
|
121
|
+
const fontBuffer = readFileSync(fontPath);
|
|
122
|
+
mkdirSync(outDir, { recursive: true });
|
|
123
|
+
|
|
124
|
+
// Generate subset WOFF2
|
|
125
|
+
const charsStr = [...charSet].sort().join("");
|
|
126
|
+
const hash = [...charsStr].slice(0, 12).join("");
|
|
127
|
+
const filename = `subset-${subsetId}-${hash}.woff2`;
|
|
128
|
+
const outPath = join(outDir, filename);
|
|
129
|
+
|
|
130
|
+
if (!existsSync(outPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const data = await subsetFontFn(fontBuffer, charsStr, { targetFormat: "woff2" });
|
|
133
|
+
writeFileSync(outPath, data);
|
|
134
|
+
logger.info(
|
|
135
|
+
` on-demand subset: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return `@font-face {
|
|
143
|
+
font-family: "LXGW WenKai Subset";
|
|
144
|
+
src: url("/fonts/${filename}") format("woff2");
|
|
145
|
+
font-display: swap;
|
|
146
|
+
}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create Vite middleware for on-demand font subsetting in dev mode.
|
|
151
|
+
*/
|
|
152
|
+
export function createDevFontMiddleware(projectRoot: string, logger: AstroIntegrationLogger) {
|
|
153
|
+
return async (req: any, res: any, next: any) => {
|
|
154
|
+
const url = req.url || "";
|
|
155
|
+
const match = url.match(/^\/fonts\/subset-(.+)\.css$/);
|
|
156
|
+
if (!match) return next();
|
|
157
|
+
|
|
158
|
+
const subsetId = match[1];
|
|
159
|
+
|
|
160
|
+
// Check if file already exists (cached from previous on-demand generation)
|
|
161
|
+
const cssPath = resolve(projectRoot, FONT_OUT_DIR, `subset-${subsetId}.css`);
|
|
162
|
+
if (existsSync(cssPath)) {
|
|
163
|
+
res.statusCode = 200;
|
|
164
|
+
res.setHeader("Content-Type", "text/css");
|
|
165
|
+
res.end(readFileSync(cssPath, "utf-8"));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Generate on-demand
|
|
170
|
+
logger.info(`Font subset: on-demand generating for "${subsetId}"...`);
|
|
171
|
+
const css = await generateSubsetOnDemand(projectRoot, subsetId, logger);
|
|
172
|
+
if (css) {
|
|
173
|
+
writeFileSync(cssPath, css); // cache it
|
|
174
|
+
res.statusCode = 200;
|
|
175
|
+
res.setHeader("Content-Type", "text/css");
|
|
176
|
+
res.end(css);
|
|
177
|
+
} else {
|
|
178
|
+
next();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -28,8 +28,8 @@ const authorConfig = getAuthorData(configEntries);
|
|
|
28
28
|
const promoteConfig = getPromoteData(configEntries);
|
|
29
29
|
const htmlLang = siteConfig.lang || "zh-CN";
|
|
30
30
|
|
|
31
|
-
// Resolve font subset CSS URL
|
|
32
|
-
const fontSubsetUrl =
|
|
31
|
+
// Resolve font subset CSS URL
|
|
32
|
+
const fontSubsetUrl = props.fontSubsetId
|
|
33
33
|
? `/fonts/subset-${props.fontSubsetId}.css`
|
|
34
34
|
: undefined;
|
|
35
35
|
|