在这个系列教程中,你将学习怎么从零开始制作一个 Hexo 主题。我很喜欢 Hexo ,并且每天都在使用,不幸的是,直到今天关于主题制作的文档还是相当稀缺。所以我打算弥补这个空缺。

其它文章的链接:

预先准备

  • Hexo 博客的基础使用。如果你是第一次接触,请前往官网阅读教程
  • 了解 Bootstrap
  • 了解 Javascript 模板引擎(我们将使用 EJS )

项目描述

这个项目旨在制作一个 Hexo 主题并详细了解 Hexo 引擎的工作方式。
我不想在 HTML 和 CSS 上花费太多时间,所以我们将重置下面这个Hexo主题:https://getbootstrap.com/docs/3.4/examples/blog/ 。它是 Bootstrap 文档中的一个标准初始模板样例。
我们将一步步地重用 CSS 、复制粘贴 HTML ,直到最后实现想要的效果。
如果你感到困惑或者只对它的代码感兴趣,请前往 github

项目结构

创建一个新的 hexo 博客

让我们从搭建一个全新的 hexo 博客开始吧

// Create a new folder
mkdir hexo-theme-creation 
cd hexo-theme-creation
// Initialise Hexo
hexo init

创建主题文件夹

// Enter the theme folder
cd themes
// bootstrap-blog-hexo is also going to be the name of our theme
mkdir bootstrap-blog-hexo

注意:如果你想在 git 中保存主题的话(你也应该这么做),请在/themes/bootstrap-blog-hexo/中初始化 git 。

文件夹结构

这是我们开始工作所需要的文件和文件夹:

|-- layout // .ejs templates 
|-- source // source files (CSS, scripts)
|-- _config.yml

创建以下两个文件夹和 _config.yml 文件。

复制 bootstrap blog 资源

bootstrap blog template 复制所有我们需要的资源并放在 source 文件夹里。你可以选择通过浏览器查看资源并复制下来,或者是下载该压缩包,之后解压到 source 文件夹里。

|-- layout 
|-- source 
    |-- bootstrap // Copy the boostrap library files here 
    |-- css // Copy the blog's css file here
    |-- favicon
        |-- favicon.ico // Your choice of favicon
    |-- js // Copy the blog's js file here
|-- _config.yml

Hexo 的基本要素

在我们开始写第一个模板文件之前,先来看看 Hexo 博客生成过程的基本要素。

页面类型

我们能够在主题中定义 6 种页面类型,与之相对应地,在 public 文件夹生成的每一个单独的 HTML 页面都属于下面模板中的其中一个:

模板回退页面描述
index这是博客的首页,也是网站的主要入口。本教程中我们将让其显示文章摘要列表
postindex这是文章的详情页。我们将展示一篇完整的文章以及一个评论区
pageindex这是页面的详情页,与 post 一样,但是是 page 类型的 post
archiveindex这是归档页。它将显示我们博客中所有文章的标题和详情页链接
categoryarchive这是分类页。与归档页类似,但是会根据类别进行筛选
tagarchive这是标签页。与分类页类似,但是会根据标签进行筛选

在本篇教程中我们将创建 index 模板。

在页面生成过程中, Hexo 将会搜索名字为 index.ejs, post.ejs , page.ejs 等的文件,这些模板之后用于创建静态 HTML 页面。

公共布局

Hexo 支持使用 公共的布局文件 ,上面的模板都将使用到该文件。
该文件命名为 layout.ejs 。不同页面类型的模板会创建一些内容,而这个文件就好比这些内容的“外壳”。
在我们的主题中,公共布局将包括:<html>标签、<head>标签、头部、菜单、底部和侧边栏。基本上是所有类型的页面都具备的元素。
不同的页面模板将只负责创建实际内容,这些内容将放在我们的主体部位。

变量

在所有的模板中,我们都可以使用 hexo 引擎提供的内置变量。以下是部分变量:

  • Site:site包含了网站的信息。例如,我们可以通过site.posts访问博客中的所有文章。当我们想要显示统计数据的时候,这将派上用场。
  • Page:page是主要变量,包含了许多与当前页面相关的信息,包括所有的文章标题、日期、内容等。
  • Config:config是一个指向站点_config.yml文件的 JavaScript 对象
  • Theme:theme是一个指向主题_config.yml文件的 JavaScript 对象

主题的布局创建

上面提及了/layout/layout.ejs文件,现在我们开始来创建它。

顶部标签

首先创建 layout.ejs 文件并插入<html></html>标签

//layout/layout.ejs
<html>
<!-- Head tag -->
<%- partial('_partial/head') %>
</html>

这里我们将所有<head>标签里的代码提取出来并放在局部视图中,这有助于实现关注点分离和代码重用。
语法是partial('path' [, arguments])
在创建layout/_partial/head.ejs文件后,从 bootstrap 源码中复制 head 标签里的代码:

