feat(opensource): 开源页改原生档案卡,个人叙述迁往关于页(5h T5/T6)

用户原话:开源页开篇是从 GitHub README 直接移植的,「部分内容没有居中,有些图片
也没有正常显示,可以自行进行美化,无需完全一致……有些可以直接移植到关于页面」。

病根:README 是 GitHub 专用 markdown——`<div align="center">` 靠 HTML 属性居中
(本站按普通块渲染,故"没居中"),且依赖 7 类外链图(readme-typing-svg /
shields.io / github-readme-stats / streak-stats / activity-graph / snake /
komarev),慢网或被墙时全空白(故"图片不显示")。

T5 开源页重构:
- 新增 geek/GithubProfile.astro:头像 / 名字@handle / 定位叙述 / 社交图标 /
  统计四格(followers、公开仓、总星标)/ 语言占比灰阶条(仅第一名 accent,
  其余 tokens 灰阶)——**零外链图、零 iframe、零第三方徽章**
- 语言聚合复用 loadProjects 的结果;followers 与公开仓数走 GitHub REST
- 任一取数失败只隐藏该项(catch + warn),绝不炸构建
- opensource.astro / en:档案卡承接原 README 位置 → 热力图 → 项目墙;
  README 渲染降级为兼容通道(intro_from=off 时不执行),yaml 注释写明
  7 类外链图与 align 失效原因并标「不推荐」
- **修既有隐患**:GitHub API 请求原先不带 token,匿名 60 次/小时在密集构建下
  被打满 → 项目墙退化成兜底 JSON(今天一度只剩 1 条)。现对 api.github.com
  自动附 Bearer(token 读宿主 0600 文件,不入仓),墙恢复 16 条、总星标 0→66、
  语言占比变真值

T6 关于页承接:
- about.yaml blocks 扩为 prose / tracks(兴趣版图)/ prose(关于我)/
  linklist(找到我:本站·邮件·GitHub·自建 gitea·RSS)/ prose(本站如何构建),
  双语齐备;AboutPage 零改动(blocks 早已驱动)
- 原则落地:关于页讲人,开源页讲开源,两边不再重复

验证: 构建 51 页 exit 0;content-lint 51 页 0/0/0;config-check 全过;
      E2E **46/46 PASS**(37 原有 + 本包新增 9 项:置顶顺序与徽标、文章页计数、
      模板彩色与折叠、瞬间评论区与只读计数、关于页块驱动);
      dist/opensource 七类第三方外链零命中、第三方 img 数 0;数据实值:
      语言占比 Python/TypeScript 各 36%、Shell/Rust/JS 各 9%

Spec: 2026-11-02_blog-5h-content-social (T5/T6)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-02 20:35:56 +08:00
1 parent 63c9c8ad4a
commit 3e377adc7f
8 files changed
+415 -21

No files matched your search

