@xingwangzhe/stalux 1.2.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.
Files changed (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/package.json +108 -0
  4. package/scripts/cli.mjs +103 -0
  5. package/src/assets/BiCcCircle.svg +1 -0
  6. package/src/assets/astro.svg +1 -0
  7. package/src/assets/background/README.md +3 -0
  8. package/src/assets/background/pattern-1.min.svg +1 -0
  9. package/src/assets/background/pattern-10.min.svg +1 -0
  10. package/src/assets/background/pattern-11.min.svg +1 -0
  11. package/src/assets/background/pattern-12.min.svg +1 -0
  12. package/src/assets/background/pattern-13.min.svg +1 -0
  13. package/src/assets/background/pattern-14.min.svg +1 -0
  14. package/src/assets/background/pattern-15.min.svg +1 -0
  15. package/src/assets/background/pattern-16.min.svg +1 -0
  16. package/src/assets/background/pattern-17.min.svg +1 -0
  17. package/src/assets/background/pattern-18.min.svg +1 -0
  18. package/src/assets/background/pattern-19.min.svg +1 -0
  19. package/src/assets/background/pattern-2.min.svg +1 -0
  20. package/src/assets/background/pattern-20.min.svg +1 -0
  21. package/src/assets/background/pattern-21.min.svg +1 -0
  22. package/src/assets/background/pattern-22.min.svg +1 -0
  23. package/src/assets/background/pattern-23.min.svg +1 -0
  24. package/src/assets/background/pattern-24.min.svg +1 -0
  25. package/src/assets/background/pattern-25.min.svg +1 -0
  26. package/src/assets/background/pattern-26.min.svg +1 -0
  27. package/src/assets/background/pattern-27.min.svg +1 -0
  28. package/src/assets/background/pattern-28.min.svg +1 -0
  29. package/src/assets/background/pattern-29.min.svg +1 -0
  30. package/src/assets/background/pattern-3.min.svg +1 -0
  31. package/src/assets/background/pattern-30.min.svg +1 -0
  32. package/src/assets/background/pattern-31.min.svg +1 -0
  33. package/src/assets/background/pattern-32.min.svg +1 -0
  34. package/src/assets/background/pattern-33.min.svg +1 -0
  35. package/src/assets/background/pattern-34.min.svg +1 -0
  36. package/src/assets/background/pattern-35.min.svg +1 -0
  37. package/src/assets/background/pattern-36.min.svg +1 -0
  38. package/src/assets/background/pattern-37.min.svg +1 -0
  39. package/src/assets/background/pattern-38.min.svg +1 -0
  40. package/src/assets/background/pattern-39.min.svg +1 -0
  41. package/src/assets/background/pattern-4.min.svg +1 -0
  42. package/src/assets/background/pattern-40.min.svg +1 -0
  43. package/src/assets/background/pattern-41.min.svg +1 -0
  44. package/src/assets/background/pattern-42.min.svg +1 -0
  45. package/src/assets/background/pattern-5.min.svg +1 -0
  46. package/src/assets/background/pattern-6.min.svg +1 -0
  47. package/src/assets/background/pattern-7.min.svg +1 -0
  48. package/src/assets/background/pattern-8.min.svg +1 -0
  49. package/src/assets/background/pattern-9.min.svg +1 -0
  50. package/src/assets/background.svg +1 -0
  51. package/src/assets/cc-icon.svg +4 -0
  52. package/src/assets/fonts/GoogleSansCode-Italic.woff2 +0 -0
  53. package/src/assets/fonts/GoogleSansCode.woff2 +0 -0
  54. package/src/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
  55. package/src/assets/fonts/LXGWWenKai-Regular.woff2 +0 -0
  56. package/src/components/stalux/analytics/clarity.astro +25 -0
  57. package/src/components/stalux/analytics/google.astro +31 -0
  58. package/src/components/stalux/analytics/umami.astro +16 -0
  59. package/src/components/stalux/archives/archivesList.astro +116 -0
  60. package/src/components/stalux/archives/toArchive.astro +14 -0
  61. package/src/components/stalux/categories/categoriesCard.astro +49 -0
  62. package/src/components/stalux/categories/categoriesList.astro +17 -0
  63. package/src/components/stalux/common/author.astro +35 -0
  64. package/src/components/stalux/common/date.astro +59 -0
  65. package/src/components/stalux/common/search.astro +14 -0
  66. package/src/components/stalux/common/textType.astro +16 -0
  67. package/src/components/stalux/common/verCount.astro +10 -0
  68. package/src/components/stalux/footer/FooterBadges.astro +150 -0
  69. package/src/components/stalux/footer/FooterBeian.astro +91 -0
  70. package/src/components/stalux/footer/FooterCopyright.astro +40 -0
  71. package/src/components/stalux/footer/FooterStats.astro +108 -0
  72. package/src/components/stalux/footer/FooterThemeInfo.astro +67 -0
  73. package/src/components/stalux/layout/footer.astro +87 -0
  74. package/src/components/stalux/layout/head.astro +194 -0
  75. package/src/components/stalux/layout/listPage.astro +26 -0
  76. package/src/components/stalux/layout/navs.astro +174 -0
  77. package/src/components/stalux/links/linkCard.astro +50 -0
  78. package/src/components/stalux/links/linkList.astro +20 -0
  79. package/src/components/stalux/links/socialMedia.astro +102 -0
  80. package/src/components/stalux/posts/PhotoSwipeStyle.astro +3 -0
  81. package/src/components/stalux/posts/PostTaxonomy.astro +65 -0
  82. package/src/components/stalux/posts/WalineComment.astro +609 -0
  83. package/src/components/stalux/posts/postAuthor.astro +23 -0
  84. package/src/components/stalux/posts/postCC.astro +52 -0
  85. package/src/components/stalux/posts/postCard.astro +83 -0
  86. package/src/components/stalux/posts/postContent.astro +174 -0
  87. package/src/components/stalux/posts/postLeft.astro +41 -0
  88. package/src/components/stalux/posts/postList.astro +25 -0
  89. package/src/components/stalux/posts/postNavigation.astro +137 -0
  90. package/src/components/stalux/posts/postRight.astro +33 -0
  91. package/src/components/stalux/posts/postToc.astro +88 -0
  92. package/src/components/stalux/posts/randomPost.astro +134 -0
  93. package/src/components/stalux/tags/tagHeader.astro +50 -0
  94. package/src/components/stalux/tags/tagsCloud.astro +100 -0
  95. package/src/components/stalux/words/wordCard.astro +62 -0
  96. package/src/components/stalux/words/wordList.astro +70 -0
  97. package/src/config.ts +58 -0
  98. package/src/i18n/en.json +193 -0
  99. package/src/i18n/zh-CN.json +193 -0
  100. package/src/index.ts +215 -0
  101. package/src/internal/components-plugin.ts +54 -0
  102. package/src/internal/dev-toolbar.ts +33 -0
  103. package/src/internal/override.ts +99 -0
  104. package/src/internal/pagefind.ts +40 -0
  105. package/src/layouts/PostLayout.astro +90 -0
  106. package/src/layouts/Stalux.astro +91 -0
  107. package/src/pages/404.astro +103 -0
  108. package/src/pages/about.astro +59 -0
  109. package/src/pages/about.md.ts +15 -0
  110. package/src/pages/api/post.abbrlink.json.ts +20 -0
  111. package/src/pages/archives.astro +23 -0
  112. package/src/pages/archives.md.ts +24 -0
  113. package/src/pages/atom.xml.ts +29 -0
  114. package/src/pages/categories/[category].astro +35 -0
  115. package/src/pages/categories/[category].md.ts +34 -0
  116. package/src/pages/categories/index.astro +23 -0
  117. package/src/pages/categories/index.md.ts +27 -0
  118. package/src/pages/index.astro +30 -0
  119. package/src/pages/index.md.ts +18 -0
  120. package/src/pages/links.astro +23 -0
  121. package/src/pages/links.md.ts +18 -0
  122. package/src/pages/llms-full.txt.ts +25 -0
  123. package/src/pages/llms.txt.ts +26 -0
  124. package/src/pages/posts/[post].astro +69 -0
  125. package/src/pages/posts/[post].md.ts +109 -0
  126. package/src/pages/rss.xml.ts +29 -0
  127. package/src/pages/tags/[tag].astro +37 -0
  128. package/src/pages/tags/[tag].md.ts +34 -0
  129. package/src/pages/tags/index.astro +23 -0
  130. package/src/pages/tags/index.md.ts +27 -0
  131. package/src/pages/words.astro +24 -0
  132. package/src/pages/words.md.ts +14 -0
  133. package/src/plugins/feature-flags.ts +68 -0
  134. package/src/plugins/satteri-temml.ts +81 -0
  135. package/src/schemas/collections.ts +140 -0
  136. package/src/schemas/config.ts +226 -0
  137. package/src/scripts/background.ts +59 -0
  138. package/src/styles/base/init.css +286 -0
  139. package/src/styles/base/math.css +411 -0
  140. package/src/styles/components/archives/archivesList.module.css +378 -0
  141. package/src/styles/components/archives/toArchive.module.css +3 -0
  142. package/src/styles/components/categories/categoriesCard.module.css +87 -0
  143. package/src/styles/components/categories/categoriesList.module.css +27 -0
  144. package/src/styles/components/common/author.module.css +73 -0
  145. package/src/styles/components/common/date.module.css +15 -0
  146. package/src/styles/components/common/textType.module.css +10 -0
  147. package/src/styles/components/footer/footer.module.css +44 -0
  148. package/src/styles/components/layout/navs.module.css +250 -0
  149. package/src/styles/components/links/linkCard.module.css +99 -0
  150. package/src/styles/components/links/linkList.module.css +67 -0
  151. package/src/styles/components/links/socialLinks.module.css +22 -0
  152. package/src/styles/components/markdown.css +528 -0
  153. package/src/styles/components/posts/postAuthor.module.css +32 -0
  154. package/src/styles/components/posts/postCC.module.css +138 -0
  155. package/src/styles/components/posts/postCard.module.css +156 -0
  156. package/src/styles/components/posts/postCategories.module.css +50 -0
  157. package/src/styles/components/posts/postContent.module.css +230 -0
  158. package/src/styles/components/posts/postLayout.module.css +142 -0
  159. package/src/styles/components/posts/postLeft.module.css +38 -0
  160. package/src/styles/components/posts/postList.module.css +24 -0
  161. package/src/styles/components/posts/postNavigation.module.css +187 -0
  162. package/src/styles/components/posts/postRight.module.css +60 -0
  163. package/src/styles/components/posts/postSidebar.module.css +57 -0
  164. package/src/styles/components/posts/postTags.module.css +51 -0
  165. package/src/styles/components/posts/postTaxonomy.module.css +87 -0
  166. package/src/styles/components/posts/postToc.module.css +74 -0
  167. package/src/styles/components/posts/randomPost.module.css +131 -0
  168. package/src/styles/components/tags/tagHeader.module.css +44 -0
  169. package/src/styles/components/tags/tagsCloud.module.css +124 -0
  170. package/src/styles/components/words/wordCard.module.css +130 -0
  171. package/src/styles/components/words/wordList.module.css +39 -0
  172. package/src/styles/pages/archives.module.css +18 -0
  173. package/src/styles/pages/categories.module.css +13 -0
  174. package/src/styles/pages/home.module.css +51 -0
  175. package/src/styles/pages/listPage.module.css +13 -0
  176. package/src/styles/pages/tags.module.css +12 -0
  177. package/src/styles/pages/words.module.css +13 -0
  178. package/src/styles/shared/animations.css +87 -0
  179. package/src/styles/shared/stagger.css +20 -0
  180. package/src/utils/ai-discovery.ts +547 -0
  181. package/src/utils/badge-generator.ts +68 -0
  182. package/src/utils/cc.ts +46 -0
  183. package/src/utils/collections-stats.ts +48 -0
  184. package/src/utils/config-utils.ts +107 -0
  185. package/src/utils/count-words.ts +20 -0
  186. package/src/utils/dayjs.ts +106 -0
  187. package/src/utils/feed.ts +70 -0
  188. package/src/utils/i18n.ts +64 -0
  189. package/src/utils/lucide-icons.ts +52 -0
  190. package/src/utils/semantic-time.ts +11 -0
  191. package/src/utils/taxonomy.ts +44 -0
  192. package/src/utils/toc-scrollspy.ts +95 -0
  193. package/src/utils/waline.ts +27 -0
  194. package/src/utils/word-count-utils.ts +83 -0
  195. package/template/astro.config.mjs +51 -0
  196. package/template/package.json +27 -0
  197. package/template/public/robots.txt +3 -0
  198. package/template/src/content.config.ts +11 -0
  199. package/template/stalux/about/index.md +14 -0
  200. package/template/stalux/config/ai-discovery.yml +2 -0
  201. package/template/stalux/config/author.yml +4 -0
  202. package/template/stalux/config/comment.yml +5 -0
  203. package/template/stalux/config/footer.yml +13 -0
  204. package/template/stalux/config/head.yml +7 -0
  205. package/template/stalux/config/links.yml +4 -0
  206. package/template/stalux/config/media-links.yml +8 -0
  207. package/template/stalux/config/navs.yml +23 -0
  208. package/template/stalux/config/promote.yml +2 -0
  209. package/template/stalux/config/site.yml +7 -0
  210. package/template/stalux/config/typetexts.yml +5 -0
  211. package/template/stalux/posts/hello-stalux.md +47 -0
  212. package/template/stalux/words/_template.md +12 -0
  213. package/template/stalux/words/einstein-imagination.md +8 -0
  214. package/template/tsconfig.json +9 -0
@@ -0,0 +1,18 @@
1
+ import { getSite, isMarkdownExportEnabled, loadConfig, renderLinksMd } from "@utils/ai-discovery";
2
+ import { createTranslator } from "@utils/i18n";
3
+ import type { APIRoute } from "astro";
4
+
5
+ export const prerender = true;
6
+
7
+ export const GET: APIRoute = async (context) => {
8
+ const config = await loadConfig();
9
+ if (!isMarkdownExportEnabled(config)) return new Response(null, { status: 404 });
10
+
11
+ const site = getSite(config, context.site?.toString());
12
+ const lang = (config.get("site")?.lang as string) || "zh-CN";
13
+ const { t } = createTranslator(lang);
14
+ const text = renderLinksMd(config, site, t);
15
+ return new Response(text, {
16
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
17
+ });
18
+ };
@@ -0,0 +1,25 @@
1
+ import { getSite, isAiFileEnabled, loadConfig, renderLlmsFullTxt } from "@utils/ai-discovery";
2
+ import { getAiDiscoveryData } from "@utils/config-utils";
3
+ import type { APIRoute } from "astro";
4
+ import { getCollection } from "astro:content";
5
+
6
+ export const prerender = true;
7
+
8
+ export const GET: APIRoute = async (context) => {
9
+ const configEntries = await getCollection("config");
10
+ const aiConfig = getAiDiscoveryData(configEntries);
11
+ if (!isAiFileEnabled(aiConfig?.conformance, "llms-full.txt")) {
12
+ return new Response("File disabled by ai-discovery config.\n", {
13
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
14
+ });
15
+ }
16
+ const config = await loadConfig();
17
+ const site = getSite(config, context.site?.toString());
18
+ const text = await renderLlmsFullTxt(config, site);
19
+
20
+ return new Response(text, {
21
+ headers: {
22
+ "Content-Type": "text/plain; charset=utf-8",
23
+ },
24
+ });
25
+ };
@@ -0,0 +1,26 @@
1
+ import { getSite, loadConfig, renderLlmsTxt } from "@utils/ai-discovery";
2
+ import { isAiFileEnabled } from "@utils/ai-discovery";
3
+ import { getAiDiscoveryData } from "@utils/config-utils";
4
+ import type { APIRoute } from "astro";
5
+ import { getCollection } from "astro:content";
6
+
7
+ export const prerender = true;
8
+
9
+ export const GET: APIRoute = async (context) => {
10
+ const configEntries = await getCollection("config");
11
+ const aiConfig = getAiDiscoveryData(configEntries);
12
+ if (!isAiFileEnabled(aiConfig?.conformance, "llms.txt")) {
13
+ return new Response("File disabled by ai-discovery config.\n", {
14
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
15
+ });
16
+ }
17
+ const config = await loadConfig();
18
+ const site = getSite(config, context.site?.toString());
19
+ const text = await renderLlmsTxt(config, site);
20
+
21
+ return new Response(text, {
22
+ headers: {
23
+ "Content-Type": "text/plain; charset=utf-8",
24
+ },
25
+ });
26
+ };
@@ -0,0 +1,69 @@
1
+ ---
2
+ import Stalux from "@layouts/Stalux.astro";
3
+ import PostLayout from "@layouts/PostLayout.astro";
4
+ import { getCollection, type CollectionEntry, render } from "astro:content";
5
+ import { toTimestamp } from "@utils/dayjs";
6
+ import Navs from "@components/stalux/layout/navs.astro";
7
+ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, getCommentData, getFooterData, getPromoteData, getTypetextsData, getMediaLinksData } from "@utils/config-utils";
8
+ interface Props {
9
+ post: CollectionEntry<"posts">;
10
+ prev?: { url: string; title: string };
11
+ next?: { url: string; title: string };
12
+ }
13
+ const configEntries = await getCollection("config");
14
+ const siteConfig = getSiteData(configEntries);
15
+ const authorConfig = getAuthorData(configEntries);
16
+ const props = Astro.props as Props;
17
+ const { Content, headings, remarkPluginFrontmatter } = await render(props.post);
18
+
19
+ // 从 remarkPluginFrontmatter 读取 feature flags 和字数统计(由 satteri 插件在构建时写入)
20
+ const flags = {
21
+ hasImage: remarkPluginFrontmatter?.hasImage ?? false,
22
+ };
23
+
24
+ // 字数统计直接从 frontmatter 读取
25
+ const minutesRead = (remarkPluginFrontmatter?.minutesRead as string) ?? "";
26
+ const readingMinutes = (remarkPluginFrontmatter?.readingMinutes as number) ?? 0;
27
+ const wordCount = (remarkPluginFrontmatter?.wordCount as number) ?? 0;
28
+ // desc 可能为空字符串,需用 || 做“空串回退”,不能用 ??(?? 会把 "" 当作有效值)
29
+ const autoDesc = props.post.data.desc || (remarkPluginFrontmatter?.desc as string) || "";
30
+
31
+ export async function getStaticPaths() {
32
+ const posts = await getCollection("posts", ({ data }) => !data.draft);
33
+ const sorted = posts.sort((a,b) => toTimestamp(b.data.date) - toTimestamp(a.data.date));
34
+ return sorted.map((post, i) => {
35
+ // sorted 按日期降序:sorted[i-1] 比当前新(next),sorted[i+1] 比当前旧(prev)
36
+ const nextPost = i > 0 ? sorted[i - 1] : undefined;
37
+ const prevPost = i < sorted.length - 1 ? sorted[i + 1] : undefined;
38
+ return {
39
+ params: { post: post.data.abbrlink },
40
+ props: {
41
+ post,
42
+ prev: prevPost
43
+ ? { url: "/posts/" + prevPost.data.abbrlink, title: prevPost.data.title }
44
+ : undefined,
45
+ next: nextPost
46
+ ? { url: "/posts/" + nextPost.data.abbrlink, title: nextPost.data.title }
47
+ : undefined,
48
+ },
49
+ };
50
+ });
51
+ }
52
+ ---
53
+ <Stalux
54
+ title={props.post.data.title + " | " + siteConfig.title}
55
+ description={autoDesc}
56
+ cover={props.post.data.cover}
57
+ isArticle={true}
58
+ date={props.post.data.date}
59
+ updated={props.post.data.updated}
60
+ tags={props.post.data.tags}
61
+ categories={props.post.data.categories}
62
+ author={authorConfig.name}
63
+ >
64
+ <div style="height:3em"></div>
65
+ <Navs /><div style="height:3em"></div>
66
+ <PostLayout title={props.post.data.title} date={props.post.data.date} updated={props.post.data.updated} readingTime={minutesRead} readingMinutes={readingMinutes} tags={props.post.data.tags||[]} categories={props.post.data.categories||[]} headings={headings} author={authorConfig.name} authorAvatar={authorConfig.avatar} prev={props.prev} next={props.next} cc={props.post.data.cc} wordCount={wordCount} hasImage={flags.hasImage}>
67
+ <Content/>
68
+ </PostLayout>
69
+ </Stalux>
@@ -0,0 +1,109 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { isMarkdownExportEnabled, loadConfig, type ConfigMap } from "@utils/ai-discovery";
5
+ import { buildCCName, buildCCLink } from "@utils/cc";
6
+ import { createTranslator } from "@utils/i18n";
7
+ import type { APIRoute, GetStaticPaths } from "astro";
8
+ import { getCollection, type CollectionEntry } from "astro:content";
9
+
10
+ export const prerender = true;
11
+
12
+ function reconstructMarkdown(post: CollectionEntry<"posts">): string {
13
+ const d = post.data;
14
+ const lines: string[] = ["---"];
15
+
16
+ lines.push(`title: ${yamlScalar(d.title)}`);
17
+ if (d.tags?.length)
18
+ lines.push(`tags:\n${d.tags.map((t) => ` - ${yamlScalar(t)}`).join("\n")}`);
19
+ if (d.categories?.length)
20
+ lines.push(`categories:\n${d.categories.map((c) => ` - ${yamlScalar(c)}`).join("\n")}`);
21
+ lines.push(`date: ${yamlScalar(d.date)}`);
22
+ if (d.updated) lines.push(`updated: ${yamlScalar(d.updated)}`);
23
+ lines.push(`desc: ${yamlScalar(d.desc)}`);
24
+ lines.push(`abbrlink: ${yamlScalar(String(d.abbrlink))}`);
25
+ if (d.draft) lines.push(`draft: true`);
26
+ if (d.cc && d.cc !== "CC-BY-NC-SA-4.0") lines.push(`cc: ${yamlScalar(d.cc)}`);
27
+ if (d.cover) lines.push(`cover: ${yamlScalar(d.cover)}`);
28
+
29
+ lines.push("---", "");
30
+ const body = typeof post.body === "string" ? post.body : "";
31
+ return lines.join("\n") + body + (body.endsWith("\n") ? "" : "\n");
32
+ }
33
+
34
+ function yamlScalar(value: string): string {
35
+ if (
36
+ value === "" ||
37
+ /[:#{}[\],&*?|>!%@`]/.test(value) ||
38
+ value.includes("\n") ||
39
+ value.startsWith(" ") ||
40
+ value.endsWith(" ") ||
41
+ value === "true" ||
42
+ value === "false" ||
43
+ value === "null"
44
+ ) {
45
+ return JSON.stringify(value);
46
+ }
47
+ return value;
48
+ }
49
+
50
+ /** 生成 Markdown 格式的版权脚注 */
51
+ function generateCCFooter(post: CollectionEntry<"posts">, config: ConfigMap): string {
52
+ const siteConfig = config.get("site") ?? {};
53
+ const lang = (siteConfig.lang as string) || "zh-CN";
54
+ const { t } = createTranslator(lang);
55
+ const cc = (post.data.cc as string) || "CC-BY-NC-SA-4.0";
56
+ const baseUrl = (siteConfig.url as string) || "";
57
+ const author = (config.get("author")?.name as string) || "";
58
+ const postUrl = baseUrl
59
+ ? `${baseUrl}/posts/${post.data.abbrlink}/`
60
+ : `/posts/${post.data.abbrlink}/`;
61
+
62
+ const lines: string[] = [];
63
+ lines.push(`**${t("post.author")}**${author}`);
64
+ lines.push(`**${t("post.articleLink")}**[${postUrl}](${postUrl})`);
65
+ lines.push(
66
+ `${t("post.licensedUnder")}[${buildCCName(cc, t)}](${buildCCLink(cc)})${t("post.licenseSuffix")}`,
67
+ );
68
+
69
+ return lines.join("\n\n");
70
+ }
71
+
72
+ export const getStaticPaths = (async () => {
73
+ const config = await loadConfig();
74
+ if (!isMarkdownExportEnabled(config)) {
75
+ return [];
76
+ }
77
+
78
+ const posts = await getCollection("posts", ({ data }) => !data.draft);
79
+ return posts.map((post) => ({
80
+ params: { post: String(post.data.abbrlink) },
81
+ props: { post },
82
+ }));
83
+ }) satisfies GetStaticPaths;
84
+
85
+ export const GET: APIRoute = async ({ props }) => {
86
+ const config = await loadConfig();
87
+ if (!isMarkdownExportEnabled(config)) return new Response(null, { status: 404 });
88
+
89
+ const { post } = props as { post: CollectionEntry<"posts"> };
90
+
91
+ let markdown: string;
92
+ if (post.filePath) {
93
+ const abs = path.resolve(process.cwd(), post.filePath);
94
+ markdown = await readFile(abs, "utf-8");
95
+ } else {
96
+ markdown = reconstructMarkdown(post);
97
+ }
98
+
99
+ // 追加版权脚注(不修改原始 md 文件)
100
+ const footer = generateCCFooter(post, config);
101
+ markdown += "\n\n---\n\n" + footer;
102
+
103
+ return new Response(markdown, {
104
+ status: 200,
105
+ headers: {
106
+ "Content-Type": "text/markdown; charset=utf-8",
107
+ },
108
+ });
109
+ };
@@ -0,0 +1,29 @@
1
+ import rss from "@astrojs/rss";
2
+ import { buildFeedItems } from "@utils/feed";
3
+ import { createTranslator, langToFeedLanguage } from "@utils/i18n";
4
+ import type { APIRoute } from "astro";
5
+ import { getCollection } from "astro:content";
6
+
7
+ export const GET: APIRoute = async (context) => {
8
+ const configCollection = await getCollection("config");
9
+ const siteEntry = configCollection.find((e) => e.id === "site");
10
+ if (!siteEntry) throw new Error("Missing site config");
11
+ const stalux = siteEntry.data;
12
+
13
+ const posts = await getCollection("posts", ({ data }) => !data.draft);
14
+ const items = await buildFeedItems(stalux, posts, "atom:updated");
15
+
16
+ const lang = stalux?.lang || "zh-CN";
17
+ const { t } = createTranslator(lang);
18
+
19
+ return rss({
20
+ title: stalux?.title || "Stalux Blog",
21
+ description: stalux?.description || "A blog powered by Stalux theme",
22
+ site: stalux?.url || context.site?.toString(),
23
+ items,
24
+ customData: `<language>${langToFeedLanguage(lang)}</language>\n<copyright>${t("rss.copyright")}</copyright>`,
25
+ xmlns: {
26
+ atom: "http://www.w3.org/2005/Atom",
27
+ },
28
+ });
29
+ };
@@ -0,0 +1,37 @@
1
+ ---
2
+ import PostList from "@components/stalux/posts/postList.astro";
3
+ import TagHeader from "@components/stalux/tags/tagHeader.astro";
4
+ import { getCollection, type CollectionEntry } from "astro:content";
5
+ import Stalux from "@layouts/Stalux.astro";
6
+ import { createTranslator } from "@utils/i18n";
7
+ import { buildTaxonomyStaticPaths } from "@utils/taxonomy";
8
+ import Navs from "@components/stalux/layout/navs.astro";
9
+ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, getCommentData, getFooterData, getPromoteData, getTypetextsData, getMediaLinksData } from "@utils/config-utils";
10
+ const configEntries = await getCollection("config");
11
+ const siteConfig = getSiteData(configEntries);
12
+ const { t } = createTranslator(siteConfig.lang || "zh-CN");
13
+
14
+ export async function getStaticPaths() {
15
+ return buildTaxonomyStaticPaths("tags");
16
+ }
17
+
18
+ interface Props {
19
+ tag: string;
20
+ count: number;
21
+ posts: CollectionEntry<"posts">[];
22
+ lastUpdateDate: string;
23
+ }
24
+
25
+ const props = Astro.props as Props;
26
+ ---
27
+
28
+ <Stalux
29
+ title={t("taxonomy.tag") + ": " + props.tag + " | " + siteConfig.title}
30
+ description={t("taxonomy.browseTag", { title: siteConfig.title, tag: props.tag, count: props.count })}
31
+ noindex={props.count < 3}
32
+ >
33
+ <div style="height: 3em;"></div>
34
+ <Navs />
35
+ <TagHeader type={t("taxonomy.tag")} name={props.tag} count={props.count} />
36
+ <PostList posts={props.posts} />
37
+ </Stalux>
@@ -0,0 +1,34 @@
1
+ import {
2
+ getSite,
3
+ isMarkdownExportEnabled,
4
+ loadConfig,
5
+ renderTaxonomyPageMd,
6
+ } from "@utils/ai-discovery";
7
+ import { buildTaxonomyStaticPaths } from "@utils/taxonomy";
8
+ import type { APIRoute } from "astro";
9
+ import type { CollectionEntry } from "astro:content";
10
+
11
+ export const prerender = true;
12
+
13
+ export async function getStaticPaths() {
14
+ const config = await loadConfig();
15
+ if (!isMarkdownExportEnabled(config)) return [];
16
+
17
+ return buildTaxonomyStaticPaths("tags");
18
+ }
19
+
20
+ export const GET: APIRoute = async (context) => {
21
+ const config = await loadConfig();
22
+ const exportMd = isMarkdownExportEnabled(config);
23
+ if (!exportMd) return new Response(null, { status: 404 });
24
+
25
+ const { tag, posts } = context.props as {
26
+ tag: string;
27
+ posts: CollectionEntry<"posts">[];
28
+ };
29
+ const site = getSite(config, context.site?.toString());
30
+ const text = renderTaxonomyPageMd(tag, posts, site, "tags", exportMd);
31
+ return new Response(text, {
32
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
33
+ });
34
+ };
@@ -0,0 +1,23 @@
1
+ ---
2
+ import Stalux from "@layouts/Stalux.astro";
3
+ import { getCollection } from "astro:content";
4
+ import { createTranslator } from "@utils/i18n";
5
+ import Navs from "@components/stalux/layout/navs.astro";
6
+ import styles from "@styles/pages/tags.module.css";
7
+ import TagsCloud from "@components/stalux/tags/tagsCloud.astro";
8
+ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, getCommentData, getFooterData, getPromoteData, getTypetextsData, getMediaLinksData } from "@utils/config-utils";
9
+ const configEntries = await getCollection("config");
10
+ const siteConfig = getSiteData(configEntries);
11
+ const { t } = createTranslator(siteConfig.lang || "zh-CN");
12
+ ---
13
+
14
+ <Stalux
15
+ title={t("tags.title") + " | " + siteConfig.title}
16
+ description={t("tags.description", { title: siteConfig.title })}
17
+ >
18
+ <div style="height: 3em;"></div>
19
+ <Navs />
20
+ <h1 class={styles.title}>{t("tags.heading")}</h1>
21
+ <div class={styles.description}>{t("tags.explore")}</div>
22
+ <TagsCloud />
23
+ </Stalux>
@@ -0,0 +1,27 @@
1
+ import {
2
+ buildTaxonomyMap,
3
+ getPublishedPosts,
4
+ getSite,
5
+ isMarkdownExportEnabled,
6
+ loadConfig,
7
+ renderTaxonomyListMd,
8
+ } from "@utils/ai-discovery";
9
+ import { createTranslator } from "@utils/i18n";
10
+ import type { APIRoute } from "astro";
11
+
12
+ export const prerender = true;
13
+
14
+ export const GET: APIRoute = async (context) => {
15
+ const config = await loadConfig();
16
+ if (!isMarkdownExportEnabled(config)) return new Response(null, { status: 404 });
17
+
18
+ const site = getSite(config, context.site?.toString());
19
+ const lang = (config.get("site")?.lang as string) || "zh-CN";
20
+ const { t } = createTranslator(lang);
21
+ const posts = await getPublishedPosts();
22
+ const map = buildTaxonomyMap(posts, "tags");
23
+ const text = renderTaxonomyListMd(map, site, "tags", t("ai.allTags"));
24
+ return new Response(text, {
25
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
26
+ });
27
+ };
@@ -0,0 +1,24 @@
1
+ ---
2
+ import Navs from "@components/stalux/layout/navs.astro";
3
+ import Stalux from "@layouts/Stalux.astro";
4
+ import WordList from "@components/stalux/words/wordList.astro";
5
+ import { getCollection } from "astro:content";
6
+ import { createTranslator } from "@utils/i18n";
7
+ import styles from "@styles/pages/words.module.css";
8
+ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, getCommentData, getFooterData, getPromoteData, getTypetextsData, getMediaLinksData } from "@utils/config-utils";
9
+
10
+ const configEntries = await getCollection("config");
11
+ const siteConfig = getSiteData(configEntries);
12
+ const { t } = createTranslator(siteConfig.lang || "zh-CN");
13
+ const entries = await getCollection("words", ({ data }) => !data.draft);
14
+ ---
15
+
16
+ <Stalux title={t("words.title") + " | " + siteConfig.title} description={t("words.description")}>
17
+ <div style="height: 3em;"></div>
18
+ <Navs />
19
+ <h1 class={styles.title}>{t("words.heading")}</h1>
20
+ <div class={styles.description}>
21
+ {t("words.desc", { n: entries.length })}
22
+ </div>
23
+ <WordList entries={entries} timezone={siteConfig.timezone} />
24
+ </Stalux>
@@ -0,0 +1,14 @@
1
+ import { isMarkdownExportEnabled, loadConfig, renderWordsMd } from "@utils/ai-discovery";
2
+ import type { APIRoute } from "astro";
3
+
4
+ export const prerender = true;
5
+
6
+ export const GET: APIRoute = async () => {
7
+ const config = await loadConfig();
8
+ if (!isMarkdownExportEnabled(config)) return new Response(null, { status: 404 });
9
+
10
+ const text = await renderWordsMd();
11
+ return new Response(text, {
12
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
13
+ });
14
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Sätteri 插件:在构建时完成字数统计和特性标记,
3
+ * 通过 ctx.data.astro.frontmatter 把最终结果传递到 frontmatter,运行时无需再算。
4
+ *
5
+ * 字数统计策略:
6
+ * - 只数 heading 和 paragraph 的内容(用 ctx.textContent 获取纯文本)。
7
+ * - 独立的 code 块、math/displayMath 公式不参与计数
8
+ * (代码是"看"不是"读",符号表达式不按词计)。
9
+ * - 行内 code 和行内 math 会被 textContent(paragraph) 自然包含。
10
+ *
11
+ * 特性标记:
12
+ * - HAST 阶段 <img> 检测 → hasImage
13
+ * - mermaid 处理交由 @xingwangzhe/satteri-mermaid 的 mdast + hast 插件
14
+ * - 数学公式由 satteri-temml 插件直接输出 MathML,无需额外标记
15
+ */
16
+ import { defineMdastPlugin, defineHastPlugin } from "satteri";
17
+
18
+ import { countWords } from "../utils/count-words.ts";
19
+
20
+ const WORDS_PER_MINUTE = 400;
21
+
22
+ /** 从纯文本统计字数并累加到 frontmatter。 */
23
+ function flushWordCount(ctx: any, text: string) {
24
+ const prev = (ctx.data.astro.frontmatter.wordCount as number) ?? 0;
25
+ const wc = prev + countWords(text);
26
+ const fm = ctx.data.astro.frontmatter;
27
+ fm.wordCount = wc;
28
+ fm.readingMinutes = Math.ceil(wc / WORDS_PER_MINUTE);
29
+ // 搜索全文:拼接所有 heading + paragraph 的纯文本
30
+ fm.searchText = ((fm.searchText as string) || "") + " " + text;
31
+ }
32
+
33
+ export const featureFlagsMdast = defineMdastPlugin({
34
+ name: "feature-flags-mdast",
35
+
36
+ code(_node, _ctx) {
37
+ // 独立的 code 块不参与字数统计
38
+ },
39
+
40
+ heading(node, ctx) {
41
+ flushWordCount(ctx, ctx.textContent(node));
42
+ },
43
+
44
+ paragraph(node, ctx) {
45
+ flushWordCount(ctx, ctx.textContent(node));
46
+ },
47
+ });
48
+
49
+ /**
50
+ * HAST 插件:检测图片,写入 frontmatter,并添加懒加载属性
51
+ */
52
+ export const featureFlagsHast = defineHastPlugin({
53
+ name: "feature-flags-hast",
54
+
55
+ element: {
56
+ filter: ["img"],
57
+ visit(node, ctx) {
58
+ ctx.data.astro.frontmatter.hasImage = true;
59
+ // 添加懒加载和异步解码(封面图不在此处理,由 postCard.astro 控制)
60
+ if (!node.properties?.loading) {
61
+ ctx.setProperty(node, "loading", "lazy");
62
+ }
63
+ if (!node.properties?.decoding) {
64
+ ctx.setProperty(node, "decoding", "async");
65
+ }
66
+ },
67
+ },
68
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Sätteri temml 插件 — 用 temml (TeX → MathML) 替代 KaTeX。
3
+ *
4
+ * 与 @nullpinter/satteri-katex 的接口兼容:
5
+ * - math 节点 → { rawHtml: string } (display math → <math display="block">)
6
+ * - inlineMath 节点 → { type: "html", value: string } (inline math → <math>)
7
+ *
8
+ * 输出纯 MathML,浏览器原生渲染,无需额外的 CSS/字体。
9
+ */
10
+ import { defineMdastPlugin } from "satteri";
11
+ import temmlLib from "temml";
12
+
13
+ const emptyOptions = {};
14
+
15
+ function escapeHtml(value: string): string {
16
+ return value
17
+ .replaceAll("&", "&amp;")
18
+ .replaceAll("<", "&lt;")
19
+ .replaceAll(">", "&gt;")
20
+ .replaceAll('"', "&quot;")
21
+ .replaceAll("'", "&#39;");
22
+ }
23
+
24
+ function renderTemmlError(value: string, error: unknown, options: Record<string, any>): string {
25
+ const title = escapeHtml(String(error));
26
+ const color = escapeHtml(options.errorColor ?? "#cc0000");
27
+ return `<span class="temml-error" style="color:${color}" title="${title}">${escapeHtml(value)}</span>`;
28
+ }
29
+
30
+ function renderMath(
31
+ node: { value: string },
32
+ displayMode: boolean,
33
+ options: Record<string, any>,
34
+ ctx: any,
35
+ ): string {
36
+ const value = node.value;
37
+ try {
38
+ return temmlLib.renderToString(value, {
39
+ ...options,
40
+ displayMode,
41
+ throwOnError: true,
42
+ });
43
+ } catch (error) {
44
+ const cause = error instanceof Error ? error : new Error(String(error));
45
+ ctx.report({
46
+ message: `Could not render math with Temml: ${cause.message}`,
47
+ node,
48
+ severity: "error",
49
+ });
50
+ try {
51
+ return temmlLib.renderToString(value, {
52
+ ...options,
53
+ displayMode,
54
+ strict: false,
55
+ throwOnError: false,
56
+ });
57
+ } catch {
58
+ return renderTemmlError(value, error, options);
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 创建一个 Sätteri temml mdast 插件。
65
+ * @param options - 透传给 temml.renderToString 的选项(displayMode 除外)
66
+ */
67
+ export function temml(options?: Record<string, any>) {
68
+ const settings = options ?? emptyOptions;
69
+ return defineMdastPlugin({
70
+ name: "temml",
71
+ math(node: any, ctx: any) {
72
+ return { rawHtml: renderMath(node, true, settings, ctx) };
73
+ },
74
+ inlineMath(node: any, ctx: any) {
75
+ return {
76
+ type: "html",
77
+ value: renderMath(node, false, settings, ctx),
78
+ };
79
+ },
80
+ });
81
+ }