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:
1 parent
63c9c8ad4a
commit
3e377adc7f
8 files changed
+415
-21
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user