feat: 首页由 navigation 首项决定

- 移除 navigation.active 配置项,默认页以 navigation[0] 为准(生成端/前端一致)
- 注入 homePageId;首页渲染用 profile.title/profile.subtitle 覆盖 title/subtitle
- 模板按 homePageId 切换首页/非首页标题 DOM 与 data-editable,避免样式错位
- 更新默认配置与文档;书签导入不再写入 active 字段
- 新增/更新单测覆盖首页规则与 profile 覆盖

BREAKING CHANGE: 不再支持 navigation[].active;通过调整 navigation 顺序设置默认页/首页
This commit is contained in:
rbetree
2025-12-26 11:04:40 +08:00
parent 9929358d56
commit 704e895773
12 changed files with 173 additions and 35 deletions

View File

@@ -121,7 +121,7 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优
5. **导航** 5. **导航**
- `navigation[]`:页面入口列表,`id` 需唯一,并与 `pages/` 中配置文件名对应(例如 `id: home` 对应 `pages/home.yml` - `navigation[]`:页面入口列表,`id` 需唯一,并与 `pages/` 中配置文件名对应(例如 `id: home` 对应 `pages/home.yml`
- 只允许一个导航项 `active: true` 作为默认页 - 默认首页由 `navigation` 数组顺序决定:**第一项即为首页(默认打开页)**,不再使用 `active` 字段
- 图标使用 Font Awesome 类名字符串(例如 `fas fa-home``fab fa-github` - 图标使用 Font Awesome 类名字符串(例如 `fas fa-home``fab fa-github`
- 导航显示顺序与数组顺序一致,可通过调整数组顺序改变导航顺序 - 导航显示顺序与数组顺序一致,可通过调整数组顺序改变导航顺序
@@ -230,7 +230,6 @@ navigation:
- name: "首页" - name: "首页"
icon: "fas fa-home" icon: "fas fa-home"
id: "home" id: "home"
active: true
- name: "项目" - name: "项目"
icon: "fas fa-project-diagram" icon: "fas fa-project-diagram"
id: "projects" id: "projects"

View File

@@ -50,12 +50,11 @@ social:
url: https://steam.com url: https://steam.com
icon: fab fa-steam icon: fab fa-steam
# 导航配置 # 导航配置(顺序第一项即首页/默认打开页)
navigation: navigation:
- name: 首页 # 菜单名称 - name: 常用 # 菜单名称
icon: fas fa-home # Font Awesome 图标类 icon: fas fa-star # Font Awesome 图标类
id: home # 页面标识符(唯一,需与 pages/home.yml 对应) id: home # 页面标识符(唯一,需与 pages/<id>.yml 对应)
active: true # 是否默认激活(全局仅一个 true
- name: 项目 - name: 项目
icon: fas fa-project-diagram icon: fas fa-project-diagram
id: projects id: projects

View File

@@ -794,8 +794,7 @@ function updateNavigationFile(filePath) {
navConfig.push({ navConfig.push({
name: '书签', name: '书签',
icon: 'fas fa-bookmark', icon: 'fas fa-bookmark',
id: 'bookmarks', id: 'bookmarks'
active: false
}); });
// 更新文件 // 更新文件

View File

@@ -462,13 +462,6 @@ function prepareRenderData(config) {
// 移除警告日志,数据处理逻辑保留 // 移除警告日志,数据处理逻辑保留
} }
// 添加序列化的配置数据,用于浏览器扩展
renderData.configJSON = JSON.stringify({
version: process.env.npm_package_version || '1.0.0',
timestamp: new Date().toISOString(),
data: renderData // 使用经过处理的renderData而不是原始config
});
// 添加导航项的活动状态标记和子菜单 // 添加导航项的活动状态标记和子菜单
if (Array.isArray(renderData.navigation)) { if (Array.isArray(renderData.navigation)) {
renderData.navigation = renderData.navigation.map((item, index) => { renderData.navigation = renderData.navigation.map((item, index) => {
@@ -476,7 +469,7 @@ function prepareRenderData(config) {
...item, ...item,
isActive: index === 0, // 默认第一项为活动项 isActive: index === 0, // 默认第一项为活动项
id: item.id || `nav-${index}`, id: item.id || `nav-${index}`,
active: index === 0 // 兼容原有逻辑 active: index === 0 // 保持旧模板兼容(由顺序决定,不读取配置的 active 字段)
}; };
// 使用辅助函数获取子菜单 // 使用辅助函数获取子菜单
@@ -489,6 +482,16 @@ function prepareRenderData(config) {
}); });
} }
// 首页默认页规则navigation 顺序第一项即首页
renderData.homePageId = renderData.navigation && renderData.navigation[0] ? renderData.navigation[0].id : null;
// 添加序列化的配置数据,用于浏览器扩展(确保包含 homePageId 等处理结果)
renderData.configJSON = JSON.stringify({
version: process.env.npm_package_version || '1.0.0',
timestamp: new Date().toISOString(),
data: renderData // 使用经过处理的renderData而不是原始config
});
// 为Handlebars模板特别准备navigationData数组 // 为Handlebars模板特别准备navigationData数组
renderData.navigationData = renderData.navigation; renderData.navigationData = renderData.navigation;
@@ -814,6 +817,17 @@ function renderPage(pageId, config) {
Object.assign(data, config[pageId]); Object.assign(data, config[pageId]);
} }
// 首页标题规则:使用 site.yml 的 profile 覆盖首页(导航第一项)的 title/subtitle 显示
const homePageId = config.homePageId
|| (Array.isArray(config.navigation) && config.navigation[0] ? config.navigation[0].id : null)
|| 'home';
// 供模板判断“当前是否首页”
data.homePageId = homePageId;
if (pageId === homePageId && config.profile) {
if (config.profile.title !== undefined) data.title = config.profile.title;
if (config.profile.subtitle !== undefined) data.subtitle = config.profile.subtitle;
}
// 检查页面配置中是否指定了模板 // 检查页面配置中是否指定了模板
let templateName = pageId; let templateName = pageId;
if (config[pageId] && config[pageId].template) { if (config[pageId] && config[pageId].template) {
@@ -844,11 +858,6 @@ function generateAllPagesHTML(config) {
}); });
} }
// 确保首页存在
if (!pages.home) {
pages.home = renderPage('home', config);
}
// 确保搜索结果页存在 // 确保搜索结果页存在
if (!pages['search-results']) { if (!pages['search-results']) {
pages['search-results'] = renderPage('search-results', config); pages['search-results'] = renderPage('search-results', config);
@@ -989,7 +998,9 @@ function main() {
} }
} }
if (require.main === module) {
main(); main();
}
// 导出供测试使用的函数 // 导出供测试使用的函数
module.exports = { module.exports = {
@@ -1002,4 +1013,3 @@ module.exports = {
renderTemplate, renderTemplate,
generateAllPagesHTML generateAllPagesHTML
}; };

View File

@@ -641,7 +641,41 @@ function extractNestedData(element) {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// 先声明所有状态变量 // 先声明所有状态变量
let isSearchActive = false; let isSearchActive = false;
let currentPageId = 'home'; // 首页不再固定为 "home":以导航顺序第一项为准
const homePageId = (() => {
// 1) 优先从生成端注入的配置数据读取(保持与实际导航顺序一致)
try {
const config = window.MeNav && typeof window.MeNav.getConfig === 'function'
? window.MeNav.getConfig()
: null;
const injectedHomePageId = config && config.data && config.data.homePageId
? String(config.data.homePageId).trim()
: '';
if (injectedHomePageId) return injectedHomePageId;
const nav = config && config.data && Array.isArray(config.data.navigation)
? config.data.navigation
: null;
const firstId = nav && nav[0] && nav[0].id ? String(nav[0].id).trim() : '';
if (firstId) return firstId;
} catch (error) {
// 忽略解析错误,继续使用 DOM 推断
}
// 2) 回退到 DOM取首个导航项的 data-page
const firstNavItem = document.querySelector('.nav-item[data-page]');
if (firstNavItem) {
const id = String(firstNavItem.getAttribute('data-page') || '').trim();
if (id) return id;
}
// 3) 最后兜底:取首个页面容器 id
const firstPage = document.querySelector('.page[id]');
if (firstPage && firstPage.id) return firstPage.id;
return 'home';
})();
let currentPageId = homePageId;
let isInitialLoad = true; let isInitialLoad = true;
let isSidebarOpen = false; let isSidebarOpen = false;
let isSearchOpen = false; let isSearchOpen = false;
@@ -1257,9 +1291,9 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} else { } else {
// 如果没有激活的导航项,默认显示首页 // 如果没有激活的导航项,默认显示首页
currentPageId = 'home'; currentPageId = homePageId;
pages.forEach(page => { pages.forEach(page => {
page.classList.toggle('active', page.id === 'home'); page.classList.toggle('active', page.id === homePageId);
}); });
} }
} catch (resetError) { } catch (resetError) {
@@ -1489,7 +1523,7 @@ document.addEventListener('DOMContentLoaded', () => {
// 立即执行初始化不再使用requestAnimationFrame延迟 // 立即执行初始化不再使用requestAnimationFrame延迟
// 显示首页 // 显示首页
showPage('home'); showPage(homePageId);
// 添加载入动画 // 添加载入动画
categories.forEach((category, index) => { categories.forEach((category, index) => {

View File

@@ -1,7 +1,14 @@
{{#ifEquals pageId @root.homePageId}}
<div class="welcome-section">
<h2 data-editable="profile-title">{{title}}</h2>
<h3 data-editable="profile-subtitle">{{subtitle}}</h3>
</div>
{{else}}
<div class="welcome-section"> <div class="welcome-section">
<h2 data-editable="page-title">{{title}}</h2> <h2 data-editable="page-title">{{title}}</h2>
<p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p> <p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p>
</div> </div>
{{/ifEquals}}
{{#each categories}} {{#each categories}}
{{> category}} {{> category}}
{{/each}} {{/each}}

View File

@@ -1,7 +1,14 @@
{{#ifEquals pageId @root.homePageId}}
<div class="welcome-section">
<h2 data-editable="profile-title">{{title}}</h2>
<h3 data-editable="profile-subtitle">{{subtitle}}</h3>
</div>
{{else}}
<div class="welcome-section"> <div class="welcome-section">
<h2 data-editable="page-title">{{title}}</h2> <h2 data-editable="page-title">{{title}}</h2>
<p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p> <p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p>
</div> </div>
{{/ifEquals}}
{{#each categories}} {{#each categories}}
{{> category}} {{> category}}
{{/each}} {{/each}}

View File

@@ -1,7 +1,14 @@
{{#ifEquals pageId @root.homePageId}}
<div class="welcome-section">
<h2 data-editable="profile-title">{{title}}</h2>
<h3 data-editable="profile-subtitle">{{subtitle}}</h3>
</div>
{{else}}
<div class="welcome-section"> <div class="welcome-section">
<h2 data-editable="page-title">{{title}}</h2> <h2 data-editable="page-title">{{title}}</h2>
<p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p> <p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p>
</div> </div>
{{/ifEquals}}
{{#each categories}} {{#each categories}}
{{> category}} {{> category}}
{{/each}} {{/each}}

View File

@@ -1,7 +1,14 @@
{{#ifEquals pageId @root.homePageId}}
<div class="welcome-section"> <div class="welcome-section">
<h2 data-editable="profile-title">{{profile.title}}</h2> <h2 data-editable="profile-title">{{title}}</h2>
<h3 data-editable="profile-subtitle">{{profile.subtitle}}</h3> <h3 data-editable="profile-subtitle">{{subtitle}}</h3>
</div> </div>
{{else}}
<div class="welcome-section">
<h2 data-editable="page-title">{{title}}</h2>
<p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p>
</div>
{{/ifEquals}}
{{#each categories}} {{#each categories}}
{{> category}} {{> category}}
{{/each}} {{/each}}

View File

@@ -1,8 +1,15 @@
<div class="page" id="{{pageId}}"> <div class="page" id="{{pageId}}">
{{#ifEquals pageId @root.homePageId}}
<div class="welcome-section">
<h2 data-editable="profile-title">{{title}}</h2>
<h3 data-editable="profile-subtitle">{{subtitle}}</h3>
</div>
{{else}}
<div class="welcome-section"> <div class="welcome-section">
<h2 data-editable="page-title">{{title}}</h2> <h2 data-editable="page-title">{{title}}</h2>
<p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p> <p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p>
</div> </div>
{{/ifEquals}}
{{#each categories}} {{#each categories}}
{{> category}} {{> category}}
{{/each}} {{/each}}

View File

@@ -1,7 +1,14 @@
{{#ifEquals pageId @root.homePageId}}
<div class="welcome-section">
<h2 data-editable="profile-title">{{title}}</h2>
<h3 data-editable="profile-subtitle">{{subtitle}}</h3>
</div>
{{else}}
<div class="welcome-section"> <div class="welcome-section">
<h2 data-editable="page-title">{{title}}</h2> <h2 data-editable="page-title">{{title}}</h2>
<p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p> <p class="subtitle" data-editable="page-subtitle">{{subtitle}}</p>
</div> </div>
{{/ifEquals}}
{{#each categories}} {{#each categories}}
{{> category}} {{> category}}
{{/each}} {{/each}}

View File

@@ -0,0 +1,55 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { loadHandlebarsTemplates, generateAllPagesHTML } = require('../src/generator.js');
test('首页navigation 第一项)应使用 profile 覆盖 title/subtitle 显示', () => {
const originalCwd = process.cwd();
process.chdir(path.join(__dirname, '..'));
try {
loadHandlebarsTemplates();
const config = {
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
social: [],
navigation: [
{ id: 'bookmarks', name: '书签', icon: 'fas fa-bookmark' },
{ id: 'home', name: '首页', icon: 'fas fa-home' },
{ 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: [] },
projects: { title: '项目页标题', subtitle: '项目页副标题', template: 'projects', categories: [] },
};
const pages = generateAllPagesHTML(config);
assert.ok(typeof pages.bookmarks === 'string' && pages.bookmarks.length > 0);
assert.ok(pages.bookmarks.includes('PROFILE_TITLE'));
assert.ok(pages.bookmarks.includes('PROFILE_SUBTITLE'));
assert.ok(pages.bookmarks.includes('data-editable="profile-title"'));
assert.ok(pages.bookmarks.includes('data-editable="profile-subtitle"'));
assert.ok(pages.bookmarks.includes('<h3'));
assert.ok(!pages.bookmarks.includes('书签页标题'));
assert.ok(!pages.bookmarks.includes('书签页副标题'));
assert.ok(!pages.bookmarks.includes('data-editable="page-title"'));
assert.ok(typeof pages.home === 'string' && pages.home.length > 0);
assert.ok(pages.home.includes('HOME_PAGE_TITLE'));
assert.ok(pages.home.includes('HOME_PAGE_SUBTITLE'));
assert.ok(pages.home.includes('data-editable="page-title"'));
assert.ok(pages.home.includes('data-editable="page-subtitle"'));
assert.ok(pages.home.includes('<p class="subtitle"'));
assert.ok(!pages.home.includes('PROFILE_TITLE'));
assert.ok(typeof pages.projects === 'string' && pages.projects.length > 0);
assert.ok(pages.projects.includes('项目页标题'));
assert.ok(pages.projects.includes('项目页副标题'));
assert.ok(pages.projects.includes('<p class="subtitle"'));
} finally {
process.chdir(originalCwd);
}
});