@jet-w/astro-blog 0.1.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 (140) hide show
  1. package/dist/chunk-FXPGR372.js +0 -0
  2. package/dist/chunk-GYLSY3OJ.js +173 -0
  3. package/dist/config/index.d.ts +166 -0
  4. package/dist/config/index.js +38 -0
  5. package/dist/index.d.ts +34 -0
  6. package/dist/index.js +59 -0
  7. package/dist/types/index.d.ts +75 -0
  8. package/dist/types/index.js +1 -0
  9. package/package.json +84 -0
  10. package/src/components/EChartsCard.vue +118 -0
  11. package/src/components/Mermaid.vue +73 -0
  12. package/src/components/about/ContentCard.astro +27 -0
  13. package/src/components/about/IconCard.astro +77 -0
  14. package/src/components/about/SocialLinks.astro +54 -0
  15. package/src/components/about/TagCard.astro +65 -0
  16. package/src/components/about/TagGroup.astro +33 -0
  17. package/src/components/about/TimelineCard.astro +52 -0
  18. package/src/components/blog/FloatingToc.vue +198 -0
  19. package/src/components/blog/Hero.astro +147 -0
  20. package/src/components/blog/NavigationTabs.vue +245 -0
  21. package/src/components/blog/PostCard.astro +161 -0
  22. package/src/components/blog/PostNavigation.astro +106 -0
  23. package/src/components/blog/RelatedPosts.astro +175 -0
  24. package/src/components/blog/TableOfContents.astro +153 -0
  25. package/src/components/blog/TagCloud.astro +91 -0
  26. package/src/components/home/FeaturedPostsSection.astro +54 -0
  27. package/src/components/home/QuickNavSection.astro +81 -0
  28. package/src/components/home/RecentPostsSection.astro +52 -0
  29. package/src/components/home/StatsSection.astro +44 -0
  30. package/src/components/layout/Footer.astro +103 -0
  31. package/src/components/layout/Header.astro +68 -0
  32. package/src/components/layout/Sidebar.astro +594 -0
  33. package/src/components/media/Bilibili.astro +114 -0
  34. package/src/components/media/Slides.astro +313 -0
  35. package/src/components/media/Video.astro +111 -0
  36. package/src/components/media/VideoPlayer.astro +89 -0
  37. package/src/components/media/YouTube.astro +92 -0
  38. package/src/components/pte/StudyCalendar.vue +1348 -0
  39. package/src/components/ui/Icon.astro +187 -0
  40. package/src/components/ui/MobileMenu.vue +201 -0
  41. package/src/components/ui/Pagination.astro +143 -0
  42. package/src/components/ui/SearchBox.vue +179 -0
  43. package/src/components/ui/SearchInterface.vue +409 -0
  44. package/src/components/ui/SidebarToggle.vue +57 -0
  45. package/src/components/ui/ThemeToggle.vue +90 -0
  46. package/src/layouts/AboutLayout.astro +18 -0
  47. package/src/layouts/BaseLayout.astro +362 -0
  48. package/src/layouts/PageLayout.astro +217 -0
  49. package/src/layouts/SlidesLayout.astro +320 -0
  50. package/src/plugins/rehype-clean-containers.mjs +24 -0
  51. package/src/plugins/rehype-relative-links.mjs +43 -0
  52. package/src/plugins/rehype-tabs.mjs +116 -0
  53. package/src/plugins/remark-containers.mjs +407 -0
  54. package/src/plugins/remark-mermaid.mjs +46 -0
  55. package/src/styles/global.css +870 -0
  56. package/src/styles/slides.css +220 -0
  57. package/src/utils/sidebar.ts +492 -0
  58. package/templates/default/astro.config.mjs +51 -0
  59. package/templates/default/content/pages/about.mdx +93 -0
  60. package/templates/default/content/pages/index.mdx +20 -0
  61. package/templates/default/content/posts/blog_docs/01-quick-start.md +162 -0
  62. package/templates/default/content/posts/blog_docs/02-frontmatter.md +277 -0
  63. package/templates/default/content/posts/blog_docs/03-markdown-basic.md +350 -0
  64. package/templates/default/content/posts/blog_docs/04-containers.md +331 -0
  65. package/templates/default/content/posts/blog_docs/05-code-blocks.md +388 -0
  66. package/templates/default/content/posts/blog_docs/06-mermaid.md +431 -0
  67. package/templates/default/content/posts/blog_docs/07-video.md +243 -0
  68. package/templates/default/content/posts/blog_docs/08-latex.md +382 -0
  69. package/templates/default/content/posts/blog_docs/09-icons.md +326 -0
  70. package/templates/default/content/posts/blog_docs/10-sidebar.md +445 -0
  71. package/templates/default/content/posts/blog_docs/11-config.md +334 -0
  72. package/templates/default/content/posts/blog_docs/12-slides.mdx +552 -0
  73. package/templates/default/content/posts/blog_docs/README.md +151 -0
  74. package/templates/default/content/slides/demo.md +146 -0
  75. package/templates/default/content/slides/docs/basic-demo.md +35 -0
  76. package/templates/default/content/slides/docs/code-demo.md +62 -0
  77. package/templates/default/content/slides/docs/echarts-demo.md +139 -0
  78. package/templates/default/content/slides/docs/fragment-demo.md +35 -0
  79. package/templates/default/content/slides/docs/math-demo.md +48 -0
  80. package/templates/default/content/slides/docs/mermaid-demo.md +105 -0
  81. package/templates/default/content/slides/docs/theme-demo.md +38 -0
  82. package/templates/default/content/slides/docs/vertical-demo.md +50 -0
  83. package/templates/default/package.json +31 -0
  84. package/templates/default/public/favicon-bak.svg +4 -0
  85. package/templates/default/public/images/avatar.jpg +0 -0
  86. package/templates/default/public/images/avatar.svg +142 -0
  87. package/templates/default/public/js/mermaid-container.js +402 -0
  88. package/templates/default/public/js/mermaid-init.js +131 -0
  89. package/templates/default/public/js/mermaid-render.js +98 -0
  90. package/templates/default/public/js/mermaid-simple.js +95 -0
  91. package/templates/default/public/js/tabs-init.js +86 -0
  92. package/templates/default/public/media/individual_portfolio/INDIVIDUAL PORTFOLIO.png +0 -0
  93. package/templates/default/public/slides/plugin/highlight/highlight.js +5 -0
  94. package/templates/default/public/slides/plugin/highlight/monokai.css +71 -0
  95. package/templates/default/public/slides/plugin/markdown/markdown.js +7 -0
  96. package/templates/default/public/slides/plugin/math/math.js +1 -0
  97. package/templates/default/public/slides/plugin/notes/notes.js +1 -0
  98. package/templates/default/public/slides/reveal.css +9 -0
  99. package/templates/default/public/slides/reveal.js +9 -0
  100. package/templates/default/public/slides/theme/beige.css +366 -0
  101. package/templates/default/public/slides/theme/black-contrast.css +362 -0
  102. package/templates/default/public/slides/theme/black.css +359 -0
  103. package/templates/default/public/slides/theme/blood.css +392 -0
  104. package/templates/default/public/slides/theme/dracula.css +385 -0
  105. package/templates/default/public/slides/theme/league.css +368 -0
  106. package/templates/default/public/slides/theme/moon.css +362 -0
  107. package/templates/default/public/slides/theme/night.css +360 -0
  108. package/templates/default/public/slides/theme/serif.css +363 -0
  109. package/templates/default/public/slides/theme/simple.css +362 -0
  110. package/templates/default/public/slides/theme/sky.css +370 -0
  111. package/templates/default/public/slides/theme/solarized.css +363 -0
  112. package/templates/default/public/slides/theme/white-contrast.css +362 -0
  113. package/templates/default/public/slides/theme/white.css +359 -0
  114. package/templates/default/public/slides/theme/white_contrast_compact_verbatim_headers.css +360 -0
  115. package/templates/default/public/test-complete.html +43 -0
  116. package/templates/default/public/test-mermaid.html +124 -0
  117. package/templates/default/src/config/index.ts +114 -0
  118. package/templates/default/src/content.config.ts +96 -0
  119. package/templates/default/src/pages/[...slug].astro +27 -0
  120. package/templates/default/src/pages/archives/[year]/[month]/page/[page].astro +176 -0
  121. package/templates/default/src/pages/archives/[year]/[month].astro +158 -0
  122. package/templates/default/src/pages/archives/index.astro +210 -0
  123. package/templates/default/src/pages/categories/[category]/page/[page].astro +218 -0
  124. package/templates/default/src/pages/categories/[category].astro +198 -0
  125. package/templates/default/src/pages/categories/index.astro +190 -0
  126. package/templates/default/src/pages/container-test.astro +79 -0
  127. package/templates/default/src/pages/mermaid-direct.html +78 -0
  128. package/templates/default/src/pages/posts/[...slug].astro +335 -0
  129. package/templates/default/src/pages/posts/index.astro +541 -0
  130. package/templates/default/src/pages/posts/page/[page].astro +146 -0
  131. package/templates/default/src/pages/rss.xml.ts +28 -0
  132. package/templates/default/src/pages/search-index.json.ts +21 -0
  133. package/templates/default/src/pages/search.astro +50 -0
  134. package/templates/default/src/pages/slides/[...slug].astro +54 -0
  135. package/templates/default/src/pages/slides/index.astro +135 -0
  136. package/templates/default/src/pages/tags/[tag]/page/[page].astro +211 -0
  137. package/templates/default/src/pages/tags/[tag].astro +191 -0
  138. package/templates/default/src/pages/tags/index.astro +167 -0
  139. package/templates/default/tailwind.config.mjs +78 -0
  140. package/templates/default/tsconfig.json +9 -0