+257
View File
@@ -0,0 +1,257 @@
---
// 开源页原生档案卡(5h T5):零第三方图片、零 iframe
// 数据来源:①content/site/home.yaml 的头像/社交(单一事实源)②content/site/opensource.yaml 的
// 定位叙述(blurb/blurb_after,双语可后台改)③GitHub REST 的 followers/公开仓数(token 在宿主
// 0600 文件,缺位或失败即隐藏该数字)④src/lib/projects.ts 的仓库列表(语言占比与星标汇总)
// 降级纪律:任何一项取数失败只隐藏该项,绝不炸构建、不出现空图片框
import { Image } from 'astro:assets';
import { homeAvatar, homeSocials, openSourceCopy } from '../../lib/site-content';
import { githubUserStats, loadProjects } from '../../lib/projects';
import { iconPath } from '../../lib/icons';
import type { Locale } from '../../lib/locale';
interface Props {
locale?: Locale;
}
const { locale = 'zh' } = Astro.props;
const avatar = await homeAvatar();
const socials = await homeSocials();
const copy = await openSourceCopy(locale);
// 仓库数据:档案卡只关心"语言占比 + 星标汇总",取全量(不受项目墙可见性影响)
let langs: { name: string; count: number; pct: number }[] = [];
let totalStars: number | null = null;
try {
const repos = await loadProjects({ mode: 'all', sort: 'stars' });
if (repos.length) {
totalStars = repos.reduce((sum, r) => sum + (r.stars ?? 0), 0);
const tally = new Map<string, number>();
for (const r of repos) {
if (!r.language) continue;
tally.set(r.language, (tally.get(r.language) ?? 0) + 1);
}
const total = [...tally.values()].reduce((a, b) => a + b, 0);
langs = [...tally.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([name, count]) => ({
name,
count,
pct: total ? Math.round((count / total) * 100) : 0,
}));
}
} catch {
console.warn('[profile] 仓库数据不可用,语言占比与星标汇总已隐藏');
}
const stats = await githubUserStats();
const L = {
zh: { followers: '关注者', repos: '公开仓库', stars: '总星标', langs: '语言占比' },
en: { followers: 'Followers', repos: 'Public repos', stars: 'Total stars', langs: 'Languages' },
}[locale];
---
<section class="profile" aria-label={locale === 'zh' ? 'GitHub 档案' : 'GitHub profile'}>
<div class="who">
{avatar ? (
<Image class="pf-avatar" src={avatar} alt="wait4x" width={64} height={64} format="webp" quality={84} />
) : (
<span class="pf-avatar pf-avatar-fallback" aria-hidden="true">w4</span>
)}
<div class="who-main">
<p class="pf-name">
wait4x<span class="pf-handle mono">@wait4xx</span>
</p>
<p class="pf-bio">
{copy.blurb}<a class="pf-link" href={copy.repoUrl}>{copy.repoLabel}</a>{copy.blurbAfter}
</p>
<ul class="pf-socials">
{socials.map((s) => (
<li>
<a
class="tap"
href={s.url}
aria-label={s.label}
title={s.label}
rel={s.url.startsWith('http') ? 'noopener noreferrer' : undefined}
>
{iconPath(s.icon) ? (
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d={iconPath(s.icon)} />
</svg>
) : (
<span class="pf-social-text mono">{s.label}</span>
)}
</a>
</li>
))}
</ul>
</div>
</div>
{(stats || totalStars !== null) && (
<dl class="pf-stats mono">
{stats && (
<>
<div><dt>{L.followers}</dt><dd>{stats.followers}</dd></div>
<div><dt>{L.repos}</dt><dd>{stats.publicRepos}</dd></div>
</>
)}
{totalStars !== null && (
<div><dt>{L.stars}</dt><dd>{totalStars}</dd></div>
)}
</dl>
)}
{langs.length > 0 && (
<div class="pf-langs">
<p class="pf-langs-title mono">{L.langs}</p>
<ul>
{langs.map((l, i) => (
<li class:list={['pf-lang', { top: i === 0 }]}>
<span class="pf-lang-name mono">{l.name}</span>
<span class="pf-bar" aria-hidden="true">
<span class="pf-bar-fill" style={`width:${Math.max(l.pct, 2)}%`} />
</span>
<span class="pf-lang-pct mono">{l.pct}%</span>
</li>
))}
</ul>
</div>
)}
</section>
<style>
.profile {
margin: var(--space-3) 0 var(--space-5);
padding: var(--space-4);
border: 1px solid var(--line);
border-radius: 12px;
max-width: var(--measure);
}
.who {
display: flex;
gap: var(--space-3);
align-items: flex-start;
}
.pf-avatar {
flex: none;
width: 64px;
height: 64px;
border-radius: 14px;
object-fit: cover;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.pf-avatar-fallback {
display: inline-flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: 15px;
font-weight: 700;
color: var(--ink-2);
background: var(--bg-soft);
}
.who-main { min-width: 0; }
.pf-name {
font-size: 18px;
font-weight: 700;
color: var(--ink-1);
display: flex;
align-items: baseline;
gap: 0.4rem;
}
.pf-handle { font-size: 12.5px; font-weight: 400; color: var(--ink-4); }
.pf-bio {
margin-top: 0.3rem;
font-size: 14.5px;
line-height: 1.65;
color: var(--ink-2);
}
.pf-link {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 3px;
}
.pf-socials {
display: flex;
gap: var(--space-2);
list-style: none;
margin: var(--space-2) 0 0;
padding: 0;
}
.pf-socials a { color: var(--ink-3); display: inline-flex; }
.pf-socials a:hover { color: var(--accent); }
.pf-social-text { font-size: 12px; }
.pf-stats {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
margin: var(--space-4) 0 0;
padding-top: var(--space-3);
border-top: 1px solid var(--line);
}
.pf-stats div { display: flex; flex-direction: column; gap: 2px; }
.pf-stats dt {
font-size: 11.5px;
color: var(--ink-4);
letter-spacing: 0.04em;
}
.pf-stats dd {
margin: 0;
font-size: 17px;
color: var(--ink-1);
font-variant-numeric: tabular-nums;
}
.pf-langs { margin-top: var(--space-3); }
.pf-langs-title {
font-size: 11.5px;
color: var(--ink-4);
letter-spacing: 0.04em;
margin-bottom: var(--space-2);
}
.pf-langs ul {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.35rem;
max-width: 32rem;
}
.pf-lang {
display: grid;
grid-template-columns: 7.5rem 1fr 3rem;
align-items: center;
gap: var(--space-2);
}
.pf-lang-name { font-size: 12px; color: var(--ink-3); }
.pf-bar {
display: block;
height: 6px;
border-radius: 3px;
background: var(--bg-soft);
overflow: hidden;
}
.pf-bar-fill {
display: block;
height: 100%;
background: var(--ink-3);
border-radius: 3px;
}
/* 灰阶体系:仅第一名用 accent 强调,其余走 ink 灰阶(不沿用 GitHub 彩色语言点) */
.pf-lang.top .pf-bar-fill { background: var(--accent); }
.pf-lang.top .pf-lang-name { color: var(--ink-2); }
.pf-lang-pct {
font-size: 11.5px;
color: var(--ink-4);
text-align: right;
font-variant-numeric: tabular-nums;
}
@media (max-width: 560px) {
.pf-lang { grid-template-columns: 5.5rem 1fr 2.6rem; }
.pf-stats { gap: var(--space-3); }
}
</style>