@xingwangzhe/stalux 1.6.0 → 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -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.
@@ -103,19 +103,81 @@ function scanContent(projectRoot: string): ContentFile[] {
103
103
 
104
104
  // Posts
105
105
  const postsDir = join(contentRoot, "posts");
106
+ const allPostContents: {
107
+ abbrlink: string;
108
+ tags: string[];
109
+ categories: string[];
110
+ content: string;
111
+ }[] = [];
106
112
  if (existsSync(postsDir)) {
107
113
  for (const f of readdirSync(postsDir)) {
108
114
  if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
109
- if (f.startsWith("_")) continue; // skip template files
110
115
  const content = readFileSync(join(postsDir, f), "utf-8");
111
- // 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
112
131
  const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
113
132
  const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
114
- files.push({
115
- route: `posts/${abbrlink}`,
116
- filePath: join(postsDir, f),
117
- abbrlink,
118
- });
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));
119
181
  }
120
182
  }
121
183
 
@@ -156,11 +218,37 @@ function scanContent(projectRoot: string): ContentFile[] {
156
218
  }
157
219
  }
158
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
+
159
229
  return files;
160
230
  }
161
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
+
162
249
  // Cache for virtual route char sets
163
250
  const wordCharsCache = new Map<string, string>();
251
+ const virtualRouteCache = new Map<string, string>();
164
252
 
165
253
  // ---------------------------------------------------------------------------
166
254
  // Common subset: extract chars from UI source files
@@ -311,6 +399,8 @@ export async function runFontSubsetting(
311
399
  let content: string;
312
400
  if (file.route === "words" && wordCharsCache.has("words")) {
313
401
  content = wordCharsCache.get("words")!;
402
+ } else if (virtualRouteCache.has(file.route)) {
403
+ content = virtualRouteCache.get(file.route)!;
314
404
  } else {
315
405
  content = readFileSync(file.filePath, "utf-8");
316
406
  }
@@ -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 />