Docsme 为了能够让使用者可以开箱即用,在插件中包含了默认的页面模板。但这种方式可能造成与网站的页面样式不兼容,比如顶部菜单栏无法复用。所以在最新的版本中(1.0.0-alpha.14),我们重构了默认主题的模板结构,可以让主题开发者更加方便地为文档页面提供模板。

复用模板

在 1.0.0-alpha.14 中,我们提供了一些专门用于复用的模板,以减少主题的开发量。在下面的文档中,我们会先列出所有可复用的模板,再讲解如何在自己开发的主题中使用。

模板列表

modules/doc.html

文档页面,包含最基本的文档结构。

点击查看模板源码
<th:block th:fragment="doc (footer)">
  <div class="dm-layout">
    <th:block th:replace="~{plugin:plugin-docsme:modules/sidebar}" />

    <main class="dm-main">
      <div class="dm-content">
        <th:block th:replace="~{plugin:plugin-docsme:modules/content-header}" />

        <article
          data-toc-content
          class="dm-content__body prose-content line-numbers"
          th:utext="${docInfo.content.content}"
        ></article>

        <div
          th:if="${haloCommentEnabled}"
          id="comment"
          class="dm-content__comment"
        >
          <halo:comment
            group="doc.halo.run"
            kind="DocTree"
            th:attr="name=${docTree.metadata.name}"
          />
        </div>

        <th:block th:replace="~{plugin:plugin-docsme:modules/navigation}" />

        <th:block th:if="${footer != null}">
          <th:block th:replace="${footer}" />
        </th:block>
      </div>

      <aside
        class="dm-toc"
        data-toc-container
        dm-data
        dm-bind:class="{'dm-toc--show' : $store.toc.open}"
      >
        <div class="dm-toc__header">
          <h2>目录</h2>
        </div>
        <div class="dm-toc__body" data-toc-list></div>
      </aside>
      <div
        class="dm-toc-backdrop"
        dm-show="$store.toc.open"
        dm-on:click="$store.toc.open = false"
      ></div>
    </main>
  </div>
</th:block>

modules/doc-catalog.html

文档目录页面。

点击查看模板源码
<th:block th:fragment="doc-catalog (footer)">
  <div class="dm-layout">
    <th:block th:replace="~{plugin:plugin-docsme:modules/sidebar}" />

    <main class="dm-main">
      <div class="dm-content">
        <th:block th:replace="~{plugin:plugin-docsme:modules/content-header}" />

        <article class="dm-content__body">
          <div class="dm-directory-grid">
            <a
              th:each="node : ${sonNodes}"
              th:href="${node.status.permalink}"
              class="dm-directory-card"
            >
              <div
                class="dm-directory-card__icon"
                th:text="${node.spec.type == 'TREE' ? '📁' : '📝'}"
              >
                {{ item.icon }}
              </div>
              <h3 class="dm-directory-card__title" th:text="${node.spec.title}">
                {{ item.name }}
              </h3>
            </a>
          </div>
        </article>

        <th:block th:replace="~{plugin:plugin-docsme:modules/navigation}" />

        <th:block th:if="${footer != null}">
          <th:block th:replace="${footer}" />
        </th:block>
      </div>
    </main>
  </div>
</th:block>

modules/docs.html

文档项目列表页面。

