feat(cms): 首页四项可配 + 开源页项目可见性 + GitHub README 开篇
- 首页:subtitle(markdown 短文本)|latestPhotos 0-6(0 隐藏)|
perTrackLimit 4-8|sections{tracks,photos,heatmap,projects} 开关,
访问器 homeConfig() 夹取越界值,字段级回落默认(零配置=原形态)
- 开源页项目可见性:mode all|only|exclude + include[]/exclude[] +
sort pinned-first|stars|updated + perSource both|github|gitea;
projects.ts 加 LoadProjectsOptions + arrange()(来源→可见性→排序)
- 开源页开篇支持 GitHub README:intro_from off|github-readme,构建期
取 wait4xx/wait4xx README(token 复用宿主 0600 文件)→ 缓存
content/.github-readme.md 入仓 → renderMarkdown;失败逐级回落
(缓存 → yaml blurb)。实测取到 3117 字符,现状仍 off 待用户开启
- 修既有 bug:orderPinned 内硬 slice(0,6) 把开源页 limit=99 也截到 6 条
→ 移除,开源页恢复全量 16 条
- 文档:写作指南加首页/项目墙两张字段表(含 YAML 1.1 `off` 需加引号坑、
README 行为与外部徽章风险说明)
车道:op-gitea(3/3);收编复验:构建 48 页 + E2E 37/37 + 开源页 16 条 +
首页默认形态(2 轨/热力图/项目墙)无回归
验证: node v26.13.0 构建 48 页 exit 0;E2E 37/37 PASS;playwright 实测
Spec: none (首页与开源页后台扩展,车道级交付)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
8d221ab36e
commit
859cac19a2
11 files changed
+387
-26
No files matched your search
@@ -131,3 +131,127 @@ export async function aboutIntroMarkdown(locale: Locale): Promise<string> {
|
||||
const entry = await getEntry('siteAbout', 'about');
|
||||
return pickBi(entry?.data.intro, SITE_DEFAULTS.aboutIntro, locale);
|
||||
}
|
||||
|
||||
// ---- 车道「首页+开源页后台扩展」:首页四项可配 + 项目墙可见性 + README 开篇 ----
|
||||
|
||||
export interface HomeSections {
|
||||
tracks: boolean;
|
||||
photos: boolean;
|
||||
heatmap: boolean;
|
||||
projects: boolean;
|
||||
}
|
||||
|
||||
export interface HomeConfig {
|
||||
/** 副标题 markdown 原文;空串 = 不渲染 */
|
||||
subtitle: string;
|
||||
/** 最新影像张数:0 = 隐藏区块 */
|
||||
latestPhotos: number;
|
||||
/** 每轨文章条数 */
|
||||
perTrackLimit: number;
|
||||
sections: HomeSections;
|
||||
}
|
||||
|
||||
const clampInt = (value: number | undefined, min: number, max: number, fallback: number): number => {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.trunc(value)));
|
||||
};
|
||||
|
||||
/** 首页可配项(字段级回落:零配置 = 现有形态;越界值夹取而非报错) */
|
||||
export async function homeConfig(locale: Locale): Promise<HomeConfig> {
|
||||
const entry = await getEntry('siteHome', 'home');
|
||||
const d = entry?.data;
|
||||
return {
|
||||
subtitle: pickBi(d?.subtitle, { zh: '', en: '' }, locale),
|
||||
latestPhotos: clampInt(d?.latestPhotos, 0, 6, 4),
|
||||
perTrackLimit: clampInt(d?.perTrackLimit, 4, 8, 6),
|
||||
sections: {
|
||||
tracks: d?.sections?.tracks ?? true,
|
||||
photos: d?.sections?.photos ?? true,
|
||||
heatmap: d?.sections?.heatmap ?? true,
|
||||
projects: d?.sections?.projects ?? true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type ProjectMode = 'all' | 'only' | 'exclude';
|
||||
export type ProjectSort = 'pinned-first' | 'stars' | 'updated';
|
||||
export type ProjectSource = 'both' | 'github' | 'gitea';
|
||||
|
||||
export interface OpenSourceOptions {
|
||||
mode: ProjectMode;
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
sort: ProjectSort;
|
||||
perSource: ProjectSource;
|
||||
introFrom: 'off' | 'github-readme';
|
||||
}
|
||||
|
||||
/** 开源页项目墙选项(缺省 = 现状:all / pinned-first / 双源) */
|
||||
export async function openSourceOptions(): Promise<OpenSourceOptions> {
|
||||
const entry = await getEntry('siteOpenSource', 'opensource');
|
||||
const d = entry?.data;
|
||||
return {
|
||||
mode: d?.mode ?? 'all',
|
||||
include: (d?.include ?? []).filter((n) => n.trim()),
|
||||
exclude: (d?.exclude ?? []).filter((n) => n.trim()),
|
||||
sort: d?.sort ?? 'pinned-first',
|
||||
perSource: d?.perSource ?? 'both',
|
||||
introFrom: d?.intro_from ?? 'off',
|
||||
};
|
||||
}
|
||||
|
||||
/** GitHub profile README 缓存:取不到网就吃缓存,都无则调用方回落 yaml blurb */
|
||||
const README_CACHE = 'content/.github-readme.md';
|
||||
const PROFILE_REPO = 'wait4xx/wait4xx';
|
||||
|
||||
/**
|
||||
* 构建期取 profile 仓 README(Accept: raw),成功后回写 content/.github-readme.md(入仓缓存)
|
||||
* @returns README markdown;网络/权限/仓无 README 时回落缓存,仍无 → null
|
||||
*/
|
||||
export async function githubReadmeMarkdown(): Promise<string | null> {
|
||||
const { readFileSync, writeFileSync, existsSync } = await import('node:fs');
|
||||
const { join } = await import('node:path');
|
||||
const cachePath = join(process.cwd(), README_CACHE);
|
||||
const readCache = (): string | null => {
|
||||
try {
|
||||
return existsSync(cachePath) ? readFileSync(cachePath, 'utf8') : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
let token: string | null = null;
|
||||
try {
|
||||
token = readFileSync(join(process.env.HOME ?? '', 'docker_apps/blog/github-token.txt'), 'utf8').trim() || null;
|
||||
} catch {
|
||||
token = null;
|
||||
}
|
||||
if (!token) {
|
||||
console.warn('[opensource] github-readme:token 缺位,用缓存/回落 yaml blurb');
|
||||
return readCache();
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${PROFILE_REPO}/readme`, {
|
||||
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.raw', 'User-Agent': 'wait4x-blog-build' },
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn(`[opensource] github-readme:${PROFILE_REPO} 取 README 失败 HTTP ${res.status},用缓存/回落`);
|
||||
return readCache();
|
||||
}
|
||||
const md = (await res.text()).trim();
|
||||
if (!md) {
|
||||
console.warn('[opensource] github-readme:README 为空,用缓存/回落');
|
||||
return readCache();
|
||||
}
|
||||
const cached = readCache();
|
||||
if (cached !== md) {
|
||||
writeFileSync(cachePath, `${md}\n`, 'utf8');
|
||||
console.log(`[opensource] github-readme:已刷新缓存 ${README_CACHE}(${md.length} 字符)`);
|
||||
}
|
||||
return md;
|
||||
} catch (err) {
|
||||
console.warn(`[opensource] github-readme:请求异常(${(err as Error).message}),用缓存/回落`);
|
||||
return readCache();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user