Skip to content

模板系统

DoraCMS 采用 Nunjucks 模板引擎,提供了一套强大的自定义模板标签,让你能够轻松实现各种内容展示需求。

概览

DoraCMS 模板系统基于 Nunjucks,并扩展了专门用于内容管理的自定义标签。这些标签可以帮助你:

  • 🚀 快速展示各类内容(文章、分类、标签等)
  • 🎨 灵活控制展示样式和数量
  • 🔄 支持分页和数据过滤
  • 📊 自动处理数据关联

核心标签

文章类标签

1. news - 最新文章

获取并展示最新发布的文章列表。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名,用于在模板中访问数据
pageSizeNumber获取数量,默认 10
typeIdString分类 ID,筛选指定分类下的文章
isPagingString是否启用分页,"1" 启用,"0" 不启用

使用示例

njk
<!-- 获取首页最新文章 -->
{% news key="latestNews", pageSize="10" %}

<!-- 获取指定分类的最新文章 -->
{% news key="categoryNews", typeId="4", pageSize="5" %}

<!-- 启用分页的最新文章 -->
{% news key="pagedNews", pageSize="20", isPaging="1" %}

数据结构

json
[
  {
    "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 - 热门文章

根据浏览量获取热门文章,适用于热门推荐、排行榜等场景。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名
pageSizeNumber获取数量,默认 10
typeIdString分类 ID
isPagingString是否启用分页

使用示例

njk
<!-- 热门文章排行 -->
{% hot key="hotItemListData", pageSize="9" %}

<!-- 指定分类的热门文章 -->
{% hot key="categoryHot", typeId="4", pageSize="5" %}

<!-- 分页热门文章 -->
{% hot key="pagedHot", pageSize="4", isPaging="1" %}

排行榜展示

njk
{% 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 - 推荐文章

获取标记为推荐的文章,用于特色内容展示。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名
pageSizeNumber获取数量
typeIdString分类 ID
isPagingString是否分页

使用示例

njk
<!-- 推荐文章列表 -->
{% 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收藏数
njk
{% 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 - 随机文章

随机获取文章,每次刷新都会显示不同内容,适用于"猜你喜欢"、"随机推荐"等场景。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名,不指定默认为 random
pageSizeNumber获取数量
typeIdString分类 ID

使用示例

njk
<!-- 基础随机文章 -->
{% random pageSize="6" %}

<!-- 指定变量名的随机文章 -->
{% random key="randomPosts", pageSize="4" %}

<!-- 指定分类的随机文章 -->
{% random key="categoryRandom", typeId="1", pageSize="3" %}

刷新按钮

html
<button onclick="location.reload()" class="refresh-btn">
  🔄 刷新获取新内容
</button>

5. nearpost - 文章导航(上一篇/下一篇)

在文章详情页获取相邻文章,实现文章间的导航。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名
idString当前文章 ID

数据结构

json
{
  "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
  }
}

完整示例

njk
{% 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 - 分类树导航

获取完整的分类树结构,支持多级分类展示。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名
parentIdString父分类 ID,获取指定分类的子分类

使用示例

njk
<!-- 获取完整分类树 -->
{% navtree key="categories" %}

<!-- 获取指定分类的子分类 -->
{% navtree key="subCategories", parentId="1" %}

数据结构

json
[
  {
    "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": []
      }
    ]
  }
]

高级示例 - 多级菜单

njk
{% 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 - 子分类导航

获取指定分类下的所有子分类,适用于侧边栏导航、分类页面等场景。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名
typeIdString父分类 ID,不传则获取所有分类

使用示例

njk
<!-- 获取指定分类的子分类 -->
{% childnav key="frontDev", typeId="Nycd05pP" %}

<!-- 获取所有分类(包含顶级和子级) -->
{% childnav key="allCates" %}

卡片式布局

njk
{% 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 - 标签云

获取所有标签,支持标签云、标签列表等多种展示方式。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名
pageSizeNumber获取数量
isPagingString是否分页

使用示例

njk
<!-- 获取所有标签 -->
{% tags key="allTags" %}

<!-- 限制数量 -->
{% tags key="limitedTags", pageSize="20" %}

<!-- 启用分页 -->
{% tags key="pagedTags", isPaging="1", pageSize="10" %}

数据结构

json
[
  {
    "id": "VkXOC5Tp",
    "name": "JavaScript",
    "alias": "javascript",
    "description": "JavaScript 编程语言",
    "url": "/tags/javascript",
    "refCount": 25,
    "enable": 1,
    "sortId": 1
  }
]

标签云样式

njk
{% 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 %}

统计分析

njk
{% 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 - 热门标签

根据引用次数获取热门标签,自动按热度排序。

基础用法

njk
{% 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 %}

参数说明

参数类型必填说明
keyString数据变量名,不指定默认为 hottags
pageSizeNumber获取数量

使用示例

njk
<!-- 热门标签云 -->
{% hottags pageSize="20" %}

<!-- 指定变量名 -->
{% hottags key="popularTags", pageSize="15" %}

排行榜展示

njk
{% 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 实现的,需要先导入宏再使用。

基础用法

njk
{# 导入广告宏 #}
{% from "path/to/ads-macro.html" import renderAds %}

{# 使用广告标签(需要在后端传递 adsData) #}
{{ renderAds(adsData) }}

{# 带自定义样式 #}
{{ renderAds(adsData, className="my-ads", style="margin: 20px 0;") }}

数据结构

json
{
  "name": "首页顶部广告",
  "items": [
    {
      "title": "广告标题",
      "sImg": "广告图片URL",
      "link": "广告链接",
      "target": "_blank",
      "alt": "图片描述",
      "width": 1200,
      "height": 400
    }
  ]
}

特性

  • 单张图片时显示普通图片
  • 多张图片时自动显示轮播
  • 支持点击跳转
  • 支持自定义样式

完整示例

njk
{% 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 - 文本截取

截取指定长度的文本,并在末尾添加省略号。

用法

njk
{{ article.discription | cutwords(100) }}

示例

njk
<!-- 截取描述文字 -->
<p>{{ article.discription | cutwords(80) }}</p>

<!-- 截取标题 -->
<h3>{{ article.title | cutwords(30) }}</h3>

date - 日期格式化

格式化日期时间。

用法

njk
{{ article.createdAt | date('YYYY-MM-DD') }}

常用格式

格式示例输出
YYYY-MM-DD2025-01-01
YYYY-MM-DD HH:mm:ss2025-01-01 12:30:45
MM-DD01-01
HH:mm12:30

示例

njk
<!-- 完整日期 -->
<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 字符串,用于调试。

用法

njk
{{ article | dump }}

示例

njk
<!-- 查看完整数据结构 -->
<details>
  <summary>查看数据</summary>
  <pre>{{ article | dump }}</pre>
</details>

<!-- 安全输出(不转义) -->
<pre>{{ article | dump | safe }}</pre>

Nunjucks 内置功能

除了 DoraCMS 提供的自定义标签,你还可以使用 Nunjucks 的所有内置功能:

条件判断

njk
{% 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 %}

循环

njk
{% 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

njk
<!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

njk
{% 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)

njk
<!-- 包含侧边栏 -->
{% include "partials/sidebar.html" %}

<!-- 带变量的包含 -->
{% include "partials/article-card.html" with { article: item } %}

宏(Macro)

njk
{# 定义宏 #}
{% 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) }}

实战示例

首页布局

njk
{% 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 %}

文章详情页

njk
{% 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 %}

分类页面

njk
{% 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

njk
<!-- ❌ 不好:一次加载过多数据 -->
{% news key="allNews", pageSize="1000" %}

<!-- ✅ 好:按需加载合适的数量 -->
{% news key="latestNews", pageSize="10" %}

使用分页

njk
<!-- ✅ 启用分页,按需加载 -->
{% news key="pagedNews", pageSize="20", isPaging="1" %}

2. 数据判断

始终检查数据是否存在

njk
<!-- ❌ 不好:直接使用可能为空的数据 -->
{% 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 %}

安全访问嵌套属性

njk
<!-- ❌ 不好:直接访问可能不存在的嵌套属性 -->
<span>{{ article.author.userName }}</span>

<!-- ✅ 好:先判断存在性 -->
{% if article.author %}
  <span>{{ article.author.userName }}</span>
{% endif %}

<!-- ✅ 也好:使用默认值 -->
<span>{{ article.author.userName or "匿名" }}</span>

3. 代码复用

使用宏(Macro)

njk
{# 定义可复用的文章卡片宏 #}
{% 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

njk
{# 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. 语义化命名

njk
<!-- ✅ 好:使用有意义的变量名 -->
{% 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. 适当注释

njk
{# 首页最新文章区域 #}
{% 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. 为什么我的标签没有返回数据?

可能原因:

  1. 变量名拼写错误
  2. 数据库中没有符合条件的数据
  3. 分类 ID 不存在

解决方案:

njk
{# 检查数据是否存在 #}
{% 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 过滤器:

njk
<!-- 不会解析 HTML 标签 -->
{{ article.content }}

<!-- 会解析 HTML 标签 -->
{{ article.content | safe }}

3. 如何实现分页?

DoraCMS 的模板标签支持分页参数,但具体的分页逻辑需要在后端处理:

njk
{% news key="pagedNews", pageSize="20", isPaging="1" %}

在控制器中处理分页逻辑并传递分页信息到模板。


4. 如何调试模板?

使用 dump 过滤器查看数据结构:

njk
<pre>{{ article | dump }}</pre>

<!-- 或在浏览器控制台查看 -->
<script>
  console.log('Article data:', {{ article | dump | safe }});
</script>

5. 标签和过滤器的区别?

  • 标签(Tag):用 {% %} 包裹,用于控制流程和获取数据
  • 过滤器(Filter):用 | 符号,用于转换数据
njk
{# 标签:获取数据 #}
{% news key="latestNews", pageSize="10" %}

{# 过滤器:转换数据 #}
{{ article.discription | cutwords(100) }}
{{ article.createdAt | date('YYYY-MM-DD') }}

进一步学习


小结

DoraCMS 的模板系统功能强大、易于使用:

丰富的内置标签 - 覆盖文章、分类、标签、导航等常见需求
灵活的参数配置 - 支持分页、筛选、排序等多种选项
强大的 Nunjucks 引擎 - 支持模板继承、宏、过滤器等高级特性
双数据库支持 - MongoDB 和 MariaDB 使用相同的模板标签

通过合理使用这些标签和遵循最佳实践,你可以快速构建功能完善、性能优异的网站前端。