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:
rbetree
2025-12-28 00:22:54 +08:00
committed by GitHub
parent 1475a8a0d3
commit 387cd2492e
35 changed files with 2927 additions and 851 deletions

View File

@@ -1,6 +1,7 @@
const fs = require('fs');
const yaml = require('js-yaml');
const path = require('path');
const { execFileSync } = require('child_process');
const Handlebars = require('handlebars');
// 导入Handlebars助手函数
@@ -96,11 +97,11 @@ function renderTemplate(templateName, data, useLayout = true) {
const genericTemplateContent = fs.readFileSync(genericTemplatePath, 'utf8');
const genericTemplate = handlebars.compile(genericTemplateContent);
// 添加 pageId 到数据中,以便通用模板使用
const enhancedData = {
...data,
pageId: templateName // 确保pageId在模板中可用
};
// 添加 pageId 到数据中,以便通用模板使用(优先保留原 pageId避免回退时语义错位
const enhancedData = {
...data,
pageId: data && data.pageId ? data.pageId : templateName
};
// 渲染页面内容
const pageContent = genericTemplate(enhancedData);
@@ -255,17 +256,6 @@ function loadModularConfig(dirPath) {
}
}
// 如果site.yml中没有navigation配置则回退到独立的navigation.yml
if (!config.navigation || config.navigation.length === 0) {
const navConfigPath = path.join(dirPath, 'navigation.yml');
const navConfig = safeLoadYamlConfig(navConfigPath);
if (navConfig) {
config.navigation = navConfig;
console.log('site.yml 中未找到导航配置,使用独立的 navigation.yml 文件');
console.log('提示:建议将导航配置迁移到 site.yml 中,以便统一管理');
}
}
// 加载页面配置
const pagesPath = path.join(dirPath, 'pages');
if (fs.existsSync(pagesPath)) {
@@ -280,11 +270,6 @@ function loadModularConfig(dirPath) {
// 提取文件名(不含扩展名)作为配置键
const configKey = path.basename(file, path.extname(file));
// 特殊处理home.yml中的categories字段
if (configKey === 'home' && fileConfig.categories) {
config.categories = fileConfig.categories;
}
// 将页面配置添加到主配置对象
config[configKey] = fileConfig;
}
@@ -326,7 +311,6 @@ function ensureConfigDefaults(config) {
result.profile = result.profile || {};
result.social = result.social || [];
result.categories = result.categories || [];
// 图标配置默认值
result.icons = result.icons || {};
// icons.mode: manual | favicon, 默认 favicon
@@ -367,17 +351,21 @@ function ensureConfigDefaults(config) {
category.sites.forEach(processSiteDefaults);
}
// 为首页的每个类别和站点设置默认值
result.categories = result.categories || [];
result.categories.forEach(processCategoryDefaults);
// 为所有页面配置中的类别和站点设置默认值
Object.keys(result).forEach(key => {
const pageConfig = result[key];
// 检查是否是页面配置对象且包含categories数组
if (pageConfig && typeof pageConfig === 'object' && Array.isArray(pageConfig.categories)) {
// 检查是否是页面配置对象
if (!pageConfig || typeof pageConfig !== 'object') return;
// 传统结构categories -> sites
if (Array.isArray(pageConfig.categories)) {
pageConfig.categories.forEach(processCategoryDefaults);
}
// 扁平结构sites用于 friends/articles 等“无层级并列卡片”页面)
if (Array.isArray(pageConfig.sites)) {
pageConfig.sites.forEach(processSiteDefaults);
}
});
return result;
@@ -412,30 +400,8 @@ function getSubmenuForNavItem(navItem, config) {
return null;
}
// 首页页面添加子菜单(分类
if (navItem.id === 'home' && Array.isArray(config.categories)) {
return config.categories;
}
// 书签页面添加子菜单(分类)
else if (navItem.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
return config.bookmarks.categories;
}
// 项目页面添加子菜单
else if (navItem.id === 'projects' && config.projects && Array.isArray(config.projects.categories)) {
return config.projects.categories;
}
// 文章页面添加子菜单
else if (navItem.id === 'articles' && config.articles && Array.isArray(config.articles.categories)) {
return config.articles.categories;
}
// 友链页面添加子菜单
else if (navItem.id === 'friends' && config.friends && Array.isArray(config.friends.categories)) {
return config.friends.categories;
}
// 通用处理:任意自定义页面的子菜单生成
else if (config[navItem.id] && config[navItem.id].categories && Array.isArray(config[navItem.id].categories)) {
return config[navItem.id].categories;
}
// 通用处理:任意页面的子菜单生成(基于 pages/<id>.yml 的 categories
if (config[navItem.id] && Array.isArray(config[navItem.id].categories)) return config[navItem.id].categories;
return null;
}
@@ -454,6 +420,359 @@ function makeJsonSafeForHtmlScript(jsonString) {
return jsonString.replace(/<\/script/gi, '<\\/script');
}
/**
* 解析页面配置文件路径(优先 user回退 _default
* 注意:仅用于构建期读取文件元信息,不会把路径注入到页面/扩展配置中。
* @param {string} pageId 页面ID与 pages/<id>.yml 文件名对应)
* @returns {string|null} 文件路径或 null
*/
function resolvePageConfigFilePath(pageId) {
if (!pageId) return null;
const candidates = [
path.join(process.cwd(), 'config', 'user', 'pages', `${pageId}.yml`),
path.join(process.cwd(), 'config', 'user', 'pages', `${pageId}.yaml`),
path.join(process.cwd(), 'config', '_default', 'pages', `${pageId}.yml`),
path.join(process.cwd(), 'config', '_default', 'pages', `${pageId}.yaml`),
];
for (const filePath of candidates) {
try {
if (fs.existsSync(filePath)) return filePath;
} catch (e) {
// 忽略 IO 异常,继续尝试下一个候选
}
}
return null;
}
/**
* 尝试获取文件最后一次 git 提交时间ISO 字符串)
* @param {string} filePath 文件路径
* @returns {string|null} ISO 字符串UTC失败返回 null
*/
function tryGetGitLastCommitIso(filePath) {
if (!filePath) return null;
try {
const relativePath = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
const output = execFileSync(
'git',
['log', '-1', '--format=%cI', '--', relativePath],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
);
const raw = String(output || '').trim();
if (!raw) return null;
const date = new Date(raw);
if (Number.isNaN(date.getTime())) return null;
return date.toISOString();
} catch (e) {
return null;
}
}
/**
* 获取文件 mtimeISO 字符串)
* @param {string} filePath 文件路径
* @returns {string|null} ISO 字符串UTC失败返回 null
*/
function tryGetFileMtimeIso(filePath) {
if (!filePath) return null;
try {
const stats = fs.statSync(filePath);
const mtime = stats && stats.mtime ? stats.mtime : null;
if (!(mtime instanceof Date) || Number.isNaN(mtime.getTime())) return null;
return mtime.toISOString();
} catch (e) {
return null;
}
}
/**
* 计算页面配置文件“内容更新时间”(优先 git回退 mtime
* @param {string} pageId 页面ID
* @returns {{updatedAt: string, updatedAtSource: 'git'|'mtime'}|null}
*/
function getPageConfigUpdatedAtMeta(pageId) {
const filePath = resolvePageConfigFilePath(pageId);
if (!filePath) return null;
const gitIso = tryGetGitLastCommitIso(filePath);
if (gitIso) {
return { updatedAt: gitIso, updatedAtSource: 'git' };
}
const mtimeIso = tryGetFileMtimeIso(filePath);
if (mtimeIso) {
return { updatedAt: mtimeIso, updatedAtSource: 'mtime' };
}
return null;
}
/**
* 读取 articles 页面 RSS 缓存Phase 2
* - 缓存默认放在 dev/(仓库默认 gitignore
* - 构建端只读缓存:缓存缺失/损坏时回退到 Phase 1渲染来源站点分类
* @param {string} pageId 页面ID用于支持多个 articles 页面的独立缓存)
* @param {Object} config 全站配置(用于读取 site.rss.cacheDir
* @returns {{items: Array<Object>, meta: Object}|null}
*/
function tryLoadArticlesFeedCache(pageId, config) {
if (!pageId) return null;
const cacheDirFromEnv = process.env.RSS_CACHE_DIR ? String(process.env.RSS_CACHE_DIR) : '';
const cacheDirFromConfig =
config && config.site && config.site.rss && config.site.rss.cacheDir ? String(config.site.rss.cacheDir) : '';
const cacheDir = cacheDirFromEnv || cacheDirFromConfig || 'dev';
const cacheBaseDir = path.isAbsolute(cacheDir) ? cacheDir : path.join(process.cwd(), cacheDir);
const cachePath = path.join(cacheBaseDir, `${pageId}.feed-cache.json`);
if (!fs.existsSync(cachePath)) return null;
try {
const raw = fs.readFileSync(cachePath, 'utf8');
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
const articles = Array.isArray(parsed.articles) ? parsed.articles : [];
const items = articles
.map(a => {
const title = a && a.title ? String(a.title) : '';
const url = a && a.url ? String(a.url) : '';
if (!title || !url) return null;
return {
// 兼容 site-card partial 字段
name: title,
url,
icon: a && a.icon ? String(a.icon) : 'fas fa-pen',
description: a && a.summary ? String(a.summary) : '',
// Phase 2 文章元信息(只读展示)
publishedAt: a && a.publishedAt ? String(a.publishedAt) : '',
source: a && a.source ? String(a.source) : '',
// 文章来源站点首页 URL用于按分类聚合展示旧缓存可能缺失
sourceUrl: a && a.sourceUrl ? String(a.sourceUrl) : '',
// 文章链接通常应在新标签页打开
external: true
};
})
.filter(Boolean);
return {
items,
meta: {
pageId: parsed.pageId || pageId,
generatedAt: parsed.generatedAt || '',
total: parsed.stats && Number.isFinite(parsed.stats.totalArticles) ? parsed.stats.totalArticles : items.length
}
};
} catch (e) {
console.warn(`[WARN] articles 缓存读取失败:${cachePath}(将回退 Phase 1`);
return null;
}
}
function normalizeUrlKey(input) {
if (!input) return '';
try {
const u = new URL(String(input));
const origin = u.origin;
let pathname = u.pathname || '/';
// 统一去掉末尾斜杠(根路径除外),避免 https://a.com 与 https://a.com/ 不匹配
if (pathname !== '/' && pathname.endsWith('/')) pathname = pathname.slice(0, -1);
return `${origin}${pathname}`;
} catch {
return String(input).trim();
}
}
function collectSitesRecursively(node, output) {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node.subcategories)) node.subcategories.forEach(child => collectSitesRecursively(child, output));
if (Array.isArray(node.groups)) node.groups.forEach(child => collectSitesRecursively(child, output));
if (Array.isArray(node.subgroups)) node.subgroups.forEach(child => collectSitesRecursively(child, output));
if (Array.isArray(node.sites)) {
node.sites.forEach(site => {
if (site && typeof site === 'object') output.push(site);
});
}
}
/**
* articles Phase 2按页面配置的“分类”聚合文章展示
* - 规则:某篇文章的 sourceUrl/source 归属到其来源站点pages/articles.yml 中配置的站点)所在的分类
* - 兼容:旧缓存缺少 sourceUrl 时回退使用 source站点名称匹配
* @param {Array<Object>} categories 页面配置 categories可包含更深层级
* @param {Array<Object>} articlesItems Phase 2 文章条目(来自缓存)
* @returns {Array<{name: string, icon: string, items: Array<Object>}>}
*/
function buildArticlesCategoriesByPageCategories(categories, articlesItems) {
const safeItems = Array.isArray(articlesItems) ? articlesItems : [];
const safeCategories = Array.isArray(categories) ? categories : [];
// 若页面未配置分类,则回退为单一分类容器
if (safeCategories.length === 0) {
return [
{
name: '最新文章',
icon: 'fas fa-rss',
items: safeItems
}
];
}
const categoryIndex = safeCategories.map(category => {
const sites = [];
collectSitesRecursively(category, sites);
const siteUrlKeys = new Set();
const siteNameKeys = new Set();
sites.forEach(site => {
const urlKey = normalizeUrlKey(site && site.url ? String(site.url) : '');
if (urlKey) siteUrlKeys.add(urlKey);
const nameKey = site && site.name ? String(site.name).trim().toLowerCase() : '';
if (nameKey) siteNameKeys.add(nameKey);
});
return { category, siteUrlKeys, siteNameKeys };
});
const buckets = categoryIndex.map(() => []);
const uncategorized = [];
safeItems.forEach(item => {
const sourceUrlKey = normalizeUrlKey(item && item.sourceUrl ? String(item.sourceUrl) : '');
const sourceNameKey = item && item.source ? String(item.source).trim().toLowerCase() : '';
let matchedIndex = -1;
if (sourceUrlKey) {
matchedIndex = categoryIndex.findIndex(idx => idx.siteUrlKeys.has(sourceUrlKey));
}
if (matchedIndex < 0 && sourceNameKey) {
matchedIndex = categoryIndex.findIndex(idx => idx.siteNameKeys.has(sourceNameKey));
}
if (matchedIndex < 0) {
uncategorized.push(item);
return;
}
buckets[matchedIndex].push(item);
});
const displayCategories = categoryIndex.map((idx, i) => ({
name: idx.category && idx.category.name ? String(idx.category.name) : '未命名分类',
icon: idx.category && idx.category.icon ? String(idx.category.icon) : 'fas fa-rss',
items: buckets[i]
}));
if (uncategorized.length > 0) {
displayCategories.push({
name: '其他',
icon: 'fas fa-ellipsis-h',
items: uncategorized
});
}
return displayCategories;
}
function tryLoadProjectsRepoCache(pageId, config) {
if (!pageId) return null;
const cacheDirFromEnv = process.env.PROJECTS_CACHE_DIR ? String(process.env.PROJECTS_CACHE_DIR) : '';
const cacheDirFromConfig =
config && config.site && config.site.github && config.site.github.cacheDir ? String(config.site.github.cacheDir) : '';
const cacheDir = cacheDirFromEnv || cacheDirFromConfig || 'dev';
const cacheBaseDir = path.isAbsolute(cacheDir) ? cacheDir : path.join(process.cwd(), cacheDir);
const cachePath = path.join(cacheBaseDir, `${pageId}.repo-cache.json`);
if (!fs.existsSync(cachePath)) return null;
try {
const raw = fs.readFileSync(cachePath, 'utf8');
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
const repos = Array.isArray(parsed.repos) ? parsed.repos : [];
const map = new Map();
repos.forEach(r => {
const url = r && r.url ? String(r.url) : '';
if (!url) return;
map.set(url, {
language: r && r.language ? String(r.language) : '',
languageColor: r && r.languageColor ? String(r.languageColor) : '',
stars: Number.isFinite(r && r.stars) ? r.stars : null,
forks: Number.isFinite(r && r.forks) ? r.forks : null
});
});
return {
map,
meta: {
pageId: parsed.pageId || pageId,
generatedAt: parsed.generatedAt || ''
}
};
} catch (e) {
console.warn(`[WARN] projects 缓存读取失败:${cachePath}(将仅展示标题与描述)`);
return null;
}
}
function normalizeGithubRepoUrl(url) {
if (!url) return '';
try {
const u = new URL(String(url));
if (u.hostname.toLowerCase() !== 'github.com') return '';
const parts = u.pathname.split('/').filter(Boolean);
if (parts.length < 2) return '';
const owner = parts[0];
const repo = parts[1].replace(/\.git$/i, '');
if (!owner || !repo) return '';
return `https://github.com/${owner}/${repo}`;
} catch {
return '';
}
}
function applyRepoMetaToCategories(categories, repoMetaMap) {
if (!Array.isArray(categories) || !(repoMetaMap instanceof Map)) return;
const walk = (node) => {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node.subcategories)) node.subcategories.forEach(walk);
if (Array.isArray(node.groups)) node.groups.forEach(walk);
if (Array.isArray(node.subgroups)) node.subgroups.forEach(walk);
if (Array.isArray(node.sites)) {
node.sites.forEach(site => {
if (!site || typeof site !== 'object' || !site.url) return;
const canonical = normalizeGithubRepoUrl(site.url);
if (!canonical) return;
const meta = repoMetaMap.get(canonical);
if (!meta) return;
site.language = meta.language || '';
site.languageColor = meta.languageColor || '';
site.stars = meta.stars;
site.forks = meta.forks;
});
}
};
categories.forEach(walk);
}
/**
* 准备渲染数据,添加模板所需的特殊属性
* @param {Object} config 配置对象
@@ -527,8 +846,7 @@ function loadConfig() {
navigation: [],
fonts: {},
profile: {},
social: [],
categories: []
social: []
};
// 检查模块化配置来源是否存在
@@ -556,20 +874,10 @@ function loadConfig() {
// 2. 次高优先级: config/_default/ 目录
config = loadModularConfig('config/_default');
} else {
// 3. 最低优先级: 旧版单文件配置 (config.yml or config.yaml)
const legacyConfigPath = fs.existsSync('config.yml') ? 'config.yml' : 'config.yaml';
if (fs.existsSync(legacyConfigPath)) {
try {
const fileContent = fs.readFileSync(legacyConfigPath, 'utf8');
config = yaml.load(fileContent);
} catch (e) {
console.error(`Error loading configuration from ${legacyConfigPath}:`, e);
}
} else {
console.error('No configuration found. Please create a configuration file.');
process.exit(1);
}
console.error('[ERROR] 未找到可用配置:缺少 config/user/ 或 config/_default/。');
console.error('[ERROR] 本版本已不再支持旧版单文件配置(config.yml / config.yaml)。');
console.error('[ERROR] 解决方法:使用模块化配置目录(建议从 config/_default/ 复制到 config/user/ 再修改)。');
process.exit(1);
}
// 确保配置有默认值并通过验证
@@ -689,8 +997,10 @@ function generatePageContent(pageId, data) {
console.error(`Missing data for page: ${pageId}`);
return `
<div class="welcome-section">
<h2>页面未配置</h2>
<p class="subtitle">请配置 ${pageId} 页面</p>
<div class="welcome-section-main">
<h2>页面未配置</h2>
<p class="subtitle">请配置 ${pageId} 页面</p>
</div>
</div>`;
}
@@ -700,8 +1010,10 @@ function generatePageContent(pageId, data) {
return `
<div class="welcome-section">
<h2>${escapeHtml(profile.title || '欢迎使用')}</h2>
<h3>${escapeHtml(profile.subtitle || '个人导航站')}</h3>
<div class="welcome-section-main">
<h2>${escapeHtml(profile.title || '欢迎使用')}</h2>
<h3>${escapeHtml(profile.subtitle || '个人导航站')}</h3>
</div>
</div>
${generateCategories(data.categories)}`;
} else {
@@ -712,8 +1024,10 @@ ${generateCategories(data.categories)}`;
return `
<div class="welcome-section">
<h2>${escapeHtml(title)}</h2>
<p class="subtitle">${escapeHtml(subtitle)}</p>
<div class="welcome-section-main">
<h2>${escapeHtml(title)}</h2>
<p class="subtitle">${escapeHtml(subtitle)}</p>
</div>
</div>
${generateCategories(categories)}`;
}
@@ -742,8 +1056,10 @@ function generateSearchResultsPage(config) {
<!-- 搜索结果页 -->
<div class="page" id="search-results">
<div class="welcome-section">
<h2>搜索结果</h2>
<p class="subtitle">在所有页面中找到的匹配项</p>
<div class="welcome-section-main">
<h2>搜索结果</h2>
<p class="subtitle">在所有页面中找到的匹配项</p>
</div>
</div>
${searchSections}
</div>`;
@@ -784,6 +1100,40 @@ function generateFontVariables(config) {
return css;
}
function normalizeGithubHeatmapColor(input) {
const raw = String(input || '').trim().replace(/^#/, '');
const color = raw.toLowerCase();
if (/^[0-9a-f]{6}$/.test(color)) return color;
if (/^[0-9a-f]{3}$/.test(color)) return color;
return '339af0';
}
function getGithubUsernameFromConfig(config) {
const username = config && config.site && config.site.github && config.site.github.username
? String(config.site.github.username).trim()
: '';
return username;
}
function buildProjectsMeta(config) {
const username = getGithubUsernameFromConfig(config);
if (!username) return null;
const color = normalizeGithubHeatmapColor(
config && config.site && config.site.github && config.site.github.heatmapColor
? config.site.github.heatmapColor
: '339af0'
);
return {
heatmap: {
username,
profileUrl: `https://github.com/${username}`,
imageUrl: `https://ghchart.rshah.org/${color}/${username}`
}
};
}
/**
* 渲染单个页面
* @param {string} pageId 页面ID
@@ -793,7 +1143,7 @@ function generateFontVariables(config) {
function renderPage(pageId, config) {
// 准备页面数据
const data = {
...config,
...(config || {}),
currentPage: pageId,
pageId // 同时保留pageId字段用于通用模板
};
@@ -833,6 +1183,74 @@ function renderPage(pageId, config) {
Object.assign(data, config[pageId]);
}
// 页面配置缺失时也尽量给出可用的默认值,避免渲染空标题/undefined
if (data.title === undefined) {
const navItem = Array.isArray(config.navigation) ? config.navigation.find(nav => nav.id === pageId) : null;
if (navItem && navItem.name !== undefined) data.title = navItem.name;
}
if (data.subtitle === undefined) data.subtitle = '';
if (!Array.isArray(data.categories)) data.categories = [];
// 检查页面配置中是否指定了模板(用于派生字段与渲染)
const explicitTemplate = typeof data.template === 'string' ? data.template.trim() : '';
let templateName = explicitTemplate || pageId;
// 未显式指定模板时:若 pages/<pageId>.hbs 不存在,则默认使用通用 page 模板(避免依赖回退日志)
if (!explicitTemplate) {
const inferredTemplatePath = path.join(process.cwd(), 'templates', 'pages', `${templateName}.hbs`);
if (!fs.existsSync(inferredTemplatePath)) {
templateName = 'page';
}
}
// 页面级卡片风格开关(用于差异化)
if (templateName === 'projects') {
data.siteCardStyle = 'repo';
data.projectsMeta = buildProjectsMeta(config);
if (Array.isArray(data.categories)) {
const repoCache = tryLoadProjectsRepoCache(pageId, config);
if (repoCache && repoCache.map) {
applyRepoMetaToCategories(data.categories, repoCache.map);
}
}
}
// friends/articles允许顶层 sites历史/兼容),自动转换为一个分类容器以保持页面结构一致
// 注意:模板名可能被统一为 page例如 friends/home 取消专属模板后),因此这里同时按 pageId 判断。
const isFriendsPage = pageId === 'friends' || templateName === 'friends';
const isArticlesPage = pageId === 'articles' || templateName === 'articles';
if ((isFriendsPage || isArticlesPage)
&& (!Array.isArray(data.categories) || data.categories.length === 0)
&& Array.isArray(data.sites)
&& data.sites.length > 0) {
const implicitName = isFriendsPage ? '全部友链' : '全部来源';
data.categories = [
{
name: implicitName,
icon: 'fas fa-link',
sites: data.sites
}
];
}
// articles 模板页面Phase 2 若存在 RSS 缓存,则注入 articlesItems缓存缺失/损坏则回退 Phase 1
if (templateName === 'articles') {
const cache = tryLoadArticlesFeedCache(pageId, config);
data.articlesItems = cache && Array.isArray(cache.items) ? cache.items : [];
data.articlesMeta = cache ? cache.meta : null;
// Phase 2按页面配置分类聚合展示用于模板渲染只读文章列表
data.articlesCategories = data.articlesItems.length
? buildArticlesCategoriesByPageCategories(data.categories, data.articlesItems)
: [];
}
// bookmarks 模板页面:注入配置文件“内容更新时间”(优先 git回退 mtime
if (templateName === 'bookmarks') {
const updatedAtMeta = getPageConfigUpdatedAtMeta(pageId);
if (updatedAtMeta) {
data.pageMeta = { ...updatedAtMeta };
}
}
// 首页标题规则:使用 site.yml 的 profile 覆盖首页(导航第一项)的 title/subtitle 显示
const homePageId = config.homePageId
|| (Array.isArray(config.navigation) && config.navigation[0] ? config.navigation[0].id : null)
@@ -844,10 +1262,7 @@ function renderPage(pageId, config) {
if (config.profile.subtitle !== undefined) data.subtitle = config.profile.subtitle;
}
// 检查页面配置中是否指定了模板
let templateName = pageId;
if (config[pageId] && config[pageId].template) {
templateName = config[pageId].template;
console.log(`页面 ${pageId} 使用指定模板: ${templateName}`);
}