// layout/_partial/head.ejs
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="favicon/favicon.ico">
    <title>Blog Template for Bootstrap</title>
    <!-- Bootstrap core CSS -->
    <%- css('bootstrap/css/bootstrap.min.css') %>
    <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
    <%- css('css/ie10-viewport-bug-workaround.css') %>
    <!-- Custom styles for this template -->
    <%- css('css/blog.css') %>
    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
        <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
        <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
</head>

这很简单,我们只需使用 CSS helper 插入样式表。
source 文件夹中的文件将会被复制到站点根目录下,所以不要在路径中包含source/
我们将让<title><meta>标签保持动态,不过现在先暂且不管它们。

底部标签

底部标签位于</body>之前。我们将在这个局部视图中包含所有脚本。
先修改一下布局:

// layout/layout.ejs

<html>
<!-- Head tag -->
<%- partial('_partial/head') %>
<body>
    <!-- After footer scripts -->
    <%- partial('_partial/after-footer') %>
</body>
</html>

然后创建layout/_partial/after-footer.ejs的内容:

// layout/_partial/after-footer.ejs

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<%- js('bootstrap/js/bootstrap.min.js') %>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<%- js('js/ie10-viewport-bug-workaround.js') %>

注意 JS helper function 的使用,它将引用本地 js 文件。

顶部菜单

类似地,在<body>标签后创建顶部菜单。

// layout/layout.ejs

// [...]
<body>
    <!-- Menu -->
    <%- partial('_partial/menu') %>
    
// [...]

layout/_partial/menu.ejs的内容:

// layout/_partial/menu.ejs

<div class="blog-masthead">
    <div class="container">
        <nav class="blog-nav">
            <% for (var i in theme.menu){ %>
                <a class="blog-nav-item" href="<%- url_for(theme.menu[i]) %>"><%= i %></a>
            <% } %>
        </nav>
    </div>
</div>

注意theme全局变量的使用,它指向的是主题的_config.yml文件。为了可以在主题配置中配置菜单,我们需要在_config.yml文件中添加配置:

_config.yml
# Header
menu:
  Home: /
  Archives: /archives

menu.ejs中我们遍历了配置文件中所有的菜单项目并创建对应的链接。

顶部

顶部将位于顶部菜单下面,它包含了博客标题和子标题:

// layout/_partial/header.ejs

<div class="blog-header">
    <h1 class="blog-title"><%= config.title %></h1>
    <p class="lead blog-description">
        <% if (config.subtitle){ %><%= config.subtitle %><% } %>
    </p>
</div>

这里我们使用了指向站点_config.yml文件的config变量,它包含了可供配置的标题和子标题属性。
注意在布局的<div class="container"><div>中嵌套顶部:

// layout/layout.ejs

<html>
<!-- Head tag -->
<%- partial('_partial/head') %>
<body>
    <!-- Menu -->
    <%- partial('_partial/menu') %>
    <div class="container">
        <!-- Blog Header: title and subtitle -->
        <%- partial('_partial/header') %>
    </div>
    
// [...]

底部

底部现在是完全静态的,内容如下:

// layout/_partial/footer.ejs

<footer class="blog-footer">
    <p>Blog template built for <a href="http://getbootstrap.com">Bootstrap</a> by <a href="https://twitter.com/mdo">@mdo</a>.</p>
    <p>Adapted to Hexo by <a href="http://www.codeblocq.com/">klugjo</a>.</p>
    <p><a href="#">Back to top</a></p>
</footer>

主要内容和侧边栏

此时,我们再加上主要内容和侧边栏,基本就差不多了。
下面是最终的layout.ejs

// layout/layout.ejs

<html>
<!-- Head tag -->
<%- partial('_partial/head') %>
<body>
    <!-- Menu -->
    <%- partial('_partial/menu') %>
    <div class="container">
        <!-- Blog Header: title and subtitle -->
        <%- partial('_partial/header') %>
        <div class="row">
            <!-- Main Content -->
            <div class="col-sm-8 blog-main">
                <%- body %>
            </div>
            <!-- Sidebar -->
            <div class="col-sm-3 col-sm-offset-1 blog-sidebar">
                <%- partial('_partial/sidebar') %>
            </div>
        </div>
    </div>
    <!-- Footer -->
    <%- partial('_partial/footer') %>
    <!-- After footer scripts -->
    <%- partial('_partial/after-footer') %>
</body>
</html>

body变量对应了不同页面类型模板创建的内容(参见上面)。
至于侧边栏,我们现在暂且使用来自 bootstrap 模板的硬编码:

// layout/_partial/sidebar.ejs

<div class="sidebar-module sidebar-module-inset">
    <h4>About</h4>
    <p>Etiam porta <em>sem malesuada magna</em> mollis euismod. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.</p>