点击查看模板源码
<th:block th:fragment="docs (footer,showHeader,showThemeSwitcher)">
  <header th:if="${showHeader}" class="dm-header">
    <div class="dm-header__content">
      <div class="dm-header__logo">
        <img th:unless="${#strings.isEmpty(site.logo)}" th:src="${site.logo}" />
        <a th:href="${site.url}" th:text="${site.title}"></a>
      </div>
      <nav class="dm-header__nav">
        <button
          th:if="${showThemeSwitcher}"
          class="dm-header__button"
          data-theme-switcher
        >
          <span
            class="theme-icon theme-icon--light icon-[mingcute--brightness-line]"
          ></span>
          <span
            class="theme-icon theme-icon--dark icon-[mingcute--moon-line]"
          ></span>
        </button>
      </nav>
    </div>
  </header>

  <main class="dm-main-content">
    <div class="dm-projects-header">
      <h1 class="dm-projects-header__title">文档</h1>
      <p class="dm-projects-header__description">所有文档项目</p>
    </div>

    <div class="dm-projects-grid">
      <a
        th:each="project : ${projects}"
        th:href="${project.status.permalink}"
        class="dm-project-card"
      >
        <div class="dm-project-card__banner">
          <div
            th:unless="${#strings.isEmpty(project.spec.icon)}"
            class="dm-project-card__icon"
          >
            <img
              th:src="${project.spec.icon}"
              th:alt="${project.spec.displayName}"
            />
          </div>
          <div
            th:if="${#strings.isEmpty(project.spec.icon)}"
            class="dm-project-card__icon"
          >
            📚
          </div>
        </div>
        <div class="dm-project-card__content">
          <h3
            class="dm-project-card__title"
            th:text="${project.spec.displayName}"
          ></h3>
          <ul class="dm-project-card__info">
            <li th:text="|共 ${project.status.totalDocs ?: 0} 篇文档|"></li>
            <li th:text="${project.status.permalink}"></li>
          </ul>
          <div class="dm-project-card__actions">
            <span class="dm-project-card__button">访问项目</span>
          </div>
        </div>
      </a>
    </div>

    <th:block th:if="${footer != null}">
      <th:block th:replace="${footer}" />
    </th:block>
  </main>
</th:block>

modules/content-header.html

文档内容顶部模块,包含面包屑和在移动端展开文档树和目录的按钮。

点击查看模板源码
<header class="dm-content__header">
  <nav class="dm-content__breadcrumb">
    <a th:href="${project.status.permalink}">首页</a>
    <a th:each="crumb : ${crumbs}" th:href="${crumb.status.permalink}" th:text="${crumb.spec.title}"></a>
  </nav>
  <div class="dm-content__controls">
    <button dm-data dm-on:click="$store.sidebar.open = !$store.sidebar.open">
      <span class="icon-[mingcute--menu-line]"></span>
      菜单
    </button>
    <button dm-data th:if="${docTree.spec.type == 'DOC'}" dm-on:click="$store.toc.open = !$store.toc.open">
      <span class="icon-[mingcute--list-ordered-line]"></span>
      本页目录
    </button>
  </div>
</header>

modules/doc-tree.html

左侧文档树模块。

点击查看模板源码
<details
  th:attr="open=${#arrays.contains(crumbs, parentDocTree) || currentDocTree.metadata.name == docTree.metadata.name}"
  th:id="${parentDocTree.metadata.name}"
  th:fragment="next (parentDocTree,docTrees)"
>
  <summary class="dm-nav-tree__toggle">
    <a
      th:href="${parentDocTree.status.permalink}"
      th:text="${parentDocTree.spec.title}"
      class="dm-nav-tree__link"
      th:classappend="${currentDocTree.metadata.name == docTree.metadata.name ? 'dm-nav-tree__link--active' : ''}"
    ></a>
  </summary>
  <ul class="dm-nav-tree dm-nav-tree__nested">
    <li class="dm-nav-tree__item" th:fragment="single (docTrees)" th:each="currentDocTree : ${docTrees}">
      <a
        class="dm-nav-tree__link"
        th:if="${currentDocTree.spec.type == 'DOC'}"
        th:href="@{${currentDocTree.status.permalink}}"
        th:title="${currentDocTree.spec.title}"
        th:classappend="${currentDocTree.metadata.name == docTree.metadata.name ? 'dm-nav-tree__link--active' : ''}"
        th:text="${currentDocTree.spec.title}"
      >
      </a>
      <th:block th:if="${currentDocTree.spec.type == 'TREE'}">
        <th:block
          th:replace="~{plugin:plugin-docsme:modules/doc-tree :: next (parentDocTree=${currentDocTree},docTrees=${currentDocTree.children})}"
        ></th:block>
      </th:block>
    </li>
  </ul>
</details>

modules/header.html

顶部菜单栏模块,包含文档 Logo、名称、文档版本切换按钮、语言切换按钮、明暗主题切换按钮。

