fix(posts): 隐藏开关是假开关——修「隐藏+置顶仍显示并排第一」;贡献数据去重

用户实测:「有的文章同时选择了置顶和隐藏,还会显示并置顶」。

根因:后台「隐藏」开关(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
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-04 22:00:15 +08:00
1 parent 34a78dacb9
commit 49acb4cbd1
21 files changed
+398 -596

No files matched your search

+1 -77
View File
@@ -5,7 +5,6 @@
// 0600 文件,缺位或失败即隐藏该数字)④src/lib/projects.ts 的仓库列表(语言占比与星标汇总)
// 降级纪律:任何一项取数失败只隐藏该项,绝不炸构建、不出现空图片框
import { Image } from 'astro:assets';
import { existsSync } from 'node:fs';
import TypingLines from './TypingLines.astro';
import { homeAvatar, homeSocials, openSourceCopy, renderRich } from '../../lib/site-content';
import { githubUserStats, loadProjects } from '../../lib/projects';
@@ -71,26 +70,7 @@ const lines = typingLines.length ? typingLines : DEFAULT_TYPING;
// 自托管 SVG 数据卡(public/github-assets/,由 scripts/build-perf/fetch-readme-assets.mjs 产出入仓)
// 缺文件即跳过该卡(异步抓取失败不影响构建),明暗各一份时用 CSS 切换
const assetPath = (name: string) => `/github-assets/${name}`;
const hasAsset = (name: string) => existsSync(`public/github-assets/${name}`);
const boardCards: { label: string; light: string; dark?: string }[] = [];
for (const [key, label] of [
['stats', locale === 'zh' ? '统计' : 'Stats'],
['top-langs', locale === 'zh' ? '语言分布' : 'Top languages'],
['streak', locale === 'zh' ? '连续贡献' : 'Streak'],
] as const) {
if (!hasAsset(`${key}-light.svg`)) continue;
boardCards.push({
label,
light: assetPath(`${key}-light.svg`),
dark: hasAsset(`${key}-dark.svg`) ? assetPath(`${key}-dark.svg`) : undefined,
});
}
// 只有一张非通栏卡时让它占满整行(否则右半空档;stats/top-langs 上游 503 时就是这种情形)
const soloCard = boardCards.length === 1;
const snakeCard = hasAsset('snake.svg')
? { label: locale === 'zh' ? '贡献蛇' : 'Contribution snake', src: assetPath('snake.svg') }
: null;
// 数据板已并入热力图区(Phase 5j T2):连续贡献由同源计算 + 更新时间标注,不再使用第三方 SVG
const L = {
zh: { followers: '关注者', repos: '公开仓库', stars: '总星标', langs: '语言占比' },
en: { followers: 'Followers', repos: 'Public repos', stars: 'Total stars', langs: 'Languages' },
@@ -171,33 +151,6 @@ const L = {
</ul>
</div>
)}
{(boardCards.length > 0 || snakeCard) && (
<div class="pf-board">
<p class="pf-board-title mono">{locale === 'zh' ? '数据板' : 'Data board'}</p>
<div class="pf-board-grid">
{boardCards.map((card) => (
<figure
class:list={[
'pf-card',
{ 'pf-card-streak': card.light.includes('streak'), 'pf-card-solo': soloCard },
]}
>
<img class="pf-card-img only-light" src={card.light} alt={card.label} loading="lazy" decoding="async" />
{card.dark && (
<img class="pf-card-img only-dark" src={card.dark} alt={card.label} loading="lazy" decoding="async" />
)}
<figcaption class="mono">{card.label}</figcaption>
</figure>
))}
{snakeCard && (
<figure class="pf-card pf-card-wide pf-card-snake">
<img class="pf-card-img" src={snakeCard.src} alt={snakeCard.label} loading="lazy" decoding="async" />
<figcaption class="mono">{snakeCard.label}</figcaption>
</figure>
)}
</div>
</div>
)}
</section>
<style>
@@ -331,11 +284,6 @@ const L = {
text-align: right;
font-variant-numeric: tabular-nums;
}
.pf-board {
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid var(--line);
}
.pf-board-title {
font-size: 11.5px;
color: var(--ink-4);
@@ -344,29 +292,6 @@ const L = {
}
/* 自适应列:只有 1 张卡时自动占满整行(避免右半空档),≥2 张时并排;
320px 下限保证车卡内 SVG 不被压到字号过小 */
.pf-board-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: var(--space-2);
}
.pf-card {
margin: 0;
padding: var(--space-2);
border: 1px solid var(--line);
border-radius: 10px;
background: transparent;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.pf-card-wide, .pf-card-solo { grid-column: 1 / -1; }
.pf-card-img { max-width: 100%; height: auto; display: block; }
.pf-card figcaption {
font-size: 11px;
color: var(--ink-4);
letter-spacing: 0.03em;
}
/* 明暗两版自托管 SVG:默认只显示亮色,暗色主题切到暗色版 */
/* 贡献蛇:SVG 内部用自带 CSS 变量(绿阶 + #ebedf0 空格),<img> 无法改写内部配色 →
滤镜归一到站点灰阶;暗色下反相,避免浅灰格子贴在深底上刺眼(想去掉绿→灰只改这一行) */
@@ -378,7 +303,6 @@ const L = {
filter: grayscale(1) invert(1) contrast(1.18) brightness(1.06);
}
.only-dark { display: none; }
:global([data-theme='dark']) .only-light { display: none; }
:global([data-theme='dark']) .only-dark { display: block; }
+21 -1
View File
@@ -1,7 +1,8 @@
---
// 贡献热力图(极客层 P0):纯 CSS grid,零运行时 JS
// 数据聚合见 src/lib/heatmap.ts;灰阶四档直接复用 tokens --ink-1..4(亮暗两套自动跟随)
import { loadHeatmap, levelOf } from '../../lib/heatmap';
import { loadHeatmap, levelOf, streakStats } from '../../lib/heatmap';
import { localeOf } from '../../lib/locale';
// 文案全部由页面层注入(locale 无关挂法),keys 与 ui.ts heatmap_* 一一对应
interface Labels {
@@ -16,6 +17,7 @@ interface Props {
}
const { labels } = Astro.props;
const locale = localeOf(Astro.url.pathname);
let weeks: import('../../lib/heatmap').HeatDay[][] = [];
let total = 0;
try {
@@ -23,12 +25,21 @@ try {
} catch {
// 三级回落已在 lib 内处理;此处再守一层:空网格不破构建
}
// 连续贡献(Phase 5j T2):与热力图同源本地计算 + 标注更新时间(不宣称实时)
const streak = streakStats(weeks.flat());
const updatedOn = new Date().toISOString().slice(0, 10);
const summary = labels.summary.replace('{n}', String(total));
---
<section class="heatmap">
<h2 class="hm-title">{labels.title}</h2>
<!-- 53 列窄栏触屏放不下,容器横滚(格子原生 title 提示,无 JS) -->
<p class="hm-streak mono">
<span class="hm-streak-item"><strong>{streak.current}</strong>{locale === 'en' ? 'day streak' : '天连续'}</span>
<span class="hm-streak-item"><strong>{streak.longest}</strong>{locale === 'en' ? 'longest' : '最长'}</span>
<span class="hm-streak-item"><strong>{streak.total}</strong>{locale === 'en' ? 'total' : '总贡献'}</span>
<span class="hm-streak-note">数据更新于 {updatedOn}</span>
</p>
<div class="hm-scroll" data-anchor-latest>
<div class="hm-grid" role="img" aria-label={summary}>
{weeks.map((week) =>
@@ -91,6 +102,15 @@ const summary = labels.summary.replace('{n}', String(total));
.hm-l2 { background: var(--ink-3); }
.hm-l3 { background: var(--ink-2); }
.hm-l4 { background: var(--ink-1); }
.hm-streak {
display: flex; align-items: baseline; gap: var(--space-4); flex-wrap: wrap;
margin: 0 0 var(--space-2); font-size: 12.5px; color: var(--ink-3);
}
.hm-streak-item strong {
font-size: 20px; color: var(--ink-1); font-variant-numeric: tabular-nums;
margin-right: 4px;
}
.hm-streak-note { color: var(--ink-4); font-size: 11.5px; }
.hm-legend {
display: flex;
align-items: center;
+2 -1
View File
@@ -13,6 +13,7 @@
* 展开/收起是另一个形状的 chevron,避免两个箭头混读
*/
import { getCollection } from 'astro:content';
import { isVisiblePost } from '../../lib/post-visibility';
import { Image } from 'astro:assets';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { t } from '../../i18n/ui';
@@ -30,7 +31,7 @@ const note = await pageNote('books', locale);
const books = sortPosts(
await getCollection(
'posts',
({ data }) => !data.draft && data.category === 'reading' && !data.tags.includes('hidden'),
({ data }) => isVisiblePost(data) && data.category === 'reading',
),
);
+2 -1
View File
@@ -2,6 +2,7 @@
// 首页(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';
@@ -33,7 +34,7 @@ const announcementHtml = cfg.announcement ? await renderRich(cfg.announcement) :
// 置顶优先 → 时间倒序(sortPosts 契约见 src/lib/post-order.ts)
const posts = sortPosts(
await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden')),
await getCollection('posts', ({ data }) => isVisiblePost(data)),
);
// en 轨列表标题吃 postsEn 译题(缺译回退 zh,与 /en/archives 同策略)