大 更 新
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)}`);
|
||||
@@ -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)
|
||||
}))
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user