Hexo 作为一款比较流行的博客框架,其主题也是各式各样,Hexo官网上有很多漂亮的主题直接可以使用,但是莫不如自己设计编写一个自己喜欢的。这篇文章将从零开始制作一个 LandScape 主题。

主题介绍

要想使用一个自定义主题,则只需要在<hexo 文件夹>/themes下新建一个文件夹,名称随自己喜欢,本文则使以test为例。如要使用此主题,还需将 hexo 目录下的_config.yml文件中的theme属性改为此主题的文件夹,即修改为theme: test

一个主题的目录结构如下:

.#
├── _config.yml # 配置文件
├── languages # 语言文件夹
├── layout # 布局文件夹
├── scripts # 脚本文件夹
└── source # 源文件文件夹
  • 配置文件:主题的配置文件,可以在内部配置一些自定义的属性,在代码内部以config.xxx 获取 xxx 的属性值,如:

    config.menu # 获取 配置文件中的 menu 属性值,如果 menu 下有多个二级属性值,则用config.menu[i]表示第i个二级属性值
  • languages 文件夹:内包含各个语言文件,以yml语言形式保存。在代码内部以如下形式自动根据配置的语言选择对应语言文件夹内的内容,如果没有对应的语言,则取default文件的内容。

    __('author.job') # 获取 author 下的 job 的内容
  • layout文件夹:所有的布局相关文件都存放在此文件夹。可以使用 swig 模版,如果要使用 ejs、haml、jade 模版,需要在 hexo 跟文件夹下安装相应的插件(本文使用 ejs 模版):

    npm install hexo-renderer-jade --save # jade
    npm install hexo-renderer-haml --save # haml
    npm install hexo-renderer-ejs --save # ejs
  • script 文件夹:JavaScript 脚本文件,在 Hexo 启动时,会载入该文件夹下的 JavaScript 文件。

  • source 文件夹:资源文件夹,存放一些模版以外的源文件,如 css、js、图片、logo 等。文件或文件夹名开投为_(下划线)。

    该文件夹下的文件如果可以被 hexo 渲染,则会在执行 hexo d或者hexo g命令是被渲染并存储到 public文件夹,如果不能渲染,则会直接拷贝文件到public文件夹下。

    如果有文件跳过渲染,则需要在 hexo 根目录下的_config.yml下的skip_render属性后面添加该文件名,如:skip_render: README.md

整体布局模版

每个主题都需要有一个layout/index.ejs模版作为入口,index模板内部可以根据需要添加或 partial 文件等作为博客的首页内容。主题的基础布局为layout/layout.ejs,所有的布局都继承该模版,其中body部分根据不同页面进行替换,使用语法为:

<%- body %>

例如:由 index.ejslayout.ejs内容生成的index.html内容如下:

生成简单的 index.html
  • ejs
  • ejs
  • html
This is a index page.

运行结果如下:

basic-index-html

同时可以在index.ejs模版内部引入局部布局,如下可以引入一个partial_layout.ejs的模版内容到index.ejs:

<%- partial('partial_layout')%>

partial_layout.ejs 的文件内容如下:

<!-- partial_layout.ejs -->
This a partial layout file.

运行结果如下:

basic-index-html

总体规划

在做自己的主题之前要多整个页面有一个整体的规划,包括页面的各个区域显示内容。在 LandScape主题中,页面布局如下:

theme-layout

  • Header区域:页面头部文件,包含 Navigation 区域和 背景图片区域;
  • Main Content区域:文章的主要显示区域;
  • Sidebar区域:目录、分类、Archiv等栏目;
  • Footer区域:显示 Copyright 等信息;

所以对于 layout.ejs文件简要内容为:

<!DOCTYPE html>
<html>
<%- partial('head 文件', null, {cache: !config.relative_link}) %>
<body>
<div id="wrap">
<%- partial('header 文件', null, {cache: !config.relative_link}) %>
<div class="outer">
<section id="main">
<%- body %>
</section>
<%- partial('sidebar 文件', null, {cache: !config.relative_link}) %>
</div>
</div>
</body>
</html>

每个博客的页面都继承于此模版,所以该模版应该定义为所有页面的对于body区域,正对不同的页面显示不同的内容:

  • Index Page: 显示为文章目录列表;
  • Category Page:显示为分类相关列表;
  • Tag Page:显示为标签相关列表;
  • Archive Page:显示为所有的文章列表;

添加配置文件

在进行页面整体规划的时候,需要根据需要动态的显示不同的内容,例如页面的导航栏的内容等,其内容可以在主体布局代码内部写确定值,但是这样就不能根据不同语言进行修改,也不利于维护。因此这类内容可以添加到_config.yml文件中,在编写模版的时候可以使用theme.xxx来获取 xxx 的属性值。例如:

