Files
blog/src/pages/posts/[slug].astro
T
2026-06-18 15:10:33 +08:00

171 lines
5.4 KiB
Plaintext

---
import ArticleSidebar from '@/components/ArticleSidebar.astro';
import PostCopyrightCard from '@/components/PostCopyrightCard.astro';
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getAllPosts } from '@/lib/posts';
export function getStaticPaths() {
return getAllPosts().map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = post;
const allPosts = getAllPosts();
const rawContent = post.rawContent?.() ?? '';
const textContent = rawContent
.replace(/```[\s\S]*?```/g, '')
.replace(/<[^>]+>/g, '')
.replace(/[#>*_`~\-\[\]\(\)!]/g, '')
.replace(/\s+/g, '');
const wordCount = textContent.length;
const wordCountText = wordCount >= 1000 ? `${(wordCount / 1000).toFixed(1)}k` : String(wordCount);
const readingMinutes = Math.max(1, Math.ceil(wordCount / 400));
function slugifyHeading(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/<[^>]+>/g, '')
.replace(/[`*_~[\]()]/g, '')
.replace(/[^\p{Letter}\p{Number}\s-]/gu, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
function makeToc(raw: string) {
const used = new Map<string, number>();
let inFence = false;
return raw
.split(/\r?\n/)
.flatMap((line) => {
if (/^\s*```/.test(line)) {
inFence = !inFence;
return [];
}
if (inFence) return [];
const match = /^(#{1,3})\s+(.+?)\s*#*$/.exec(line);
if (!match) return [];
const text = match[2].replace(/<[^>]+>/g, '').trim();
if (!text) return [];
const baseId = slugifyHeading(text) || 'section';
const count = used.get(baseId) ?? 0;
used.set(baseId, count + 1);
return [{
depth: match[1].length,
text,
id: count === 0 ? baseId : `${baseId}-${count}`,
}];
});
}
const toc = makeToc(post.rawContent?.() ?? '');
const relatedPosts = allPosts
.filter((item) => item.slug !== post.slug)
.filter((item) => item.categories.some((category) => post.categories.includes(category)))
.slice(0, 8);
---
<BaseLayout title={post.title} description={post.summary}>
<section class="post-layout">
<article class="article post-article">
<header class={post.cover ? 'article-header article-header-cover' : 'article-header'}>
{post.cover && <img class="article-header-cover-img" src={post.cover} alt="" loading="eager" decoding="async" />}
<div class="article-header-content">
<time datetime={post.date.toISOString()}>{post.dateText}</time>
<h1>{post.title}</h1>
{post.cover ? (
<div class="article-hero-meta" aria-label="文章信息">
<span>发布于 {post.dateText}</span>
{post.categories[0] && <a href={`/categories/${encodeURIComponent(post.categories[0])}/`}>{post.categories[0]}</a>}
<span>总字数:{wordCountText}</span>
<span>阅读时长:{readingMinutes} 分钟</span>
</div>
) : (
<div class="meta-list">
{post.categories.map((category) => <a href={`/categories/${encodeURIComponent(category)}/`}>{category}</a>)}
{post.tags.map((tag) => <a href={`/tags/${encodeURIComponent(tag)}/`}>#{tag}</a>)}
</div>
)}
</div>
</header>
{post.summary && (
<section class="article-summary-card" aria-label="AI 文章摘要">
<div class="article-summary-dots" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<p class="article-summary-content">
<span class="article-summary-text" data-summary-text={post.summary}>{post.summary}</span>
<span class="article-summary-caret" aria-hidden="true"></span>
</p>
<div class="article-summary-footer">
<span class="article-summary-brand">
<span aria-hidden="true">✣</span>
清羽のAI摘要
</span>
<span class="article-summary-model">KIMI-K2.5</span>
</div>
</section>
)}
<div class="prose" data-toc={JSON.stringify(toc)}>
<Content />
</div>
<PostCopyrightCard post={post} />
</article>
<ArticleSidebar post={post} toc={toc} relatedPosts={relatedPosts} />
</section>
</BaseLayout>
<script is:inline>
(() => {
const prose = document.querySelector('.prose[data-toc]');
if (!prose) return;
let toc = [];
try {
toc = JSON.parse(prose.dataset.toc || '[]');
} catch {
toc = [];
}
prose.querySelectorAll('h1, h2, h3').forEach((heading, index) => {
const item = toc[index];
if (item?.id && !heading.id) heading.id = item.id;
});
const summaryText = document.querySelector('.article-summary-text');
if (!summaryText) return;
const fullText = summaryText.dataset.summaryText || summaryText.textContent || '';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion) {
summaryText.textContent = fullText;
return;
}
summaryText.textContent = '';
let index = 0;
function typeSummary() {
index += 1;
summaryText.textContent = fullText.slice(0, index);
if (index < fullText.length) {
window.setTimeout(typeSummary, 34);
}
}
window.setTimeout(typeSummary, 280);
})();
</script>