@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,547 @@
1
+ /**
2
+ * AI 发现文件(llms.txt / llms-full.txt)生成工具
3
+ *
4
+ * 为 /llms.txt、/llms-full.txt 以及所有页面 .md 导出提供共享数据与格式化函数。
5
+ */
6
+ import type { CollectionEntry } from "astro:content";
7
+ import { getCollection } from "astro:content";
8
+
9
+ import { toTimestamp } from "./dayjs";
10
+ import { createTranslator } from "./i18n";
11
+
12
+ export type ConfigMap = Map<string, Record<string, unknown>>;
13
+ export type Post = CollectionEntry<"posts">;
14
+ type About = CollectionEntry<"about">;
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // AI 发现文件配置
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export type AiFileName = "llms.txt" | "llms-full.txt";
21
+
22
+ const conformanceFiles: Record<string, AiFileName[]> = {
23
+ disabled: [],
24
+ essential: ["llms.txt", "llms-full.txt"],
25
+ recommended: ["llms.txt", "llms-full.txt"],
26
+ complete: ["llms.txt", "llms-full.txt"],
27
+ };
28
+
29
+ /** 根据 conformance 等级判断指定 AI 文件是否应生成 */
30
+ export function isAiFileEnabled(conformance: string | undefined, file: AiFileName): boolean {
31
+ const files = conformanceFiles[conformance ?? "essential"];
32
+ if (!files) return true; // unknown conformance → default to enabled
33
+ return files.includes(file);
34
+ }
35
+
36
+ /** Markdown 导出仅在 promote.export_md 严格为 true 时启用 */
37
+ export function isMarkdownExportEnabled(config: ConfigMap): boolean {
38
+ return config.get("promote")?.export_md === true;
39
+ }
40
+
41
+ /** 加载 config 集合,返回按 id 索引的 Map */
42
+ export async function loadConfig(): Promise<ConfigMap> {
43
+ const configCollection = await getCollection("config");
44
+ return new Map(configCollection.map((entry) => [entry.id, entry.data]));
45
+ }
46
+
47
+ /** 快捷获取 site 对象 */
48
+ function getSiteConfig(config: ConfigMap): Record<string, unknown> {
49
+ return config.get("site") ?? {};
50
+ }
51
+
52
+ /** 解析站点根 URL */
53
+ export function getSite(config: ConfigMap, contextSite?: string): string {
54
+ const siteData = getSiteConfig(config);
55
+ return (contextSite || (siteData.url as string) || "").replace(/\/$/, "");
56
+ }
57
+
58
+ /** 将任意空白压缩为单行 */
59
+ function inlineText(text: string | undefined): string {
60
+ return (text || "").replace(/\s+/g, " ").trim();
61
+ }
62
+
63
+ /** 转义 markdown 文本中的特殊字符(用于描述) */
64
+ function escapeMd(text: string | undefined): string {
65
+ return inlineText(text).replace(/([\\[*_`#])/g, "\\$1");
66
+ }
67
+
68
+ /** 转义 markdown 链接标题中的特殊字符 */
69
+ function escapeMdLinkTitle(text: string | undefined): string {
70
+ return inlineText(text).replace(/([\\[\]])/g, "\\$1");
71
+ }
72
+
73
+ /** 构建 markdown 列表项 */
74
+ function buildMdLink(title: string, url: string, desc?: string): string {
75
+ const line = `- [${escapeMdLinkTitle(title)}](${url})`;
76
+ return desc ? `${line}: ${escapeMd(desc)}` : line;
77
+ }
78
+
79
+ /** 生成页面链接,支持 HTML 或 .md 版本 */
80
+ function pageUrl(site: string, path: string, exportMd: boolean): string {
81
+ return exportMd ? `${site}/${path}.md` : `${site}/${path}/`;
82
+ }
83
+
84
+ /** 生成文章链接,可切换 .md 源码版本 */
85
+ export function postUrl(site: string, abbrlink: string | number, exportMd: boolean): string {
86
+ return exportMd && abbrlink ? `${site}/posts/${abbrlink}.md` : `${site}/posts/${abbrlink}/`;
87
+ }
88
+
89
+ /** 获取已发布文章并按日期降序排列 */
90
+ export async function getPublishedPosts(): Promise<Post[]> {
91
+ const posts = await getCollection("posts", ({ data }) => !data.draft);
92
+ return posts.sort((a, b) => {
93
+ const aTime = toTimestamp(a.data.date || 0);
94
+ const bTime = toTimestamp(b.data.date || 0);
95
+ return bTime - aTime; // newest first
96
+ });
97
+ }
98
+
99
+ /** 获取 about 页面 */
100
+ async function loadAbout(): Promise<About | undefined> {
101
+ const aboutPages = await getCollection("about");
102
+ return aboutPages[0];
103
+ }
104
+
105
+ /** 从 media-links section 中解析社交媒体链接 */
106
+ function getMediaLinks(config: ConfigMap): Array<{ name: string; url: string }> {
107
+ const mediaSection = config.get("media-links");
108
+ if (!mediaSection) return [];
109
+ const items = (mediaSection as Record<string, unknown>).items as
110
+ | Array<{ icon: string; link: string }>
111
+ | undefined;
112
+ if (!items) return [];
113
+ return items
114
+ .filter((link) => link.link && !link.link.startsWith("mailto:"))
115
+ .map((link) => {
116
+ const url = link.link || "";
117
+ let name = "Link";
118
+ try {
119
+ const hostname = new URL(url.startsWith("http") ? url : `https://example.com${url}`)
120
+ .hostname;
121
+ const domain = hostname.replace(/^www\./, "").split(".")[0];
122
+ name = domain ? domain.charAt(0).toUpperCase() + domain.slice(1) : "Link";
123
+ } catch {
124
+ // 无法解析 URL 时保持默认
125
+ }
126
+ return { name, url };
127
+ });
128
+ }
129
+
130
+ /** 尝试从 media-links 中找出邮箱地址 */
131
+ function getEmail(config: ConfigMap): string | undefined {
132
+ const mediaSection = config.get("media-links");
133
+ if (!mediaSection) return undefined;
134
+ const items = (mediaSection as Record<string, unknown>).items as
135
+ | Array<{ icon: string; link: string }>
136
+ | undefined;
137
+ if (!items) return undefined;
138
+ const mail = items.find((l) => l.link?.startsWith("mailto:"))?.link;
139
+ if (mail) return mail.replace(/^mailto:/, "");
140
+ return undefined;
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // 页面 Markdown 渲染函数(用于 .md 端点和 llms-full.txt)
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /** 构建分类/标签名 → 文章列表的映射 */
148
+ export function buildTaxonomyMap(posts: Post[], key: "tags" | "categories"): Map<string, Post[]> {
149
+ const map = new Map<string, Post[]>();
150
+ for (const post of posts) {
151
+ for (const name of (post.data[key] ?? []) as string[]) {
152
+ if (!map.has(name)) map.set(name, []);
153
+ map.get(name)!.push(post);
154
+ }
155
+ }
156
+ return map;
157
+ }
158
+
159
+ /** 渲染文章列表 Markdown */
160
+ export function renderPostListMd(
161
+ posts: Post[],
162
+ site: string,
163
+ exportMd: boolean,
164
+ title?: string,
165
+ ): string {
166
+ const lines: string[] = [];
167
+ if (title) {
168
+ lines.push(`## ${title}`);
169
+ lines.push("");
170
+ }
171
+ if (posts.length === 0) {
172
+ lines.push("- No posts");
173
+ } else {
174
+ for (const post of posts) {
175
+ const postTitle = post.data.title || "Untitled";
176
+ const url = postUrl(site, post.data.abbrlink, exportMd);
177
+ const desc = post.data.desc || "";
178
+ lines.push(buildMdLink(postTitle, url, desc));
179
+ }
180
+ }
181
+ lines.push("");
182
+ return lines.join("\n");
183
+ }
184
+
185
+ /** 渲染分类/标签索引列表 Markdown */
186
+ export function renderTaxonomyListMd(
187
+ map: Map<string, Post[]>,
188
+ site: string,
189
+ type: "categories" | "tags",
190
+ title?: string,
191
+ ): string {
192
+ const lines: string[] = [];
193
+ if (title) {
194
+ lines.push(`# ${title}`);
195
+ lines.push("");
196
+ }
197
+ lines.push(`URL: ${site}/${type}/`);
198
+ lines.push("");
199
+ const sorted = [...map].sort((a, b) => b[1].length - a[1].length);
200
+ for (const [name, posts] of sorted) {
201
+ const slug = encodeURIComponent(name);
202
+ const url = `${site}/${type}/${slug}/`;
203
+ lines.push(buildMdLink(name, url, `${posts.length} posts`));
204
+ }
205
+ lines.push("");
206
+ return lines.join("\n").trimEnd() + "\n";
207
+ }
208
+
209
+ /** 渲染单个分类/标签页 Markdown */
210
+ export function renderTaxonomyPageMd(
211
+ name: string,
212
+ posts: Post[],
213
+ site: string,
214
+ type: "categories" | "tags",
215
+ exportMd: boolean,
216
+ ): string {
217
+ const label = type === "categories" ? "Category" : "Tag";
218
+ const slug = encodeURIComponent(name);
219
+ const pageUrl = `${site}/${type}/${slug}/`;
220
+ const lines: string[] = [];
221
+ lines.push(`# ${label}: ${name}`);
222
+ lines.push("");
223
+ lines.push(`URL: ${pageUrl}`);
224
+ lines.push("");
225
+ lines.push(renderPostListMd(posts, site, exportMd, `${posts.length} posts`));
226
+ return lines.join("\n").trimEnd() + "\n";
227
+ }
228
+
229
+ /** 渲染 About 页 Markdown */
230
+ export async function renderAboutMd(site: string): Promise<string> {
231
+ const aboutPages = await getCollection("about");
232
+ const about = aboutPages[0];
233
+ if (!about) return "";
234
+ const lines: string[] = [];
235
+ lines.push(`# ${about.data.title || "About"}`);
236
+ lines.push("");
237
+ lines.push(`URL: ${site}/about/`);
238
+ lines.push("");
239
+ lines.push((about.body as string) || "");
240
+ lines.push("");
241
+ return lines.join("\n").trimEnd() + "\n";
242
+ }
243
+
244
+ /** 渲染 Words 页 Markdown */
245
+ export async function renderWordsMd(): Promise<string> {
246
+ const words = await getCollection("words", ({ data }) => !data.draft);
247
+ words.sort((a, b) => toTimestamp(b.data.date || 0) - toTimestamp(a.data.date || 0));
248
+ const lines: string[] = [];
249
+ lines.push("# Words");
250
+ lines.push("");
251
+ lines.push("URL: /words/");
252
+ lines.push("");
253
+ for (const word of words) {
254
+ const title = word.data.title || "Untitled";
255
+ lines.push(`## ${title}`);
256
+ lines.push((word.body as string) || "");
257
+ lines.push("");
258
+ }
259
+ return lines.join("\n").trimEnd() + "\n";
260
+ }
261
+
262
+ /** 渲染 Links 页 Markdown */
263
+ export function renderLinksMd(config: ConfigMap, site: string, t: (key: string) => string): string {
264
+ const linksSection = config.get("links") as Record<string, unknown> | undefined;
265
+ const lines: string[] = [];
266
+ lines.push(`# ${(linksSection?.title as string) || t("ai.links")}`);
267
+ lines.push("");
268
+ lines.push(`URL: ${site}/links/`);
269
+ lines.push("");
270
+ if (linksSection?.sites) {
271
+ const sites = linksSection.sites as Array<{
272
+ name: string;
273
+ link: string;
274
+ description: string;
275
+ }>;
276
+ for (const s of sites) {
277
+ lines.push(buildMdLink(s.name, s.link, s.description));
278
+ }
279
+ }
280
+ lines.push("");
281
+ return lines.join("\n").trimEnd() + "\n";
282
+ }
283
+
284
+ /** 渲染 Archives 页 Markdown */
285
+ export async function renderArchivesMd(
286
+ site: string,
287
+ exportMd: boolean,
288
+ t: (key: string) => string,
289
+ ): Promise<string> {
290
+ const posts = await getPublishedPosts();
291
+ const lines: string[] = [];
292
+ lines.push(`# ${t("archives.title")}`);
293
+ lines.push("");
294
+ lines.push(`URL: ${site}/archives/`);
295
+ lines.push("");
296
+ lines.push(renderPostListMd(posts, site, exportMd));
297
+ return lines.join("\n").trimEnd() + "\n";
298
+ }
299
+
300
+ /** 渲染首页 Markdown */
301
+ export async function renderIndexMd(
302
+ config: ConfigMap,
303
+ site: string,
304
+ t: (key: string) => string,
305
+ ): Promise<string> {
306
+ const posts = await getPublishedPosts();
307
+ const siteData = getSiteConfig(config);
308
+ const lang = (siteData.lang as string) || "zh-CN";
309
+ const about = await loadAbout();
310
+ const lines: string[] = [];
311
+
312
+ lines.push(`# ${(siteData.title as string) || t("ai.blog")}`);
313
+ lines.push("");
314
+ lines.push(`URL: ${site}/`);
315
+ lines.push("");
316
+ lines.push(`> ${escapeMd(siteData.description as string)}`);
317
+ lines.push("");
318
+ lines.push(`Lang: ${lang}`);
319
+ lines.push("");
320
+
321
+ lines.push(`## ${t("ai.markdownSources")}`);
322
+ lines.push(buildMdLink("Home", `${site}/index.md`));
323
+ lines.push(buildMdLink("About", `${site}/about.md`));
324
+ lines.push(buildMdLink("Words", `${site}/words.md`));
325
+ lines.push(buildMdLink("Links", `${site}/links.md`));
326
+ lines.push(buildMdLink("Archives", `${site}/archives.md`));
327
+ lines.push(buildMdLink("Categories", `${site}/categories.md`));
328
+ lines.push(buildMdLink("Tags", `${site}/tags.md`));
329
+ lines.push(buildMdLink("LLMs.txt", `${site}/llms.txt`));
330
+ lines.push(buildMdLink("LLMs-full.txt", `${site}/llms-full.txt`));
331
+ lines.push("");
332
+
333
+ if (about) {
334
+ lines.push(`## ${t("ai.about")}`);
335
+ lines.push((about.body as string) || "");
336
+ lines.push("");
337
+ }
338
+
339
+ lines.push(`## ${t("ai.latestPosts")}`);
340
+ lines.push(renderPostListMd(posts.slice(0, 10), site, true));
341
+ return lines.join("\n").trimEnd() + "\n";
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // llms.txt / llms-full.txt 渲染
346
+ // ---------------------------------------------------------------------------
347
+
348
+ /** 生成 llms.txt 核心 markdown 内容 */
349
+ export async function renderLlmsTxt(config: ConfigMap, site: string): Promise<string> {
350
+ const posts = await getPublishedPosts();
351
+ const about = await loadAbout();
352
+ const siteData = getSiteConfig(config);
353
+ const authorSection = config.get("author") as Record<string, unknown> | undefined;
354
+ const linksSection = config.get("links") as Record<string, unknown> | undefined;
355
+ const mediaLinks = getMediaLinks(config);
356
+ const email = getEmail(config);
357
+ const lang = (siteData.lang as string) || "zh-CN";
358
+ const { t } = createTranslator(lang);
359
+ const exportMd = isMarkdownExportEnabled(config);
360
+
361
+ const lines: string[] = [];
362
+
363
+ // Lang 头部
364
+ lines.push(`Lang: ${lang}`);
365
+ lines.push("");
366
+
367
+ // H1 与 blockquote
368
+ lines.push(`# ${(siteData.title as string) || t("ai.blog")}`);
369
+ lines.push("");
370
+ lines.push(`> ${escapeMd(siteData.description as string)}`);
371
+ lines.push("");
372
+ lines.push(`> ${t("ai.siteLicense")}`);
373
+ lines.push("");
374
+
375
+ // 简介段落
376
+ const authorName = authorSection?.name as string | undefined;
377
+ const authorBio = authorSection?.bio as string | undefined;
378
+ if (authorName) {
379
+ lines.push(`${t("ai.author")}${authorName}`);
380
+ if (authorBio) lines.push(inlineText(authorBio));
381
+ lines.push("");
382
+ } else if (authorBio) {
383
+ lines.push(inlineText(authorBio));
384
+ lines.push("");
385
+ }
386
+
387
+ // Contact
388
+ lines.push(`## ${t("ai.contact")}`);
389
+ if (authorName) {
390
+ lines.push(`- ${t("ai.author")}: ${escapeMd(authorName)}`);
391
+ }
392
+ if (email) {
393
+ lines.push(`- ${t("ai.email")}: ${email}`);
394
+ }
395
+ for (const { name, url } of mediaLinks) {
396
+ lines.push(`- ${t("ai.socialMedia")} (${escapeMd(name)}): ${url}`);
397
+ }
398
+ if (!authorName && !email && mediaLinks.length === 0) {
399
+ lines.push(`- ${t("ai.noContact")}`);
400
+ }
401
+ lines.push("");
402
+
403
+ // Posts
404
+ lines.push(`## ${t("ai.posts")}`);
405
+ if (posts.length === 0) {
406
+ lines.push(`- ${t("ai.noPosts")}`);
407
+ } else {
408
+ for (const post of posts) {
409
+ const title = post.data.title || t("ai.untitled");
410
+ const url = postUrl(site, post.data.abbrlink, exportMd);
411
+ const desc = post.data.desc || "";
412
+ lines.push(buildMdLink(title, url, desc));
413
+ }
414
+ }
415
+ lines.push("");
416
+
417
+ // Pages
418
+ lines.push(`## ${t("ai.pages")}`);
419
+ if (about) {
420
+ lines.push(
421
+ buildMdLink(
422
+ about.data.title || t("ai.about"),
423
+ pageUrl(site, "about", exportMd),
424
+ about.data.description || t("ai.aboutDesc"),
425
+ ),
426
+ );
427
+ }
428
+ lines.push(
429
+ buildMdLink(t("ai.archives"), pageUrl(site, "archives", exportMd), t("ai.archivesDesc")),
430
+ );
431
+ lines.push(buildMdLink(t("ai.tags"), pageUrl(site, "tags", exportMd), t("ai.tagsDesc")));
432
+ lines.push(
433
+ buildMdLink(
434
+ t("ai.categories"),
435
+ pageUrl(site, "categories", exportMd),
436
+ t("ai.categoriesDesc"),
437
+ ),
438
+ );
439
+ lines.push(buildMdLink(t("ai.words"), pageUrl(site, "words", exportMd), t("ai.wordsDesc")));
440
+ lines.push(buildMdLink(t("ai.links"), pageUrl(site, "links", exportMd), t("ai.links")));
441
+ lines.push("");
442
+
443
+ // Links(友情链接)
444
+ if (linksSection?.sites) {
445
+ const sites = linksSection.sites as Array<{
446
+ name: string;
447
+ link: string;
448
+ description: string;
449
+ }>;
450
+ if (sites.length > 0) {
451
+ lines.push(`## ${(linksSection.title as string) || t("ai.links")}`);
452
+ for (const siteLink of sites) {
453
+ lines.push(buildMdLink(siteLink.name, siteLink.link, siteLink.description));
454
+ }
455
+ lines.push("");
456
+ }
457
+ }
458
+
459
+ // AI Discovery Files
460
+ lines.push(`## ${t("ai.aiDiscoveryFiles")}`);
461
+ lines.push(buildMdLink("llms.txt", `${site}/llms.txt`, t("ai.llmsTxtDesc")));
462
+ lines.push(buildMdLink("llms-full.txt", `${site}/llms-full.txt`, t("ai.llmsFullTxtDesc")));
463
+ lines.push("");
464
+
465
+ // Optional
466
+ lines.push(`## ${t("ai.optional")}`);
467
+ lines.push(buildMdLink(t("ai.rss"), `${site}/rss.xml`));
468
+ lines.push(buildMdLink(t("ai.atom"), `${site}/atom.xml`));
469
+ if (exportMd) {
470
+ lines.push(`- ${t("ai.mdAvailable")}`);
471
+ }
472
+ lines.push("");
473
+
474
+ return lines.join("\n").trimEnd() + "\n";
475
+ }
476
+
477
+ /** 生成 llms-full.txt 内容:全站 Markdown 镜像 */
478
+ export async function renderLlmsFullTxt(config: ConfigMap, site: string): Promise<string> {
479
+ const siteData = getSiteConfig(config);
480
+ const lang = (siteData.lang as string) || "zh-CN";
481
+ const { t } = createTranslator(lang);
482
+ const posts = await getPublishedPosts();
483
+ const categoryMap = buildTaxonomyMap(posts, "categories");
484
+ const tagMap = buildTaxonomyMap(posts, "tags");
485
+ const exportMd = isMarkdownExportEnabled(config);
486
+
487
+ const sections: string[] = [];
488
+
489
+ sections.push(`Lang: ${lang}`);
490
+ sections.push("");
491
+ sections.push(`# ${t("ai.fullDataset")}`);
492
+ sections.push("");
493
+ sections.push(`> ${t("ai.fullDatasetDesc")}`);
494
+ sections.push("");
495
+ sections.push(`- ${t("ai.license")}: CC-BY-NC-SA-4.0`);
496
+ sections.push(`- ${t("ai.postCount")}: ${posts.length}`);
497
+ sections.push("");
498
+
499
+ sections.push("---");
500
+ sections.push("");
501
+ sections.push(await renderIndexMd(config, site, t));
502
+ sections.push("---");
503
+ sections.push("");
504
+ sections.push(await renderAboutMd(site));
505
+ sections.push("---");
506
+ sections.push("");
507
+ sections.push(await renderWordsMd());
508
+ sections.push("---");
509
+ sections.push("");
510
+ sections.push(renderLinksMd(config, site, t));
511
+ sections.push("---");
512
+ sections.push("");
513
+ sections.push(await renderArchivesMd(site, exportMd, t));
514
+ sections.push("---");
515
+ sections.push("");
516
+ sections.push(renderTaxonomyListMd(categoryMap, site, "categories", t("ai.allCategories")));
517
+ sections.push("---");
518
+ sections.push("");
519
+ for (const [name, catPosts] of [...categoryMap].sort((a, b) => b[1].length - a[1].length)) {
520
+ sections.push(renderTaxonomyPageMd(name, catPosts, site, "categories", exportMd));
521
+ sections.push("---");
522
+ sections.push("");
523
+ }
524
+ sections.push(renderTaxonomyListMd(tagMap, site, "tags", t("ai.allTags")));
525
+ sections.push("---");
526
+ sections.push("");
527
+ for (const [name, tagPosts] of [...tagMap].sort((a, b) => b[1].length - a[1].length)) {
528
+ sections.push(renderTaxonomyPageMd(name, tagPosts, site, "tags", exportMd));
529
+ sections.push("---");
530
+ sections.push("");
531
+ }
532
+ for (const post of posts) {
533
+ const title = post.data.title || t("ai.untitled");
534
+ const url = postUrl(site, post.data.abbrlink, false);
535
+ const cc = post.data.cc || "CC-BY-NC-SA-4.0";
536
+ sections.push(`## ${escapeMd(title)}`);
537
+ sections.push(`URL: ${url}`);
538
+ sections.push(`License: ${cc}`);
539
+ sections.push("");
540
+ sections.push((post.body as string) || "");
541
+ sections.push("");
542
+ sections.push("---");
543
+ sections.push("");
544
+ }
545
+
546
+ return sections.join("\n").trimEnd() + "\n";
547
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 徽章生成器工具
3
+ * 基于 badge-maker 库生成徽章
4
+ */
5
+ import { makeBadge } from "badge-maker";
6
+
7
+ export interface BadgeOptions {
8
+ label: string;
9
+ message: string;
10
+ color?: string;
11
+ style?: "plastic" | "flat" | "flat-square" | "for-the-badge" | "social";
12
+ labelColor?: string;
13
+ logo?: string;
14
+ logoWidth?: number;
15
+ alt?: string;
16
+ href?: string;
17
+ }
18
+
19
+ export interface BadgeGroup {
20
+ title: string;
21
+ collapsed?: boolean;
22
+ items: BadgeOptions[];
23
+ }
24
+
25
+ /** 单个 badge 或 badge 分组 */
26
+ export type BadgeItem = BadgeOptions | BadgeGroup;
27
+
28
+ /** 判断是否为 badge 分组 */
29
+ export function isBadgeGroup(item: BadgeItem): item is BadgeGroup {
30
+ return "title" in item && "items" in item;
31
+ }
32
+
33
+ /**
34
+ * 生成徽章SVG
35
+ * @param options 徽章选项
36
+ * @returns 返回徽章SVG字符串
37
+ */
38
+ export function generateBadge(options: BadgeOptions): string {
39
+ try {
40
+ const format = Object.fromEntries(
41
+ Object.entries({
42
+ label: options.label,
43
+ message: options.message,
44
+ color: options.color ?? "blue",
45
+ style: options.style ?? "flat",
46
+ labelColor: options.labelColor,
47
+ logo: options.logo,
48
+ logoWidth: options.logoWidth,
49
+ }).filter(([, v]) => v !== undefined),
50
+ ) as Parameters<typeof makeBadge>[0];
51
+
52
+ return makeBadge(format);
53
+ } catch (error) {
54
+ console.error("Badge generation error:", error);
55
+ return "";
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 将SVG字符串转为Data URL
61
+ * @param svg SVG字符串
62
+ * @returns data URL
63
+ */
64
+ export function svgToDataUrl(svg: string): string {
65
+ if (!svg) return "";
66
+ const encoded = encodeURIComponent(svg).replaceAll("'", "%27").replaceAll('"', "%22");
67
+ return `data:image/svg+xml,${encoded}`;
68
+ }
@@ -0,0 +1,46 @@
1
+ import type { Translator } from "@utils/i18n";
2
+
3
+ /**
4
+ * 从许可证代码(如 "CC-BY-NC-SA-4.0")推导 CC 官网链接。
5
+ *
6
+ * "CC-BY-NC-SA-4.0" → "https://creativecommons.org/licenses/by-nc-sa/4.0/"
7
+ * "CC0-1.0" → "https://creativecommons.org/publicdomain/zero/1.0/"
8
+ */
9
+ export function buildCCLink(licenseCode: string): string {
10
+ if (licenseCode === "CC0-1.0") {
11
+ return "https://creativecommons.org/publicdomain/zero/1.0/";
12
+ }
13
+
14
+ const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/);
15
+ if (!match) {
16
+ return "https://creativecommons.org/licenses/by-nc-sa/4.0/";
17
+ }
18
+
19
+ const [, pathPart, version] = match;
20
+ return `https://creativecommons.org/licenses/${pathPart.toLowerCase()}/${version}/`;
21
+ }
22
+
23
+ /**
24
+ * 用 i18n 翻译组装许可证显示名称。
25
+ *
26
+ * "CC-BY-NC-SA-4.0" + zh-CN → "知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议"
27
+ * "CC-BY-NC-SA-4.0" + en → "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License"
28
+ * "CC0-1.0" + zh-CN → "CC0 1.0 通用"
29
+ */
30
+ export function buildCCName(licenseCode: string, t: Translator["t"]): string {
31
+ if (licenseCode === "CC0-1.0") {
32
+ return t("cc.zero");
33
+ }
34
+
35
+ const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/);
36
+ if (!match) {
37
+ // 无法解析时回退为默认 CC-BY-NC-SA-4.0
38
+ const elements = ["BY", "NC", "SA"].map((code) => t("cc." + code)).join("-");
39
+ return t("cc.format", { elements, version: "4.0" });
40
+ }
41
+
42
+ const [, pathPart, version] = match;
43
+ const codes = pathPart.split("-");
44
+ const elements = codes.map((code) => t("cc." + code)).join("-");
45
+ return t("cc.format", { elements, version });
46
+ }