Files
blog/src/lib/projects.ts
T
wait4xandGLM-5.3 264148f0a6 feat(cms): 内容管理闭环——站点单例三件套+文章视频字段
- A 站点文案单例:content/site/{home,about,opensource}.yaml +
  src/lib/site-content.ts 访问层(字段级回落默认,默认值=原文案逐字
  一致,零回归);接线主页 tagline(en 首页新增该块)、关于页 intro、
  开源页叙述三段 + PINNED 参数化(projects.ts: loadProjects(pinned)
  /orderPinned,PINNED_DEFAULT 仅缺省兜底)
- loader 语义修正(返工如实入档):Astro 7 的 file() 把 YAML 顶层对象
  按 Object.entries 拆成多条目(id=key、data=value),单例字段必炸
  schema;改 glob 精确到单文件 = 真单例语义,data 即文件根对象,
  Sveltia 可编辑根字段零嵌套(比包 main: 层的方案后台体验更好)
- B 文章媒体:posts +video/+poster;Sveltia file 字段
  (media_folder=public/media、public_folder=/media、hint ≤50MB);
  正文后渲染 video controls preload=metadata playsinline,无则不渲染;
  图片链路核对本已一致(./assets 走 Astro 管线);链接 markdown 原生
- docs/写作指南.md 补「后台上传」节(三类媒体 + 三单例操作 + 体积纪律)

Sveltia 集合:posts / moments / photos / site_pages(三单例)

车道:op-gitea(实测 tagline 改 yaml 产物联动、置空回落默认、pinned
改名排序变、视频临时件渲染全对、构建绿 + lint 45 页 0/0/0);收编
复验:构建 44 页 + E2E 37/37 + 三 yaml 扁平键就位

验证: pnpm astro build 44 页 exit 0;E2E 37/37 PASS;content-lint
      0/0/0;播放器/单例/置顶 playwright 实测全过

Spec: none (内容管理闭环,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 12:42:10 +08:00

91 lines
3.2 KiB
TypeScript
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.
// 项目墙数据(极客层 P0):构建时双源拉取 + 仓内缓存兜底
// 双源 = 自家 gitea(数据自控叙事)+ GitHub(社区 star 真值);置顶清单人工维护
export interface RepoCard {
source: 'gitea' | 'github';
name: string;
description: string;
language: string | null;
stars: number;
url: string;
updatedAt: string;
}
interface RawRepo {
full_name?: string;
name?: string;
description?: string | null;
language?: string | null;
stars_count?: number;
stargazers_count?: number;
html_url?: string;
updated_at?: string;
}
export const GITEA_API = 'https://git.wait4x.online/api/v1/users/wait4x/repos?limit=50';
export const GITHUB_API = 'https://api.github.com/users/wait4xx/repos?per_page=100&sort=updated';
/** 默认置顶(仅在调用方未传 pinned 时生效):现由 content/site/opensource.yaml 的 pinned 供数 */
export const PINNED_DEFAULT: string[] = ['blog'];
async function fetchJson(url: string): Promise<RawRepo[]> {
const res = await fetch(url, {
headers: { Accept: 'application/json', 'User-Agent': 'wait4x-blog-build' },
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new Error(`${url} -> ${res.status}`);
return (await res.json()) as RawRepo[];
}
function toCard(r: RawRepo, source: RepoCard['source']): RepoCard | null {
const name = r.name ?? r.full_name?.split('/')[1];
if (!name || !r.html_url) return null;
return {
source,
name,
description: r.description ?? '',
language: r.language ?? null,
stars: r.stars_count ?? r.stargazers_count ?? 0,
url: r.html_url,
updatedAt: r.updated_at ?? '',
};
}
/**
* 拉双源合并去重(同名优先 gitea 自托管叙事,star 取 GitHub 真值)。
* 任一源失败不抛:置顶项回落仓内缓存 JSON(构建产物绝不被网络单点卡死)。
*/
/**
* 拉取双源并排序(置顶名单由 content/site/opensource.yaml 供给,缺省用 PINNED_DEFAULT)
* @param pinned 置顶仓短名,顺序即展示顺序
*/
export async function loadProjects(pinned: string[] = PINNED_DEFAULT): Promise<RepoCard[]> {
const [gitea, github] = await Promise.allSettled([fetchJson(GITEA_API), fetchJson(GITHUB_API)]);
const byName = new Map<string, RepoCard>();
for (const r of gitea.status === 'fulfilled' ? gitea.value : []) {
const c = toCard(r, 'gitea');
if (c) byName.set(c.name, c);
}
for (const r of github.status === 'fulfilled' ? github.value : []) {
const c = toCard(r, 'github');
if (!c) continue;
const existing = byName.get(c.name);
if (!existing) byName.set(c.name, c);
else if (c.stars > existing.stars) existing.stars = c.stars; // 社区真值覆盖镜像计数
}
if (byName.size === 0) {
const fb = (await import('../data/projects-fallback.json')).default as RepoCard[];
return orderPinned(fb, pinned);
}
return orderPinned([...byName.values()], pinned);
}
function orderPinned(list: RepoCard[], pinned: string[]): RepoCard[] {
return [...list].sort((a, b) => {
const pa = pinned.indexOf(a.name), pb = pinned.indexOf(b.name);
if (pa !== -1 || pb !== -1) return (pa === -1 ? 99 : pa) - (pb === -1 ? 99 : pb);
return b.stars - a.stars;
}).slice(0, 6);
}