点击查看模板源码
<th:block th:fragment="header(showThemeSwitcher)">
  <header class="dm-header">
    <div class="dm-header__content">
      <div class="dm-header__logo">
        <img
          th:unless="${#strings.isEmpty(project.spec.icon)}"
          th:src="${project.spec.icon}"
        />
        <a
          th:href="${project.status.permalink}"
          th:text="${project.spec.displayName}"
        ></a>
      </div>
      <nav class="dm-header__nav">
        <div
          th:if="${#lists.size(versions) gt 1}"
          dm-data="{
                          open: false,
                          toggle() {
                              if (this.open) {
                                  return this.close()
                              }
                              this.$refs.button.focus()
                              this.open = true
                          },
                          close(focusAfter) {
                              if (! this.open) return
              
                              this.open = false
                              focusAfter && focusAfter.focus()
                          }
                      }"
          dm-on:keydown.escape.prevent.stop="close($refs.button)"
          dm-on:focusin.window="$refs.panel && ! $refs.panel.contains($event.target) && close()"
          dm-id="['version-switcher']"
          class="dm-header__switcher"
        >
          <!-- Button -->
          <div
            dm-ref="button"
            dm-on:click="toggle()"
            :aria-expanded="open"
            :aria-controls="$id('version-switcher')"
            class="dm-header__switcher-button"
          >
            <span th:text="${currentVersion.spec.slug}"></span>
            <span
              class="dm-header__switcher-icon icon-[mingcute--down-line]"
            ></span>
          </div>
          <div
            dm-ref="panel"
            dm-show="open"
            dm-transition.origin.top.left
            dm-on:click.outside="close($refs.button)"
            :id="$id('version-switcher')"
            dm-cloak
            th:attr="dm-data=|{ currentVersionLink : '${currentVersion.status.permalink}' }|"
            class="dm-header__switcher-panel"
          >
            <a
              th:attr="dm-data=|{ versionLink : '${version.status.permalink}' }|"
              th:each="version : ${versions}"
              dm-bind:href="location.pathname.replace(currentVersionLink, versionLink)"
              th:text="${version.spec.slug}"
            >
            </a>
          </div>
        </div>

        <div
          th:if="${#lists.size(languages) gt 1}"
          dm-data="{
                          open: false,
                          toggle() {
                              if (this.open) {
                                  return this.close()
                              }
                              this.$refs.button.focus()
                              this.open = true
                          },
                          close(focusAfter) {
                              if (! this.open) return
              
                              this.open = false
                              focusAfter && focusAfter.focus()
                          }
                      }"
          dm-on:keydown.escape.prevent.stop="close($refs.button)"
          dm-on:focusin.window="$refs.panel && ! $refs.panel.contains($event.target) && close()"
          dm-id="['language-switcher']"
          class="dm-header__switcher"
        >
          <!-- Button -->
          <div
            dm-ref="button"
            dm-on:click="toggle()"
            :aria-expanded="open"
            :aria-controls="$id('language-switcher')"
            class="dm-header__switcher-button"
          >
            <span
              th:if="${currentLanguage != null}"
              th:text="${currentLanguage.label}"
            ></span>
            <span
              th:if="${currentLanguage == null}"
              class="icon-[mingcute--translate-2-line]"
            ></span>
            <span
              class="dm-header__switcher-icon icon-[mingcute--down-line]"
            ></span>
          </div>
          <div
            dm-ref="panel"
            dm-show="open"
            dm-transition.origin.top.left
            dm-on:click.outside="close($refs.button)"
            :id="$id('language-switcher')"
            dm-cloak
            th:attr="dm-data=|{ currentLanguageLink : '${currentLanguage.link}' }|"
            class="dm-header__switcher-panel"
          >
            <a
              th:attr="dm-data=|{ languageLink : '${language.link}' }|"
              th:each="language : ${languages}"
              dm-bind:href="location.pathname.replace(currentLanguageLink,languageLink)"
            >
              <span th:text="${language.language}"></span>
              <span th:text="${language.label}"></span>
            </a>
          </div>
        </div>
        <button
          th:if="${pluginFinder.available('PluginSearchWidget')}"
          id="btn-search"
          class="dm-header__button"
          title="搜索"
        >
          <span class="icon-[mingcute--search-line]"></span>
        </button>
        <button
          th:if="${showThemeSwitcher}"
          class="dm-header__button"
          data-theme-switcher
        >
          <span
            class="theme-icon theme-icon--light icon-[mingcute--brightness-line]"
          ></span>
          <span
            class="theme-icon theme-icon--dark icon-[mingcute--moon-line]"
          ></span>
        </button>
      </nav>
    </div>
  </header>
