@xingwangzhe/stalux 1.5.4 → 1.6.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.5.4",
4
- "description": "A powerful, modern Astro blog theme \u2014 use as template or install as plugin",
3
+ "version": "1.6.1",
4
+ "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
7
7
  "astro-integration",
@@ -83,15 +83,15 @@
83
83
  "@xingwangzhe/satteri-photoswipe": "^0.2.0",
84
84
  "@xingwangzhe/tags-cloud": "^1.2.4",
85
85
  "animejs": "^4.5.0",
86
- "astro": "^7.1.3",
87
86
  "astro-expressive-code": "^0.44.1",
88
87
  "astro-seo": "^1.1.0",
89
88
  "astro-typewriter": "^1.2.0",
90
89
  "badge-maker": "^6.0.0",
91
90
  "pagefind": "^1.5.2",
92
91
  "photoswipe": "^5.4.4",
92
+ "satteri": "^0.9.5",
93
93
  "simple-icons": "^16.27.1",
94
- "taetype": "0.1.3",
94
+ "subset-font": "^2.5.0",
95
95
  "temml": "^0.13.3"
96
96
  },
97
97
  "devDependencies": {
package/src/index.ts CHANGED
@@ -8,10 +8,10 @@
8
8
  * 基于 Astro 7.1.3 Integration API(astro:config:setup / injectRoute / injectScript)
9
9
  */
10
10
 
11
+ import { existsSync, readdirSync } from "node:fs";
11
12
  import path from "node:path";
12
13
  import { resolve } from "node:path";
13
14
  import { fileURLToPath } from "node:url";
14
- import { existsSync, readdirSync } from "node:fs";
15
15
 
16
16
  import type { AstroIntegration } from "astro";
17
17
  // pagefind 是 ESM-only 包,需要在模块顶层导入
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dev mode on-demand font subsetting middleware.
3
3
  *
4
- * Intercepts /fonts/subset-*.css requests, generates the WOFF2 subset
4
+ * Intercepts /fonts/subset-*.css requests, generates the TTF subset
5
5
  * for the requested route on-the-fly, and serves it.
6
6
  *
7
7
  * Handles ALL HTML routes: posts, about, words, home, archives, tags, etc.
@@ -12,7 +12,7 @@ import { resolve, join, dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
14
  import type { AstroIntegrationLogger } from "astro";
15
- import { register_font_raw, get_glyph_ids, subset_font_full } from "taetype";
15
+ import subsetFont from "subset-font";
16
16
 
17
17
  const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
18
18
  const CONTENT_ROOT = "stalux";
@@ -63,10 +63,37 @@ function getRouteChars(projectRoot: string, subsetId: string): string | null {
63
63
  return all.join("\n") || null;
64
64
  }
65
65
 
66
- // ── Other HTML routes (home, archives, tags, categories, links, 404) ──
66
+ // ── Archives ────────────────────────────────────────────
67
+ if (subsetId === "archives") {
68
+ return getAllPostContent(contentRoot);
69
+ }
70
+
71
+ // ── Tags index ──────────────────────────────────────────
72
+ if (subsetId === "tags") {
73
+ return getAllTagsAndPostContent(contentRoot);
74
+ }
75
+
76
+ // ── Tags detail ─────────────────────────────────────────
77
+ if (subsetId.startsWith("tags-")) {
78
+ const tagName = decodeURIComponent(subsetId.slice(5));
79
+ return getTagContent(contentRoot, tagName);
80
+ }
81
+
82
+ // ── Categories index ────────────────────────────────────
83
+ if (subsetId === "categories") {
84
+ return getAllCategoriesAndPostContent(contentRoot);
85
+ }
86
+
87
+ // ── Categories detail ───────────────────────────────────
88
+ if (subsetId.startsWith("categories-")) {
89
+ const catName = decodeURIComponent(subsetId.slice(11));
90
+ return getCategoryContent(contentRoot, catName);
91
+ }
92
+
93
+ // ── Other HTML routes (home, links, 404) ──
67
94
  // These pages use i18n text + config YAML text, which are already
68
95
  // covered by the common subset. Return config text for extra safety.
69
- if (["home", "archives", "tags", "categories", "links", "common"].includes(subsetId)) {
96
+ if (["home", "links", "common"].includes(subsetId)) {
70
97
  const configDir = join(contentRoot, "config");
71
98
  if (!existsSync(configDir)) return null;
72
99
  const all: string[] = [];
@@ -81,6 +108,99 @@ function getRouteChars(projectRoot: string, subsetId: string): string | null {
81
108
  return null;
82
109
  }
83
110
 
111
+ /** Get all post content concatenated (including template/_ files) */
112
+ function getAllPostContent(contentRoot: string): string | null {
113
+ const postsDir = join(contentRoot, "posts");
114
+ if (!existsSync(postsDir)) return null;
115
+ const all: string[] = [];
116
+ for (const f of readdirSync(postsDir)) {
117
+ if (f.endsWith(".md") || f.endsWith(".mdx")) {
118
+ all.push(readFileSync(join(postsDir, f), "utf-8"));
119
+ }
120
+ }
121
+ return all.join("\n") || null;
122
+ }
123
+
124
+ /** Get all tags + all post content */
125
+ function getAllTagsAndPostContent(contentRoot: string): string | null {
126
+ const postContent = getAllPostContent(contentRoot);
127
+ if (!postContent) return null;
128
+ const tags = extractAllYamlValues(postContent, "tags");
129
+ return postContent + " " + tags.join(" ");
130
+ }
131
+
132
+ /** Get all categories + all post content */
133
+ function getAllCategoriesAndPostContent(contentRoot: string): string | null {
134
+ const postContent = getAllPostContent(contentRoot);
135
+ if (!postContent) return null;
136
+ const categories = extractAllYamlValues(postContent, "categories");
137
+ return postContent + " " + categories.join(" ");
138
+ }
139
+
140
+ /** Get content for a specific tag: tag name + posts with that tag */
141
+ function getTagContent(contentRoot: string, tagName: string): string | null {
142
+ const postsDir = join(contentRoot, "posts");
143
+ if (!existsSync(postsDir)) return null;
144
+ const all: string[] = [tagName];
145
+ for (const f of readdirSync(postsDir)) {
146
+ if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
147
+ const content = readFileSync(join(postsDir, f), "utf-8");
148
+ const tags = extractYamlListFromContent(content, "tags");
149
+ if (tags.includes(tagName)) {
150
+ all.push(content);
151
+ }
152
+ }
153
+ }
154
+ return all.join("\n") || null;
155
+ }
156
+
157
+ /** Get content for a specific category: category name + posts in that category */
158
+ function getCategoryContent(contentRoot: string, catName: string): string | null {
159
+ const postsDir = join(contentRoot, "posts");
160
+ if (!existsSync(postsDir)) return null;
161
+ const all: string[] = [catName];
162
+ for (const f of readdirSync(postsDir)) {
163
+ if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
164
+ const content = readFileSync(join(postsDir, f), "utf-8");
165
+ const categories = extractYamlListFromContent(content, "categories");
166
+ if (categories.includes(catName)) {
167
+ all.push(content);
168
+ }
169
+ }
170
+ }
171
+ return all.join("\n") || null;
172
+ }
173
+
174
+ /** Extract a YAML list value from frontmatter by key */
175
+ function extractYamlListFromContent(content: string, key: string): string[] {
176
+ const lines: string[] = [];
177
+ const regex = new RegExp(`^${key}:$`, "m");
178
+ const match = content.match(regex);
179
+ if (!match) return [];
180
+ const startIdx = match.index! + match[0].length;
181
+ const afterKey = content.slice(startIdx);
182
+ const listMatch = afterKey.match(/^\n((?:\s+-\s+.+\n?)*)/);
183
+ if (!listMatch) return [];
184
+ for (const line of listMatch[1].split("\n")) {
185
+ const item = line.match(/^\s+-\s+(.+)/)?.[1];
186
+ if (item) lines.push(item.trim());
187
+ }
188
+ return lines;
189
+ }
190
+
191
+ /** Extract all YAML list values from all posts by key */
192
+ function extractAllYamlValues(postContent: string, key: string): string[] {
193
+ const values = new Set<string>();
194
+ // Split by frontmatter boundaries (---)
195
+ const parts = postContent.split(/^---$/m);
196
+ for (let i = 1; i < parts.length; i += 2) {
197
+ const fm = parts[i];
198
+ const list = extractYamlListFromContent("---\n" + fm + "\n---\n", key);
199
+ for (const v of list) values.add(v);
200
+ }
201
+ return [...values];
202
+ }
203
+
84
204
  /**
85
205
  * Generate and serve a font subset on demand.
86
206
  * Returns the CSS content to be served.
@@ -98,14 +218,6 @@ async function generateSubsetOnDemand(
98
218
  for (const ch of chars) charSet.add(ch);
99
219
  if (charSet.size === 0) return null;
100
220
 
101
- // Deduplicate against common subset
102
- const outDir = resolve(projectRoot, FONT_OUT_DIR);
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
221
  // Locate font file
110
222
  const fontPaths = [
111
223
  resolve(projectRoot, FONT_INPUT),
@@ -122,23 +234,23 @@ async function generateSubsetOnDemand(
122
234
  if (!fontPath) return null;
123
235
 
124
236
  const fontBuffer = readFileSync(fontPath);
237
+ const outDir = resolve(projectRoot, FONT_OUT_DIR);
125
238
  mkdirSync(outDir, { recursive: true });
126
239
 
127
- // Generate subset TTF using taetype
240
+ // Generate subset TTF using subset-font (WASM)
128
241
  const charsStr = [...charSet].sort().join("");
129
- try { register_font_raw("stalux", fontBuffer); } catch { /* already registered */ }
130
- const glyphs = get_glyph_ids(charsStr, "stalux", "normal", 400);
131
- if (glyphs.length === 0) return null;
132
- const subsetResult = subset_font_full("stalux", "normal", 400, 0, glyphs);
133
- const hash = [...charsStr].slice(0, 12).join("");
242
+ const hash = hashStr(charsStr);
134
243
  const filename = `subset-${subsetId}-${hash}.ttf`;
135
244
  const outPath = join(outDir, filename);
136
245
 
137
246
  if (!existsSync(outPath)) {
138
247
  try {
139
- writeFileSync(outPath, subsetResult.fontBytes);
248
+ const result = await subsetFont(fontBuffer, charsStr, {
249
+ targetFormat: "sfnt",
250
+ });
251
+ writeFileSync(outPath, result);
140
252
  logger.info(
141
- ` on-demand subset: ${(subsetResult.fontBytes.length / 1024).toFixed(1)} KB → ${filename}`,
253
+ ` on-demand subset: ${(result.length / 1024).toFixed(1)} KB → ${filename}`,
142
254
  );
143
255
  } catch {
144
256
  return null;
@@ -152,6 +264,17 @@ async function generateSubsetOnDemand(
152
264
  }`;
153
265
  }
154
266
 
267
+ /** Simple hash function for strings */
268
+ function hashStr(s: string): string {
269
+ let hash = 0;
270
+ for (let i = 0; i < s.length && i < 100; i++) {
271
+ const ch = s.charCodeAt(i);
272
+ hash = (hash << 5) - hash + ch;
273
+ hash |= 0;
274
+ }
275
+ return Math.abs(hash).toString(36).slice(0, 8);
276
+ }
277
+
155
278
  /**
156
279
  * Create Vite middleware for on-demand font subsetting in dev mode.
157
280
  */
@@ -2,26 +2,32 @@
2
2
  * Per-route font subsetting engine.
3
3
  *
4
4
  * Scans each content file individually, extracts its unique character set,
5
- * and generates a minimal WOFF2 subset per route. Also generates a "common"
5
+ * and generates a minimal TTF subset per route. Also generates a "common"
6
6
  * subset for UI text (navs, i18n, layout).
7
7
  *
8
8
  * Output layout (under public/fonts/):
9
9
  * fonts/
10
- * common.ttf — UI text subset (ASCII, nav, i18n)
11
- * post-{abbrlink}.ttf — per-post subset
12
- * about.ttf — about page subset
13
- * words.ttf — combined words subset
14
- * manifest.json — route → subset file mapping
15
- * subset.css — global @font-face declarations
10
+ * common-{hash}.ttf — UI text subset (ASCII, nav, i18n)
11
+ * subset-{hash}.ttf — per-route subset (shared by routes with same chars)
12
+ * common.css — @font-face for common subset
13
+ * subset-{route}.css — per-route @font-face
14
+ * manifest.json — route → subset file mapping
16
15
  */
17
16
 
18
17
  import { createHash } from "node:crypto";
19
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from "node:fs";
18
+ import {
19
+ existsSync,
20
+ mkdirSync,
21
+ readFileSync,
22
+ writeFileSync,
23
+ readdirSync,
24
+ unlinkSync,
25
+ } from "node:fs";
20
26
  import { resolve, basename, extname, join, dirname } from "node:path";
21
27
  import { fileURLToPath } from "node:url";
22
28
 
23
29
  import type { AstroIntegrationLogger } from "astro";
24
- import { register_font_raw, get_glyph_ids, subset_font_full } from "taetype";
30
+ import subsetFont from "subset-font";
25
31
 
26
32
  // ---------------------------------------------------------------------------
27
33
  // Configuration
@@ -97,19 +103,81 @@ function scanContent(projectRoot: string): ContentFile[] {
97
103
 
98
104
  // Posts
99
105
  const postsDir = join(contentRoot, "posts");
106
+ const allPostContents: {
107
+ abbrlink: string;
108
+ tags: string[];
109
+ categories: string[];
110
+ content: string;
111
+ }[] = [];
100
112
  if (existsSync(postsDir)) {
101
113
  for (const f of readdirSync(postsDir)) {
102
114
  if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
103
- if (f.startsWith("_")) continue; // skip template files
104
115
  const content = readFileSync(join(postsDir, f), "utf-8");
105
- // Extract abbrlink from frontmatter
116
+ const isTemplate = f.startsWith("_");
117
+
118
+ // Per-post routes: skip template (_) files
119
+ if (!isTemplate) {
120
+ const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
121
+ const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
122
+ files.push({
123
+ route: `posts/${abbrlink}`,
124
+ filePath: join(postsDir, f),
125
+ abbrlink,
126
+ });
127
+ }
128
+
129
+ // Virtual routes (archives/tags/categories): include ALL posts including templates
130
+ // because content collections load them and they render on those pages
106
131
  const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
107
132
  const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
108
- files.push({
109
- route: `posts/${abbrlink}`,
110
- filePath: join(postsDir, f),
111
- abbrlink,
112
- });
133
+ const tags = extractYamlList(content, "tags");
134
+ const categories = extractYamlList(content, "categories");
135
+ allPostContents.push({ abbrlink, tags, categories, content });
136
+ }
137
+ }
138
+
139
+ // Build virtual routes for archives, tags, categories (aggregated pages)
140
+ // These pages render content from ALL posts
141
+ if (allPostContents.length > 0) {
142
+ const allContent = allPostContents.map((p) => p.content).join("\n");
143
+
144
+ // Archives: all posts' content (titles, dates, etc.)
145
+ virtualRouteCache.set("archives", extractChars(allContent));
146
+
147
+ // Tags index: all tags + all posts' content
148
+ const allTags = [...new Set(allPostContents.flatMap((p) => p.tags))].join(" ");
149
+ virtualRouteCache.set("tags", extractChars(allContent + allTags));
150
+
151
+ // Categories index: all categories + all posts' content
152
+ const allCategories = [...new Set(allPostContents.flatMap((p) => p.categories))].join(" ");
153
+ virtualRouteCache.set("categories", extractChars(allContent + allCategories));
154
+
155
+ // Per-tag routes: tag name + posts with that tag
156
+ const tagPosts = new Map<string, string[]>();
157
+ for (const p of allPostContents) {
158
+ for (const tag of p.tags) {
159
+ const list = tagPosts.get(tag) ?? [];
160
+ list.push(p.content);
161
+ tagPosts.set(tag, list);
162
+ }
163
+ }
164
+ for (const [tag, contents] of tagPosts) {
165
+ const tagContent = tag + " " + contents.join("\n");
166
+ virtualRouteCache.set(`tags-${tag}`, extractChars(tagContent));
167
+ }
168
+
169
+ // Per-category routes: category name + posts in that category
170
+ const catPosts = new Map<string, string[]>();
171
+ for (const p of allPostContents) {
172
+ for (const cat of p.categories) {
173
+ const list = catPosts.get(cat) ?? [];
174
+ list.push(p.content);
175
+ catPosts.set(cat, list);
176
+ }
177
+ }
178
+ for (const [cat, contents] of catPosts) {
179
+ const catContent = cat + " " + contents.join("\n");
180
+ virtualRouteCache.set(`categories-${cat}`, extractChars(catContent));
113
181
  }
114
182
  }
115
183
 
@@ -150,11 +218,37 @@ function scanContent(projectRoot: string): ContentFile[] {
150
218
  }
151
219
  }
152
220
 
221
+ // Add virtual routes (archives, tags, categories) as content files
222
+ for (const [route] of virtualRouteCache) {
223
+ files.push({
224
+ route,
225
+ filePath: resolve(projectRoot, CONTENT_ROOT, `_virtual/${route}.md`),
226
+ });
227
+ }
228
+
153
229
  return files;
154
230
  }
155
231
 
232
+ /** Extract a YAML list value from frontmatter by key */
233
+ function extractYamlList(content: string, key: string): string[] {
234
+ const lines: string[] = [];
235
+ const regex = new RegExp(`^${key}:$`, "m");
236
+ const match = content.match(regex);
237
+ if (!match) return [];
238
+ const startIdx = match.index! + match[0].length;
239
+ const afterKey = content.slice(startIdx);
240
+ const listMatch = afterKey.match(/^\n((?:\s+-\s+.+\n?)*)/);
241
+ if (!listMatch) return [];
242
+ for (const line of listMatch[1].split("\n")) {
243
+ const item = line.match(/^\s+-\s+(.+)/)?.[1];
244
+ if (item) lines.push(item.trim());
245
+ }
246
+ return lines;
247
+ }
248
+
156
249
  // Cache for virtual route char sets
157
250
  const wordCharsCache = new Map<string, string>();
251
+ const virtualRouteCache = new Map<string, string>();
158
252
 
159
253
  // ---------------------------------------------------------------------------
160
254
  // Common subset: extract chars from UI source files
@@ -277,20 +371,16 @@ export async function runFontSubsetting(
277
371
  }
278
372
  const fontBuffer = readFileSync(fontPath);
279
373
 
280
- // Register font with taetype for fast Rust subsetting
281
- try {
282
- register_font_raw("stalux", fontBuffer);
283
- } catch (e) {
284
- logger.warn(`Font registration failed: ${e}, skipping subsetting`);
285
- return;
286
- }
287
-
288
374
  // Clean old generated font files to avoid stale cached references
289
375
  const outDir = resolve(projectRoot, FONT_OUT_DIR);
290
376
  if (existsSync(outDir)) {
291
377
  for (const f of readdirSync(outDir)) {
292
378
  if (f.startsWith("common-") || f.startsWith("subset-")) {
293
- try { unlinkSync(join(outDir, f)); } catch { /* skip */ }
379
+ try {
380
+ unlinkSync(join(outDir, f));
381
+ } catch {
382
+ /* skip */
383
+ }
294
384
  }
295
385
  }
296
386
  }
@@ -309,6 +399,8 @@ export async function runFontSubsetting(
309
399
  let content: string;
310
400
  if (file.route === "words" && wordCharsCache.has("words")) {
311
401
  content = wordCharsCache.get("words")!;
402
+ } else if (virtualRouteCache.has(file.route)) {
403
+ content = virtualRouteCache.get(file.route)!;
312
404
  } else {
313
405
  content = readFileSync(file.filePath, "utf-8");
314
406
  }
@@ -320,25 +412,32 @@ export async function runFontSubsetting(
320
412
  });
321
413
  }
322
414
 
323
- // 5. Deduplicate and generate subsets using taetype (Rust native, fast)
415
+ // 5. Deduplicate and generate subsets using subset-font (WASM)
324
416
  mkdirSync(outDir, { recursive: true });
325
417
 
326
- // Helper: subset using taetype
327
- function doSubset(chars: string): Uint8Array {
328
- const glyphs = get_glyph_ids(chars, "stalux", "normal", 400);
329
- if (glyphs.length === 0) return new Uint8Array(0);
330
- const result = subset_font_full("stalux", "normal", 400, 0, glyphs);
331
- return result.fontBytes;
418
+ // Helper: subset using subset-font (async WASM)
419
+ async function doSubset(chars: string): Promise<Uint8Array> {
420
+ if (!chars) return new Uint8Array(0);
421
+ try {
422
+ const result = await subsetFont(fontBuffer, chars, {
423
+ targetFormat: "sfnt", // "sfnt" = TTF output
424
+ });
425
+ return result;
426
+ } catch {
427
+ return new Uint8Array(0);
428
+ }
332
429
  }
333
430
 
334
431
  // Generate common subset
335
432
  const commonFilename = `common-${commonHash}.ttf`;
336
433
  const commonOutPath = join(outDir, commonFilename);
337
434
  if (!existsSync(commonOutPath)) {
338
- const data = doSubset(commonChars);
435
+ const data = await doSubset(commonChars);
339
436
  if (data.length > 0) {
340
437
  writeFileSync(commonOutPath, data);
341
- logger.info(` common subset: ${(data.length / 1024).toFixed(1)} KB → ${commonFilename}`);
438
+ logger.info(
439
+ ` common subset: ${(data.length / 1024).toFixed(1)} KB → ${commonFilename}`,
440
+ );
342
441
  }
343
442
  }
344
443
 
@@ -370,13 +469,12 @@ export async function runFontSubsetting(
370
469
  .filter((ch) => !commonChars.includes(ch))
371
470
  .join("");
372
471
  if (uniqueChars.length > 0) {
373
- const glyphs = get_glyph_ids(uniqueChars, "stalux", "normal", 400);
374
- if (glyphs.length > 0) {
375
- const result = subset_font_full("stalux", "normal", 400, 0, glyphs);
376
- writeFileSync(outPath, result.fontBytes);
472
+ const data = await doSubset(uniqueChars);
473
+ if (data.length > 0) {
474
+ writeFileSync(outPath, data);
377
475
  subsetCount++;
378
476
  logger.info(
379
- ` subset ${subsetCount}: ${(result.fontBytes.length / 1024).toFixed(1)} KB → ${filename}`,
477
+ ` subset ${subsetCount}: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
380
478
  );
381
479
  }
382
480
  }