menu:
Home: /
Archives: /archives
...

如果要添加新的配置项,则只需要按键值对写入配置文件即可。使用时只需要在布局文件中使用循环取出menu的各项内容进行动态填充。

详细设计

Head 部分

Head 部分是加载网页相关的文件,如title、css、meta相关、link相关等,head.ejs文件在layout/partial文件夹下,文件内容如下:

<head>
<meta charset="utf-8">
<%- partial('google-analytics') %>
<%
var title = page.title;

if (is_archive()){
title = __('archive_a');
if (is_month()){
title += ': ' + page.year + '/' + page.month;
} else if (is_year()){
title += ': ' + page.year;
}
} else if (is_category()){
title = __('category') + ': ' + page.category;
} else if (is_tag()){
title = __('tag') + ': ' + page.tag;
}
%>
<!-- is_tag()、is_category()、is_year()、is_archive() 都是 hexo 辅助函数 -->
<title><% if (title){ %><%= title %> | <% } %><%= config.title %></title>
<% if (config.highlight.enable){ %> <!-- 根据配置文件加载代码高亮css文件 -->
<link href="//fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet" type="text/css">
<% } %>
<%- css('css/style') %> <!-- 加载css文件 -->
</head>

Header 部分

Header 区域主要是菜单、搜索框等内容。菜单项是通过主题的配置文件,通过循环语句加载配置文件的菜单选项:

<% for (var i in theme.menu){ %>
<a class="main-nav-link" href="<%- url_for(theme.menu[i]) %>"><%= i %></a>
<% } %>

搜索框则是利用 hexo 的辅助函数进行添加:

<%- search_form({button: '&#xF002;'}) %>

Header 文件内容以及对于的css文件如下:

partial/header
  • ejs
  • Stylus
<header id="header">
<div id="banner"></div>
<div id="header-outer" class="outer"><div id="header-title" class="inner">
<h1 id="logo-wrap">
<a href="<%- url_for() %>" id="logo"><%= config.title %></a>
</h1>
<% if (theme.subtitle){ %>
<h2 id="subtitle-wrap">
<a href="<%- url_for() %>" id="subtitle"><%= theme.subtitle %></a>
</h2>
<% } %>
</div>
<div id="header-inner" class="inner">
<nav id="main-nav">
<a id="main-nav-toggle" class="nav-icon"></a>
<% for (var i in theme.menu){ %>
<a class="main-nav-link" href="<%- url_for(theme.menu[i]) %>"><%= i %></a>
<% } %>
</nav>
<nav id="sub-nav">
<% if (theme.rss){ %>
<a id="nav-rss-link"
class="nav-icon" href="<%- url_for(theme.rss) %>"title="<%= __('rss_feed') %>">

</a>
<% } %>
<a id="nav-search-btn" class="nav-icon" title="<%= __('search') %>"></a>
</nav>
<div id="search-form-wrap">
<%- search_form({button: '&#xF002;'}) %>
</div>
</div>
</div>
</header>

Main Content部分

Main Content部分 是整个页面中的主体部分,其内容在不同页面中显示的内容不一样:

  • Index Page: 显示为文章目录列表;
  • Category Page:显示为分类相关列表;
  • Tag Page:显示为标签相关列表;
  • Archive Page:显示为所有的文章列表;
  • Post Page:显示文章内容

针对上述不同的页面,在layout文件夹下有index.ejscategory.ejstag.ejsarchive.ejspost.ejs

ìndex.ejspost.ejs为例:
ìndex.ejs模版主要是显示首页文章列表的,hexo 本身提供了很多page相关的变量,常用的变量及描述如下:

变量 描述
page.title 页面标题
page.date 页面建立日期(Moment.js 对象)
page.updated 页面更新日期(Moment.js 对象)
page.comments 留言是否开启
page.content 页面的完整内容
page.excerpt 页面摘要,文章开头到<!-- excerpt -->标签为止的内容
page.more 除了页面摘要的其余内容,和page.excerpt类似,使用<!-- more -->标签
page.path 页面网址(不含根路径)。我们通常在主题中使用 url_for(page.path)。
page.permalink 页面的完整网址
page.prev 上一个页面。如果此为第一个页面则为 null。
page.next 下一个页面。如果此为最后一个页面则为 null。

post相关的变量和page类似,但是多出了三个变量:

变量 描述
page.published 如果该文章已发布则为True
page.categories 该文章的所有分类
page.tags 该文章的所有标签

除了上述变量之外,可以在每一篇文章内的Front-matter区域内(Front-matter具体可参考 Hexo 官方文档)以键值对的方式自定义变量。如:

actions: true

