- 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
91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
// 项目墙数据(极客层 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);
|
||
}
|