555 lines
18 KiB
Markdown
555 lines
18 KiB
Markdown
# MeNav 模板目录
|
||
|
||
## 目录
|
||
|
||
- [模板系统概述](#模板系统概述)
|
||
- [目录结构](#目录结构)
|
||
- [模板类型](#模板类型)
|
||
- [布局模板](#布局模板)
|
||
- [页面模板](#页面模板)
|
||
- [组件模板](#组件模板)
|
||
- [多层级嵌套模板组件](#多层级嵌套模板组件)
|
||
- [模板数据流](#模板数据流)
|
||
- [模板使用示例](#模板使用示例)
|
||
- [最佳实践](#最佳实践)
|
||
- [扩展指南](#扩展指南)
|
||
|
||
## 模板系统概述
|
||
|
||
MeNav 项目使用 Handlebars 作为模板引擎,实现了组件化架构,将页面内容与逻辑分离。模板系统的核心优势:
|
||
|
||
- **组件复用** - 通过组件拆分实现代码复用
|
||
- **结构清晰** - 布局、页面、组件分离管理
|
||
- **扩展灵活** - 易于添加新页面和组件
|
||
- **维护简便** - 修改单个组件不影响其他部分
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
templates/
|
||
├── layouts/ # 布局模板 - 定义页面整体结构
|
||
│ └── default.hbs # 默认布局
|
||
├── pages/ # 页面模板 - 对应不同页面内容
|
||
│ ├── page.hbs # 通用页面模板(默认/回退模板;普通页面常用)
|
||
│ ├── content.hbs # 内容页(Markdown 内容页:构建期渲染)
|
||
│ ├── projects.hbs # 项目页(repo 风格卡片)
|
||
│ ├── articles.hbs # 文章页(RSS 聚合/只读文章条目)
|
||
│ ├── bookmarks.hbs # 书签页
|
||
│ └── search-results.hbs # 搜索结果页(内置)
|
||
├── components/ # 组件模板 - 可复用的界面元素
|
||
│ ├── page-header.hbs # 统一标题区(首页/非首页/书签更新时间/项目热力图)
|
||
│ ├── navigation.hbs # 导航组件
|
||
│ ├── site-card.hbs # 站点卡片组件
|
||
│ ├── category.hbs # 分类组件
|
||
│ └── ...
|
||
└── README.md # 本文档
|
||
```
|
||
|
||
## 模板类型
|
||
|
||
### 布局模板
|
||
|
||
布局模板定义了整个页面的HTML结构,包含头部、导航栏、内容区和底部等基本框架。
|
||
|
||
**位置**: `templates/layouts/`
|
||
|
||
**主要布局**:
|
||
|
||
- `default.hbs` - 默认布局,定义整个页面框架
|
||
|
||
**示例**:
|
||
|
||
```handlebars
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>{{site.title}}</title>
|
||
<!-- 其他头部元素 -->
|
||
</head>
|
||
<body>
|
||
<div class="layout">
|
||
<!-- 导航部分 -->
|
||
<nav class="sidebar">
|
||
{{> navigation navigationData}}
|
||
</nav>
|
||
|
||
<!-- 内容区域 -->
|
||
<main class="content">
|
||
{{#each pages}}
|
||
<div class="page {{@key}}{{#if @first}} active{{/if}}" id="{{@key}}">
|
||
{{{this}}}
|
||
</div>
|
||
{{/each}}
|
||
</main>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
### 页面模板
|
||
|
||
页面模板对应网站的不同页面,每个页面模板通常包含多个组件组合。
|
||
|
||
**位置**: `templates/pages/`
|
||
|
||
**主要页面**:
|
||
|
||
- `page.hbs` - 通用页面模板(默认/回退模板;普通页面常用)
|
||
- `content.hbs` - 内容页(从本地 Markdown 文件构建期渲染为 HTML)
|
||
- `bookmarks.hbs` - 书签页
|
||
- `projects.hbs` - 项目页
|
||
- `articles.hbs` - 文章页
|
||
- `search-results.hbs` - 搜索结果
|
||
- 其他自定义页面
|
||
|
||
> 说明:MeNav 不再依赖 `home.hbs` 作为首页模板。
|
||
> “首页/默认打开页”由 `site.yml -> navigation` 的**第一项**决定;首页可使用任意页面模板,具体取决于该页面配置(`pages/<homePageId>.yml` 的 `template` 字段与回退规则)。
|
||
|
||
#### 内容页(template: content)
|
||
|
||
内容页用于承载“关于 / 帮助 / 使用说明 / 更新日志 / 迁移指南”等纯文本内容。
|
||
|
||
- 内容来源:页面配置中的 `content.file` 指向本地 `.md` 文件(例如 `content/about.md`)
|
||
- 渲染时机:**构建期** Markdown -> HTML(不是运行时 fetch)
|
||
- 安全约束:
|
||
- 禁止 raw HTML
|
||
- 禁止图片(`![]()` 不会被渲染)
|
||
- 链接会按 `site.yml -> security.allowedSchemes` 白名单策略处理,不安全链接会被降级为 `#`
|
||
|
||
对应模板文件:`templates/pages/content.hbs`
|
||
|
||
> 配置写法示例见:`config/README.md` 的“内容页(template: content)”。
|
||
|
||
**示例** (`page.hbs`):
|
||
|
||
```handlebars
|
||
<div class="page-template page-template-{{pageId}}">
|
||
{{> page-header}}
|
||
{{#each categories}}
|
||
{{> category}}
|
||
{{/each}}
|
||
</div>
|
||
```
|
||
|
||
### 组件模板
|
||
|
||
组件是可复用的UI元素,用于在不同页面中重复使用。
|
||
|
||
**位置**: `templates/components/`
|
||
|
||
> 说明:生成器启动时会自动扫描 `templates/components/` 下的所有 `.hbs` 并注册为 Handlebars partial(partial 名称=文件名去掉 `.hbs`)。因此新增组件后无需手动“注册步骤”,可直接通过 `{{> component-name}}` 引用。
|
||
|
||
**主要组件**:
|
||
|
||
- `page-header.hbs` - 统一页面标题区(首页/非首页/书签更新时间/项目热力图)
|
||
- `home-dashboard.hbs` - 首页仪表盘(时钟卡片、TODO 待办卡片)
|
||
- `navigation.hbs` - 导航菜单
|
||
- `site-card.hbs` - 站点卡片
|
||
- `category.hbs` - 分类容器(支持多层级嵌套)
|
||
- `group.hbs` - 分组容器(支持多层级嵌套)
|
||
- `social-links.hbs` - 社交链接
|
||
|
||
**示例** (`site-card.hbs`,精简展示关键结构):
|
||
|
||
```handlebars
|
||
{{#if url}}
|
||
<a href="{{url}}"
|
||
class="site-card{{#if style}} site-card-{{style}}{{/if}}"
|
||
{{#if external}}target="_blank" rel="noopener"{{/if}}
|
||
data-type="{{#if type}}{{type}}{{else}}site{{/if}}"
|
||
data-name="{{name}}"
|
||
data-url="{{url}}">
|
||
<div class="site-card-icon">...</div>
|
||
<div class="site-card-content">
|
||
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
|
||
<p>{{description}}</p>
|
||
</div>
|
||
</a>
|
||
{{/if}}
|
||
```
|
||
|
||
说明:
|
||
|
||
- `type=article`:用于 articles Phase 2 的只读文章条目卡片(仍保留 `data-*` 结构;扩展解析应以 `data-type="article"` 区分类型)
|
||
- `style=repo`:用于 projects 的代码仓库风卡片(展示 language/stars/forks 等只读元信息)
|
||
|
||
### 多层级嵌套模板组件
|
||
|
||
#### category.hbs - 分类容器组件
|
||
|
||
`category.hbs` 是多层级嵌套的核心组件,可渲染 `categories -> subcategories -> groups -> sites` 的结构;更深一层的 `subgroups` 由 `group.hbs` 负责渲染。
|
||
|
||
**功能特性**:
|
||
|
||
- 支持 2~4 层嵌套(`categories -> subcategories -> groups -> subgroups -> sites`,其中 `subgroups` 可选)
|
||
- 自动计算标题层级(h2/h3/h4/h5)
|
||
- 根据层级自动应用对应的 CSS 类(如 `category-level-2`、`group-level-4`)
|
||
- 分类容器支持三种内容:子分类、分组、站点(分组内可继续包含子分组)
|
||
|
||
**递归渲染原理**:
|
||
通过在模板内部调用自身实现递归渲染:
|
||
|
||
```handlebars
|
||
{{#each subcategories}}
|
||
{{> category level=2}}
|
||
{{/each}}
|
||
```
|
||
|
||
**level参数的作用**:
|
||
|
||
- 用于跟踪当前嵌套层级
|
||
- 控制标题标签的层级(h{{add level 1}})
|
||
- 应用对应的CSS类(category-level-{{level}})
|
||
- 传递给子组件以保持层级一致性
|
||
|
||
**使用示例**:
|
||
|
||
```handlebars
|
||
<!-- 顶级分类 -->
|
||
{{> category category}}
|
||
|
||
<!-- 指定层级的分类 -->
|
||
{{> category category level=2}}
|
||
```
|
||
|
||
#### group.hbs - 分组容器组件
|
||
|
||
`group.hbs` 是用于在分类内组织站点的组件,同样支持层级参数。
|
||
|
||
**功能特性**:
|
||
|
||
- 支持在分类内创建站点分组
|
||
- 支持子分组(`subgroups`,用于第 4 层结构)
|
||
- 自动应用层级样式
|
||
- 支持展开/折叠功能
|
||
- 与category.hbs保持一致的层级系统
|
||
|
||
**使用示例**:
|
||
|
||
```handlebars
|
||
<!-- 在分类模板中使用 -->
|
||
{{#each groups}}
|
||
{{> group}}
|
||
{{/each}}
|
||
|
||
<!-- 指定层级的分组 -->
|
||
{{> group group level=3}}
|
||
```
|
||
|
||
#### 多层级嵌套结构示例
|
||
|
||
典型的(最多 4 层)结构:分类 → 子分类 → 分组 → 子分组 → 站点(`subgroups` 可选)
|
||
|
||
```yaml
|
||
# 配置示例
|
||
categories:
|
||
- name: '技术'
|
||
icon: 'fas fa-code'
|
||
subcategories:
|
||
- name: '前端开发'
|
||
icon: 'fas fa-laptop-code'
|
||
groups:
|
||
- name: '框架'
|
||
icon: 'fas fa-cubes'
|
||
subgroups:
|
||
- name: 'React生态'
|
||
icon: 'fab fa-react'
|
||
sites:
|
||
- name: 'React'
|
||
url: 'https://reactjs.org'
|
||
icon: 'fab fa-react'
|
||
- name: 'Next.js'
|
||
url: 'https://nextjs.org'
|
||
icon: 'fas fa-triangle'
|
||
```
|
||
|
||
对应的模板渲染:
|
||
|
||
```handlebars
|
||
<!-- category.hbs 会递归渲染 -->
|
||
<section class="category category-level-1" data-level="1">
|
||
<div class="category-header">
|
||
<h2><i class="fas fa-code"></i> 技术</h2>
|
||
</div>
|
||
<div class="category-content">
|
||
<div class="subcategories-container">
|
||
<!-- 递归调用 category.hbs,level=2 -->
|
||
<section class="category category-level-2" data-level="2">
|
||
<div class="category-header">
|
||
<h3><i class="fas fa-laptop-code"></i> 前端开发</h3>
|
||
</div>
|
||
<div class="category-content">
|
||
<div class="groups-container">
|
||
<!-- 使用 group.hbs,默认 level=3 -->
|
||
<div class="group group-level-3" data-level="3">
|
||
<div class="group-header">
|
||
<h4><i class="fas fa-cubes"></i> 框架</h4>
|
||
</div>
|
||
<div class="group-content">
|
||
<div class="subgroups-container">
|
||
<!-- group.hbs 渲染 subgroups,level=4 -->
|
||
<div class="group group-level-4" data-level="4">
|
||
<div class="group-header">
|
||
<h5><i class="fab fa-react"></i> React生态</h5>
|
||
</div>
|
||
<div class="group-content">
|
||
<div class="sites-grid">
|
||
<!-- 渲染站点卡片 -->
|
||
{{> site-card site}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
```
|
||
|
||
#### 层级系统说明
|
||
|
||
- **层级1 (level=1)**: 顶级分类,使用h2标题
|
||
- **层级2 (level=2)**: 子分类,使用h3标题
|
||
- **层级3 (level=3)**: 分组,使用h4标题
|
||
- **层级4 (level=4)**: 子分组,使用h5标题(用于 4 层结构)
|
||
|
||
每个层级都有对应的CSS类:
|
||
|
||
- `category-level-1`, `category-level-2`
|
||
- `group-level-3`, `group-level-4`
|
||
|
||
这种设计确保了:
|
||
|
||
1. 语义化的HTML结构
|
||
2. 一致的视觉层级
|
||
3. 可预测的嵌套深度(当前导入脚本与样式保证到 level=4)
|
||
4. 灵活的样式定制
|
||
|
||
### 站点图标渲染(favicon/manual)
|
||
|
||
当启用 `icons.mode: favicon`(默认)时,站点卡片会优先显示站点 favicon;当 URL 非 http/https、加载失败或网络受限,则自动回退到 Font Awesome 图标。相关助手:`ifHttpUrl`(条件)与 `encodeURIComponent`(工具)。
|
||
|
||
站点级覆盖(可选,写在每个 `sites[]` 节点上):
|
||
|
||
- `faviconUrl`:为单站点指定图标链接(优先级最高,失败回退到手动图标;本地路径建议以 `assets/` 开头,构建会复制到 `dist/` 同路径)
|
||
- `forceIconMode: favicon | manual`:强制该站点使用指定模式(不设置则跟随全局 `icons.mode`)
|
||
- 优先级:`faviconUrl` > `forceIconMode` > 全局 `icons.mode`
|
||
|
||
> 注意:用于根据站点 URL 生成 faviconV2 地址的模板 helper 已更名为 `faviconV2Url`,从而避免与站点字段 `faviconUrl` 同名冲突;自定义模板如需生成 faviconV2 地址,请使用 `{{faviconV2Url url}}`。如需强制读取站点字段 `faviconUrl`,也可使用 `{{lookup . "faviconUrl"}}`(推荐在复杂上下文中显式读取字段)。
|
||
|
||
示例(与内置组件实现保持一致):
|
||
|
||
```handlebars
|
||
{{#if url}}
|
||
<a href="{{url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}"
|
||
{{#if external}}target="_blank" rel="noopener"{{/if}}
|
||
data-type="site"
|
||
data-name="{{name}}"
|
||
data-url="{{url}}"
|
||
data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"
|
||
data-description="{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}">
|
||
{{#ifEquals @root.icons.mode "favicon"}}
|
||
{{#ifHttpUrl url}}
|
||
<div class="icon-container">
|
||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
||
<img
|
||
class="favicon-icon"
|
||
src="https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url={{encodeURIComponent url}}&size=32&drop_404_icon=true"
|
||
alt="{{name}} favicon"
|
||
loading="lazy"
|
||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
||
onerror="this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');"
|
||
/>
|
||
<i class="fas fa-link icon-fallback" aria-hidden="true"></i>
|
||
</div>
|
||
{{else}}
|
||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
|
||
{{/ifHttpUrl}}
|
||
{{else}}
|
||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
|
||
{{/ifEquals}}
|
||
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
|
||
<p>{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p>
|
||
</a>
|
||
{{/if}}
|
||
```
|
||
|
||
提示:关于 `icons.mode` 的配置与隐私说明,请参见:
|
||
|
||
- `config/README.md` 的 `site.yml 常用字段`:[`../config/README.md#siteyml-常用字段`](../config/README.md#siteyml-%E5%B8%B8%E7%94%A8%E5%AD%97%E6%AE%B5)
|
||
- 根目录 `README.md` 的“近期更新”:[`../README.md#近期更新`](../README.md#%E8%BF%91%E6%9C%9F%E6%9B%B4%E6%96%B0)
|
||
|
||
## 模板数据流
|
||
|
||
MeNav 模板系统的数据流如下:
|
||
|
||
1. `generator.js` 加载配置文件并处理数据
|
||
2. 数据通过 Handlebars 上下文传递给模板
|
||
3. 布局模板 (`layouts/default.hbs`) 作为外层容器
|
||
4. 页面模板 (`pages/*.hbs`) 填充布局中的内容区域
|
||
5. 组件模板 (`components/*.hbs`) 在页面中通过 `{{> component-name}}` 引用
|
||
|
||
主要数据对象:
|
||
|
||
- `site` - 网站配置信息
|
||
- `navigationData` - 导航菜单数据
|
||
- `categories` - 分类和站点数据
|
||
- `profile` - 个人资料数据
|
||
- `social` - 社交链接数据
|
||
|
||
常见派生字段(由生成器注入,供模板差异化使用):
|
||
|
||
- `homePageId`:首页页面 ID(始终等于 `navigation` 第一项的 `id`)
|
||
- `pageId`:当前页面 ID(用于 `.page-template-{{pageId}}` 等)
|
||
- `pageMeta.updatedAt/updatedAtSource`:仅 bookmarks 模板页用于“update: YYYY-MM-DD | from: ...”展示
|
||
- `projectsMeta.heatmap`:仅 projects 模板页用于右侧 GitHub 热力图展示(需要配置 `site.github.username`)
|
||
- `articlesItems/articlesCategories`:仅 articles 模板页(Phase 2)用于渲染只读文章条目(RSS 缓存存在时)
|
||
|
||
> 提示:页面模板是“页面内容片段”,不要包含 `<!DOCTYPE html>` 等整页骨架;整页骨架由 `layouts/default.hbs` 负责。
|
||
|
||
## 模板使用示例
|
||
|
||
### 布局模板使用
|
||
|
||
布局模板通常只有一个 `default.hbs`,会自动被系统使用。
|
||
|
||
### 页面模板使用
|
||
|
||
页面模板对应导航中的各个页面,有两种使用方式:
|
||
|
||
1. **自动匹配**:系统会尝试使用与页面ID同名的模板(例如:页面ID为 `projects` 时会使用 `projects.hbs`)
|
||
2. **显式指定**:在页面配置中使用 `template` 字段指定要使用的模板
|
||
|
||
#### 模板指定示例
|
||
|
||
在 `config/user/pages/项目.yml` 中:
|
||
|
||
```yaml
|
||
title: '我的项目'
|
||
subtitle: '这里展示我的所有项目'
|
||
template: 'projects' # 使用 projects.hbs 模板而不是使用页面ID命名的模板
|
||
categories:
|
||
- name: '网站项目'
|
||
icon: 'fas fa-globe'
|
||
sites:
|
||
- name: '个人博客'
|
||
# ... 其他字段
|
||
```
|
||
|
||
**注意**:当系统找不到指定的模板或与页面ID匹配的模板时,会自动使用通用模板 `page.hbs`。
|
||
|
||
### 引用组件
|
||
|
||
在页面或其他组件中引用组件:
|
||
|
||
```handlebars
|
||
{{> navigation navigationData}}
|
||
{{> site-card}}
|
||
```
|
||
|
||
### 条件渲染
|
||
|
||
根据条件显示内容:
|
||
|
||
```handlebars
|
||
{{#if profile.title}}
|
||
<h2>{{profile.title}}</h2>
|
||
{{else}}
|
||
<h2>欢迎使用</h2>
|
||
{{/if}}
|
||
```
|
||
|
||
### 循环渲染
|
||
|
||
循环渲染数据列表:
|
||
|
||
```handlebars
|
||
{{#each categories}}
|
||
<section class="category" id="{{name}}">
|
||
<h2><i class="{{icon}}"></i> {{name}}</h2>
|
||
<div class="sites-grid">
|
||
{{#each sites}}
|
||
{{> site-card}}
|
||
{{/each}}
|
||
</div>
|
||
</section>
|
||
{{/each}}
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
1. **组件粒度** - 保持组件的适当粒度,既不过大也不过小
|
||
- 过大:难以复用和维护
|
||
- 过小:增加复杂性和引用管理难度
|
||
|
||
2. **数据传递** - 使用合适的方式传递数据
|
||
- 直接上下文:`{{> component}}` (继承父上下文)
|
||
- 指定数据:`{{> component customData}}` (传递特定数据)
|
||
|
||
3. **命名规范**
|
||
- 使用连字符命名:`site-card.hbs`、`search-results.hbs`
|
||
- 使用描述性名称,体现组件用途
|
||
|
||
4. **注释**
|
||
- 对复杂逻辑添加注释说明
|
||
- 标注可选参数和默认行为
|
||
|
||
## 扩展指南
|
||
|
||
### 添加新页面
|
||
|
||
1. 在 `templates/pages/` 创建新的 `.hbs` 文件
|
||
2. 在 `config/user/site.yml` 的 `navigation` 部分添加页面配置(配置采用“完全替换”策略,推荐使用 user 配置)
|
||
3. 页面内容可引用现有组件或创建新组件
|
||
|
||
示例:
|
||
|
||
```handlebars
|
||
<!-- templates/pages/about.hbs -->
|
||
<div class='about-page'>
|
||
<h2>关于我</h2>
|
||
<p>{{about.description}}</p>
|
||
|
||
{{#if about.skills}}
|
||
<div class='skills'>
|
||
<h3>技能</h3>
|
||
<ul>
|
||
{{#each about.skills}}
|
||
<li>{{this}}</li>
|
||
{{/each}}
|
||
</ul>
|
||
</div>
|
||
{{/if}}
|
||
</div>
|
||
```
|
||
|
||
### 添加新组件
|
||
|
||
1. 在 `templates/components/` 创建新的 `.hbs` 文件
|
||
2. 在页面或其他组件中引用
|
||
|
||
示例:
|
||
|
||
```handlebars
|
||
<!-- templates/components/skill-card.hbs -->
|
||
<div class='skill-card'>
|
||
<h4>{{name}}</h4>
|
||
<div class='skill-level' data-level='{{level}}'>
|
||
<div class='skill-bar' style='width: {{level}}%'></div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
使用新组件:
|
||
|
||
```handlebars
|
||
{{#each skills}}
|
||
{{> skill-card}}
|
||
{{/each}}
|
||
```
|