Files
blog/src/pages/archives/[...slug].astro
T
wait4xandGLM-5.3 a4388df35e fix(media): 详情页路径全量+prev/next 滤 hidden(op-artalk 车道指正)
- getStaticPaths 恢复只滤 draft:hidden 样张需出静态页(可访问不列出)
- prev/next 从 hidden 过滤后的导航数组取位:真实文章上下篇不链样张

验证: build 23 页;media-sample zh/en 双页产物在;/media/ 绝对引用 ✓;
      首页与 RSS 零 media-sample 泄漏

Spec: 2026-10-25_add-media-and-polish

Co-authored-by: GLM-5.3
2026-10-25 11:53:22 +08:00

281 lines
8.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import type { CollectionEntry } from 'astro:content';
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import ArtalkComments from '../../components/ArtalkComments.astro';
import { trackOf } from '../../content.config';
// URL = /archives/{slug},slug 即文件名,与 Halo 旧结构 1:1(不可改)
export async function getStaticPaths() {
// 路径用全量(hidden 样张也要出静态页);prev/next 导航滤 hidden
// (真实文章的上下篇不链向样张——op-artalk 车道连带提醒的实现)
const all = (await getCollection('posts', ({ data }) => !data.draft)).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
);
const nav = all.filter(({ data }) => !data.tags.includes('hidden'));
return all.map((entry) => {
const i = nav.findIndex((e) => e.id === entry.id);
return {
params: { slug: entry.id },
props: {
entry,
prev: i >= 0 ? (nav[i + 1] ?? null) : null,
next: i > 0 ? (nav[i - 1] ?? null) : null,
},
};
});
}
interface Props {
entry: CollectionEntry<'posts'>;
prev: CollectionEntry<'posts'> | null;
next: CollectionEntry<'posts'> | null;
}
const { entry, prev, next } = Astro.props;
const { Content, headings } = await render(entry);
const isReading = entry.data.category === 'reading';
const date = entry.data.pubDate.toISOString().slice(0, 10);
// 阅读时长:正文汉字数 / 400 向上取整(最低 1 分钟)
const hanzi = entry.body?.match(/[\u4e00-\u9fff]/g)?.length ?? 0;
const minutes = Math.max(1, Math.ceil(hanzi / 400));
const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
---
<BaseLayout title={`${entry.data.title} · wait4x`} track={isReading ? 'reading' : 'tech'}>
{isReading ? (
<article class="post post-reading">
<header class="post-head">
<p class="post-cat">Reading · {trackOf(entry.data.category)}</p>
<h1 class="post-title">{entry.data.title}</h1>
<p class="post-meta">
<i
class="emo"
style={`background:${entry.data.mood ?? 'var(--mood)'}`}
title="情绪色"
aria-hidden="true"
/>
<time datetime={entry.data.pubDate.toISOString()}>{date}</time>
<span aria-hidden="true">·</span>
<span>阅读时长 {minutes} 分钟</span>
</p>
</header>
<div class="prose-reading">
<Content />
</div>
</article>
) : (
<>
<div class="read-progress" id="read-progress" aria-hidden="true"></div>
<article class="post post-tech">
<header class="post-head">
<p class="post-cat">Tech · {trackOf(entry.data.category)}</p>
<h1 class="post-title">{entry.data.title}</h1>
<p class="post-meta">
{entry.data.tags.map((tag) => <code class="tag">{`#${tag}`}</code>)}
<span aria-hidden="true">·</span>
<time datetime={entry.data.pubDate.toISOString()}>{date}</time>
<span aria-hidden="true">·</span>
<span>阅读时长 {minutes} 分钟</span>
</p>
</header>
<div class="tech-grid">
<div class="prose-tech">
<Content />
</div>
{toc.length > 0 && (
<aside class="toc" aria-label="目录">
<p class="toc-h">目录</p>
{toc.map((h) => (
<a class={h.depth === 3 ? 'toc-sub' : undefined} href={`#${h.slug}`}>{h.text}</a>
))}
</aside>
)}
</div>
</article>
</>
)}
<nav class="pn" aria-label="上一篇下一篇">
{prev ? (
<a class="pn-item" href={`/archives/${prev.id}/`}>
<span>← 上一篇</span>
<strong>{prev.data.title}</strong>
</a>
) : (
<span class="pn-item" aria-hidden="true"></span>
)}
{next ? (
<a class="pn-item right" href={`/archives/${next.id}/`}>
<span>下一篇 →</span>
<strong>{next.data.title}</strong>
</a>
) : (
<span class="pn-item right" aria-hidden="true"></span>
)}
</nav>
<ArtalkComments pageKey={`/archives/${entry.id}/`} pageTitle={entry.data.title} />
</BaseLayout>
<script>
// 阅读进度条(技术轨):ClientRouter 下模块脚本只执行一次,
// 单例滚动监听 + astro:page-load 重绘,条不在当前页时零开销;
// prefers-reduced-motion 时整条不初始化(CSS 层同步隐藏)
const paint = () => {
const bar = document.getElementById('read-progress');
if (!bar) return;
const el = document.documentElement;
const total = el.scrollHeight - el.clientHeight;
bar.style.transform = `scaleX(${total > 0 ? el.scrollTop / total : 0})`;
};
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
addEventListener('scroll', paint, { passive: true });
document.addEventListener('astro:page-load', paint);
}
</script>
<style>
/* 版心放行:技术轨带 TOC 时加宽到 measure+200(丙路线 §3;BaseLayout 默认 720) */
@media (min-width: 1120px) {
:global(main#main:has(.post-tech)) {
max-width: calc(var(--measure) + 200px);
}
}
.read-progress {
position: fixed;
top: 0;
left: 0;
z-index: 20;
height: 2px;
width: 100%;
background: var(--accent);
transform: scaleX(0);
transform-origin: 0 0;
}
@media (prefers-reduced-motion: reduce) {
.read-progress { display: none; }
}
.post { margin-top: var(--space-5); }
.post-cat {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink-4);
}
.post-title {
font-size: var(--text-h1);
line-height: var(--leading-heading);
color: var(--ink-1);
margin-top: var(--space-2);
text-wrap: balance;
}
.post-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: var(--space-3);
font-size: 14px;
color: var(--ink-3);
}
.emo {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 3px;
flex: none;
}
.tag {
font-family: var(--font-mono);
font-size: 12px;
color: var(--ink-3);
background: var(--bg-soft);
border-radius: 5px;
padding: 1px 7px;
white-space: nowrap;
}
/* 书评轨:Content 渲染的 blockquote 打不了 .quote-block 类,此处镜像同一引文形状 */
.prose-reading { margin-top: var(--space-4); }
.prose-reading :global(blockquote) {
border-left: 2px solid var(--paper-line);
padding-left: 1.35em;
margin-block: 1.2em;
color: color-mix(in oklab, var(--paper-ink) 72%, transparent);
}
/* 技术轨:TOC 侧栏仅 ≥1120 常驻,窄屏隐藏 */
.tech-grid { margin-top: var(--space-4); }
.toc { display: none; }
@media (min-width: 1120px) {
.tech-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 176px;
gap: var(--space-5);
align-items: start;
}
.toc {
display: block;
position: sticky;
top: 80px;
max-height: calc(100dvh - 120px);
overflow: auto;
font-size: 13px;
padding-left: var(--space-3);
border-left: 1px solid var(--line);
}
}
.toc-h {
font-weight: 600;
color: var(--ink-1);
margin-bottom: var(--space-2);
}
.toc a {
display: block;
padding: var(--space-1) 0;
color: var(--ink-3);
transition: color var(--dur) var(--ease);
}
.toc a:hover { color: var(--accent); }
.toc .toc-sub { padding-left: var(--space-3); }
.prose-tech :global(h2),
.prose-tech :global(h3),
.prose-reading :global(h2),
.prose-reading :global(h3) {
scroll-margin-top: 80px;
}
.pn {
display: flex;
justify-content: space-between;
gap: var(--space-4);
margin-top: var(--space-5);
}
.pn-item {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
padding: var(--space-1) var(--space-2);
}
.pn-item.right {
text-align: right;
align-items: flex-end;
}
.pn-item span { font-size: 12px; color: var(--ink-4); }
.pn-item strong {
font-size: 15px;
font-weight: 600;
color: var(--ink-1);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color var(--dur) var(--ease);
}
.pn-item:hover strong { color: var(--accent); }
#artalk-container { margin-top: var(--space-5); }
</style>