@@ -0,0 +1,28 @@
1
+ import rss from '@astrojs/rss';
2
+ import { getCollection } from 'astro:content';
3
+ import { siteConfig } from '@/config';
4
+
5
+ export async function GET(context: { site: URL }) {
6
+ const posts = await getCollection('posts', ({ data }) => !data.draft);
7
+
8
+ // 按日期排序,最新的在前
9
+ const sortedPosts = posts.sort((a, b) => {
10
+ const dateA = new Date(a.data.date || 0);
11
+ const dateB = new Date(b.data.date || 0);
12
+ return dateB.getTime() - dateA.getTime();
13
+ });
14
+
15
+ return rss({
16
+ title: siteConfig.title,
17
+ description: siteConfig.description,
18
+ site: context.site,
19
+ items: sortedPosts.map((post) => ({
20
+ title: post.data.title,
21
+ pubDate: post.data.date ? new Date(post.data.date) : new Date(),
22
+ description: post.data.description || '',
23
+ link: `/posts/${post.id.toLowerCase()}/`,
24
+ categories: [...(post.data.categories || []), ...(post.data.tags || [])]
25
+ })),
26
+ customData: `<language>zh-CN</language>`
27
+ });
28
+ }
@@ -0,0 +1,21 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getCollection } from 'astro:content';
3
+
4
+ export const GET: APIRoute = async () => {
5
+ const posts = await getCollection('posts', ({ data }) => !data.draft);
6
+
7
+ const searchIndex = posts.map(post => ({
8
+ title: post.data.title,
9
+ description: post.data.description || '',
10
+ url: `/posts/${post.id.toLowerCase()}`,
11
+ content: post.body?.substring(0, 500) || '', // 取前500字符作为搜索内容
12
+ tags: post.data.tags || [],
13
+ categories: post.data.categories || []
14
+ }));
15
+
16
+ return new Response(JSON.stringify(searchIndex), {
17
+ headers: {
18
+ 'Content-Type': 'application/json'
19
+ }
20
+ });
21
+ };
@@ -0,0 +1,50 @@
1
+ ---
2
+ import PageLayout from '@jet-w/astro-blog/layouts/PageLayout.astro';
3
+ import SearchInterface from '@jet-w/astro-blog/components/ui/SearchInterface.vue';
4
+ ---
5
+
6
+ <PageLayout
7
+ title="搜索"
8
+ description="搜索博客文章和内容"
9
+ showSidebar={true}
10
+ >
11
+ <div class="max-w-4xl mx-auto">
12
+ <!-- 页面头部 -->
13
+ <div class="text-center mb-12">
14
+ <h1 class="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
15
+ 搜索文章
16
+ </h1>
17
+ <p class="text-xl text-slate-600 dark:text-slate-400">
18
+ 在所有文章中查找您感兴趣的内容
19
+ </p>
20
+ </div>
21
+
22
+ <!-- 搜索界面 -->
23
+ <SearchInterface client:load />
24
+
25
+ <!-- 搜索提示 -->
26
+ <div class="mt-12 p-6 bg-slate-50 dark:bg-slate-800 rounded-xl">
27
+ <h2 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4">
28
+ 搜索技巧
29
+ </h2>
30
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-slate-600 dark:text-slate-400">
31
+ <div>
32
+ <h3 class="font-medium text-slate-900 dark:text-slate-100 mb-2">基础搜索</h3>
33
+ <ul class="space-y-1">
34
+ <li>• 输入关键词搜索标题和内容</li>
35
+ <li>• 支持中英文混合搜索</li>
36
+ <li>• 自动忽略大小写</li>
37
+ </ul>
38
+ </div>
39
+ <div>
40
+ <h3 class="font-medium text-slate-900 dark:text-slate-100 mb-2">高级功能</h3>
41
+ <ul class="space-y-1">
42
+ <li>• 实时搜索建议</li>
43
+ <li>• 按标签和分类筛选</li>
44
+ <li>• 支持模糊匹配</li>
45
+ </ul>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </PageLayout>
@@ -0,0 +1,54 @@
1
+ ---
2
+ import { getCollection, render } from 'astro:content';
3
+ import SlidesLayout from '@jet-w/astro-blog/layouts/SlidesLayout.astro';
4
+
5
+ export async function getStaticPaths() {
6
+ const slides = await getCollection('slides', ({ data }) => !data.draft);
7
+ return slides.map((slide) => ({
8
+ params: { slug: slide.id },
9
+ props: { slide },
10
+ }));
11
+ }
12
+
13
+ const { slide } = Astro.props;
14
+ const { Content } = await render(slide);
15
+
16
+ // 获取 Markdown 原始内容用于 Reveal.js
17
+ const slideContent = slide.body || '';
18
+
19
+ // 将 Markdown 内容解析为幻灯片
20
+ // 使用 --- 作为水平分隔符,---- 作为垂直分隔符
21
+ function parseSlides(markdown: string): string[][] {
22
+ // 按水平分隔符分割
23
+ const horizontalSlides = markdown.split(/\n---\n/);
24
+
25
+ return horizontalSlides.map((hSlide) => {
26
+ // 按垂直分隔符分割
27
+ return hSlide.split(/\n----\n/);
28
+ });
29
+ }
30
+
31
+ const slidesData = parseSlides(slideContent);
32
+ ---
33
+
34
+ <SlidesLayout
35
+ title={slide.data.title}
36
+ description={slide.data.description}
37
+ theme={slide.data.theme}
38
+ transition={slide.data.transition}
39
+ controls={slide.data.controls}
40
+ progress={slide.data.progress}
41
+ center={slide.data.center}
42
+ hash={slide.data.hash}
43
+ slideNumber={slide.data.slideNumber}
44
+ >
45
+ {slidesData.map((verticalSlides) => (
46
+ <section>
47
+ {verticalSlides.map((slideContent) => (
48
+ <section data-markdown>
49
+ <textarea data-template set:text={slideContent}></textarea>
50
+ </section>
51
+ ))}
52
+ </section>
53
+ ))}
54
+ </SlidesLayout>
@@ -0,0 +1,135 @@
1
+ ---
2
+ import { getCollection } from 'astro:content';
3
+ import PageLayout from '@jet-w/astro-blog/layouts/PageLayout.astro';
4
+
5
+ // 获取所有非草稿的 slides
6
+ const slides = (await getCollection('slides', ({ data }) => !data.draft))
7
+ .sort((a, b) => {
8
+ const dateA = a.data.pubDate ? new Date(a.data.pubDate).getTime() : 0;
9
+ const dateB = b.data.pubDate ? new Date(b.data.pubDate).getTime() : 0;
10
+ return dateB - dateA;
11
+ });
12
+
13
+ function formatDate(date: Date | undefined): string {
14
+ if (!date) return '';
15
+ return new Date(date).toLocaleDateString('zh-CN', {
16
+ year: 'numeric',
17
+ month: 'long',
18
+ day: 'numeric'
19
+ });
20
+ }
21
+ ---
22
+
23
+ <PageLayout title="幻灯片" description="演示文稿列表" showSidebar={false}>
24
+ <div class="slides-list-page">
25
+ <header class="mb-8">
26
+ <h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-2">
27
+ <span class="mr-2">📽️</span>幻灯片
28
+ </h1>
29
+ <p class="text-slate-600 dark:text-slate-400">
30
+ 基于 Reveal.js 的演示文稿,支持 Markdown 语法
31
+ </p>
32
+ </header>
33
+
34
+ {slides.length === 0 ? (
35
+ <div class="text-center py-16 text-slate-500 dark:text-slate-400">
36
+ <div class="text-6xl mb-4">📭</div>
37
+ <p>暂无幻灯片</p>
38
+ <p class="text-sm mt-2">在 <code class="px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded">content/slides/</code> 目录下创建 Markdown 文件即可开始</p>
39
+ </div>
40
+ ) : (
41
+ <div class="grid gap-6 md:grid-cols-2">
42
+ {slides.map((slide) => (
43
+ <a
44
+ href={`/slides/${slide.id}`}
45
+ class="slide-card group block p-6 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 hover:border-primary-500 dark:hover:border-primary-500 transition-all hover:shadow-lg hover:-translate-y-1"
46
+ >
47
+ <div class="flex items-start justify-between mb-3">
48
+ <h2 class="text-xl font-semibold text-slate-900 dark:text-slate-100 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
49
+ {slide.data.title}
50
+ </h2>
51
+ <span class="flex-shrink-0 ml-2 px-2 py-1 text-xs bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 rounded">
52
+ {slide.data.theme || 'black'}
53
+ </span>
54
+ </div>
55
+
56
+ {slide.data.description && (
57
+ <p class="text-slate-600 dark:text-slate-400 text-sm mb-4 line-clamp-2">
58
+ {slide.data.description}
59
+ </p>
60
+ )}
61
+
62
+ <div class="flex items-center justify-between text-sm text-slate-500 dark:text-slate-400">
63
+ <div class="flex items-center gap-4">
64
+ {slide.data.pubDate && (
65
+ <span class="flex items-center gap-1">
66
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
67
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
68
+ </svg>
69
+ {formatDate(slide.data.pubDate)}
70
+ </span>
71
+ )}
72
+ {slide.data.author && (
73
+ <span class="flex items-center gap-1">
74
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
76
+ </svg>
77
+ {slide.data.author}
78
+ </span>
79
+ )}
80
+ </div>
81
+
82
+ <span class="flex items-center gap-1 text-primary-600 dark:text-primary-400 group-hover:translate-x-1 transition-transform">
83
+ 查看
84
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
86
+ </svg>
87
+ </span>
88
+ </div>
89
+
90
+ {slide.data.tags && slide.data.tags.length > 0 && (
91
+ <div class="flex flex-wrap gap-2 mt-4 pt-4 border-t border-slate-100 dark:border-slate-700">
92
+ {slide.data.tags.map((tag: string) => (
93
+ <span class="px-2 py-0.5 text-xs bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full">
94
+ #{tag}
95
+ </span>
96
+ ))}
97
+ </div>
98
+ )}
99
+ </a>
100
+ ))}
101
+ </div>
102
+ )}
103
+
104
+ <!-- 使用说明 -->
105
+ <div class="mt-12 p-6 bg-slate-50 dark:bg-slate-800/50 rounded-xl">
106
+ <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4">快速开始</h3>
107
+ <div class="prose prose-sm dark:prose-invert max-w-none">
108
+ <ol class="list-decimal list-inside space-y-2 text-slate-600 dark:text-slate-400">
109
+ <li>在 <code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-700 rounded text-sm">content/slides/</code> 目录下创建 Markdown 文件</li>
110
+ <li>使用 <code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-700 rounded text-sm">---</code> 分隔水平幻灯片</li>
111
+ <li>使用 <code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-700 rounded text-sm">----</code> 分隔垂直幻灯片</li>
112
+ <li>支持代码高亮、数学公式和 Mermaid 图表</li>
113
+ </ol>
114
+ </div>
115
+
116
+ <div class="mt-4 flex flex-wrap gap-2">
117
+ <span class="text-sm text-slate-500 dark:text-slate-400">可用主题:</span>
118
+ {['black', 'white', 'league', 'beige', 'night', 'serif', 'simple', 'solarized'].map((t) => (
119
+ <span class="px-2 py-0.5 text-xs bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300 rounded">
120
+ {t}
121
+ </span>
122
+ ))}
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </PageLayout>
127
+
128
+ <style>
129
+ .line-clamp-2 {
130
+ display: -webkit-box;
131
+ -webkit-line-clamp: 2;
132
+ -webkit-box-orient: vertical;
133
+ overflow: hidden;
134
+ }
135
+ </style>
@@ -0,0 +1,211 @@
1
+ ---
2
+ import { getCollection } from 'astro:content';
3
+ import PageLayout from '@jet-w/astro-blog/layouts/PageLayout.astro';
4
+ import PostCard from '@jet-w/astro-blog/components/blog/PostCard.astro';
5
+ import Pagination from '@jet-w/astro-blog/components/ui/Pagination.astro';
6
+
7
+ export async function getStaticPaths() {
8
+ const postsPerPage = 10;
9
+ const allPosts = await getCollection('posts', ({ data }) => !data.draft);
10
+
11
+ // 从所有文章中收集标签
12
+ const tagMap = new Map<string, { name: string; posts: typeof allPosts }>();
13
+
14
+ allPosts.forEach(post => {
15
+ (post.data.tags || []).forEach(tag => {
16
+ const slug = tag.toLowerCase().replace(/\s+/g, '-');
17
+ if (tagMap.has(slug)) {
18
+ tagMap.get(slug)!.posts.push(post);
19
+ } else {
20
+ tagMap.set(slug, { name: tag, posts: [post] });
21
+ }
22
+ });
23
+ });
24
+
25
+ // 为每个标签的每一页生成路径
26
+ const paths: Array<{
27
+ params: { tag: string; page: string };
28
+ props: { tagSlug: string; tagName: string; page: number; totalPages: number };
29
+ }> = [];
30
+
31
+ tagMap.forEach(({ name, posts }, slug) => {
32
+ // 按日期排序
33
+ const sortedPosts = posts.sort((a, b) => {
34
+ const dateA = a.data.pubDate ? new Date(a.data.pubDate).getTime() : 0;
35
+ const dateB = b.data.pubDate ? new Date(b.data.pubDate).getTime() : 0;
36
+ return dateB - dateA;
37
+ });
38
+
39
+ const totalPages = Math.ceil(sortedPosts.length / postsPerPage);
40
+
41
+ // 为每一页生成路径(从第2页开始,第1页由 [tag].astro 处理)
42
+ for (let page = 2; page <= totalPages; page++) {
43
+ paths.push({
44
+ params: { tag: slug, page: page.toString() },
45
+ props: { tagSlug: slug, tagName: name, page, totalPages }
46
+ });
47
+ }
48
+ });
49
+
50
+ return paths;
51
+ }
52
+
53
+ const { tagSlug, tagName, page: currentPage, totalPages } = Astro.props;
54
+ const postsPerPage = 10;
55
+
56
+ // 获取所有文章并筛选包含该标签的文章
57
+ const allPosts = await getCollection('posts', ({ data }) => !data.draft);
58
+
59
+ // 筛选包含该标签的文章
60
+ const filteredPosts = allPosts
61
+ .filter(post =>
62
+ (post.data.tags || []).some(t => t.toLowerCase().replace(/\s+/g, '-') === tagSlug)
63
+ )
64
+ .sort((a, b) => {
65
+ const dateA = a.data.pubDate ? new Date(a.data.pubDate).getTime() : 0;
66
+ const dateB = b.data.pubDate ? new Date(b.data.pubDate).getTime() : 0;
67
+ return dateB - dateA;
68
+ });
69
+
70
+ // 分页逻辑
71
+ const totalPosts = filteredPosts.length;
72
+ const startIndex = (currentPage - 1) * postsPerPage;
73
+ const endIndex = startIndex + postsPerPage;
74
+ const posts = filteredPosts.slice(startIndex, endIndex);
75
+
76
+ // 获取相关标签(从当前标签文章中提取其他标签)
77
+ const relatedTagMap = new Map<string, { name: string; count: number }>();
78
+ filteredPosts.forEach(post => {
79
+ (post.data.tags || []).forEach(t => {
80
+ const slug = t.toLowerCase().replace(/\s+/g, '-');
81
+ if (slug !== tagSlug) {
82
+ if (relatedTagMap.has(slug)) {
83
+ relatedTagMap.get(slug)!.count++;
84
+ } else {
85
+ relatedTagMap.set(slug, { name: t, count: 1 });
86
+ }
87
+ }
88
+ });
89
+ });
90
+
91
+ const relatedTags = Array.from(relatedTagMap.entries())
92
+ .map(([slug, { name, count }]) => ({ slug, name, count }))
93
+ .sort((a, b) => b.count - a.count)
94
+ .slice(0, 8);
95
+ ---
96
+
97
+ <PageLayout
98
+ title={`标签: ${tagName} - 第 ${currentPage} 页`}
99
+ description={`浏览所有关于 ${tagName} 的文章 - 第 ${currentPage} 页`}
100
+ showSidebar={true}
101
+ >
102
+ <!-- 面包屑导航 -->
103
+ <nav class="flex items-center space-x-2 text-sm text-slate-600 dark:text-slate-400 mb-8">
104
+ <a href="/" class="hover:text-primary-500 transition-colors">首页</a>
105
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
106
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
107
+ </svg>
108
+ <a href="/tags" class="hover:text-primary-500 transition-colors">标签</a>
109
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
110
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
111
+ </svg>
112
+ <a href={`/tags/${tagSlug}`} class="hover:text-primary-500 transition-colors">{tagName}</a>
113
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
115
+ </svg>
116
+ <span class="text-slate-900 dark:text-slate-100">第 {currentPage} 页</span>
117
+ </nav>
118
+
119
+ <!-- 页面头部 -->
120
+ <div class="text-center mb-12">
121
+ <div class="inline-flex items-center justify-center w-16 h-16 bg-primary-100 dark:bg-primary-900/30 rounded-full mb-4">
122
+ <svg class="w-8 h-8 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
123
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
124
+ </svg>
125
+ </div>
126
+
127
+ <h1 class="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
128
+ {tagName}
129
+ </h1>
130
+
131
+ <p class="text-xl text-slate-600 dark:text-slate-400 mb-8">
132
+ 找到 {totalPosts} 篇关于 {tagName} 的文章
133
+ </p>
134
+
135
+ <!-- 标签统计 -->
136
+ <div class="inline-flex items-center space-x-6 text-sm text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-800 px-6 py-3 rounded-lg">
137
+ <span>#{tagName}</span>
138
+ <span>•</span>
139
+ <span>{totalPosts} 篇文章</span>
140
+ <span>•</span>
141
+ <span>第 {currentPage} / {totalPages} 页</span>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- 文章列表 -->
146
+ {posts.length > 0 ? (
147
+ <div class="space-y-8 mb-12">
148
+ {posts.map((post) => (
149
+ <PostCard
150
+ post={{
151
+ slug: post.id.toLowerCase(),
152
+ title: post.data.title,
153
+ description: post.data.description,
154
+ pubDate: post.data.pubDate,
155
+ tags: post.data.tags,
156
+ categories: post.data.categories,
157
+ author: post.data.author,
158
+ image: post.data.image
159
+ }}
160
+ layout="horizontal"
161
+ />
162
+ ))}
163
+ </div>
164
+ ) : (
165
+ <div class="text-center py-16">
166
+ <div class="text-6xl mb-4">📝</div>
167
+ <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100 mb-2">
168
+ 暂无相关文章
169
+ </h3>
170
+ <p class="text-slate-600 dark:text-slate-400 mb-6">
171
+ 目前还没有关于 {tagName} 的文章,请查看其他标签。
172
+ </p>
173
+ <a href="/tags" class="btn-secondary">
174
+ 浏览所有标签
175
+ </a>
176
+ </div>
177
+ )}
178
+
179
+ <!-- 分页导航 -->
180
+ {totalPages > 1 && (
181
+ <Pagination
182
+ currentPage={currentPage}
183
+ totalPages={totalPages}
184
+ baseUrl={`/tags/${tagSlug}`}
185
+ />
186
+ )}
187
+
188
+ <!-- 相关标签 -->
189
+ {relatedTags.length > 0 && (
190
+ <section class="mt-16 pt-8 border-t border-slate-200 dark:border-slate-700">
191
+ <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-6">
192
+ 相关标签
193
+ </h2>
194
+ <div class="flex flex-wrap gap-3">
195
+ {relatedTags.map((relatedTag) => (
196
+ <a
197
+ href={`/tags/${relatedTag.slug}`}
198
+ class="inline-flex items-center px-4 py-2 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg hover:border-primary-300 dark:hover:border-primary-600 hover:shadow-md transition-all duration-200 group"
199
+ >
200
+ <span class="font-medium text-slate-900 dark:text-slate-100 group-hover:text-primary-500 transition-colors">
201
+ {relatedTag.name}
202
+ </span>
203
+ <span class="ml-2 text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 rounded-full">
204
+ {relatedTag.count}
205
+ </span>
206
+ </a>
207
+ ))}
208
+ </div>
209
+ </section>
210
+ )}
211
+ </PageLayout>