</th:block>

modules/navigation.html

文档底部上一篇/下一篇按钮。

点击查看模板源码
<nav th:if="${linkNavigation}" class="dm-doc-nav">
  <a
    th:if="${linkNavigation.hasPrevious}"
    th:href="${linkNavigation.previous.link}"
    class="dm-doc-nav__item dm-doc-nav__item--prev"
  >
    <span class="dm-doc-nav__icon icon-[mingcute--right-line]"></span>
    <div class="dm-doc-nav__content">
      <div class="dm-doc-nav__label">上一篇</div>
      <h3 class="dm-doc-nav__title" th:text="${linkNavigation.previous.title}"></h3>
    </div>
  </a>
  <a
    th:if="${linkNavigation.hasNext}"
    th:href="${linkNavigation.next.link}"
    class="dm-doc-nav__item dm-doc-nav__item--next"
  >
    <span class="dm-doc-nav__icon icon-[mingcute--right-line]"></span>
    <div class="dm-doc-nav__content">
      <div class="dm-doc-nav__label">下一篇</div>
      <h3 class="dm-doc-nav__title" th:text="${linkNavigation.next.title}"></h3>
    </div>
  </a>
</nav>

modules/plugin-scripts.html

插件适配脚本,包含搜索、文本绘图KaTeX

点击查看模板源码
<script th:if="${pluginFinder.available('PluginSearchWidget')}" th:inline="javascript">
  document.addEventListener("DOMContentLoaded", () => {
    const btnSearch = document.getElementById("btn-search");
    if (btnSearch) {
      btnSearch.addEventListener("click", () => {
        const categoryNames = {
          project: "[(${project.metadata.name})]",
          version: "[(${currentVersion.metadata.name})]",
          language: "[(${currentLanguage.language})]",
        };

        const options = {
          includeCategoryNames: Object.entries(categoryNames)
            .map(([key, value]) => {
              if (value) {
                return `${key}:${value}`;
              }
            })
            .filter(Boolean),
        };
        SearchWidget.open(options);
      });
    }
  });
</script>

<th:block th:if="${pluginFinder.available('text-diagram')} and ${docTree.spec.type == 'DOC'}">
  <script defer src="/plugins/text-diagram/assets/static/mermaid.min.js"></script>
  <script>
    document.addEventListener("DOMContentLoaded", function () {
      const isDark = document.documentElement.dataset.theme === "dark";
      mermaid.initialize({
        startOnLoad: false,
        theme: isDark ? "dark" : "default",
      });
      mermaid.run({
        querySelector: "#content text-diagram[data-type=mermaid]",
      });
    });
  </script>
</th:block>

<th:block th:if="${pluginFinder.available('plugin-katex')} and ${docTree.spec.type == 'DOC'}">
  <link rel="stylesheet" href="/plugins/plugin-katex/assets/static/katex.min.css" />
  <script defer src="/plugins/plugin-katex/assets/static/katex.min.js"></script>
  <script>
    document.addEventListener("DOMContentLoaded", function () {
      const body = document.getElementById("content");
      const renderMath = (selector, displayMode) => {
        const els = body.querySelectorAll(selector);
        els.forEach((el) => {
          katex.render(el.innerText, el, { displayMode });
        });
      };
      if (body) {
        renderMath("[math-inline]", false);
        renderMath("[math-display]", true);
      }
    });
  </script>
</th:block>

modules/prism.html

默认代码高亮插件的样式和脚本文件,如果你需要使用其他代码高亮插件,可以不引入。

点击查看模板源码
<link
  rel="stylesheet"
  th:href="@{/plugins/plugin-docsme/assets/static/libs/prism/prism.css?v={version}(version=${plugin.version})}"
/>
<script
  th:src="@{/plugins/plugin-docsme/assets/static/libs/prism/prism.js?v={version}(version=${plugin.version})}"
></script>

modules/script.html

基本的文档页面交互脚本,包括明暗主题切换、文档目录生成。

点击查看模板源码
<script
  th:src="@{/plugins/plugin-docsme/assets/static/dist/main.iife.js?v={version}(version=${plugin.version})}"
></script>

modules/sidebar.html

文档页面侧边栏,主要包含文档目录树。

