用户实测:「有的文章同时选择了置顶和隐藏,还会显示并置顶」。
根因:后台「隐藏」开关(hiddenTag)是个假开关——它只写 `hiddenTag: true` 这个
没人读的字段,而全站过滤只认 `tags` 里的 `hidden`。于是隐藏从未生效;
叠加置顶后,这篇还在列表最前面。
- 新增 src/lib/post-visibility.ts:可见性契约单一事实源——`!draft && !hidden &&
!tags.includes('hidden')`(两条通道都认,历史写法兼容)
- postsSchema 增真字段 `hidden`;七处消费点(首页双轨/归档 zh/en/书架/RSS zh/en/
文章页导航)统一走 isVisiblePost
- 后台假开关 hiddenTag 换成真开关 hidden,hint 写清语义:不进列表/RSS/sitemap,
页面本身仍生成(链接可分享);与置顶同时打开时以隐藏为准
- sitemap 过滤隐藏文章(astro.config 配置期扫 frontmatter,两种写法都认;
内容目录不可读时宁可不排除也不炸构建)
贡献数据去重(5j T2,回答用户「连续贡献是否实时」):
- 连续贡献不再构建期抓取 streak.svg 快照(不实时、暗色需滤镜、与热力图同源重复),
改为热力图同一份 GraphQL 日历本地计算(src/lib/heatmap.ts streakStats):
当前连续(以最后有贡献日为终点)/ 最长 / 总贡献,界面上标注「数据更新于 X」
- 贡献蛇块与自托管 snake/streak SVG 删除,fetch 脚本 ASSETS 清空,
build.sh 守卫改为退役说明;开源页数据板整块并入热力图区
验证: 构建 51 页 exit 0;content-lint 52 页 0/0/0;E2E 56/56;
临时两篇「置顶+隐藏」(新字段/旧 tags 各一)实测——首页/归档/书架/
双语首页/RSS 双语/sitemap 全部零命中,页面本身仍生成;
清理后归档首条恢复 writing-template(普通置顶不受影响);
开源页 snake/streak 资产零残留、连续统计条与更新时间标注就位
Spec: 2026-11-03_blog-5j-content-refine (T2 + 隐藏 bug 修复)
Co-authored-by: GLM-5.3
218 lines
7.2 KiB
Plaintext
218 lines
7.2 KiB
Plaintext
---
|
||
// 首页(zh/en 共用):名片区 / 文章列表 / 最新影像 / 热力图 / 项目墙 / 心跳
|
||
// 两个 index.astro 只是薄壳(传 locale),区块逻辑与样式集中在这里,避免双份漂移。
|
||
import { Image } from 'astro:assets';
|
||
import { isVisiblePost } from '../../lib/post-visibility';
|
||
import { getCollection } from 'astro:content';
|
||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||
import IdCard from '../IdCard.astro';
|
||
import PostList from '../PostList.astro';
|
||
import ProjectWall from '../geek/ProjectWall.astro';
|
||
import Heatmap from '../geek/Heatmap.astro';
|
||
import SocialLinks from '../SocialLinks.astro';
|
||
import { t } from '../../i18n/ui';
|
||
import { sortPosts } from '../../lib/post-order';
|
||
import type { Locale } from '../../lib/locale';
|
||
import {
|
||
homeConfig,
|
||
homeTagline,
|
||
pageTypography,
|
||
renderMarkdown,
|
||
renderRich,
|
||
} from '../../lib/site-content';
|
||
import { fetchHeartbeat, agoZh } from '../../lib/heartbeat';
|
||
|
||
interface Props {
|
||
locale?: Locale;
|
||
}
|
||
const { locale = 'zh' } = Astro.props;
|
||
|
||
// 首页可配项(后台主页设置):副标题/影像张数/区块开关/每轨条数/项目墙条数/公告/轨细分
|
||
const cfg = await homeConfig(locale);
|
||
const subtitleHtml = cfg.subtitle ? await renderMarkdown(cfg.subtitle) : '';
|
||
const announcementHtml = cfg.announcement ? await renderRich(cfg.announcement) : '';
|
||
|
||
// 置顶优先 → 时间倒序(sortPosts 契约见 src/lib/post-order.ts)
|
||
const posts = sortPosts(
|
||
await getCollection('posts', ({ data }) => isVisiblePost(data)),
|
||
);
|
||
|
||
// en 轨列表标题吃 postsEn 译题(缺译回退 zh,与 /en/archives 同策略)
|
||
const enTitle = locale === 'en'
|
||
? Object.fromEntries((await getCollection('postsEn')).map((e) => [e.id, e.data.title]))
|
||
: undefined;
|
||
|
||
// 最新影像取集合前 N 张;照片时间序由照片管线治理,此处只裁切
|
||
const photos = (await getCollection('photos')).slice(0, Math.max(1, cfg.latestPhotos));
|
||
|
||
const tagline = await homeTagline(locale);
|
||
const taglineHtml = await renderRich(tagline, { inline: true });
|
||
const typo = await pageTypography('siteHome', 'home');
|
||
// 心跳只出现在中文首页(en 轨历史形态如此,保持零回归)
|
||
const heartbeat = locale === 'zh' ? await fetchHeartbeat() : null;
|
||
const galleryHref = locale === 'en' ? '/en/gallery/' : '/gallery/';
|
||
|
||
// 轨细分开关:只保留开启的轨道(全开时等价于不过滤,零回归)
|
||
const activeTracks = Object.entries(cfg.postsTracks)
|
||
.filter(([, on]) => on)
|
||
.map(([k]) => k);
|
||
const allTracksOn = activeTracks.length === Object.keys(cfg.postsTracks).length;
|
||
const visiblePosts = allTracksOn
|
||
? posts
|
||
: posts.filter((p) => activeTracks.includes(p.data.category));
|
||
---
|
||
|
||
<BaseLayout
|
||
title="wait4x"
|
||
description={locale === 'en' ? "wait4x's blog: reading · code · photography" : undefined}
|
||
>
|
||
<div class={typo.className || undefined} style={typo.style || undefined}>
|
||
<IdCard>
|
||
<p class="tagline rich" set:html={taglineHtml} />
|
||
{subtitleHtml && <div class="subtitle" set:html={subtitleHtml} />}
|
||
<SocialLinks />
|
||
</IdCard>
|
||
|
||
{announcementHtml && (
|
||
<div class="announcement rich" set:html={announcementHtml} aria-label={locale === 'en' ? 'Announcement' : '公告'} />
|
||
)}
|
||
|
||
{cfg.sections.tracks && (
|
||
<PostList
|
||
posts={visiblePosts}
|
||
locale={locale}
|
||
titleOverrides={enTitle}
|
||
perTrack={cfg.perTrackLimit}
|
||
/>
|
||
)}
|
||
|
||
{cfg.sections.photos && cfg.latestPhotos > 0 && (
|
||
<section class="latest" aria-label={t('latest_photos', locale)}>
|
||
<div class="latest-head">
|
||
<span class="lab">{t('latest_photos', locale)}</span>
|
||
<a class="tap more" href={galleryHref}>{t('enter_gallery', locale)}</a>
|
||
</div>
|
||
<div class="thumbs">
|
||
{photos.map((photo, i) => (
|
||
<a class="thumb" href={galleryHref} aria-label={`${t('latest_photos', locale)}: ${photo.data.title}`}>
|
||
{/* 首两图 eager(Astro Image 默认 lazy 会把 LCP 压到 6s+),首图再拔 fetchpriority */}
|
||
<Image
|
||
src={photo.data.image}
|
||
alt={photo.data.alt ?? photo.data.title}
|
||
widths={[180, 360]}
|
||
sizes="(min-width: 820px) 20vw, 42vw"
|
||
loading={i < 2 ? 'eager' : 'lazy'}
|
||
fetchpriority={i === 0 ? 'high' : undefined}
|
||
/>
|
||
</a>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{cfg.sections.heatmap && (
|
||
<Heatmap
|
||
labels={{
|
||
title: t('heatmap_title', locale),
|
||
summary: t('heatmap_summary', locale),
|
||
less: t('heatmap_less', locale),
|
||
more: t('heatmap_more', locale),
|
||
note: t('heatmap_note', locale),
|
||
}}
|
||
/>
|
||
)}
|
||
{cfg.sections.projects && <ProjectWall limit={cfg.projectsLimit} />}
|
||
{heartbeat && (
|
||
<p class="heartbeat" aria-label="最近心跳">
|
||
<span class="beat-dot" aria-hidden="true"></span>
|
||
最近心跳:{agoZh(heartbeat.when)} · {heartbeat.msg}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</BaseLayout>
|
||
|
||
<style>
|
||
.subtitle {
|
||
font-size: 13.5px;
|
||
color: var(--ink-4);
|
||
margin-top: 2px;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
.subtitle :global(p) { margin: 0; }
|
||
.subtitle :global(a) { color: var(--accent); }
|
||
.tagline {
|
||
font-size: 14px;
|
||
color: var(--ink-3);
|
||
margin-top: 4px;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
/* 公告块:独立于区块开关,空则不渲染(markdown 富文本,左缘细线做提示) */
|
||
.announcement {
|
||
margin-top: var(--space-4);
|
||
padding: var(--space-2) 0 var(--space-2) var(--space-3);
|
||
border-left: 2px solid var(--line);
|
||
font-size: 14px;
|
||
line-height: 1.7;
|
||
color: var(--ink-2);
|
||
}
|
||
.announcement :global(p) { margin: 0; }
|
||
.announcement :global(p + p) { margin-top: 0.6em; }
|
||
.announcement :global(a) { color: var(--accent); }
|
||
.heartbeat {
|
||
font-family: var(--font-mono);
|
||
font-size: 12.5px;
|
||
color: var(--ink-3);
|
||
margin-top: var(--space-2);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.beat-dot {
|
||
width: 7px; height: 7px; border-radius: 50%;
|
||
background: var(--ink-2);
|
||
animation: beat 2.4s ease-in-out infinite;
|
||
}
|
||
@keyframes beat { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
|
||
@media (prefers-reduced-motion: reduce) { .beat-dot { animation: none; } }
|
||
.latest { margin-top: var(--space-6); }
|
||
.latest-head {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: var(--space-3);
|
||
}
|
||
.lab {
|
||
font-size: 12.5px;
|
||
color: var(--ink-4);
|
||
letter-spacing: 0.08em;
|
||
}
|
||
.more {
|
||
font-size: 13px;
|
||
color: var(--accent);
|
||
padding-inline: var(--space-2);
|
||
transition: color var(--dur) var(--ease);
|
||
}
|
||
.more:hover { color: var(--ink-1); }
|
||
.thumbs {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: var(--space-2);
|
||
margin-top: var(--space-3);
|
||
}
|
||
.thumb {
|
||
aspect-ratio: 1;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
.thumb :global(img) {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: transform var(--dur) var(--ease);
|
||
}
|
||
.thumb:hover :global(img) { transform: scale(1.03); }
|
||
@media (min-width: 820px) {
|
||
.thumbs { grid-template-columns: repeat(4, 1fr); }
|
||
}
|
||
</style>
|