</div>
<div class="sidebar-module">
    <h4>Archives</h4>
    <ol class="list-unstyled">
        <li><a href="#">March 2014</a></li>
        <li><a href="#">February 2014</a></li>
        <li><a href="#">January 2014</a></li>
        <li><a href="#">December 2013</a></li>
        <li><a href="#">November 2013</a></li>
    </ol>
</div>
<div class="sidebar-module">
    <h4>Elsewhere</h4>
    <ol class="list-unstyled">
        <li><a href="#">GitHub</a></li>
        <li><a href="#">Twitter</a></li>
        <li><a href="#">Facebook</a></li>
    </ol>
</div>

首页文件

布局到位后,我们就可以开始创建第一个页面类型模板 index.ejs 了。
这是比较简陋的第一个版本:

// layout/index.ejs
<span>Content</span>

别小瞧它,我们可以用这个在浏览器中测试主题:

# Verify that everything is alright
hexo generate
# Start hexo server
hexo server

访问 http://localhost:4000/ 。哇!
注意:不要忘记在站点的 config 文件中更新主题:

_config.yml
# Extensions
## Plugins: http://hexo.io/plugins/
## Themes: http://hexo.io/themes/
theme: bootstrap-blog-hexo

遍历博客文章

我们想要在首页显示各篇文章的摘要。
首先,在我们的 index.ejs 文件中遍历文章:

// layout.index.ejs

<% page.posts.each(function(item){ %>
    <%- partial('_partial/article-excerpt', {item: item}) %>
<% }); %>
  • 通过page.posts获取该页面的所有文章
  • 通过<%- partial('name', args) %>给 partial 传参

文章布局

创建article-excerpt.ejs文件,添加适合主题的代码。这是我的布局:

// layout/_partial/article-excerpt.ejs

<div class="blog-post">
    <!-- Title -->
    <h2 class="blog-post-title">
        <a href="<%- config.root %><%- item.path %>">
            <%- item.title %>
        </a>
    </h2>
    <!-- Date and Author -->
    <p class="blog-post-meta">
        <%= item.date.format(config.date_format) %>
        <% if(item.author) { %>
            by <%- item.author %>
        <% } %>
    </p>
    <!-- Content -->
    <%- item.excerpt || item.content %>
    <!-- Only display the Read More link if we are displaying an excerpt -->
    <% if(item.excerpt) { %>
        <p>
            <a href="<%- config.root %><%- item.path %>">
                <%= theme.excerpt_link %>
            </a>
        </p>
    <% } %>
</div>

全文链接

全文链接是由config.root(配置选项,相当于/)和item.path(相对路径或者绝对路径,指向全文)连接组成的。

文章作者

默认情况下, Hexo 没有关于作者属性的的文章变量。不过我们可以在 front matter 中添加任意自己想要的变量。
如果你想要在文章中显示作者名字,那么文章的 front matter 应该类似如下进行设置:

---
title: Hello World
author: Klughertz Jonathan
---
Item excerpt 和 Item content

当用 Hexo 编辑文章时,你可以用<!-- more -->标签从文章内容中截取摘要。在本教程中,因为我们展示的是文章列表,所以选择显示摘要。之后用户可以通过点击文章标题或者“阅读更多”的链接浏览全文。

“阅读更多”文本

别忘了,你需要像我这样在主题的配置文件中添加一个新的属性:

_config.yml
# Read More text
excerpt_link: Read More

希望接下来的代码容易理解。现在,我建议你写一些除了默认的 Hello World 之外的文章并享受这个结果。

分页器

在本篇文章中,我们最后需要处理的是首页的分页器。
index.ejs文件中增加一个分页器的 partial :

// layout/index.ejs

<% page.posts.each(function(item){ %>
    <%- partial('_partial/article-excerpt', {item: item}) %>
<% }); %>
<%- partial('_partial/pagination') %>

之后开始创建分页器的内容,layout/_partial/pagination.ejs:

// layout/_partial/pagination.ejs

<nav>
    <ul class="pager">
        <% if (page.prev){ %>
            <li><a href="<%- config.root %><%- page.prev_link %>">Previous</a></li>
        <% } %>
        <% if (page.next){ %>
            <li><a href="<%- config.root %><%- page.next_link %>">Next</a></li>
        <% } %>
    </ul>
</nav>
  • page.prev:上一页的页码。如果当前页是第一页,则为0
  • page.next:下一页的页码。如果当前页是最后一页,则为0
  • page.next_linkpage.prev_link是什么就不用多说了。

如果你没有足够的文章用来查看分页器的工作效果,可以在主配置文件中(per_page属性)调整每一页的文章数。

这就是今天的内容,在下一篇教程中,我们将完成博客剩下的所有页面。

目前可以找到的比较完备的 Hexo 主题制作教程,我自己从中受益良多。有打算自己撸一个主题的都可以参考下,真的写得很不错。如果发现译文存在错误或其他需要改进的地方,欢迎在下面评论指出。