模板系统
DoraCMS 采用 Nunjucks 模板引擎,提供了一套强大的自定义模板标签,让你能够轻松实现各种内容展示需求。
概览
DoraCMS 模板系统基于 Nunjucks,并扩展了专门用于内容管理的自定义标签。这些标签可以帮助你:
- 🚀 快速展示各类内容(文章、分类、标签等)
- 🎨 灵活控制展示样式和数量
- 🔄 支持分页和数据过滤
- 📊 自动处理数据关联
核心标签
文章类标签
1. news - 最新文章
获取并展示最新发布的文章列表。
基础用法
{% news key="latestNews", pageSize="10" %}
{% if latestNews and latestNews.length > 0 %}
{% for article in latestNews %}
<h3>{{ article.title }}</h3>
<p>{{ article.discription }}</p>
{% endfor %}
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 是 | 数据变量名,用于在模板中访问数据 |
pageSize | Number | 否 | 获取数量,默认 10 |
typeId | String | 否 | 分类 ID,筛选指定分类下的文章 |
isPaging | String | 否 | 是否启用分页,"1" 启用,"0" 不启用 |
使用示例
<!-- 获取首页最新文章 -->
{% news key="latestNews", pageSize="10" %}
<!-- 获取指定分类的最新文章 -->
{% news key="categoryNews", typeId="4", pageSize="5" %}
<!-- 启用分页的最新文章 -->
{% news key="pagedNews", pageSize="20", isPaging="1" %}数据结构
[
{
"id": "E1LuMa5ee",
"title": "文章标题",
"discription": "文章描述",
"sImg": "文章封面图片URL",
"clickNum": 100,
"likeNum": 10,
"commentNum": 5,
"author": {
"userName": "作者名称"
},
"categories": [
{
"id": "4",
"name": "分类名称"
}
],
"tags": [
{
"name": "标签名称",
"url": "/tags/tagname"
}
],
"createdAt": "2025-01-01T00:00:00.000Z"
}
]2. hot - 热门文章
根据浏览量获取热门文章,适用于热门推荐、排行榜等场景。
基础用法
{% hot key="hotItemListData", pageSize="9" %}
{% if hotItemListData and hotItemListData.length > 0 %}
{% for article in hotItemListData %}
<div class="hot-item">
<span class="rank">{{ loop.index }}</span>
<a href="/details/{{ article.id }}.html">{{ article.title }}</a>
<span class="views">{{ article.clickNum }} 浏览</span>
</div>
{% endfor %}
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 是 | 数据变量名 |
pageSize | Number | 否 | 获取数量,默认 10 |
typeId | String | 否 | 分类 ID |
isPaging | String | 否 | 是否启用分页 |
使用示例
<!-- 热门文章排行 -->
{% hot key="hotItemListData", pageSize="9" %}
<!-- 指定分类的热门文章 -->
{% hot key="categoryHot", typeId="4", pageSize="5" %}
<!-- 分页热门文章 -->
{% hot key="pagedHot", pageSize="4", isPaging="1" %}排行榜展示
{% hot key="hotItemListData", pageSize="10" %}
{% if hotItemListData and hotItemListData.length > 0 %}
<table class="ranking-table">
<thead>
<tr>
<th>排名</th>
<th>标题</th>
<th>浏览量</th>
<th>点赞数</th>
</tr>
</thead>
<tbody>
{% for item in hotItemListData %}
<tr>
<td>
{% if loop.index0 == 0 %}🥇
{% elif loop.index0 == 1 %}🥈
{% elif loop.index0 == 2 %}🥉
{% else %}{{ loop.index }}
{% endif %}
</td>
<td><a href="/details/{{ item.id }}.html">{{ item.title }}</a></td>
<td>{{ item.clickNum }}</td>
<td>{{ item.likeNum }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}3. recommend - 推荐文章
获取标记为推荐的文章,用于特色内容展示。
基础用法
{% recommend key="reCommendList", pageSize="8" %}
{% if reCommendList and reCommendList.length > 0 %}
{% for article in reCommendList %}
<div class="recommend-card">
{% if article.sImg %}
<img src="{{ article.sImg }}" alt="{{ article.title }}">
{% endif %}
<h3>{{ article.title }}</h3>
<p>{{ article.discription | cutwords(100) }}</p>
</div>
{% endfor %}
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 是 | 数据变量名 |
pageSize | Number | 否 | 获取数量 |
typeId | String | 否 | 分类 ID |
isPaging | String | 否 | 是否分页 |
使用示例
<!-- 推荐文章列表 -->
{% recommend key="reCommendList", pageSize="8" %}
<!-- 指定分类的推荐内容 -->
{% recommend key="featuredContent", typeId="4", pageSize="5" %}
<!-- 不分页获取 -->
{% recommend key="allRecommend", pageSize="15", isPaging="0" %}推荐文章特殊属性
推荐文章包含以下特殊属性,可用于条件判断和展示:
| 属性 | 说明 |
|---|---|
isTop | 是否置顶,1 表示置顶 |
roofPlacement | 是否推荐到首页,"1" 表示是 |
state | 文章状态,"2" 表示已发布 |
favoriteNum | 收藏数 |
{% for item in reCommendList %}
{% if item.isTop == 1 %}
<span class="badge-top">置顶</span>
{% endif %}
{% if item.roofPlacement == "1" %}
<span class="badge-featured">首页推荐</span>
{% endif %}
{% endfor %}4. random - 随机文章
随机获取文章,每次刷新都会显示不同内容,适用于"猜你喜欢"、"随机推荐"等场景。
基础用法
{% random pageSize="6" %}
{% if random and random.length > 0 %}
<div class="random-posts">
{% for article in random %}
<div class="random-item">
<h4>{{ article.title }}</h4>
<p>{{ article.discription | cutwords(80) }}</p>
<a href="/details/{{ article.id }}.html">阅读更多</a>
</div>
{% endfor %}
</div>
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 否 | 数据变量名,不指定默认为 random |
pageSize | Number | 否 | 获取数量 |
typeId | String | 否 | 分类 ID |
使用示例
<!-- 基础随机文章 -->
{% random pageSize="6" %}
<!-- 指定变量名的随机文章 -->
{% random key="randomPosts", pageSize="4" %}
<!-- 指定分类的随机文章 -->
{% random key="categoryRandom", typeId="1", pageSize="3" %}刷新按钮
<button onclick="location.reload()" class="refresh-btn">
🔄 刷新获取新内容
</button>5. nearpost - 文章导航(上一篇/下一篇)
在文章详情页获取相邻文章,实现文章间的导航。
基础用法
{% nearpost key="navigation", id="{{ post.id }}" %}
{% if navigation %}
<nav class="post-navigation">
{% if navigation.prePost %}
<div class="nav-prev">
<a href="/details/{{ navigation.prePost.id }}.html">
<span class="direction">← 上一篇</span>
<span class="title">{{ navigation.prePost.title }}</span>
</a>
</div>
{% endif %}
{% if navigation.nextPost %}
<div class="nav-next">
<a href="/details/{{ navigation.nextPost.id }}.html">
<span class="title">{{ navigation.nextPost.title }}</span>
<span class="direction">下一篇 →</span>
</a>
</div>
{% endif %}
</nav>
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 是 | 数据变量名 |
id | String | 是 | 当前文章 ID |
数据结构
{
"prePost": {
"id": "E1LuMa5ee",
"title": "上一篇文章标题",
"discription": "文章描述",
"createdAt": "2025-01-01T00:00:00.000Z",
"clickNum": 100
},
"nextPost": {
"id": "E1jEavW5",
"title": "下一篇文章标题",
"discription": "文章描述",
"createdAt": "2025-01-02T00:00:00.000Z",
"clickNum": 150
}
}完整示例
{% nearpost key="navigation", id="{{ post.id }}" %}
{% if navigation %}
<div class="post-navigation">
{% if navigation.prePost %}
<div class="nav-prev">
<div class="nav-direction">← 上一篇</div>
<a href="/details/{{ navigation.prePost.id }}.html">
<h6>{{ navigation.prePost.title }}</h6>
{% if navigation.prePost.discription %}
<p>{{ navigation.prePost.discription | cutwords(60) }}</p>
{% endif %}
<div class="nav-meta">
<span>{{ navigation.prePost.createdAt | date('YYYY-MM-DD') }}</span>
<span>{{ navigation.prePost.clickNum }} 浏览</span>
</div>
</a>
</div>
{% else %}
<div class="nav-prev nav-empty">
<p>已经是第一篇文章了</p>
</div>
{% endif %}
{% if navigation.nextPost %}
<div class="nav-next">
<div class="nav-direction">下一篇 →</div>
<a href="/details/{{ navigation.nextPost.id }}.html">
<h6>{{ navigation.nextPost.title }}</h6>
{% if navigation.nextPost.discription %}
<p>{{ navigation.nextPost.discription | cutwords(60) }}</p>
{% endif %}
<div class="nav-meta">
<span>{{ navigation.nextPost.createdAt | date('YYYY-MM-DD') }}</span>
<span>{{ navigation.nextPost.clickNum }} 浏览</span>
</div>
</a>
</div>
{% else %}
<div class="nav-next nav-empty">
<p>已经是最后一篇文章了</p>
</div>
{% endif %}
</div>
{% endif %}分类导航标签
6. navtree - 分类树导航
获取完整的分类树结构,支持多级分类展示。
基础用法
{% navtree key="categories" %}
{% if categories and categories.length > 0 %}
<ul class="category-tree">
{% for category in categories %}
<li>
<a href="{{ category.url }}">{{ category.name }}</a>
{% if category.children and category.children.length > 0 %}
<ul class="sub-categories">
{% for child in category.children %}
<li>
<a href="{{ child.url }}">{{ child.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 是 | 数据变量名 |
parentId | String | 否 | 父分类 ID,获取指定分类的子分类 |
使用示例
<!-- 获取完整分类树 -->
{% navtree key="categories" %}
<!-- 获取指定分类的子分类 -->
{% navtree key="subCategories", parentId="1" %}数据结构
[
{
"id": "Nycd05pP",
"name": "前端开发",
"url": "/frontend___Nycd05pP",
"icon": "fa fa-code",
"comments": "前端技术文章",
"contentCount": 25,
"enable": true,
"depth": 1,
"sortId": 1,
"children": [
{
"id": "N1xdR5ap",
"name": "Vue.js",
"url": "/vue___N1xdR5ap",
"contentCount": 12,
"enable": true,
"depth": 2,
"children": []
}
]
}
]高级示例 - 多级菜单
{% navtree key="categories" %}
{% if categories and categories.length > 0 %}
<ul class="menu">
{% for category in categories %}
<li>
{% if category.children and category.children.length > 0 %}
<details>
<summary>
{% if category.icon %}
<i class="{{ category.icon }}"></i>
{% endif %}
<a href="{{ category.url }}">{{ category.name }}</a>
<span class="badge">{{ category.contentCount }}</span>
</summary>
{% if category.comments %}
<div class="description">{{ category.comments }}</div>
{% endif %}
<ul>
{% for child in category.children %}
<li>
{% if child.children and child.children.length > 0 %}
<details>
<summary>
<a href="{{ child.url }}">{{ child.name }}</a>
<span class="badge">{{ child.contentCount }}</span>
</summary>
<ul>
{% for grandchild in child.children %}
<li>
<a href="{{ grandchild.url }}">{{ grandchild.name }}</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<a href="{{ child.url }}">
{{ child.name }}
<span class="badge">{{ child.contentCount }}</span>
</a>
{% endif %}
</li>
{% endfor %}
</ul>
</details>
{% else %}
<a href="{{ category.url }}">
{% if category.icon %}
<i class="{{ category.icon }}"></i>
{% endif %}
{{ category.name }}
<span class="badge">{{ category.contentCount }}</span>
</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}7. childnav - 子分类导航
获取指定分类下的所有子分类,适用于侧边栏导航、分类页面等场景。
基础用法
{% childnav key="subCategories", typeId="Nycd05pP" %}
{% if subCategories and subCategories.length > 0 %}
<nav class="sub-navigation">
{% for cateItem in subCategories %}
{% if cateItem.parentId != '0' %}
<a href="/{{ cateItem.defaultUrl }}___{{ cateItem.id }}">
{% if cateItem.icon %}
<i class="{{ cateItem.icon }}"></i>
{% endif %}
{{ cateItem.name }}
<span class="count">({{ cateItem.contentCount }})</span>
</a>
{% endif %}
{% endfor %}
</nav>
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 是 | 数据变量名 |
typeId | String | 否 | 父分类 ID,不传则获取所有分类 |
使用示例
<!-- 获取指定分类的子分类 -->
{% childnav key="frontDev", typeId="Nycd05pP" %}
<!-- 获取所有分类(包含顶级和子级) -->
{% childnav key="allCates" %}卡片式布局
{% childnav key="subCategories", typeId="Nycd05pP" %}
{% if subCategories and subCategories.length > 0 %}
<div class="category-cards">
{% for cateItem in subCategories %}
{% if cateItem.parentId != '0' %}
<div class="category-card">
<div class="card-header">
{% if cateItem.icon %}
<i class="{{ cateItem.icon }}"></i>
{% endif %}
<h6>{{ cateItem.name }}</h6>
</div>
<div class="card-body">
{% if cateItem.comments %}
<p>{{ cateItem.comments | cutwords(80) }}</p>
{% endif %}
<div class="card-meta">
<span>排序: {{ cateItem.sortId }}</span>
<span>类型: {{ cateItem.type }}</span>
<span>状态: {{ "启用" if cateItem.enable else "禁用" }}</span>
</div>
</div>
<div class="card-footer">
<a href="/{{ cateItem.defaultUrl }}___{{ cateItem.id }}">
查看文章 ({{ cateItem.contentCount }}) →
</a>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}标签类标签
8. tags - 标签云
获取所有标签,支持标签云、标签列表等多种展示方式。
基础用法
{% tags key="allTags" %}
{% if allTags and allTags.length > 0 %}
<div class="tag-cloud">
{% for tag in allTags %}
<a href="{{ tag.url }}" class="tag-item">
{{ tag.name }}
{% if tag.refCount %}
<span class="count">{{ tag.refCount }}</span>
{% endif %}
</a>
{% endfor %}
</div>
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 是 | 数据变量名 |
pageSize | Number | 否 | 获取数量 |
isPaging | String | 否 | 是否分页 |
使用示例
<!-- 获取所有标签 -->
{% tags key="allTags" %}
<!-- 限制数量 -->
{% tags key="limitedTags", pageSize="20" %}
<!-- 启用分页 -->
{% tags key="pagedTags", isPaging="1", pageSize="10" %}数据结构
[
{
"id": "VkXOC5Tp",
"name": "JavaScript",
"alias": "javascript",
"description": "JavaScript 编程语言",
"url": "/tags/javascript",
"refCount": 25,
"enable": 1,
"sortId": 1
}
]标签云样式
{% tags key="allTags" %}
{% if allTags and allTags.length > 0 %}
<div class="tag-cloud">
{% for tag in allTags %}
{% set refCount = tag.refCount or 0 %}
{% set size = 'sm' %}
{% if refCount > 20 %}
{% set size = 'xl' %}
{% elif refCount > 10 %}
{% set size = 'lg' %}
{% elif refCount > 5 %}
{% set size = 'md' %}
{% endif %}
<a href="{{ tag.url }}" class="tag-{{ size }}">
{{ tag.name }}
<span class="count">{{ refCount }}</span>
</a>
{% endfor %}
</div>
{% endif %}统计分析
{% tags key="allTags" %}
{% if allTags and allTags.length > 0 %}
<div class="tag-stats">
<div class="stat-item">
<h6>总标签数</h6>
<span class="value">{{ allTags.length }}</span>
</div>
<div class="stat-item">
<h6>启用标签</h6>
{% set enabledCount = 0 %}
{% for tag in allTags %}
{% if tag.enable == 1 %}
{% set enabledCount = enabledCount + 1 %}
{% endif %}
{% endfor %}
<span class="value">{{ enabledCount }}</span>
</div>
<div class="stat-item">
<h6>总引用次数</h6>
{% set totalRefs = 0 %}
{% for tag in allTags %}
{% set totalRefs = totalRefs + (tag.refCount or 0) %}
{% endfor %}
<span class="value">{{ totalRefs }}</span>
</div>
</div>
{% endif %}9. hottags - 热门标签
根据引用次数获取热门标签,自动按热度排序。
基础用法
{% hottags pageSize="20" %}
{% if hottags and hottags.length > 0 %}
<div class="hot-tags">
{% for tag in hottags %}
<a href="{{ tag.url }}" class="hot-tag">
<span class="name">{{ tag.name }}</span>
<span class="count">{{ tag.refCount }}</span>
</a>
{% endfor %}
</div>
{% endif %}参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 否 | 数据变量名,不指定默认为 hottags |
pageSize | Number | 否 | 获取数量 |
使用示例
<!-- 热门标签云 -->
{% hottags pageSize="20" %}
<!-- 指定变量名 -->
{% hottags key="popularTags", pageSize="15" %}排行榜展示
{% hottags pageSize="10" %}
{% if hottags and hottags.length > 0 %}
<div class="hot-tags-ranking">
<h4>🔥 热门标签排行榜</h4>
<ol class="ranking-list">
{% for tag in hottags %}
<li class="ranking-item {% if loop.index0 < 3 %}top-three{% endif %}">
<span class="rank">
{% if loop.index0 == 0 %}🥇
{% elif loop.index0 == 1 %}🥈
{% elif loop.index0 == 2 %}🥉
{% else %}{{ loop.index }}
{% endif %}
</span>
<a href="{{ tag.url }}" class="tag-name">{{ tag.name }}</a>
<span class="tag-count">{{ tag.refCount }} 次引用</span>
</li>
{% endfor %}
</ol>
</div>
{% endif %}广告类标签
10. ads - 广告位
注意
ads 标签是通过 Macro 实现的,需要先导入宏再使用。
基础用法
{# 导入广告宏 #}
{% from "path/to/ads-macro.html" import renderAds %}
{# 使用广告标签(需要在后端传递 adsData) #}
{{ renderAds(adsData) }}
{# 带自定义样式 #}
{{ renderAds(adsData, className="my-ads", style="margin: 20px 0;") }}数据结构
{
"name": "首页顶部广告",
"items": [
{
"title": "广告标题",
"sImg": "广告图片URL",
"link": "广告链接",
"target": "_blank",
"alt": "图片描述",
"width": 1200,
"height": 400
}
]
}特性
- 单张图片时显示普通图片
- 多张图片时自动显示轮播
- 支持点击跳转
- 支持自定义样式
完整示例
{% from "./ads-macro.html" import renderAds %}
<!-- 单张广告 -->
{% if bannerAds %}
{{ renderAds(bannerAds, className="banner-ads") }}
{% endif %}
<!-- 轮播广告 -->
{% if carouselAds %}
{{ renderAds(carouselAds, className="carousel-ads") }}
{% endif %}
<!-- 侧边栏广告(紧凑版) -->
{% from "./ads-macro.html" import adsCompact %}
{% if sidebarAds %}
{{ adsCompact(sidebarAds) }}
{% endif %}内置过滤器
DoraCMS 提供了一些实用的 Nunjucks 过滤器来处理数据:
cutwords - 文本截取
截取指定长度的文本,并在末尾添加省略号。
用法
{{ article.discription | cutwords(100) }}示例
<!-- 截取描述文字 -->
<p>{{ article.discription | cutwords(80) }}</p>
<!-- 截取标题 -->
<h3>{{ article.title | cutwords(30) }}</h3>date - 日期格式化
格式化日期时间。
用法
{{ article.createdAt | date('YYYY-MM-DD') }}常用格式
| 格式 | 示例输出 |
|---|---|
YYYY-MM-DD | 2025-01-01 |
YYYY-MM-DD HH:mm:ss | 2025-01-01 12:30:45 |
MM-DD | 01-01 |
HH:mm | 12:30 |
示例
<!-- 完整日期 -->
<time>{{ article.createdAt | date('YYYY-MM-DD HH:mm:ss') }}</time>
<!-- 简短日期 -->
<span>{{ article.createdAt | date('MM-DD') }}</span>
<!-- 当前时间 -->
<span>{{ "now" | date("HH:mm") }}</span>dump - 调试输出
将对象转换为 JSON 字符串,用于调试。
用法
{{ article | dump }}示例
<!-- 查看完整数据结构 -->
<details>
<summary>查看数据</summary>
<pre>{{ article | dump }}</pre>
</details>
<!-- 安全输出(不转义) -->
<pre>{{ article | dump | safe }}</pre>Nunjucks 内置功能
除了 DoraCMS 提供的自定义标签,你还可以使用 Nunjucks 的所有内置功能:
条件判断
{% if article.isTop == 1 %}
<span class="badge-top">置顶</span>
{% endif %}
{% if article.clickNum > 1000 %}
<span class="badge-hot">热门</span>
{% elif article.clickNum > 100 %}
<span class="badge-warm">热</span>
{% else %}
<span class="badge-normal">普通</span>
{% endif %}循环
{% for article in articles %}
<div class="article-item">
<span class="index">{{ loop.index }}</span>
<h3>{{ article.title }}</h3>
{% if loop.first %}
<span class="badge">最新</span>
{% endif %}
</div>
{% endfor %}循环变量
| 变量 | 说明 |
|---|---|
loop.index | 当前迭代索引(从 1 开始) |
loop.index0 | 当前迭代索引(从 0 开始) |
loop.first | 是否第一次迭代 |
loop.last | 是否最后一次迭代 |
loop.length | 序列长度 |
模板继承
布局文件 layouts/default.html
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}默认标题{% endblock %}</title>
{% block styles %}{% endblock %}
</head>
<body>
<header>
{% include "partials/header.html" %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% include "partials/footer.html" %}
</footer>
{% block scripts %}{% endblock %}
</body>
</html>页面文件 pages/index.html
{% extends "layouts/default.html" %}
{% block title %}首页{% endblock %}
{% block content %}
<h1>欢迎来到 DoraCMS</h1>
{% news key="latestNews", pageSize="10" %}
{% if latestNews and latestNews.length > 0 %}
<div class="news-list">
{% for article in latestNews %}
<div class="news-item">
<h3>{{ article.title }}</h3>
<p>{{ article.discription | cutwords(100) }}</p>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}包含(Include)
<!-- 包含侧边栏 -->
{% include "partials/sidebar.html" %}
<!-- 带变量的包含 -->
{% include "partials/article-card.html" with { article: item } %}宏(Macro)
{# 定义宏 #}
{% macro articleCard(article, showImage=true) %}
<div class="article-card">
{% if showImage and article.sImg %}
<img src="{{ article.sImg }}" alt="{{ article.title }}">
{% endif %}
<h3>{{ article.title }}</h3>
<p>{{ article.discription | cutwords(100) }}</p>
<a href="/details/{{ article.id }}.html">阅读更多</a>
</div>
{% endmacro %}
{# 使用宏 #}
{% for article in articles %}
{{ articleCard(article) }}
{% endfor %}
{# 带参数使用 #}
{{ articleCard(article, showImage=false) }}实战示例
首页布局
{% extends "../layouts/default.html" %}
{% block content %}
<!-- Hero 区域 -->
<div class="hero">
<h1>欢迎来到我的博客</h1>
<p>分享技术,记录生活</p>
</div>
<!-- 推荐文章 -->
<section class="featured-section">
<h2>⭐ 精选推荐</h2>
{% recommend key="featured", pageSize="3" %}
{% if featured and featured.length > 0 %}
<div class="featured-grid">
{% for article in featured %}
<div class="featured-card">
{% if article.sImg %}
<img src="{{ article.sImg }}" alt="{{ article.title }}">
{% endif %}
<h3>{{ article.title }}</h3>
<p>{{ article.discription | cutwords(120) }}</p>
<a href="/details/{{ article.id }}.html">阅读全文 →</a>
</div>
{% endfor %}
</div>
{% endif %}
</section>
<!-- 最新文章 -->
<section class="latest-section">
<h2>📰 最新文章</h2>
{% news key="latestNews", pageSize="10" %}
{% if latestNews and latestNews.length > 0 %}
<div class="article-list">
{% for article in latestNews %}
<article class="article-item">
<div class="article-content">
<h3><a href="/details/{{ article.id }}.html">{{ article.title }}</a></h3>
<p>{{ article.discription | cutwords(150) }}</p>
<div class="article-meta">
<span>📅 {{ article.createdAt | date('YYYY-MM-DD') }}</span>
<span>👁️ {{ article.clickNum }} 浏览</span>
<span>💬 {{ article.commentNum }} 评论</span>
</div>
{% if article.tags and article.tags.length > 0 %}
<div class="article-tags">
{% for tag in article.tags %}
<a href="{{ tag.url }}" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% if article.sImg %}
<div class="article-image">
<img src="{{ article.sImg }}" alt="{{ article.title }}">
</div>
{% endif %}
</article>
{% endfor %}
</div>
{% endif %}
</section>
<!-- 侧边栏 -->
<aside class="sidebar">
<!-- 热门文章 -->
<div class="widget">
<h3>🔥 热门文章</h3>
{% hot key="hotList", pageSize="5" %}
{% if hotList and hotList.length > 0 %}
<ol class="hot-list">
{% for article in hotList %}
<li class="hot-item {% if loop.index0 < 3 %}top-three{% endif %}">
<span class="rank">{{ loop.index }}</span>
<a href="/details/{{ article.id }}.html">{{ article.title | cutwords(30) }}</a>
<span class="views">{{ article.clickNum }}</span>
</li>
{% endfor %}
</ol>
{% endif %}
</div>
<!-- 热门标签 -->
<div class="widget">
<h3>🏷️ 热门标签</h3>
{% hottags pageSize="20" %}
{% if hottags and hottags.length > 0 %}
<div class="tag-cloud">
{% for tag in hottags %}
<a href="{{ tag.url }}" class="tag">
{{ tag.name }}
<span class="count">{{ tag.refCount }}</span>
</a>
{% endfor %}
</div>
{% endif %}
</div>
<!-- 分类导航 -->
<div class="widget">
<h3>📁 分类导航</h3>
{% navtree key="categories" %}
{% if categories and categories.length > 0 %}
<ul class="category-list">
{% for category in categories %}
<li>
<a href="{{ category.url }}">
{{ category.name }}
<span class="count">({{ category.contentCount }})</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</aside>
{% endblock %}文章详情页
{% extends "../layouts/default.html" %}
{% block title %}{{ post.title }} - 我的博客{% endblock %}
{% block content %}
<article class="post-detail">
<!-- 文章头部 -->
<header class="post-header">
<h1 class="post-title">{{ post.title }}</h1>
<div class="post-meta">
<span>👤 {{ post.author.userName }}</span>
<span>📅 {{ post.createdAt | date('YYYY-MM-DD HH:mm') }}</span>
<span>👁️ {{ post.clickNum }} 浏览</span>
<span>💬 {{ post.commentNum }} 评论</span>
<span>❤️ {{ post.likeNum }} 点赞</span>
</div>
<!-- 分类和标签 -->
<div class="post-taxonomy">
{% if post.categories and post.categories.length > 0 %}
<div class="categories">
📁 分类:
{% for category in post.categories %}
<a href="{{ category.url }}">{{ category.name }}</a>
{% endfor %}
</div>
{% endif %}
{% if post.tags and post.tags.length > 0 %}
<div class="tags">
🏷️ 标签:
{% for tag in post.tags %}
<a href="{{ tag.url }}">{{ tag.name }}</a>
{% endfor %}
</div>
{% endif %}
</div>
</header>
<!-- 文章内容 -->
<div class="post-content">
{{ post.content | safe }}
</div>
<!-- 文章导航 -->
{% nearpost key="navigation", id="{{ post.id }}" %}
{% if navigation %}
<nav class="post-navigation">
{% if navigation.prePost %}
<a href="/details/{{ navigation.prePost.id }}.html" class="nav-prev">
<div class="nav-label">← 上一篇</div>
<div class="nav-title">{{ navigation.prePost.title }}</div>
</a>
{% endif %}
{% if navigation.nextPost %}
<a href="/details/{{ navigation.nextPost.id }}.html" class="nav-next">
<div class="nav-label">下一篇 →</div>
<div class="nav-title">{{ navigation.nextPost.title }}</div>
</a>
{% endif %}
</nav>
{% endif %}
<!-- 相关推荐 -->
<section class="related-posts">
<h3>📖 相关推荐</h3>
{% random key="related", typeId="{{ post.categories[0].id }}", pageSize="4" %}
{% if related and related.length > 0 %}
<div class="related-grid">
{% for article in related %}
{% if article.id != post.id %}
<div class="related-card">
{% if article.sImg %}
<img src="{{ article.sImg }}" alt="{{ article.title }}">
{% endif %}
<h4><a href="/details/{{ article.id }}.html">{{ article.title }}</a></h4>
<p>{{ article.discription | cutwords(60) }}</p>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</section>
</article>
{% endblock %}分类页面
{% extends "../layouts/default.html" %}
{% block title %}{{ category.name }} - 我的博客{% endblock %}
{% block content %}
<div class="category-page">
<!-- 分类信息 -->
<header class="category-header">
<h1>{{ category.name }}</h1>
{% if category.comments %}
<p class="category-description">{{ category.comments }}</p>
{% endif %}
<div class="category-stats">
<span>📊 文章总数: {{ category.contentCount }}</span>
</div>
</header>
<!-- 子分类导航 -->
{% childnav key="subCategories", typeId="{{ category.id }}" %}
{% if subCategories and subCategories.length > 0 %}
<nav class="sub-categories">
<h3>📁 子分类</h3>
<div class="sub-category-list">
{% for subCat in subCategories %}
{% if subCat.parentId != '0' %}
<a href="/{{ subCat.defaultUrl }}___{{ subCat.id }}" class="sub-category-card">
{% if subCat.icon %}
<i class="{{ subCat.icon }}"></i>
{% endif %}
<span class="name">{{ subCat.name }}</span>
<span class="count">{{ subCat.contentCount }}</span>
</a>
{% endif %}
{% endfor %}
</div>
</nav>
{% endif %}
<!-- 分类文章列表 -->
<section class="category-articles">
<h3>📄 文章列表</h3>
{% news key="categoryArticles", typeId="{{ category.id }}", pageSize="20" %}
{% if categoryArticles and categoryArticles.length > 0 %}
<div class="article-list">
{% for article in categoryArticles %}
<article class="article-card">
{% if article.sImg %}
<div class="article-image">
<img src="{{ article.sImg }}" alt="{{ article.title }}">
</div>
{% endif %}
<div class="article-content">
<h3><a href="/details/{{ article.id }}.html">{{ article.title }}</a></h3>
<p>{{ article.discription | cutwords(150) }}</p>
<div class="article-meta">
<span>{{ article.createdAt | date('YYYY-MM-DD') }}</span>
<span>{{ article.clickNum }} 浏览</span>
</div>
</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<p>该分类下暂无文章</p>
</div>
{% endif %}
</section>
<!-- 侧边栏 -->
<aside class="sidebar">
<!-- 热门文章 -->
<div class="widget">
<h4>🔥 本分类热门</h4>
{% hot key="categoryHot", typeId="{{ category.id }}", pageSize="5" %}
{% if categoryHot and categoryHot.length > 0 %}
<ol class="hot-list">
{% for article in categoryHot %}
<li>
<a href="/details/{{ article.id }}.html">{{ article.title | cutwords(30) }}</a>
</li>
{% endfor %}
</ol>
{% endif %}
</div>
</aside>
</div>
{% endblock %}最佳实践
1. 性能优化
合理设置 pageSize
<!-- ❌ 不好:一次加载过多数据 -->
{% news key="allNews", pageSize="1000" %}
<!-- ✅ 好:按需加载合适的数量 -->
{% news key="latestNews", pageSize="10" %}使用分页
<!-- ✅ 启用分页,按需加载 -->
{% news key="pagedNews", pageSize="20", isPaging="1" %}2. 数据判断
始终检查数据是否存在
<!-- ❌ 不好:直接使用可能为空的数据 -->
{% for article in articles %}
<h3>{{ article.title }}</h3>
{% endfor %}
<!-- ✅ 好:先判断数据存在性 -->
{% if articles and articles.length > 0 %}
{% for article in articles %}
<h3>{{ article.title }}</h3>
{% endfor %}
{% else %}
<p>暂无数据</p>
{% endif %}安全访问嵌套属性
<!-- ❌ 不好:直接访问可能不存在的嵌套属性 -->
<span>{{ article.author.userName }}</span>
<!-- ✅ 好:先判断存在性 -->
{% if article.author %}
<span>{{ article.author.userName }}</span>
{% endif %}
<!-- ✅ 也好:使用默认值 -->
<span>{{ article.author.userName or "匿名" }}</span>3. 代码复用
使用宏(Macro)
{# 定义可复用的文章卡片宏 #}
{% macro articleCard(article, layout='default') %}
<div class="article-card article-card-{{ layout }}">
{% if article.sImg %}
<img src="{{ article.sImg }}" alt="{{ article.title }}">
{% endif %}
<h3>{{ article.title }}</h3>
<p>{{ article.discription | cutwords(100) }}</p>
<a href="/details/{{ article.id }}.html">阅读更多</a>
</div>
{% endmacro %}
{# 在不同地方使用 #}
{{ articleCard(article) }}
{{ articleCard(article, layout='compact') }}使用 Include
{# partials/article-card.html #}
<div class="article-card">
<h3>{{ article.title }}</h3>
<p>{{ article.discription | cutwords(100) }}</p>
</div>
{# 在页面中使用 #}
{% for article in articles %}
{% include "partials/article-card.html" with { article: article } %}
{% endfor %}4. 语义化命名
<!-- ✅ 好:使用有意义的变量名 -->
{% news key="latestNews", pageSize="10" %}
{% hot key="hotArticles", pageSize="5" %}
{% recommend key="featuredPosts", pageSize="3" %}
<!-- ❌ 不好:使用模糊的变量名 -->
{% news key="data1", pageSize="10" %}
{% hot key="list", pageSize="5" %}5. 适当注释
{# 首页最新文章区域 #}
{% news key="latestNews", pageSize="10" %}
{% if latestNews and latestNews.length > 0 %}
<section class="latest-section">
{# 显示文章列表 #}
{% for article in latestNews %}
<article class="article-item">
<h3>{{ article.title }}</h3>
</article>
{% endfor %}
</section>
{% endif %}常见问题
1. 为什么我的标签没有返回数据?
可能原因:
- 变量名拼写错误
- 数据库中没有符合条件的数据
- 分类 ID 不存在
解决方案:
{# 检查数据是否存在 #}
{% news key="latestNews", pageSize="10" %}
{% if latestNews %}
{% if latestNews.length > 0 %}
{# 有数据 #}
<p>找到 {{ latestNews.length }} 篇文章</p>
{% else %}
{# 数据为空数组 #}
<p>暂无文章</p>
{% endif %}
{% else %}
{# 变量不存在 #}
<p>数据加载失败</p>
{% endif %}2. 如何在文章中显示 HTML 内容?
使用 | safe 过滤器:
<!-- 不会解析 HTML 标签 -->
{{ article.content }}
<!-- 会解析 HTML 标签 -->
{{ article.content | safe }}3. 如何实现分页?
DoraCMS 的模板标签支持分页参数,但具体的分页逻辑需要在后端处理:
{% news key="pagedNews", pageSize="20", isPaging="1" %}在控制器中处理分页逻辑并传递分页信息到模板。
4. 如何调试模板?
使用 dump 过滤器查看数据结构:
<pre>{{ article | dump }}</pre>
<!-- 或在浏览器控制台查看 -->
<script>
console.log('Article data:', {{ article | dump | safe }});
</script>5. 标签和过滤器的区别?
- 标签(Tag):用
{% %}包裹,用于控制流程和获取数据 - 过滤器(Filter):用
|符号,用于转换数据
{# 标签:获取数据 #}
{% news key="latestNews", pageSize="10" %}
{# 过滤器:转换数据 #}
{{ article.discription | cutwords(100) }}
{{ article.createdAt | date('YYYY-MM-DD') }}进一步学习
- Nunjucks 官方文档
- Nunjucks 模板语法
- DoraCMS 完整标签 Demo
- DoraCMS 模板标签源码示例:
/server/app/view/nunjucks-template-test
小结
DoraCMS 的模板系统功能强大、易于使用:
✅ 丰富的内置标签 - 覆盖文章、分类、标签、导航等常见需求
✅ 灵活的参数配置 - 支持分页、筛选、排序等多种选项
✅ 强大的 Nunjucks 引擎 - 支持模板继承、宏、过滤器等高级特性
✅ 双数据库支持 - MongoDB 和 MariaDB 使用相同的模板标签
通过合理使用这些标签和遵循最佳实践,你可以快速构建功能完善、性能优异的网站前端。