@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,108 @@
1
+ ---
2
+ import { getCollection } from "astro:content";
3
+ import { createTranslator } from "@utils/i18n";
4
+ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, getCommentData, getFooterData, getPromoteData, getTypetextsData, getMediaLinksData } from "@utils/config-utils";
5
+
6
+ interface Props {
7
+ showStats?: boolean;
8
+ wordCount?: string;
9
+ buildtime?: string;
10
+ }
11
+
12
+ const configEntries = await getCollection("config");
13
+ const siteConfig = getSiteData(configEntries);
14
+ const { t } = createTranslator(siteConfig.lang || "zh-CN");
15
+ const { showStats = true, wordCount, buildtime } = Astro.props;
16
+ ---
17
+
18
+ {
19
+ showStats && (
20
+ <p class="site-stats">
21
+ <span id="vercount_container_site_uv">
22
+ {t("footer.visitors")}<span id="vercount_value_site_uv">Loading...</span>{" "}
23
+ {t("footer.visits")}
24
+ </span>
25
+ <span class="stats-separator">|</span>
26
+ <span id="vercount_container_site_pv">
27
+ {t("footer.totalVisits")}
28
+ <span id="vercount_value_site_pv">Loading...</span> {t("footer.times")}
29
+ </span>
30
+ <span class="stats-separator">|</span>
31
+ <span id="word-count-container">
32
+ {t("footer.totalWords")}
33
+ <span id="word-count-value">
34
+ {wordCount ?? "Loading..."}
35
+ </span>{" "}
36
+ {t("footer.chars")}
37
+ </span>
38
+ </p>
39
+ )
40
+ }
41
+
42
+ {
43
+ buildtime && (
44
+ <div class="site-build-time">
45
+ <span>
46
+ {t("footer.runningTime")}
47
+ <span id="runtime-counter" />
48
+ </span>
49
+ </div>
50
+ )
51
+ }
52
+
53
+ <script define:vars={{ buildtime, runtimeFormat: t("footer.runtimeFormat") }}>
54
+ document.addEventListener("astro:page-load", () => {
55
+ if (!buildtime) return;
56
+ const runtimeCounter = document.getElementById("runtime-counter");
57
+ if (!runtimeCounter) return;
58
+
59
+ const updateRuntime = () => {
60
+ const now = new Date();
61
+ const start = new Date(buildtime);
62
+ const diffMs = now.getTime() - start.getTime();
63
+ if (Number.isNaN(diffMs) || diffMs < 0) return;
64
+
65
+ const totalSeconds = Math.floor(diffMs / 1000);
66
+ const days = Math.floor(totalSeconds / 86400);
67
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
68
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
69
+ const seconds = totalSeconds % 60;
70
+
71
+ runtimeCounter.textContent = runtimeFormat
72
+ .replace("{days}", String(days))
73
+ .replace("{hours}", String(hours))
74
+ .replace("{minutes}", String(minutes))
75
+ .replace("{seconds}", String(seconds));
76
+ };
77
+
78
+ updateRuntime();
79
+ // define:vars 内联脚本会在每次视图切换时重新执行,
80
+ // 因此用 window 句柄记录定时器,创建新定时器前先清掉旧的,避免累加 1Hz 定时器。
81
+ const w = window;
82
+ if (w.__staluxRuntimeTimer != null) {
83
+ clearInterval(w.__staluxRuntimeTimer);
84
+ }
85
+ w.__staluxRuntimeTimer = setInterval(updateRuntime, 1000);
86
+ });
87
+ </script>
88
+
89
+ <style>
90
+ .site-stats {
91
+ display: flex;
92
+ justify-content: center;
93
+ flex-wrap: wrap;
94
+ gap: 0.5rem;
95
+ font-size: 0.9rem;
96
+ }
97
+
98
+ .stats-separator {
99
+ color: rgba(255, 255, 255, 0.5);
100
+ }
101
+
102
+ .site-build-time {
103
+ margin-top: 0.5rem;
104
+ font-size: 0.9rem;
105
+ color: rgba(255, 255, 255, 0.8);
106
+ text-align: center;
107
+ }
108
+ </style>
@@ -0,0 +1,67 @@
1
+ ---
2
+ interface Props {
3
+ showPoweredBy?: boolean;
4
+ showThemeInfo?: boolean;
5
+ }
6
+
7
+ const { showPoweredBy = true, showThemeInfo = true } = Astro.props;
8
+ const showThemeInfoSection = !!(showPoweredBy || showThemeInfo);
9
+ ---
10
+
11
+ {
12
+ showThemeInfoSection && (
13
+ <div class="theme-info">
14
+ {showPoweredBy && (
15
+ <span>
16
+ Powered by
17
+ <a
18
+ href="https://astro.build"
19
+ target="_blank"
20
+ rel="noopener noreferrer"
21
+ >
22
+ Astro
23
+ </a>
24
+ </span>
25
+ )}
26
+ {showPoweredBy && showThemeInfo && <span> | </span>}
27
+ {showThemeInfo && (
28
+ <span>
29
+ Theme is
30
+ <a
31
+ href="https://github.com/xingwangzhe/stalux"
32
+ target="_blank"
33
+ rel="noopener noreferrer"
34
+ >
35
+ Stalux
36
+ </a>
37
+ </span>
38
+ )}
39
+ </div>
40
+ )
41
+ }
42
+
43
+ <style>
44
+ .theme-info {
45
+ font-size: 0.85rem;
46
+ display: flex;
47
+ justify-content: center;
48
+ gap: 0.3em;
49
+ flex-wrap: wrap;
50
+ }
51
+
52
+ .theme-info span {
53
+ margin: 0;
54
+ }
55
+
56
+ .theme-info a {
57
+ color: rgba(255, 255, 255, 0.8);
58
+ text-decoration: none;
59
+ transition: color 0.3s ease;
60
+ margin: 0 0.2em;
61
+ }
62
+
63
+ .theme-info a:hover {
64
+ color: rgba(255, 255, 255, 1);
65
+ text-decoration: none;
66
+ }
67
+ </style>
@@ -0,0 +1,87 @@
1
+ ---
2
+ import FooterCopyright from "@components/stalux/footer/FooterCopyright.astro";
3
+ import FooterBeian from "@components/stalux/footer/FooterBeian.astro";
4
+ import FooterThemeInfo from "@components/stalux/footer/FooterThemeInfo.astro";
5
+ import FooterStats from "@components/stalux/footer/FooterStats.astro";
6
+ import FooterBadges from "@components/stalux/footer/FooterBadges.astro";
7
+ import { getSiteData, getAuthorData, getFooterData } from "@utils/config-utils";
8
+ import type { BadgeItem, BadgeOptions, isBadgeGroup } from "@utils/badge-generator";
9
+ import { getCollection } from "astro:content";
10
+ import { getTotalWordCount, formatWordCount } from "@utils/word-count-utils";
11
+ import footerStyle from "@styles/components/footer/footer.module.css";
12
+
13
+ // 获取配置
14
+ const configEntries = await getCollection("config");
15
+ const siteConfig = getSiteData(configEntries);
16
+ const footerConfig = getFooterData(configEntries);
17
+ const authorConfig = getAuthorData(configEntries);
18
+ const author = authorConfig.name;
19
+
20
+ // 标准化 badges(扁平 badge 或 分组),确保 style 字段合法
21
+ const badges: BadgeItem[] | undefined = footerConfig?.badges?.map(
22
+ (item: any) => {
23
+ if (item.title && item.items) {
24
+ return {
25
+ title: item.title,
26
+ collapsed: item.collapsed ?? true,
27
+ items: item.items.map((b: any) => ({
28
+ ...b,
29
+ style: (["flat","plastic","flat-square","for-the-badge","social"].includes(b.style) ? b.style : "flat") as BadgeOptions["style"],
30
+ })),
31
+ };
32
+ }
33
+ return {
34
+ ...item,
35
+ style: (["flat","plastic","flat-square","for-the-badge","social"].includes(item.style) ? item.style : "flat") as BadgeOptions["style"],
36
+ };
37
+ },
38
+ );
39
+
40
+ const totalWordCount = await getTotalWordCount();
41
+ const formattedWordCount = formatWordCount(totalWordCount, siteConfig.lang || "zh-CN");
42
+ ---
43
+
44
+ <footer class={footerStyle.siteFooter} transition:persist-props>
45
+ <div class={footerStyle.footerContent}>
46
+ <!-- 版权信息 -->
47
+ {
48
+ footerConfig?.copyright?.enabled && (
49
+ <FooterCopyright
50
+ author={author}
51
+ startYear={footerConfig?.copyright?.startYear}
52
+ />
53
+ )
54
+ }
55
+
56
+ <!-- 徽章展示 -->
57
+ <FooterBadges badges={badges} />
58
+
59
+ <!-- 备案信息 -->
60
+ <FooterBeian
61
+ icp={footerConfig?.beian?.icp?.enabled
62
+ ? footerConfig?.beian?.icp?.number
63
+ : undefined}
64
+ pubsec={footerConfig?.beian?.security?.enabled
65
+ ? footerConfig?.beian?.security?.text
66
+ : undefined}
67
+ pubsecNumber={footerConfig?.beian?.security?.enabled
68
+ ? footerConfig?.beian?.security?.number
69
+ : undefined}
70
+ />
71
+
72
+ <!-- 主题信息 -->
73
+ <FooterThemeInfo
74
+ showPoweredBy={footerConfig?.theme?.showPoweredBy}
75
+ showThemeInfo={footerConfig?.theme?.showThemeInfo}
76
+ />
77
+
78
+ <!-- 站点统计 -->
79
+ <FooterStats
80
+ wordCount={formattedWordCount}
81
+ buildtime={footerConfig?.buildtime}
82
+ />
83
+
84
+ <!-- 自定义内容 -->
85
+ {footerConfig?.custom && <Fragment set:html={footerConfig.custom} />}
86
+ </div>
87
+ </footer>
@@ -0,0 +1,194 @@
1
+ ---
2
+ import { SEO } from "astro-seo";
3
+ import { ClientRouter } from "astro:transitions";
4
+ import { getCollection } from "astro:content";
5
+ import { Font } from "astro:assets";
6
+ import VerCount from "@components/stalux/common/verCount.astro";
7
+ import GoogleAnalytics from "@components/stalux/analytics/google.astro";
8
+ import BingClarity from "@components/stalux/analytics/clarity.astro";
9
+ import "@styles/base/init.css";
10
+ import Umami from "@components/stalux/analytics/umami.astro";
11
+ // 预子集化的中文字体(由 scripts/font-subset.ts 在 prebuild 阶段生成)
12
+ // subset-font (harfbuzz WASM),纯 JS,无 native 依赖,CI 友好
13
+ import "@assets/../../public/fonts/subset.css";
14
+
15
+ import { langToOpenGraphLocale } from "@utils/i18n";
16
+ import { getAuthorData, getHeadData, getPromoteData, getSiteData } from "@utils/config-utils";
17
+
18
+ const configEntries = await getCollection("config");
19
+ const siteConfig = getSiteData(configEntries);
20
+ const authorConfig = getAuthorData(configEntries);
21
+ const headConfig = getHeadData(configEntries);
22
+ const promoteConfig = getPromoteData(configEntries);
23
+
24
+ // Twitter 卡片类型定义
25
+ type TwitterCardType = "summary" | "summary_large_image";
26
+
27
+ // 最小必要 SEO 配置
28
+ interface Props {
29
+ title: string;
30
+ description: string;
31
+ cover?: string;
32
+ isArticle?: boolean;
33
+ date?: string;
34
+ updated?: string;
35
+ tags?: string[];
36
+ categories?: string[];
37
+ author?: string;
38
+ noindex?: boolean;
39
+ }
40
+
41
+ const props = Astro.props as Props;
42
+ const effectiveTitle = props.title;
43
+ const effectiveDescription = props.description;
44
+ const isArticle = props.isArticle ?? false;
45
+
46
+ const canonicalBase = siteConfig.canonical || siteConfig.url;
47
+ const canonicalUrl = canonicalBase
48
+ ? new URL(Astro.url.pathname, canonicalBase).href
49
+ : Astro.url.href;
50
+ const faviconPath = siteConfig.favicon || "/favicon.ico";
51
+ // 社交分享图片 / JSON-LD image 必须用绝对 URL
52
+ const baseImgUrl = canonicalBase.replace(/\/$/, "");
53
+ const toAbsolute = (p: string) => !p || p.startsWith("http") ? p : baseImgUrl + (p.startsWith("/") ? p : "/" + p);
54
+ const ogImagePath = toAbsolute(props.cover || authorConfig.avatar || faviconPath);
55
+ const isNotFoundPage =
56
+ Astro.url.pathname === "/404" || Astro.url.pathname === "/404/";
57
+ const resolvedNoindex = Boolean(siteConfig.noindex) || isNotFoundPage || Boolean(props.noindex);
58
+ const resolvedNofollow = Boolean(siteConfig.nofollow) || isNotFoundPage;
59
+
60
+ const openGraphConfig: Record<string, unknown> = {
61
+ basic: {
62
+ title: effectiveTitle,
63
+ type: isArticle ? "article" : "website",
64
+ image: ogImagePath,
65
+ url: canonicalUrl,
66
+ },
67
+ optional: {
68
+ description: effectiveDescription,
69
+ locale: langToOpenGraphLocale(siteConfig.lang || "zh-CN"),
70
+ },
71
+ image: {
72
+ url: ogImagePath,
73
+ alt: effectiveTitle,
74
+ },
75
+ };
76
+
77
+ // 文章页:添加 article Open Graph 标签
78
+ if (isArticle) {
79
+ const article: Record<string, unknown> = {};
80
+ if (props.date) article.publishedTime = props.date;
81
+ if (props.updated) article.modifiedTime = props.updated;
82
+ if (props.author) article.authors = [props.author];
83
+ if (props.categories?.length) article.section = props.categories[0];
84
+ if (props.tags?.length) article.tags = props.tags;
85
+ openGraphConfig.article = article;
86
+ }
87
+
88
+ const twitterConfig = {
89
+ card: "summary_large_image" as TwitterCardType,
90
+ title: effectiveTitle,
91
+ description: effectiveDescription,
92
+ image: ogImagePath,
93
+ imageAlt: effectiveTitle,
94
+ ...(siteConfig.twitterSite ? { site: siteConfig.twitterSite } : {}),
95
+ };
96
+
97
+ const extendedLink = [
98
+ { rel: "icon", href: faviconPath },
99
+ {
100
+ rel: "alternate",
101
+ type: "application/rss+xml",
102
+ title: siteConfig.title || "RSS Feed",
103
+ href: "/rss.xml",
104
+ },
105
+ ];
106
+
107
+ // 向爬虫暴露 Markdown 替代链接
108
+ // 支持首页、文章、关于、一言、友链、归档、分类、标签
109
+ const exportMd = Boolean(promoteConfig?.export_md);
110
+ const normalizedPath = Astro.url.pathname.replace(/\/$/, "") || "/";
111
+ const mdRootPaths = ["/about", "/words", "/links", "/archives", "/categories", "/tags", "/posts"];
112
+ const hasMarkdownVersion =
113
+ exportMd &&
114
+ (normalizedPath === "/" ||
115
+ mdRootPaths.some((p) => normalizedPath === p || normalizedPath.startsWith(p + "/")));
116
+ if (hasMarkdownVersion) {
117
+ const mdHref = normalizedPath === "/" ? "/index.md" : normalizedPath + ".md";
118
+ extendedLink.push({
119
+ rel: "alternate",
120
+ type: "text/markdown",
121
+ title: "Markdown source",
122
+ href: mdHref,
123
+ });
124
+ }
125
+ const extendedMeta = [
126
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
127
+ { name: "color-scheme", content: "dark" },
128
+ ];
129
+
130
+ let schema: Record<string, unknown>;
131
+ if (isArticle) {
132
+ schema = {
133
+ "@context": "https://schema.org",
134
+ "@type": "BlogPosting",
135
+ headline: effectiveTitle,
136
+ description: effectiveDescription,
137
+ url: canonicalUrl,
138
+ image: ogImagePath,
139
+ datePublished: props.date,
140
+ dateModified: props.updated || props.date,
141
+ keywords: props.tags?.join(", "),
142
+ author: props.author
143
+ ? {
144
+ "@type": "Person",
145
+ name: props.author,
146
+ }
147
+ : undefined,
148
+ mainEntityOfPage: {
149
+ "@type": "WebPage",
150
+ "@id": canonicalUrl,
151
+ },
152
+ };
153
+ } else {
154
+ schema = {
155
+ "@context": "https://schema.org",
156
+ "@type": "WebSite",
157
+ url: canonicalUrl,
158
+ name: effectiveTitle,
159
+ description: effectiveDescription,
160
+ };
161
+ }
162
+ const ldJson = JSON.stringify(schema, null, 2).replace(/<\//g, "<\\/");
163
+ ---
164
+
165
+ <head transition:persist-props>
166
+ <meta charset="utf-8" />
167
+ <ClientRouter />
168
+ <SEO
169
+ title={effectiveTitle}
170
+ description={effectiveDescription}
171
+ canonical={canonicalUrl}
172
+ nofollow={resolvedNofollow}
173
+ noindex={resolvedNoindex}
174
+ openGraph={openGraphConfig}
175
+ twitter={twitterConfig}
176
+ extend={{
177
+ link: extendedLink,
178
+ meta: extendedMeta,
179
+ }}
180
+ />
181
+ <style set:html={`:root{--font-body:"LXGW WenKai Subset","Noto Sans SC","Noto Sans CJK SC",system-ui,-apple-system,sans-serif}`} />
182
+ <Font cssVariable="--font-code" />
183
+ {
184
+ (headConfig?.anyhead) && (
185
+ <Fragment set:html={headConfig.anyhead} />
186
+ )
187
+ }
188
+ {headConfig?.googleAnalyticsId && <GoogleAnalytics />}
189
+ {headConfig?.bingClarityId && <BingClarity />}
190
+ {headConfig?.umami?.id && headConfig?.umami?.url && <Umami />}
191
+ <script type="application/ld+json" set:html={ldJson} />
192
+ <VerCount />
193
+ </head>
194
+
@@ -0,0 +1,26 @@
1
+ ---
2
+ import Navs from "@components/stalux/layout/navs.astro";
3
+ import Stalux from "@layouts/Stalux.astro";
4
+ import { getCollection } from "astro:content";
5
+ import { getSiteData } from "@utils/config-utils";
6
+ import styles from "@styles/pages/listPage.module.css";
7
+
8
+ interface Props {
9
+ title: string;
10
+ description: string;
11
+ heading: string;
12
+ subheading?: string;
13
+ }
14
+
15
+ const { title, description, heading, subheading } = Astro.props;
16
+ const configEntries = await getCollection("config");
17
+ const siteConfig = getSiteData(configEntries);
18
+ ---
19
+
20
+ <Stalux title={title + " | " + siteConfig.title} description={description}>
21
+ <div style="height: 3em;"></div>
22
+ <Navs />
23
+ <h1 class={styles.title}>{heading}</h1>
24
+ {subheading && <div class={styles.description}>{subheading}</div>}
25
+ <slot />
26
+ </Stalux>
@@ -0,0 +1,174 @@
1
+ ---
2
+ import { getCollection } from "astro:content";
3
+ import { createTranslator } from "@utils/i18n";
4
+ import { getLucideIcon } from "@utils/lucide-icons";
5
+ import styles from "@styles/components/layout/navs.module.css";
6
+ import Search from "@components/stalux/common/search.astro";
7
+ import { getSiteData, getNavsData } from "@utils/config-utils";
8
+
9
+ const MenuIcon = getLucideIcon("menu");
10
+ const SearchIcon = getLucideIcon("search");
11
+ const configEntries = await getCollection("config");
12
+ const siteConfig = getSiteData(configEntries);
13
+ const navsConfig = getNavsData(configEntries);
14
+ const { t } = createTranslator(siteConfig.lang || "zh-CN");
15
+ const navs = navsConfig?.items ?? [];
16
+ ---
17
+
18
+ <nav aria-label="Main navigation" transition:persist-props>
19
+ <!-- 汉堡菜单按钮 (仅窄屏显示) -->
20
+ <button
21
+ data-ref="stalux-nav-toggle"
22
+ class={styles.navToggle}
23
+ aria-label={t("nav.menu")}
24
+ >
25
+ {MenuIcon && <MenuIcon size={20} stroke-width={2} class="nav-icon" />}
26
+ </button>
27
+
28
+ <!-- 侧边栏遮罩 -->
29
+ <div data-ref="stalux-nav-overlay" class={styles.navOverlay}></div>
30
+
31
+ <!-- 导航列表 -->
32
+ <div class={styles.navContainer}>
33
+ <button
34
+ data-ref="stalux-nav-scroll-left"
35
+ class={styles.scrollBtn}
36
+ data-side="left"
37
+ aria-label={t("nav.scrollLeft")}
38
+ >
39
+ <svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
40
+ </button>
41
+
42
+ <ul data-ref="stalux-nav-list" class={styles.navList}>
43
+ {
44
+ navs.map((item) => {
45
+ const NavIcon = item.icon ? getLucideIcon(item.icon) : null;
46
+ return (
47
+ <li class={styles.navItem}>
48
+ <a
49
+ href={item.link}
50
+ class={styles.navLink}
51
+ data-ref="stalux-nav-link"
52
+ data-astro-prefetch
53
+ >
54
+ {NavIcon && (
55
+ <NavIcon size={20} stroke-width={2} class="nav-icon" />
56
+ )}
57
+ <span class={styles.navText}>{item.title}</span>
58
+ </a>
59
+ </li>
60
+ );
61
+ })
62
+ }
63
+ <li class={styles.navItem}>
64
+ <a
65
+ href="javascript:void(0)"
66
+ class={styles.navLink}
67
+ data-ref="stalux-search-btn"
68
+ aria-label={t("search.title")}
69
+ >
70
+ {SearchIcon && (
71
+ <SearchIcon size={20} stroke-width={2} class="nav-icon" />
72
+ )}
73
+ <span class={styles.navText}>{t("nav.search")}</span>
74
+ </a>
75
+ </li>
76
+ </ul>
77
+
78
+ <button
79
+ data-ref="stalux-nav-scroll-right"
80
+ class={styles.scrollBtn}
81
+ data-side="right"
82
+ aria-label={t("nav.scrollRight")}
83
+ >
84
+ <svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
85
+ </button>
86
+ </div>
87
+ </nav>
88
+ <Search />
89
+ <script>
90
+ document.addEventListener("astro:page-load", () => {
91
+ const searchBtn = document.querySelector(
92
+ '[data-ref="stalux-search-btn"]',
93
+ );
94
+ searchBtn?.addEventListener("click", (e) => {
95
+ e.preventDefault();
96
+ closeNav();
97
+ document.getElementById('search')?.open();
98
+ });
99
+
100
+ // 汉堡菜单交互
101
+ const navToggle = document.querySelector(
102
+ '[data-ref="stalux-nav-toggle"]',
103
+ );
104
+ const navList = document.querySelector('[data-ref="stalux-nav-list"]');
105
+ const navOverlay = document.querySelector(
106
+ '[data-ref="stalux-nav-overlay"]',
107
+ );
108
+
109
+ function openNav() {
110
+ navList?.setAttribute("data-state", "open");
111
+ navOverlay?.setAttribute("data-state", "show");
112
+ document.body.style.overflow = "hidden";
113
+ }
114
+
115
+ function closeNav() {
116
+ navList?.removeAttribute("data-state");
117
+ navOverlay?.removeAttribute("data-state");
118
+ document.body.style.overflow = "";
119
+ }
120
+
121
+ navToggle?.addEventListener("click", openNav);
122
+ navOverlay?.addEventListener("click", closeNav);
123
+
124
+ // 点击导航链接后关闭侧边栏
125
+ const navLinks = document.querySelectorAll(
126
+ '[data-ref="stalux-nav-link"]',
127
+ );
128
+ navLinks?.forEach((link) => {
129
+ link.addEventListener("click", closeNav);
130
+ });
131
+
132
+ // --- 动态导航滚动按钮 ---
133
+ initNavScroll();
134
+ });
135
+
136
+ function initNavScroll() {
137
+ const navList = document.querySelector('[data-ref="stalux-nav-list"]');
138
+ const btnLeft = document.querySelector('[data-ref="stalux-nav-scroll-left"]');
139
+ const btnRight = document.querySelector('[data-ref="stalux-nav-scroll-right"]');
140
+ if (!navList || !btnLeft || !btnRight) return;
141
+
142
+ function updateButtons() {
143
+ const hasOverflow = navList.scrollWidth > navList.clientWidth;
144
+ const atStart = navList.scrollLeft <= 4;
145
+ const atEnd = navList.scrollLeft >= navList.scrollWidth - navList.clientWidth - 4;
146
+
147
+ if (hasOverflow) {
148
+ btnLeft.classList.toggle("visible", !atStart);
149
+ btnRight.classList.toggle("visible", !atEnd);
150
+ } else {
151
+ btnLeft.classList.remove("visible");
152
+ btnRight.classList.remove("visible");
153
+ }
154
+ }
155
+
156
+ btnLeft.addEventListener("click", () => {
157
+ navList.scrollBy({ left: -300, behavior: "smooth" });
158
+ });
159
+
160
+ btnRight.addEventListener("click", () => {
161
+ navList.scrollBy({ left: 300, behavior: "smooth" });
162
+ });
163
+
164
+ // 监听滚动事件更新按钮状态
165
+ navList.addEventListener("scroll", updateButtons);
166
+
167
+ // 用 ResizeObserver 监听容器尺寸变化(窗口缩窄/变宽时重新判断)
168
+ const ro = new ResizeObserver(updateButtons);
169
+ ro.observe(navList);
170
+
171
+ // 初始检测
172
+ updateButtons();
173
+ }
174
+ </script>