大 更 新

This commit is contained in:
RiseForever
2026-08-12 01:11:32 +08:00
parent 6570961102
commit c382cd642a
69 changed files with 19471 additions and 16 deletions
View File
+8
View File
@@ -0,0 +1,8 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
---
<BaseLayout title="页面未找到" noindex>
<section class="content-shell flex min-h-[55vh] flex-col items-center justify-center py-16 text-center" data-pagefind-ignore="all">
<p class="m-0 font-bold text-mirages-accent">404</p><h1 class="mt-2 mb-3 text-[2rem] leading-tight font-light tracking-[0]">页面未找到</h1><span class="mb-6 text-subtle">地址可能已经改变,或者这个页面已经不存在了。</span><a class="rounded-[4px] bg-mirages-accent px-6 py-2 text-sm font-bold text-white no-underline hover:text-white" href="/">返回首页</a>
</section>
</BaseLayout>
+37
View File
@@ -0,0 +1,37 @@
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import TableOfContents from '@/components/TableOfContents.astro';
import Mermaid from '@/components/Mermaid.astro';
import ImageLightbox from '@/components/ImageLightbox.astro';
import { getContentCacheKey } from '@/lib/content';
export async function getStaticPaths() {
const reservedRoutes = new Set(['archives', 'categories', 'links', 'page', 'posts', 'search', 'tags']);
const pages = await getCollection('pages', ({ data }) => !data.draft);
return pages.flatMap((page) => {
const slug = (page.data.slug ?? page.id).replace(/\\/g, '/').replace(/\.(md|mdx)$/i, '').replace(/^\/+|\/+$/g, '');
return !slug || reservedRoutes.has(slug.split('/')[0]) ? [] : [{ params: { slug }, props: { page }, cacheKey: getContentCacheKey('page', [page]) }];
});
}
const { page } = Astro.props;
const { Content, headings } = await render(page);
const toc = typeof page.data.toc === 'boolean' ? { enabled: page.data.toc, position: 'right' as const } : page.data.toc;
const slug = (page.data.slug ?? page.id).replace(/\\/g, '/').replace(/\.(md|mdx)$/i, '').replace(/^\/+|\/+$/g, '');
const isAbout = slug === 'about';
---
<BaseLayout title={page.data.title} description={page.data.description} image={page.data.hero ?? page.data.cover} math={page.data.math} navbarOverlay={!isAbout && Boolean(page.data.hero ?? page.data.cover)}>
<Fragment slot="masthead">
<Masthead
about={isAbout}
textTone={page.data.textTone}
banner={{ title: page.data.title, subtitle: page.data.description, image: page.data.hero ?? page.data.cover, textTone: page.data.textTone }}
/>
</Fragment>
<article class:list={['article-shell', 'relative pt-8 pb-14 md:pt-10 md:pb-20', { 'has-toc': toc.enabled, 'about-page': isAbout }]}>
{toc.enabled && <TableOfContents headings={headings} position={toc.position} />}
<div class="prose" data-pagefind-body><Content /></div>
<ImageLightbox />
</article>
<Mermaid enabled={page.data.mermaid} />
</BaseLayout>
+81
View File
@@ -0,0 +1,81 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import WritingHeatmap from '@/components/WritingHeatmap.astro';
import { deriveTaxonomy, getArchiveYears, getPostPath, getPublicPosts, slugify } from '@/lib/content';
import { siteConfig } from '@/site.config';
const posts = await getPublicPosts();
const archives = getArchiveYears(posts);
const tagLimit = siteConfig.archives.tagLimit;
const tags = deriveTaxonomy(posts, 'tags')
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
const visibleTags = tagLimit === 0 ? tags : tags.slice(0, Math.max(0, tagLimit));
---
<BaseLayout title="归档" description="按日期和主题整理的文章。">
<Fragment slot="masthead"><Masthead banner={{ title: '归档', subtitle: '按日期和主题整理的文章。',image: 'https://image.luming.cool/i/2026/06/06/6a23f222a1641.webp', textTone: 'auto' }} /></Fragment>
<div class="article-shell archives-shell pt-8 pb-14 md:pt-10 md:pb-20">
<WritingHeatmap posts={posts} />
<section class="archives-tags" aria-labelledby="tag-cloud-title" data-pagefind-ignore="all">
<h2 id="tag-cloud-title">标签云</h2>
<div class="tag-cloud">{visibleTags.map((tag) => <a href={`/tags/${slugify(tag.name)}/`}>{tag.name}</a>)}</div>
</section>
<section class="archives-content" aria-label="文章归档">
{[...archives].map(([year, months]) => <section class="archive-year">
<h2>{year} 年</h2>
{[...months].map(([month, entries]) => <div class="archive-month">
<h3>{String(month).padStart(2, '0')} 月</h3>
<ol>{entries.map((post) => <li><a class="archive-entry" data-archive-entry data-no-text-link href={getPostPath(post)}><time datetime={post.data.pubDate.toISOString()}>{post.data.pubDate.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }).replaceAll('/', '-')}</time><span class="archive-entry-title">{post.data.title}</span></a></li>)}</ol>
</div>)}
</section>)}
</section>
</div>
</BaseLayout>
<style>
.archives-tags { margin-bottom: 3.5rem; }
.archives-tags h2,
.archive-year > h2,
.archive-month h3 { font-weight: 400; }
.archives-tags h2 { margin: 0 0 1rem; font-size: 1.375rem; }
.tag-cloud { display: flex; flex-wrap: wrap; gap: .625rem 1.25rem; font-size: 1rem; line-height: 1.75; }
.tag-cloud a { position: relative; color: var(--color-text); }
.tag-cloud a::after { position: absolute; right: 0; bottom: -.12em; left: 0; border-bottom: 1px solid var(--color-accent); content: ''; transform: scaleX(0); transform-origin: left center; transition: transform 180ms ease-out; }
.tag-cloud a:is(:hover, :focus-visible) { color: var(--color-accent); }
.tag-cloud a:is(:hover, :focus-visible)::after { transform: scaleX(1); }
.archive-year { padding: 1.75rem 0; }
.archive-year:first-child { padding-top: 0; }
.archive-year > h2 { margin: 0 0 1.25rem; color: var(--color-accent); font-size: 1.625rem; }
.archive-month { margin-bottom: 1.5rem; }
.archive-month h3 { margin: 0 0 .625rem; font-size: 1.1875rem; }
.archive-month ol { margin: 0; padding: 0; list-style: none; }
.archive-entry {
display: grid;
grid-template-columns: 64px minmax(0, 1fr);
gap: 1rem;
align-items: baseline;
padding: .5rem .75rem;
border-radius: var(--radius-sm);
color: var(--color-text);
font-size: 1.0625rem;
line-height: 1.75;
transition: background-color var(--transition-fast);
}
.archive-entry:hover,
.archive-entry:focus-visible {
background-color: var(--color-archive-hover);
}
.archive-entry time {
color: var(--color-muted);
font-size: .9375rem;
}
@media (max-width: 37.5rem) {
.archives-tags { margin-bottom: 2.75rem; }
.tag-cloud { gap: .5rem 1rem; font-size: .9375rem; }
.archive-year > h2 { font-size: 1.5rem; }
.archive-month h3 { font-size: 1.125rem; }
.archive-entry { grid-template-columns: 58px minmax(0, 1fr); gap: .75rem; padding-inline: .5rem; font-size: 1rem; }
}
</style>
@@ -0,0 +1,22 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import PostList from '@/components/PostList.astro';
import { deriveTaxonomy, getContentCacheKey, getPublicPosts, getTaxonomyPosts, POSTS_PER_PAGE, slugify } from '@/lib/content';
import { categoryBanners, resolveTaxonomyBanner } from '@/category-banner.config';
export async function getStaticPaths() {
const posts = await getPublicPosts();
return deriveTaxonomy(posts, 'categories').map(({ name }) => {
const category = slugify(name);
const entries = getTaxonomyPosts(posts, 'categories', category);
return { params: { category }, props: { name, posts: entries }, cacheKey: getContentCacheKey(`category:${category}`, entries) };
});
}
const { name, posts } = Astro.props;
const total = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE));
const banner = resolveTaxonomyBanner(categoryBanners, slugify(name), { title: name, subtitle: `归入“${name}”分类的文章。` });
---
<BaseLayout title={`分类:${name}`} description={`归入“${name}”分类的文章。`} navbarOverlay={Boolean(banner.image)}>
<Fragment slot="masthead"><Masthead banner={banner} textTone={banner.textTone} /></Fragment>
<PostList posts={posts.slice(0, POSTS_PER_PAGE)} total={total} basePath={`/categories/${slugify(name)}`} />
</BaseLayout>
@@ -0,0 +1,21 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import PostList from '@/components/PostList.astro';
import { deriveTaxonomy, getContentCacheKey, getPublicPosts, getTaxonomyPosts, POSTS_PER_PAGE, slugify } from '@/lib/content';
import { categoryBanners, resolveTaxonomyBanner } from '@/category-banner.config';
export async function getStaticPaths() {
const posts = await getPublicPosts();
return deriveTaxonomy(posts, 'categories').flatMap(({ name }) => {
const entries = getTaxonomyPosts(posts, 'categories', slugify(name));
const total = Math.ceil(entries.length / POSTS_PER_PAGE);
return Array.from({ length: Math.max(0, total - 1) }, (_, index) => { const page = index + 2; const category = slugify(name); return { params: { category, page: String(page) }, props: { name, posts: entries.slice((page - 1) * POSTS_PER_PAGE, page * POSTS_PER_PAGE), page, total }, cacheKey: getContentCacheKey(`category:${category}:${page}`, entries) }; });
});
}
const { name, posts, page, total } = Astro.props;
const banner = resolveTaxonomyBanner(categoryBanners, slugify(name), { title: name, subtitle: `归入“${name}”分类的文章。` });
---
<BaseLayout title={`${name} - 第 ${page} 页`} navbarOverlay={Boolean(banner.image)}>
<Fragment slot="masthead"><Masthead banner={banner} textTone={banner.textTone} /></Fragment>
<PostList posts={posts} current={page} total={total} basePath={`/categories/${slugify(name)}`} />
</BaseLayout>
+14
View File
@@ -0,0 +1,14 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import PostList from '@/components/PostList.astro';
import { getPublicPosts, POSTS_PER_PAGE } from '@/lib/content';
import { siteConfig } from '@/site.config';
const posts = await getPublicPosts();
const total = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE));
---
<BaseLayout navbarOverlay={Boolean(siteConfig.banner.image)}>
<Fragment slot="masthead"><Masthead /></Fragment>
<PostList posts={posts.slice(0, POSTS_PER_PAGE)} total={total} basePath="" />
</BaseLayout>
+20
View File
@@ -0,0 +1,20 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import PostList from '@/components/PostList.astro';
import { getContentCacheKey, getPublicPosts, POSTS_PER_PAGE } from '@/lib/content';
import { siteConfig } from '@/site.config';
export async function getStaticPaths() {
const posts = await getPublicPosts();
const total = Math.ceil(posts.length / POSTS_PER_PAGE);
return Array.from({ length: Math.max(0, total - 1) }, (_, index) => {
const page = index + 2;
return { params: { page: String(page) }, props: { posts: posts.slice((page - 1) * POSTS_PER_PAGE, page * POSTS_PER_PAGE), page, total }, cacheKey: getContentCacheKey(`page:${page}`, posts) };
});
}
const { posts, page, total } = Astro.props;
---
<BaseLayout title={`文章 - 第 ${page} 页`} navbarOverlay={Boolean(siteConfig.banner.image)}>
<Fragment slot="masthead"><Masthead /></Fragment>
<PostList posts={posts} current={page} total={total} basePath="" />
</BaseLayout>
+54
View File
@@ -0,0 +1,54 @@
---
import { render } from 'astro:content';
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import { getContentCacheKey, getExcerpt, getPostCardImage, getPostPath, getPostTaxonomy, getPublicPosts, slugify } from '@/lib/content';
import { siteConfig } from '@/site.config';
import TwikooComments from '@/components/TwikooComments.astro';
import TableOfContents from '@/components/TableOfContents.astro';
import Mermaid from '@/components/Mermaid.astro';
import ImageLightbox from '@/components/ImageLightbox.astro';
export async function getStaticPaths() {
const posts = await getPublicPosts();
return posts.map((post, index) => ({
params: { slug: getPostPath(post).replace(/^\/posts\//, '').replace(/\/$/, '') },
props: { post, newer: posts[index - 1], older: posts[index + 1] },
cacheKey: getContentCacheKey('post', [post, posts[index - 1], posts[index + 1]].filter(Boolean))
}));
}
const { post, newer, older } = Astro.props;
const { Content, headings } = await render(post);
const cover = getPostCardImage(post, siteConfig.cards.defaultCovers, siteConfig.site.url);
const toc = typeof post.data.toc === 'boolean' ? { enabled: post.data.toc, position: 'right' as const } : post.data.toc;
const taxonomy = getPostTaxonomy(post);
const tags = taxonomy.tags;
const date = {
year: String(post.data.pubDate.getFullYear()),
month: String(post.data.pubDate.getMonth() + 1).padStart(2, '0'),
day: String(post.data.pubDate.getDate()).padStart(2, '0')
};
const dateText = `${date.year} 年 ${date.month} 月 ${date.day} 日`;
---
<BaseLayout title={post.data.title} description={getExcerpt(post)} image={cover} type="article" math={post.data.math} navbarOverlay={Boolean(cover)}>
<Fragment slot="masthead"><Masthead article={{ title: post.data.title, date, categories: taxonomy.categories, image: cover, textTone: post.data.textTone }} /></Fragment>
<article class:list={['article-shell', 'relative pt-8 pb-14 md:pt-10 md:pb-20', { 'has-toc': toc.enabled, [`toc-${toc.position}`]: toc.enabled }]}>
<div class="sr-only" aria-hidden="true">
<time data-pagefind-meta="date" datetime={post.data.pubDate.toISOString()}>{dateText}</time>
{taxonomy.categories.length > 0 && <span data-pagefind-meta="categories">{taxonomy.categories.join(', ')}</span>}
{tags.length > 0 && <span data-pagefind-meta="tags">{tags.join(', ')}</span>}
{cover && <span data-pagefind-meta="cover">{cover}</span>}
</div>
{toc.enabled && <TableOfContents headings={headings} position={toc.position} />}
<div class="prose" data-pagefind-body><Content /></div>
<ImageLightbox />
{tags.length > 0 && <div class="mt-10 flex flex-wrap justify-center gap-2 border-t border-line pt-5 text-sm" aria-label="标签">
{tags.map((tag) => <a class="rounded-[3px] bg-[var(--color-post-tag-bg)] px-2.5 py-1 no-underline" href={`/tags/${slugify(tag)}/`}>#{tag}</a>)}
</div>}
<nav class="mt-10 grid grid-cols-1 overflow-hidden rounded-[4px] border border-line bg-raised sm:grid-cols-2" aria-label="相邻文章" data-pagefind-ignore="all">
{newer ? <a class="grid min-h-24 content-center p-4 no-underline sm:border-r sm:border-line" href={getPostPath(newer)}><span class="text-xs text-subtle">下一篇</span><strong class="mt-1 font-sans text-base font-normal">{newer.data.title}</strong></a> : <span class="grid min-h-24 content-center p-4 sm:border-r sm:border-line"><span class="text-xs text-subtle">下一篇</span><strong class="mt-1 font-sans text-base font-normal">没有更多了</strong></span>}
{older ? <a class="grid min-h-24 content-center border-t border-line p-4 no-underline sm:border-t-0 sm:text-right" href={getPostPath(older)}><span class="text-xs text-subtle">上一篇</span><strong class="mt-1 font-sans text-base font-normal">{older.data.title}</strong></a> : <span class="grid min-h-24 content-center border-t border-line p-4 sm:border-t-0 sm:text-right"><span class="text-xs text-subtle">上一篇</span><strong class="mt-1 font-sans text-base font-normal">没有更多了</strong></span>}
</nav>
<TwikooComments enabled={post.data.comments} path={Astro.url.pathname} />
</article>
<Mermaid enabled={post.data.mermaid} />
</BaseLayout>
+3
View File
@@ -0,0 +1,3 @@
import type { APIRoute } from 'astro';
export const GET: APIRoute = ({ site }) => new Response(`User-agent: *\nAllow: /\nSitemap: ${new URL('sitemap-index.xml', site)}`);
+20
View File
@@ -0,0 +1,20 @@
import rss from '@astrojs/rss';
import { siteConfig } from '@/site.config';
import { getExcerpt, getPostPath, getPublicPosts } from '@/lib/content';
import { renderMarkdown } from '@/lib/shortcodes';
export async function GET(context) {
const posts = (await getPublicPosts()).slice(0, 10);
return rss({
title: siteConfig.site.title,
description: siteConfig.site.description,
site: context.site,
items: posts.map((post) => ({
title: post.data.title,
description: getExcerpt(post),
content: renderMarkdown(post.body ?? ''),
pubDate: post.data.pubDate,
link: getPostPath(post)
}))
});
}
+110
View File
@@ -0,0 +1,110 @@
---
import { Search as SearchIcon } from '@lucide/astro';
import BaseLayout from '@/layouts/BaseLayout.astro';
const query = Astro.url.searchParams.get('q')?.trim() ?? '';
---
<BaseLayout title={query ? `搜索:${query}` : '搜索'} description="搜索 Mirages 中的文章。" noindex>
<section class="mx-auto w-full px-[48px] pt-16 pb-14 max-[336px]:px-5 md:max-w-[720px] md:px-0 md:pt-20 min-[1302px]:max-w-[864px] min-[1600px]:max-w-[896px] min-[1800px]:max-w-[960px] min-[2000px]:max-w-[992px] min-[2400px]:max-w-[1024px]" data-search-page>
<header class="mb-8 border-b border-line pb-4">
<h1 class="m-0 text-xl leading-tight font-normal">搜索结果</h1>
<p class="mt-2 mb-0 text-sm text-subtle" data-search-query>{query ? `查询词:${query}` : '请输入查询词开始搜索。'}</p>
</header>
<form class="mb-8 flex items-center border-b border-line text-current" action={`${import.meta.env.BASE_URL}search/`} method="get" role="search">
<SearchIcon class="shrink-0 text-subtle" aria-hidden="true" size={17} />
<input class="min-w-0 flex-1 border-0 bg-transparent px-2 py-2 text-sm text-current outline-none focus:border-0 focus:outline-none focus:ring-0" type="search" name="q" value={query} placeholder="搜索文章" required aria-label="搜索文章" />
<button class="border-0 bg-transparent px-2 py-2 text-sm text-mirages-accent hover:bg-raised" type="submit">搜索</button>
</form>
<div data-search-page-results aria-live="polite">
{query ? <p class="px-3 py-4 text-sm text-subtle">正在搜索...</p> : <p class="px-3 py-4 text-sm text-subtle">请输入查询词开始搜索。</p>}
</div>
</section>
</BaseLayout>
<script>
interface PagefindResult { data: () => Promise<{ url: string; meta?: { title?: string; date?: string; category?: string; categories?: string; tags?: string; cover?: string } }>; }
interface PagefindModule { options: (options: { excerptLength: number }) => Promise<void>; search: (query: string) => Promise<{ results: PagefindResult[] }>; }
const root = document.querySelector<HTMLElement>('[data-search-page]');
const results = root?.querySelector<HTMLElement>('[data-search-page-results]');
const queryLabel = root?.querySelector<HTMLElement>('[data-search-query]');
const input = root?.querySelector<HTMLInputElement>('input[name="q"]');
const query = new URLSearchParams(window.location.search).get('q')?.trim() ?? '';
if (query) {
if (queryLabel) queryLabel.textContent = `查询词:${query}`;
if (input) input.value = query;
}
const cardBackgrounds = ['#547a82', '#806b76', '#657657', '#876d56', '#5f718d', '#796f54'];
const cardBackground = (url: string) => {
let hash = 0;
for (const character of url) hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
return cardBackgrounds[hash % cardBackgrounds.length];
};
const run = async () => {
if (!query || !results) return;
if (import.meta.env.DEV) { results.innerHTML = '<p class="px-3 py-4 text-sm text-subtle">搜索功能需在构建并预览后使用。</p>'; return; }
try {
const pagefindUrl = new URL(`${import.meta.env.BASE_URL}pagefind/pagefind.js`, document.baseURI).href;
const pagefind = await import(/* @vite-ignore */ pagefindUrl) as unknown as PagefindModule;
await pagefind.options({ excerptLength: 0 });
const search = await pagefind.search(query);
results.replaceChildren();
if (!search.results.length) {
results.innerHTML = '<p class="px-3 py-4 text-sm text-subtle">没有找到相关内容。</p>';
return;
}
for (const result of search.results) {
const data = await result.data();
const article = document.createElement('article');
article.className = 'py-[0.9375rem] md:py-[0.9375rem]';
const link = document.createElement('a');
link.className = 'group relative block h-50 overflow-hidden rounded-[0.3125rem] bg-[var(--card-background)] text-white no-underline shadow-sm transition-[transform,box-shadow] duration-200 ease-out hover:-translate-y-0.5 hover:scale-[1.005] hover:text-white hover:shadow-mirages max-[21rem]:h-42 md:h-62';
link.href = data.url;
link.style.setProperty('--card-background', cardBackground(data.url));
if (data.meta?.cover) {
const image = document.createElement('img');
image.className = 'absolute inset-0 size-full object-cover';
image.src = data.meta.cover;
image.alt = '';
image.loading = 'lazy';
link.append(image);
}
const mask = document.createElement('span');
mask.className = 'absolute inset-0 z-10 bg-black/25';
mask.setAttribute('aria-hidden', 'true');
link.append(mask);
const content = document.createElement('div');
content.className = 'absolute inset-0 z-20 flex flex-col items-center justify-center px-4 py-4 text-center md:px-6';
const title = document.createElement('h2');
title.className = 'm-0 max-w-[90%] [overflow-wrap:anywhere] font-sans text-[1.5625rem] leading-tight font-normal tracking-[0]';
title.textContent = data.meta?.title || data.url;
content.append(title);
const date = data.meta?.date;
const categories = data.meta?.categories ?? data.meta?.category;
const tags = data.meta?.tags;
if (date || categories || tags) {
const info = document.createElement('p');
info.className = 'mt-3 mb-0 max-w-[90%] [overflow-wrap:anywhere] text-[0.8125rem] leading-relaxed font-normal text-[#eee] [font-family:Consolas,Menlo,Monaco,"lucida_console","Liberation_Mono","Courier_New","andale_mono",monospaceX,monospace,sans-serif]';
const appendMeta = (value: string | undefined, className?: string) => {
if (!value) return;
if (info.childNodes.length > 0) info.append(' · ');
const item = document.createElement('span');
item.textContent = value;
if (className) item.className = className;
info.append(item);
};
appendMeta(date);
appendMeta(categories, '[font-family:var(--mirages-font-ui)]');
appendMeta(tags);
content.append(info);
}
link.append(content);
article.append(link);
results.append(article);
}
} catch { results.innerHTML = '<p class="px-3 py-4 text-sm text-subtle">搜索暂时不可用。</p>'; }
};
run();
</script>
+18
View File
@@ -0,0 +1,18 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import PostList from '@/components/PostList.astro';
import { deriveTaxonomy, getContentCacheKey, getPublicPosts, getTaxonomyPosts, POSTS_PER_PAGE, slugify } from '@/lib/content';
import { resolveTaxonomyBanner, tagBanners } from '@/category-banner.config';
export async function getStaticPaths() {
const posts = await getPublicPosts();
return deriveTaxonomy(posts, 'tags').map(({ name }) => { const tag = slugify(name); const entries = getTaxonomyPosts(posts, 'tags', tag); return { params: { tag }, props: { name, posts: entries }, cacheKey: getContentCacheKey(`tag:${tag}`, entries) }; });
}
const { name, posts } = Astro.props;
const total = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE));
const banner = resolveTaxonomyBanner(tagBanners, slugify(name), { title: `#${name}`, subtitle: `带有“${name}”标签的文章。` });
---
<BaseLayout title={`标签:${name}`} description={`带有“${name}”标签的文章。`} navbarOverlay={Boolean(banner.image)}>
<Fragment slot="masthead"><Masthead banner={banner} textTone={banner.textTone} /></Fragment>
<PostList posts={posts.slice(0, POSTS_PER_PAGE)} total={total} basePath={`/tags/${slugify(name)}`} />
</BaseLayout>
+21
View File
@@ -0,0 +1,21 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Masthead from '@/components/Masthead.astro';
import PostList from '@/components/PostList.astro';
import { deriveTaxonomy, getContentCacheKey, getPublicPosts, getTaxonomyPosts, POSTS_PER_PAGE, slugify } from '@/lib/content';
import { resolveTaxonomyBanner, tagBanners } from '@/category-banner.config';
export async function getStaticPaths() {
const posts = await getPublicPosts();
return deriveTaxonomy(posts, 'tags').flatMap(({ name }) => {
const entries = getTaxonomyPosts(posts, 'tags', slugify(name));
const total = Math.ceil(entries.length / POSTS_PER_PAGE);
return Array.from({ length: Math.max(0, total - 1) }, (_, index) => { const page = index + 2; const tag = slugify(name); return { params: { tag, page: String(page) }, props: { name, posts: entries.slice((page - 1) * POSTS_PER_PAGE, page * POSTS_PER_PAGE), page, total }, cacheKey: getContentCacheKey(`tag:${tag}:${page}`, entries) }; });
});
}
const { name, posts, page, total } = Astro.props;
const banner = resolveTaxonomyBanner(tagBanners, slugify(name), { title: `#${name}`, subtitle: `带有“${name}”标签的文章。` });
---
<BaseLayout title={`${name} - 第 ${page} 页`} navbarOverlay={Boolean(banner.image)}>
<Fragment slot="masthead"><Masthead banner={banner} textTone={banner.textTone} /></Fragment>
<PostList posts={posts} current={page} total={total} basePath={`/tags/${slugify(name)}`} />
</BaseLayout>