@xingwangzhe/stalux 1.6.0 → 1.6.2

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.2",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -63,22 +63,156 @@ 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) ──
67
- // These pages use i18n text + config YAML text, which are already
68
- // covered by the common subset. Return config text for extra safety.
69
- if (["home", "archives", "tags", "categories", "links", "common"].includes(subsetId)) {
70
- const configDir = join(contentRoot, "config");
71
- if (!existsSync(configDir)) return null;
72
- const all: string[] = [];
73
- for (const f of readdirSync(configDir)) {
74
- if (f.endsWith(".yml") || f.endsWith(".yaml")) {
75
- all.push(readFileSync(join(configDir, f), "utf-8"));
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
+ // ── Home page ───────────────────────────────────────────
94
+ if (subsetId === "home") {
95
+ return getAllConfigContent(contentRoot);
96
+ }
97
+
98
+ // ── Links page ──────────────────────────────────────────
99
+ if (subsetId === "links") {
100
+ return getAllConfigContent(contentRoot);
101
+ }
102
+
103
+ // ── Other HTML routes (common) ──
104
+ // The common subset includes UI text from source + config YAML
105
+ if (subsetId === "common") {
106
+ return getAllConfigContent(contentRoot);
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ /** Get all config YAML content */
113
+ function getAllConfigContent(contentRoot: string): string | null {
114
+ const configDir = join(contentRoot, "config");
115
+ if (!existsSync(configDir)) return null;
116
+ const all: string[] = [];
117
+ for (const f of readdirSync(configDir)) {
118
+ if (f.endsWith(".yml") || f.endsWith(".yaml")) {
119
+ all.push(readFileSync(join(configDir, f), "utf-8"));
120
+ }
121
+ }
122
+ return all.join("\n") || null;
123
+ }
124
+
125
+ /** Get all post content concatenated (including template/_ files) */
126
+ function getAllPostContent(contentRoot: string): string | null {
127
+ const postsDir = join(contentRoot, "posts");
128
+ if (!existsSync(postsDir)) return null;
129
+ const all: string[] = [];
130
+ for (const f of readdirSync(postsDir)) {
131
+ if (f.endsWith(".md") || f.endsWith(".mdx")) {
132
+ all.push(readFileSync(join(postsDir, f), "utf-8"));
133
+ }
134
+ }
135
+ return all.join("\n") || null;
136
+ }
137
+
138
+ /** Get all tags + all post content */
139
+ function getAllTagsAndPostContent(contentRoot: string): string | null {
140
+ const postContent = getAllPostContent(contentRoot);
141
+ if (!postContent) return null;
142
+ const tags = extractAllYamlValues(postContent, "tags");
143
+ return postContent + " " + tags.join(" ");
144
+ }
145
+
146
+ /** Get all categories + all post content */
147
+ function getAllCategoriesAndPostContent(contentRoot: string): string | null {
148
+ const postContent = getAllPostContent(contentRoot);
149
+ if (!postContent) return null;
150
+ const categories = extractAllYamlValues(postContent, "categories");
151
+ return postContent + " " + categories.join(" ");
152
+ }
153
+
154
+ /** Get content for a specific tag: tag name + posts with that tag */
155
+ function getTagContent(contentRoot: string, tagName: string): string | null {
156
+ const postsDir = join(contentRoot, "posts");
157
+ if (!existsSync(postsDir)) return null;
158
+ const all: string[] = [tagName];
159
+ for (const f of readdirSync(postsDir)) {
160
+ if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
161
+ const content = readFileSync(join(postsDir, f), "utf-8");
162
+ const tags = extractYamlListFromContent(content, "tags");
163
+ if (tags.includes(tagName)) {
164
+ all.push(content);
76
165
  }
77
166
  }
78
- return all.join("\n") || null;
79
167
  }
168
+ return all.join("\n") || null;
169
+ }
80
170
 
81
- return null;
171
+ /** Get content for a specific category: category name + posts in that category */
172
+ function getCategoryContent(contentRoot: string, catName: string): string | null {
173
+ const postsDir = join(contentRoot, "posts");
174
+ if (!existsSync(postsDir)) return null;
175
+ const all: string[] = [catName];
176
+ for (const f of readdirSync(postsDir)) {
177
+ if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
178
+ const content = readFileSync(join(postsDir, f), "utf-8");
179
+ const categories = extractYamlListFromContent(content, "categories");
180
+ if (categories.includes(catName)) {
181
+ all.push(content);
182
+ }
183
+ }
184
+ }
185
+ return all.join("\n") || null;
186
+ }
187
+
188
+ /** Extract a YAML list value from frontmatter by key */
189
+ function extractYamlListFromContent(content: string, key: string): string[] {
190
+ const lines: string[] = [];
191
+ const regex = new RegExp(`^${key}:$`, "m");
192
+ const match = content.match(regex);
193
+ if (!match) return [];
194
+ const startIdx = match.index! + match[0].length;
195
+ const afterKey = content.slice(startIdx);
196
+ const listMatch = afterKey.match(/^\n((?:\s+-\s+.+\n?)*)/);
197
+ if (!listMatch) return [];
198
+ for (const line of listMatch[1].split("\n")) {
199
+ const item = line.match(/^\s+-\s+(.+)/)?.[1];
200
+ if (item) lines.push(item.trim());
201
+ }
202
+ return lines;
203
+ }
204
+
205
+ /** Extract all YAML list values from all posts by key */
206
+ function extractAllYamlValues(postContent: string, key: string): string[] {
207
+ const values = new Set<string>();
208
+ // Split by frontmatter boundaries (---)
209
+ const parts = postContent.split(/^---$/m);
210
+ for (let i = 1; i < parts.length; i += 2) {
211
+ const fm = parts[i];
212
+ const list = extractYamlListFromContent("---\n" + fm + "\n---\n", key);
213
+ for (const v of list) values.add(v);
214
+ }
215
+ return [...values];
82
216
  }
83
217
 
84
218
  /**
@@ -103,19 +103,100 @@ 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));
181
+ }
182
+ }
183
+
184
+ // Build virtual routes for config-driven pages (home, links)
185
+ // These pages render text from config YAML files (nav titles, site title, etc.)
186
+ const configDir = join(contentRoot, "config");
187
+ if (existsSync(configDir)) {
188
+ const allConfigContent: string[] = [];
189
+ for (const f of readdirSync(configDir)) {
190
+ if (f.endsWith(".yml") || f.endsWith(".yaml")) {
191
+ allConfigContent.push(readFileSync(join(configDir, f), "utf-8"));
192
+ }
193
+ }
194
+ if (allConfigContent.length > 0) {
195
+ const configText = allConfigContent.join("\n");
196
+ // Home page: site title, author, navs, typetexts, social media, footer
197
+ virtualRouteCache.set("home", extractChars(configText));
198
+ // Links page: links config + navs + footer
199
+ virtualRouteCache.set("links", extractChars(configText));
119
200
  }
120
201
  }
121
202
 
@@ -156,11 +237,37 @@ function scanContent(projectRoot: string): ContentFile[] {
156
237
  }
157
238
  }
158
239
 
240
+ // Add virtual routes (archives, tags, categories) as content files
241
+ for (const [route] of virtualRouteCache) {
242
+ files.push({
243
+ route,
244
+ filePath: resolve(projectRoot, CONTENT_ROOT, `_virtual/${route}.md`),
245
+ });
246
+ }
247
+
159
248
  return files;
160
249
  }
161
250
 
251
+ /** Extract a YAML list value from frontmatter by key */
252
+ function extractYamlList(content: string, key: string): string[] {
253
+ const lines: string[] = [];
254
+ const regex = new RegExp(`^${key}:$`, "m");
255
+ const match = content.match(regex);
256
+ if (!match) return [];
257
+ const startIdx = match.index! + match[0].length;
258
+ const afterKey = content.slice(startIdx);
259
+ const listMatch = afterKey.match(/^\n((?:\s+-\s+.+\n?)*)/);
260
+ if (!listMatch) return [];
261
+ for (const line of listMatch[1].split("\n")) {
262
+ const item = line.match(/^\s+-\s+(.+)/)?.[1];
263
+ if (item) lines.push(item.trim());
264
+ }
265
+ return lines;
266
+ }
267
+
162
268
  // Cache for virtual route char sets
163
269
  const wordCharsCache = new Map<string, string>();
270
+ const virtualRouteCache = new Map<string, string>();
164
271
 
165
272
  // ---------------------------------------------------------------------------
166
273
  // Common subset: extract chars from UI source files
@@ -311,6 +418,8 @@ export async function runFontSubsetting(
311
418
  let content: string;
312
419
  if (file.route === "words" && wordCharsCache.has("words")) {
313
420
  content = wordCharsCache.get("words")!;
421
+ } else if (virtualRouteCache.has(file.route)) {
422
+ content = virtualRouteCache.get(file.route)!;
314
423
  } else {
315
424
  content = readFileSync(file.filePath, "utf-8");
316
425
  }
@@ -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 />
@@ -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}>
@@ -13,12 +13,12 @@ const linksTitle = linksConfig.title;
13
13
  const linksDescription = linksConfig.description;
14
14
  ---
15
15
 
16
- <ListPage
17
- title={siteTitle}
18
- description={siteDescription}
19
- heading={linksTitle}
20
- subheading={linksDescription}
21
-
22
- >
16
+ <ListPage
17
+ title={siteTitle}
18
+ description={siteDescription}
19
+ heading={linksTitle}
20
+ subheading={linksDescription}
21
+ fontSubsetId="links"
22
+ >
23
23
  <LinkList />
24
24
  </ListPage>
@@ -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 />