feat: 页面模板差异化改进 + 配置优化 + 兼容清理 (#29)
- 首页判定:navigation 第一项 - 模板:page/projects/articles/bookmarks/search-results - bookmarks:update: YYYY-MM-DD | from: git|mtime - articles:RSS 聚合只读条目 + 分类聚合 + 影子写回结构 - projects:repo 卡片 + 可选热力图 + 自动抓取元信息 - 工作流:构建前 sync + schedule 定时刷新 - 移除兼容:config.yml/config.yaml、navigation.yml、home 特例 - 迁移说明:config/update-instructions.md
This commit is contained in:
@@ -64,11 +64,13 @@ test('templates:subgroups(第4层)应可渲染到页面', () => {
|
||||
|
||||
const category = fs.readFileSync(path.join(__dirname, '..', 'templates', 'components', 'category.hbs'), 'utf8');
|
||||
const group = fs.readFileSync(path.join(__dirname, '..', 'templates', 'components', 'group.hbs'), 'utf8');
|
||||
const pageHeader = fs.readFileSync(path.join(__dirname, '..', 'templates', 'components', 'page-header.hbs'), 'utf8');
|
||||
const siteCard = fs.readFileSync(path.join(__dirname, '..', 'templates', 'components', 'site-card.hbs'), 'utf8');
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'templates', 'pages', 'bookmarks.hbs'), 'utf8');
|
||||
|
||||
hbs.registerPartial('category', category);
|
||||
hbs.registerPartial('group', group);
|
||||
hbs.registerPartial('page-header', pageHeader);
|
||||
hbs.registerPartial('site-card', siteCard);
|
||||
|
||||
const tpl = hbs.compile(page);
|
||||
@@ -165,12 +167,12 @@ test('ensureUserConfigInitialized/ensureUserSiteYmlExists:可在空目录初
|
||||
try {
|
||||
fs.mkdirSync('config/_default/pages', { recursive: true });
|
||||
fs.writeFileSync('config/_default/site.yml', 'title: Default\n', 'utf8');
|
||||
fs.writeFileSync('config/_default/pages/home.yml', 'categories: []\n', 'utf8');
|
||||
fs.writeFileSync('config/_default/pages/common.yml', 'categories: []\n', 'utf8');
|
||||
|
||||
const init = ensureUserConfigInitialized();
|
||||
assert.equal(init.initialized, true);
|
||||
assert.ok(fs.existsSync('config/user/site.yml'));
|
||||
assert.ok(fs.existsSync('config/user/pages/home.yml'));
|
||||
assert.ok(fs.existsSync('config/user/pages/common.yml'));
|
||||
|
||||
// 若 site.yml 已存在,应直接返回 true
|
||||
assert.equal(ensureUserSiteYmlExists(), true);
|
||||
|
||||
@@ -21,7 +21,7 @@ test('首页(navigation 第一项)应使用 profile 覆盖 title/subtitle
|
||||
{ id: 'projects', name: '项目', icon: 'fas fa-project-diagram' },
|
||||
],
|
||||
bookmarks: { title: '书签页标题', subtitle: '书签页副标题', template: 'bookmarks', categories: [] },
|
||||
home: { title: 'HOME_PAGE_TITLE', subtitle: 'HOME_PAGE_SUBTITLE', template: 'home', categories: [] },
|
||||
home: { title: 'HOME_PAGE_TITLE', subtitle: 'HOME_PAGE_SUBTITLE', template: 'page', categories: [] },
|
||||
projects: { title: '项目页标题', subtitle: '项目页副标题', template: 'projects', categories: [] },
|
||||
};
|
||||
|
||||
|
||||
295
test/template-diff-phase1.node-test.js
Normal file
295
test/template-diff-phase1.node-test.js
Normal file
@@ -0,0 +1,295 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { loadHandlebarsTemplates, generateAllPagesHTML } = require('../src/generator.js');
|
||||
|
||||
function withRepoRoot(fn) {
|
||||
const originalCwd = process.cwd();
|
||||
process.chdir(path.join(__dirname, '..'));
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
}
|
||||
|
||||
test('friends/articles:应恢复分类展示(扩展仍以 data-* 结构为准)', () => {
|
||||
withRepoRoot(() => {
|
||||
loadHandlebarsTemplates();
|
||||
|
||||
const config = {
|
||||
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
|
||||
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
|
||||
social: [],
|
||||
navigation: [
|
||||
{ id: 'home', name: '首页', icon: 'fas fa-home' },
|
||||
{ id: 'friends', name: '朋友', icon: 'fas fa-users' },
|
||||
{ id: 'articles', name: '文章', icon: 'fas fa-book' },
|
||||
],
|
||||
home: { title: 'HOME', subtitle: 'HOME_SUB', template: 'page', categories: [] },
|
||||
friends: {
|
||||
title: '友情链接',
|
||||
subtitle: '朋友们',
|
||||
template: 'page',
|
||||
categories: [
|
||||
{
|
||||
name: '技术博主',
|
||||
icon: 'fas fa-user-friends',
|
||||
sites: [{ name: 'Example', url: 'https://example.com', icon: 'fas fa-link', description: 'desc' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
articles: {
|
||||
title: '文章',
|
||||
subtitle: '文章入口',
|
||||
template: 'articles',
|
||||
categories: [
|
||||
{
|
||||
name: '最新文章',
|
||||
icon: 'fas fa-pen',
|
||||
sites: [{ name: 'Article A', url: 'https://example.com/a', icon: 'fas fa-link', description: 'summary' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const pages = generateAllPagesHTML(config);
|
||||
|
||||
assert.ok(typeof pages.friends === 'string' && pages.friends.length > 0);
|
||||
assert.ok(pages.friends.includes('page-template-friends'));
|
||||
assert.ok(pages.friends.includes('sites-grid'));
|
||||
assert.ok(pages.friends.includes('class="site-card'), 'friends 应使用普通 site-card 样式(图标在左,标题在右)');
|
||||
assert.ok(!pages.friends.includes('site-card-friend'), 'friends 不应使用 site-card-friend 变体样式');
|
||||
assert.ok(pages.friends.includes('category-header'), 'friends 应输出分类标题结构');
|
||||
|
||||
assert.ok(typeof pages.articles === 'string' && pages.articles.length > 0);
|
||||
assert.ok(pages.articles.includes('page-template-articles'));
|
||||
assert.ok(pages.articles.includes('sites-grid'));
|
||||
assert.ok(pages.articles.includes('class="site-card'), 'articles 应使用普通 site-card 样式(图标在左,标题在右)');
|
||||
assert.ok(!pages.articles.includes('site-card-article'), 'articles 不应使用 site-card-article 变体样式');
|
||||
assert.ok(pages.articles.includes('category-header'), 'articles 应输出分类标题结构');
|
||||
});
|
||||
});
|
||||
|
||||
test('friends/articles:页面配置使用顶层 sites 时应自动映射为分类容器', () => {
|
||||
withRepoRoot(() => {
|
||||
loadHandlebarsTemplates();
|
||||
|
||||
const config = {
|
||||
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
|
||||
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
|
||||
social: [],
|
||||
navigation: [
|
||||
{ id: 'home', name: '首页', icon: 'fas fa-home' },
|
||||
{ id: 'friends', name: '朋友', icon: 'fas fa-users' },
|
||||
{ id: 'articles', name: '文章', icon: 'fas fa-book' },
|
||||
],
|
||||
home: { title: 'HOME', subtitle: 'HOME_SUB', template: 'page', categories: [] },
|
||||
friends: {
|
||||
title: '友情链接',
|
||||
subtitle: '朋友们',
|
||||
template: 'page',
|
||||
sites: [{ name: 'Example', url: 'https://example.com', icon: 'fas fa-link', description: 'desc' }],
|
||||
},
|
||||
articles: {
|
||||
title: '文章',
|
||||
subtitle: '文章入口',
|
||||
template: 'articles',
|
||||
sites: [{ name: 'Article A', url: 'https://example.com/a', icon: 'fas fa-link', description: 'summary' }],
|
||||
},
|
||||
};
|
||||
|
||||
const pages = generateAllPagesHTML(config);
|
||||
|
||||
assert.ok(typeof pages.friends === 'string' && pages.friends.length > 0);
|
||||
assert.ok(pages.friends.includes('page-template-friends'));
|
||||
assert.ok(pages.friends.includes('sites-grid'));
|
||||
assert.ok(pages.friends.includes('class="site-card'), 'friends 应使用普通 site-card 样式(图标在左,标题在右)');
|
||||
assert.ok(!pages.friends.includes('site-card-friend'), 'friends 不应使用 site-card-friend 变体样式');
|
||||
assert.ok(pages.friends.includes('category-header'), 'friends 应输出分类标题结构');
|
||||
|
||||
assert.ok(typeof pages.articles === 'string' && pages.articles.length > 0);
|
||||
assert.ok(pages.articles.includes('page-template-articles'));
|
||||
assert.ok(pages.articles.includes('sites-grid'));
|
||||
assert.ok(pages.articles.includes('class="site-card'), 'articles 应使用普通 site-card 样式(图标在左,标题在右)');
|
||||
assert.ok(!pages.articles.includes('site-card-article'), 'articles 不应使用 site-card-article 变体样式');
|
||||
assert.ok(pages.articles.includes('category-header'), 'articles 应输出分类标题结构');
|
||||
});
|
||||
});
|
||||
|
||||
test('缺少 friends 页面配置时:仍应渲染页面(标题回退为导航名称)', () => {
|
||||
withRepoRoot(() => {
|
||||
loadHandlebarsTemplates();
|
||||
|
||||
const config = {
|
||||
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
|
||||
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
|
||||
social: [],
|
||||
navigation: [
|
||||
{ id: 'home', name: '首页', icon: 'fas fa-home' },
|
||||
{ id: 'friends', name: '朋友', icon: 'fas fa-users' },
|
||||
],
|
||||
home: { title: 'HOME', subtitle: 'HOME_SUB', template: 'page', categories: [] },
|
||||
// 刻意不提供 friends 配置
|
||||
};
|
||||
|
||||
const pages = generateAllPagesHTML(config);
|
||||
const html = pages.friends;
|
||||
|
||||
assert.ok(typeof html === 'string' && html.length > 0);
|
||||
assert.ok(html.includes('page-template-friends'));
|
||||
assert.ok(html.includes('data-editable="page-title"'));
|
||||
assert.ok(html.includes('朋友'));
|
||||
});
|
||||
});
|
||||
|
||||
test('bookmarks:标题区应显示内容更新时间(日期 + 来源)', () => {
|
||||
withRepoRoot(() => {
|
||||
loadHandlebarsTemplates();
|
||||
|
||||
const config = {
|
||||
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
|
||||
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
|
||||
social: [],
|
||||
navigation: [
|
||||
{ id: 'home', name: '首页', icon: 'fas fa-home' },
|
||||
{ id: 'bookmarks', name: '书签', icon: 'fas fa-bookmark' },
|
||||
],
|
||||
home: { title: 'HOME', subtitle: 'HOME_SUB', template: 'page', categories: [] },
|
||||
bookmarks: { title: '书签', subtitle: '书签页', template: 'bookmarks', categories: [] },
|
||||
};
|
||||
|
||||
const pages = generateAllPagesHTML(config);
|
||||
const html = pages.bookmarks;
|
||||
|
||||
assert.ok(typeof html === 'string' && html.length > 0);
|
||||
assert.ok(html.includes('page-updated-inline'));
|
||||
assert.ok(html.includes('update:'), '应显示 update: 前缀');
|
||||
assert.ok(html.includes('from:'), '应显示 from: 前缀');
|
||||
assert.ok(/update:\s*\d{4}-\d{2}-\d{2}/.test(html), '应显示 YYYY-MM-DD 日期');
|
||||
assert.ok(/from:\s*(git|mtime)/.test(html), '应显示来源(git|mtime)');
|
||||
});
|
||||
});
|
||||
|
||||
test('projects:应输出代码仓库风格卡片(site-card-repo)', () => {
|
||||
withRepoRoot(() => {
|
||||
loadHandlebarsTemplates();
|
||||
|
||||
const config = {
|
||||
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
|
||||
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
|
||||
social: [],
|
||||
navigation: [{ id: 'projects', name: '项目', icon: 'fas fa-project-diagram' }],
|
||||
projects: {
|
||||
title: '项目',
|
||||
subtitle: '项目页',
|
||||
template: 'projects',
|
||||
categories: [
|
||||
{
|
||||
name: '项目',
|
||||
icon: 'fas fa-code',
|
||||
sites: [{ name: 'Proj', url: 'https://example.com', icon: 'fas fa-link', description: 'desc' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const pages = generateAllPagesHTML(config);
|
||||
const html = pages.projects;
|
||||
|
||||
assert.ok(typeof html === 'string' && html.length > 0);
|
||||
assert.ok(html.includes('page-template-projects'), 'projects 应包含模板容器 class');
|
||||
assert.ok(html.includes('sites-grid'), 'projects 应包含网格容器(sites-grid)');
|
||||
assert.ok(html.includes('site-card-repo'), 'projects 应包含代码仓库风格卡片类');
|
||||
});
|
||||
});
|
||||
|
||||
test('articles Phase 2:存在 RSS 缓存时渲染文章条目,并隐藏扩展写回结构', () => {
|
||||
withRepoRoot(() => {
|
||||
loadHandlebarsTemplates();
|
||||
|
||||
const previousCacheDir = process.env.RSS_CACHE_DIR;
|
||||
const tmpCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'menav-rss-cache-'));
|
||||
process.env.RSS_CACHE_DIR = tmpCacheDir;
|
||||
|
||||
const cachePath = path.join(tmpCacheDir, 'articles.feed-cache.json');
|
||||
fs.writeFileSync(
|
||||
cachePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
version: '1.0',
|
||||
pageId: 'articles',
|
||||
generatedAt: '2025-12-26T00:00:00.000Z',
|
||||
articles: [
|
||||
{
|
||||
title: 'Article A',
|
||||
url: 'https://example.com/a',
|
||||
summary: 'summary',
|
||||
publishedAt: '2025-12-25T12:00:00.000Z',
|
||||
source: 'Example Blog',
|
||||
sourceUrl: 'https://example.com',
|
||||
icon: 'fas fa-pen'
|
||||
}
|
||||
],
|
||||
stats: { totalArticles: 1 }
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const config = {
|
||||
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
|
||||
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
|
||||
social: [],
|
||||
navigation: [
|
||||
{ id: 'home', name: '首页', icon: 'fas fa-home' },
|
||||
{ id: 'articles', name: '文章', icon: 'fas fa-book' },
|
||||
],
|
||||
home: { title: 'HOME', subtitle: 'HOME_SUB', template: 'page', categories: [] },
|
||||
articles: {
|
||||
title: '文章',
|
||||
subtitle: '文章入口',
|
||||
template: 'articles',
|
||||
categories: [
|
||||
{
|
||||
name: '来源',
|
||||
icon: 'fas fa-pen',
|
||||
sites: [{ name: 'Source A', url: 'https://example.com', icon: 'fas fa-link', description: 'desc' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const pages = generateAllPagesHTML(config);
|
||||
const html = pages.articles;
|
||||
|
||||
assert.ok(typeof html === 'string' && html.length > 0);
|
||||
assert.ok(html.includes('data-type="article"'), '文章条目卡片应为 data-type="article"(只读)');
|
||||
assert.ok(html.includes('site-card-meta'), '文章条目应展示日期/来源元信息');
|
||||
assert.ok(html.includes('Example Blog'));
|
||||
assert.ok(html.includes('2025-12-25'));
|
||||
assert.match(
|
||||
html,
|
||||
/<section class="category category-level-1 category-readonly">[\s\S]*?来源[\s\S]*?Article A[\s\S]*?<\/section>/,
|
||||
'文章条目应按页面配置分类聚合展示'
|
||||
);
|
||||
assert.ok(html.includes('data-extension-shadow="true"'), '应保留隐藏的扩展写回结构');
|
||||
assert.ok(html.includes('data-search-exclude="true"'), '扩展影子结构应排除搜索索引');
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpCacheDir, { recursive: true, force: true });
|
||||
} finally {
|
||||
if (previousCacheDir === undefined) {
|
||||
delete process.env.RSS_CACHE_DIR;
|
||||
} else {
|
||||
process.env.RSS_CACHE_DIR = previousCacheDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user