在模版代码内部使用的时候使用如下的方式:

post.actions

index.ejs相关代码如下:

<% page.posts.each(function(post){ %>
<article id="<%= post.layout %>-<%= post.slug %>" class="article article-type-<%= post.layout %>" itemscope itemprop="blogPost">
<div class="article-meta">
<%- partial('post/date', {class_name: 'article-date', date_format: null}) %>
<%- partial('post/category') %>
</div>
<div class="article-inner">
<%- partial('post/gallery') %>
<% if (post.link || post.title){ %>
<header class="article-header">
<%- partial('post/title', {class_name: 'article-title'}) %>
</header>
<% } %>
<div class="article-entry" itemprop="articleBody">
<% if (post.excerpt){ %>
<%- post.excerpt %>
<% if (theme.excerpt_link){ %>
<p class="article-more-link">
<a href="<%- url_for(post.path) %>#more"><%= theme.excerpt_link %></a>
</p>
<% } %>
<% } else { %>
<%- post.content %>
<% } %>
</div>
<footer class="article-footer">
<a data-url="<%- post.permalink %>" data-id="<%= post._id %>" class="article-share-link"><%= __('share') %></a>
<% if (post.comments && config.disqus_shortname){ %>
<a href="<%- post.permalink %>#disqus_thread" class="article-comment-link"><%= __('comment') %></a>
<% } %>
<%- partial('post/tag') %>
</footer>
</div>
</article>
<% } %>
<% }) %>

如果文章数量较多,则在 index page中会分页依次显示,每页显示的数量由 hexo 的 config 配置文件决定:

index_generator:
per_page: 10

因此在index.ejs 模版中要添加分页显示:

<% if (page.total > 1){ %>
<nav class="page-nav">
<%- paginator({
prev_text: "&laquo; Prev",
next_text: "Next &raquo;"
}) %>
</nav>
<% } %>

post.ejs内容和index.ejs内容类似,只是对每一篇文章添加了添加了上一篇、下一篇导航,并且去除了excerpt的显示,即在article标签的最后添加如下内容:

<% if (post.prev || post.next){ %>
<nav id="article-nav">
<% if (post.prev){ %>
<a href="<%- url_for(post.prev.path) %>" id="article-nav-newer" class="article-nav-link-wrap">
<strong class="article-nav-caption"><%= __('newer') %></strong>
<div class="article-nav-title">
<% if (post.prev.title){ %>
<%= post.prev.title %>
<% } else { %>
(no title)
<% } %>
</div>
</a>
<% } %>
<% if (post.next){ %>
<a href="<%- url_for(post.next.path) %>" id="article-nav-older" class="article-nav-link-wrap">
<strong class="article-nav-caption"><%= __('older') %></strong>
<div class="article-nav-title"><%= post.next.title %></div>
</a>
<% } %>
</nav>
<% } %>

Footer部分

Footer区域相对比较简单,只需添加简单的 copyRight 就可以:

<footer id="footer">
<div class="outer">
<div id="footer-info" class="inner">
&copy; <%= date(new Date(), 'YYYY') %> <%= config.author || config.title %><br>
<%= __('powered_by') %> <a href="http://hexo.io/" target="_blank">Hexo</a>
</div>
</div>
</footer>

最后将 css 文件引入到 layout 模版中即可,引入 css 文件使用 hexo 的辅助函数 css():

<%- css('css/style') %>

最终简单的界面运行如下:

result

国际化(i18n)

在制作主题的过程中,有些内容需要以不同语言来展示,例如导航栏、鼠标提示等。这些内容需要借助 hexo 的国际化功能去实现。文章所使用的语言由 hexo 的配置文件_config.ymllanguage属性控制。此属性可以设置单个的预设语言,也可以设置多个语言来顺位。

# 单个
language: zh-cn

# 多个
language:
- zh-cn
- en

在主题的language文件夹中对每种语言的文字进行实现,例如:

  • en.yml

    index:
    title: Home
    add: Add
    video:
    zero: No videos
    one: One video
    other: %d videos
  • zh-cn.yml

    index:
    title: 首页
    add: 添加
    video:
    zero: 无视频文件
    one: 一个视频文件
    other: %d个视频文件

在模版中使用多语言时使用__或者_p辅助函数,即可取得翻译后的字符串,前者用于一般使用;而后者用于复数字符串。例如:

<%= __('index.title') %>
// Home

<%= _p('index.video', 3) %>
// 3 videos

总结

至此,一个简单的 hexo 主题就制作完成了,本文省略了部分css文件和js文件。当然,这只是最简单、最基础的主体,如果要制作自己个性的主题还需要在此基础上添加很多有趣的定制和修改。

感谢阅读,希望对你有所帮助。