点击查看模板源码
<aside
  class="dm-sidebar"
  dm-data
  dm-bind:class="{'dm-sidebar--show' : $store.sidebar.open}"
>
  <div class="dm-sidebar__content">
    <ul class="dm-nav-tree">
      <li
        th:replace="~{plugin:plugin-docsme:modules/doc-tree :: single(docTrees=${docTrees})}"
      />
    </ul>
  </div>
</aside>
<div
  class="dm-sidebar-backdrop"
  dm-show="$store.sidebar.open"
  dm-on:click="$store.sidebar.open = false"
></div>

modules/style.html

基本的文档样式文件,如果你需要完全定制文档页面的样式,可以根据页面的 dom 结构自行编写样式。

点击查看模板源码
<link
  rel="stylesheet"
  th:href="@{/plugins/plugin-docsme/assets/static/dist/main.css?v={version}(version=${plugin.version})}"
/>

主题模板编写

文档项目列表页面

在主题中创建一个名为 docs.html 的模板,最基础的示例如下:

<th:block th:replace="~{plugin:plugin-docsme:modules/style}" />
<th:block th:replace="~{plugin:plugin-docsme:modules/script}" />
<div class="dm-container">
  <th:block th:replace="~{plugin:plugin-docsme:modules/docs :: docs (footer = null, showHeader = true)}" />
</div>

文档页面

在主题中创建一个名为 doc.html 的模板,最基础的示例如下:

<th:block th:replace="~{plugin:plugin-docsme:modules/style}" />
<th:block th:replace="~{plugin:plugin-docsme:modules/script}" />
<th:block th:replace="~{plugin:plugin-docsme:modules/prism}" />
<th:block th:replace="~{plugin:plugin-docsme:modules/plugin-scripts}" />
<div class="dm-container">
  <th:block th:replace="~{plugin:plugin-docsme:modules/header :: header (showThemeSwitcher = false)}" />
  <th:block th:replace="~{plugin:plugin-docsme:modules/doc :: doc(footer = null)}" />
</div>

文档目录页面

在主题中创建一个名为 doc-catalog.html 的模板,最基础的示例如下:

<th:block th:replace="~{plugin:plugin-docsme:modules/style}" />
<th:block th:replace="~{plugin:plugin-docsme:modules/script}" />
<th:block th:replace="~{plugin:plugin-docsme:modules/plugin-scripts}" />
<div class="dm-container">
  <th:block th:replace="~{plugin:plugin-docsme:modules/header :: header (showThemeSwitcher = false)}" />
  <th:block th:replace="~{plugin:plugin-docsme:modules/doc-catalog :: doc-catalog(footer = null)}" />
</div>

以上只是最基本的模板结构,如果你要与主题布局相结合,那么需要自行进行调整,比如在各个模板外层引入 layout.html 布局模板,假设你的 layout.html 模板为:

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org" th:fragment="html (head,content)">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2" />
    <title th:text="${site.title}"></title>
    <link rel="stylesheet" th:href="@{/assets/dist/style.css}" />
    <script th:src="@{/assets/dist/main.iife.js}"></script>
    <th:block th:if="${head != null}">
      <th:block th:replace="${head}" />
    </th:block>
  </head>
  <body>
    <section>
      <th:block th:replace="${content}" />
    </section>
  </body>
</html>

那么你的 doc.html 模板就可以这样写:

<!DOCTYPE html>
<html
  xmlns:th="https://www.thymeleaf.org"
  th:replace="~{modules/layout :: html(head = ~{::head},content = ~{::content})}"
>
  <th:block th:fragment="head">
    <th:block th:replace="~{plugin:plugin-docsme:modules/style}" />
    <th:block th:replace="~{plugin:plugin-docsme:modules/script}" />
    <th:block th:replace="~{plugin:plugin-docsme:modules/prism}" />
    <th:block th:replace="~{plugin:plugin-docsme:modules/plugin-scripts}" />
  </th:block>
  <th:block th:fragment="content">
    <div class="dm-container">
      <th:block
        th:replace="~{plugin:plugin-docsme:modules/header :: header (showThemeSwitcher = false)}"
      />
      <th:block
        th:replace="~{plugin:plugin-docsme:modules/doc :: doc(footer = null)}"
      />
    </div>
  </th:block>
</html>