@@ -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={posts.length > 0 ? "archives" : undefined}>
16
16
  <div style="height: 3em;"></div>
17
17
  <Navs />
18
18
  <h1 class={styles.h1}>{t("archives.heading")}</h1>
@@ -27,7 +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
-
30
+ fontSubsetId={props.posts.length > 0 ? `categories-${encodeURIComponent(props.category)}` : undefined}
31
31
  >
32
32
  <div style="height: 3em;"></div>
33
33
  <Navs />
@@ -9,12 +9,14 @@ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, get
9
9
  const configEntries2 = await getCollection("config");
10
10
  const siteConfig = getSiteData(configEntries2);
11
11
  const { t } = createTranslator(siteConfig.lang || "zh-CN");
12
+ const posts = await getCollection("posts", ({ data }) => !data.draft);
13
+ const hasContent = posts.length > 0;
12
14
  ---
13
15
 
14
16
  <Stalux
15
17
  title={t("categories.title") + " | " + siteConfig.title}
16
18
  description={t("categories.description", { title: siteConfig.title })}
17
-
19
+ fontSubsetId={hasContent ? "categories" : undefined}
18
20
  >
19
21
  <div style="height: 3em;"></div>
20
22
  <Navs />
@@ -29,7 +29,7 @@ const props = Astro.props as Props;
29
29
  title={t("taxonomy.tag") + ": " + props.tag + " | " + siteConfig.title}
30
30
  description={t("taxonomy.browseTag", { title: siteConfig.title, tag: props.tag, count: props.count })}
31
31
  noindex={props.count < 3}
32
-
32
+ fontSubsetId={props.posts.length > 0 ? `tags-${encodeURIComponent(props.tag)}` : undefined}
33
33
  >
34
34
  <div style="height: 3em;"></div>
35
35
  <Navs />
@@ -9,12 +9,14 @@ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, get
9
9
  const configEntries = await getCollection("config");
10
10
  const siteConfig = getSiteData(configEntries);
11
11
  const { t } = createTranslator(siteConfig.lang || "zh-CN");
12
+ const posts = await getCollection("posts", ({ data }) => !data.draft);
13
+ const hasContent = posts.length > 0;
12
14
  ---
13
15
 
14
16
  <Stalux
15
17
  title={t("tags.title") + " | " + siteConfig.title}
16
18
  description={t("tags.description", { title: siteConfig.title })}
17
-
19
+ fontSubsetId={hasContent ? "tags" : undefined}
18
20
  >
19
21
  <div style="height: 3em;"></div>
20
22
  <Navs />