1. 概述
使用Typecho部署的Blog,由于设置了禁止评论,就想,运行一个PHP语言驱动的Blog还有什么意义?于是就想玩玩静态化的Blog。
1.1. Typecho的问题
- Typecho项目几乎处于停更状态。
- 需要运行PHP-FPM + MySQL数据库,对内存小的VPS不友好。
- 部署相对麻烦。当然,内嵌Apache的Docker镜像会简单点,但资源占用没下降。
1.2. Hugo的概述
- 生成的Blog静态文件,几乎可以到处运行。例如无需购买VPS的GitHub Pages、Cloudflare Pages等。
- 入门相对不容易。部署过才发现,难的不是Hugo本身,而是选用的模板。
1.3. 需求
- 采用Hugo生成静态化Blog。
- 功能包括:以文章列表填充的首页、全站搜索、文章分类、按时间归档、Tag。
- 不需要评论功能。
2. 迁移步骤
总体步骤如下:
- Typecho修正文章(post)格式。
- Typecho导出文章。
- 安装Hugo。
- 创建Blog。
- 选择主题(Theme)。
- 配置Blog。
- 生成静态文件。
- 部署Blog。
参考:
2.1. Typecho修正文章格式
- 就是把文章都调整为Markdown格式。
- 这步略过。主要修正历史文章的格式。
2.2. Typecho导出文章
导出前,先了解Hugo的文章格式:
- 详见官方说明:Docs > Content management > Archetypes
- Markdown格式的文章,分为“前置项”(front matter)和“正文”(markup)两部分。
- 我采用
toml
作为前置项的格式,正文就是以Markdown格式编写的文章正文。 - 前置项的
slug
,是文章标题转为小写后,以“-”替换所有空格和符号的字符串。后续用于生成文章URL。
所使用的导出插件的项目源码:typecho-export-hugo
安装:Typecho部署目录的usr/plugins
目录下,创建目录Export2Hugo
(注意名称不能改,大小写也不能变),再把该项目的源码放进去。
导出:登录Typecho后台,在“控制台”菜单下会有一个“导出至Hugo”菜单,点击后执行导出。
注意:
- 部署Hugo的Blog后,需要考虑文章的网址(Post的URL)。Typecho一般采用文章ID(整数自增ID),但是Hugo采用文章ID需要手工维护,不好玩。我改为使用文章的英文标题。
- 根据上面的需求,修改了一下文章的导出格式。即把
Action.php
修改为如下内容:
<?php
class Export2Hugo_Action extends Typecho_Widget implements Widget_Interface_Do
{
/**
* 导出文章
*
* @access public
* @return void
*/
public function doExport() {
$db = Typecho_Db::get();
$prefix = $db->getPrefix();
$sql=<<<TEXT
select u.screenName author,url authorUrl,title,type,text,created,c.status status,password,t2.category,t1.tags,slug from {$prefix}contents c
left join
(select cid,CONCAT('"',group_concat(m.name SEPARATOR '","'),'"') tags from {$prefix}metas m,{$prefix}relationships r where m.mid=r.mid and m.type='tag' group by cid ) t1
on c.cid=t1.cid
left join
(select cid,CONCAT('"',GROUP_CONCAT(m.name SEPARATOR '","'),'"') category from {$prefix}metas m,{$prefix}relationships r where m.mid=r.mid and m.type='category' group by cid) t2
on c.cid=t2.cid
left join ( select uid, screenName ,url from {$prefix}users) as u
on c.authorId = u.uid
where c.type in ('post', 'page')
TEXT;
$contents = $db->fetchAll($db->query($sql));
$dir = sys_get_temp_dir()."/Export2Hugo";
if(file_exists($dir)) {
exec("rm -rf $dir");
}
mkdir($dir);
$contentDir = $dir."/content/";
mkdir($contentDir);
mkdir($contentDir."/posts");
foreach($contents as $content) {
$title = $content["title"];
$categories = $content["category"];
$tags = $content["tags"];
$slug = str_replace(' ','-',trim(preg_replace('/[^0-9a-z_]+/', ' ', strtolower($title))));
$time = date('Y-m-d H:i:s', $content["created"]);
$text = str_replace("<!--markdown-->", "", $content["text"]);
$text = str_replace(array("\r\n", "\r"), "\n", $text);
$draft = $content["status"] !== "publish" || $content["password"] ? "true" : "false";
$hugo = <<<TMP
+++
title = "$title"
categories = [ $categories ]
tags = [ $tags ]
draft = $draft
slug = "$slug"
date = "$time"
+++
$text
TMP;
$filename = date('Y-m-d-', $content["created"]) .$slug .'.md';
if($content["type"] === "post") {
$filename = "posts/".$filename;
}
file_put_contents($contentDir.$filename, $hugo);
echo $contentDir.$filename;
}
$filename = "hugo.".date('Y-m-d').".zip";
$outputFile = $dir."/".$filename;
exec("cd $dir && zip -q -r $outputFile content");
header("Content-Type:application/zip");
header("Content-Disposition: attachment; filename=$filename");
header("Content-length: " . filesize($outputFile));
header("Pragma: no-cache");
header("Expires: 0");
readfile($outputFile);
}
/**
* 绑定动作
*
* @access public
* @return void
*/
public function action() {
$this->widget('Widget_User')->pass('administrator');
$this->on($this->request->is('export'))->doExport();
}
}
2.3. 安装Hugo
2.3.1. 安装概述
Linux上安装Hugo,参考官方文档:Docs > Installation > Linux
Hugo的Github发布版本,一般分为3个:
- hugo
- 基础版。包含Hugo的基本功能。
- hugo_extended
- 扩展版。在基础版上,增加了两个功能:把图片转码为WebP,把Sass转为CSS。
- hugo_extended_withdeploy
- 扩展部署版。在扩展版上,增加了直接部署功能,支持Google Cloud Storage、AWS S3、Azure Storage等。
由于是Golang开发的,Hugo一般就只有一个可执行文件。但是,如果用到其它插件,可能还需要用到Git、Node.js等。
安装Hugo有多种方式:
- 从Github项目发布下载所需版本:https://github.com/gohugoio/hugo/releases
- 操作系统自带软件库中安装。例如Debian可使用
apt install hugo
命令安装 - 使用Docker部署,可用镜像:https://hub.docker.com/r/hugomods/hugo
如果只是简单使用Hugo,没涉及其它插件,建议”从Github项目发布下载“。如果需要用到Git、Node.js等,建议使用已打包的Docker镜像部署。
注意,所选用的主题(Theme),也会对Hugo版本和第三方工具有要求。例如后文会提到使用Stack
主题,该主题会要求使用hugo_extended
版本。
2.3.2. Docker Compose安装
参考:
- hugomods的Docker镜像:hugomods/hugo - Docker Image | Docker Hub
- hugomods官网:Hugo Docker Images
- hugomods的Docker Compose介绍:Developing With Docker Compose - Development - Docs - Hugo Docker Images | HugoMods
使用Dockcer Compose部署的docker-compose.yaml
参考如下:
services:
hugo:
image: hugomods/hugo:base-non-root-0.145.0
volumes:
- ./src:/src
- ./hugo_cache:/tmp/hugo_cache
ports:
- 1313:1313
# hugomods notice: Since 0.136.2, both of server and hugo server bind 0.0.0.0 by default.
command: server --watch --buildDrafts --disableFastRender
working_dir: /opt/hugo_blog/myblog
注意:
- 这里采用hugomods的Docker镜像,不是Hugo官方出品。
- 通过Docker Compose部署的Hugo,建议放在开发环境或者线下。生产环境或线上,没必要采用这个。
- 示例采用非root用户的Docker镜像,其镜像内的用户ID为1000,用户组也为1000,一般对应宿主机的第一个非root用户。用起来很方便。
docker-compose.yaml
里配置的volumes
,相关宿主机目录要设置用户ID1000可读写。参考命令:sudo chown -R 1000:1000 ./src/ ./hugo_cache/
。
2.4. 创建Blog
Hugo创建Blog的文件目录,并把导出的Blog文章文件复制进去。
Hugo创建Blog的命令如下:
# 确定Blog所在的目录,进入该目录
cd /opt/hugo_blog
# 创建Blog,其目录为myblog,该名称可以自定义
hugo new site myblog
创建成功后,Blog存放在/opt/hugo_blog/myblog
目录,进入该目录可以看到Blog的目录结构。
目录结果说明,详见:Docs > Getting started > Directory structure
myblog/
├─ archetypes/ # 存放创建新文章的模板文件
│ └─ default.md # 新文章的默认模板
├─ assets/ # 存放通常通过asset pipeline传递的全局资源。包括图片、CSS、Sass、JavaScript和TypeScript等。
├─ content/ # 存放文章和自定义页面的文件。
├─ data/ # 存放包含增强内容、配置、本地化和导航的数据文件(JSON,TOML,YAML或XML)。
├─ i18n/ # 存放用于多语言站点的翻译表。
├─ layouts/ # 存放将内容、数据和资源转换为完整网站的模板文件。
├─ static/ # 存放的文件将在建立网站时复制到`public`目录。例如:favicon.ico、robots.txt 和验证网站所有权的文件。
├─ themes/ # 存放一个或多个主题,每个主题拥有自己的子目录。
└─ hugo.toml # Blog的配置文件
注:网站构建(build)后,会自动创建public
和resources
两个目录,详见后文。
进入content
目录,建立两个目录,page
和posts
。把上一步导出的文章文件,复制到posts
目录下。在page
目录下存放自定义页面(其格式跟文章的文本格式类似)。
其中page
和posts
两个目录的名称可以自定义,并修改配置文件的对应配置项,详见后文。
2.5. 选择主题(Theme)
选择所需主题,并安装到所创建的Blog。官方提供的主题列表:Complete List | Hugo Themes。
我选择了Stack
主题,相关介绍参考:
- Hugo Themes - Stack
- Getting Started | Stack
- GitHub - CaiJimmy/hugo-theme-stack: Card-style Hugo theme designed for bloggers
选择该主题的理由:
- 喜欢其选用颜色和页面布局设计。
- 自带全文搜索功能。跟
Fuse.js
类似,是通过浏览器端js实现的。 - 首页能显示按年份、分类、标签归档。
- 支持暗黑(Dark)模式。
安装主题:
- 参考官方文档:Installation | Getting Started | Stack
- 我选择了官方不推荐的方式,从Github发布中下载最新版,并解压到Blog的
themes
目录下。 - 主题所存放的目录名称,要记住,后面配置要用。比如我命名该目录为
hugo-theme-stack
。即相关主题文件放在themes/hugo-theme-stack
目录。
2.6. 配置Blog
配置Blog是最麻烦的,除了需要了解Hugo的配置,还要学习Stack
主题的配置。
参考:
- Hugo的配置项说明:Docs > Configuration > All settings
- Stack主题的配置项说明:Config Introduction | Stack
但是Stack的主题配置,文档不够清晰,建议直接参考其代码的示例或默认配置:
配置有两种组织方式,详见:Docs > Configuration > Introduction。
- 全部写在
hugo.toml
文件。 - 写在
config/_default/
目录下,可以分文件组织配置。
我结合Stack主题,整理出来的hugo.toml
如下。除了开头的9个配置项,后面基本都是Stack主题的配置。
baseURL = 'https://myblog.xxx.xxx/' # Blog正式部署的网址
languageCode = 'zh-CN' # 语言编号
title = "My-Blog" # Blog标题
copyright = 'myblog. All Rights Reserved.' # 版权信息
defaultContentLanguage = "zh-cn" # 默认语言
hasCJKLanguage = true # 是否含有亚洲文字
enableEmoji = true # 启用Emoji
enableRobotsTXT = true # 启用robots.txt文件
theme = "hugo-theme-stack" # 设置选用的主题,值为theme目录下的对应主题目录名称
[params]
mainSections = ["posts"]
#favicon = "/favicon.png"
[params.contact]
email = 'myblog@xxx.com' # Blog拥有者的Email
[params.footer]
since = 2009 # 版权信息的开始年份
[params.dateFormat]
published = "2006-01-02" # 发布日期的日期格式
lastUpdated = "2006-01-02 15:04 MST" # 最后修改日期的日期格式
[params.sidebar]
subtitle = "All About Me" # Blog的子标题
avatar.enabled = false # 显示Blog拥有者的头像
[params.comments]
enabled = false # 启用评论
[params.article]
headingAnchor = true
math = false
readingTime = false # 文章显示阅读时间
toc = true
[params.widgets] # 首页右侧的模型设置
[[params.widgets.homepage]]
type = "search"
[[params.widgets.homepage]]
type = "archives"
[params.widgets.homepage.params]
limit = 5
[[params.widgets.homepage]]
type = "categories"
[params.widgets.homepage.params]
limit = 100
#[[params.widgets.homepage]]
# type = "toc"
[[params.widgets.homepage]]
type = "tag-cloud"
[params.widgets.homepage.params]
limit = 50
[menus] # 菜单设置
[[menus.main]]
identifier = 'home' # 菜单ID
name = 'Home' # 菜单名称
url = "/" # 菜单点击跳转的URL
weight = 10 # 菜单位置权重,值越小,位置越靠前
[menus.main.params]
icon = "home" # 菜单图标,这里使用主题所提供的
[[menus.main]]
identifier = 'search'
name = '搜索'
url = "/page/search"
weight = 20
[menus.main.params]
icon = "search"
[[menus.main]]
identifier = 'archives'
name = '归档'
url = "/page/archives"
weight = 30
[menus.main.params]
icon = "archives"
[[menus.main]]
identifier = 'categories'
name = '分类'
url = "/page/categories"
weight = 40
[menus.main.params]
icon = "categories"
[[menus.main]]
identifier = 'about'
name = '关于'
url = '/page/about'
weight = 100
[menus.main.params]
icon = "link"
[pagination] # 分页设置
disableAliases = false
pagerSize = 10
path = 'list'
[permalinks]
[permalinks.page] # 自定义文章的URL
posts = '/post/:year-:month-:day-:slug' # 带冒号的变量,可以在文章开头找到
[permalinks.section]
posts = '/post/'
#[services]
# [services.googleAnalytics]
# ID = 'G-PPPPPPPPP' # 设置GA帐号ID,用于启用GA功能
2.7. 生成静态文件
使用hugo server
可以写完文章,立马预览效果。但是需要发布时,还是重新构建一下。
# 进入Blog目录
cd /opt/hugo_blog/myblog
# 删除已构建的静态文件
rm -rf ./public
# 执行构建
hugo
注意:
hugo
命令和hugo build
命令是一样的。- 构建前,删除已构建的静态文件,能避免出现静态文件没更新的问题。
- 构建时,默认取当前目录作为Blog目录,也可以使用
-s
参数指定Blog目录。
2.8. 部署Blog
Blog构建成功后,所有静态文件保存在public
目录。把该目录部署到正式网站即可。
官方文档已列出各种可部署环境和部署说明,详见:Docs > Host and deploy
如果使用Nginx部署,注意配置Gzip压缩和浏览器缓存。参考配置:
server {
server_name myblog.xxx.xxx;
listen 80;
listen [::]:80;
rewrite ^(.*)$ https://$host$1 permanent;
}
server {
server_name myblog.xxx.xxx;
#listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
http2 on;
# Hugo所生成Blog的静态文件,即public目录
root /var/www/myblog/public;
index index.html;
ssl_certificate /etc/letsencrypt/live/myblog.xxx.xxx/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myblog.xxx.xxx/privkey.pem;
gzip on;
gzip_vary on;
gzip_comp_level 9;
gzip_min_length 1k;
gzip_buffers 16 8k;
gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
#gzip_http_version 1.1;
#gzip_disable "MSIE [1-6]\.";
add_header X-Frame-Options SAMEORIGIN;
add_header Content-Security-Policy "frame-ancestors 'self';";
location / {
try_files $uri $uri/index.html =404;
etag off;
if ($request_filename ~* ^.*?\.(ico|icon|gif|jpg|jpeg|png|webp)$) {
access_log off;
expires max;
}
if ($request_filename ~* ^.*?\.(css|js)$){
access_log off;
expires 2h;
}
if ($request_filename ~* ^.*?\.(html|htm|json|xml)$){
expires 2h;
}
add_header Cache-Control public;
}
}
3. 维护
3.1. 总体规划
- 需要分开开发环境和生产环境。
- 一般开发环境在线下,生产环境在线上。
- 开发环境安装Hugo。
- 创建Blog、选择主题、写文章、构建静态文件等。
- 生产环境只存放Hugo构建后的静态文件,即
public
目录。
3.2. 新增文章
概述。
- 定义好新文章的模板,方便新建文章时自动生成想要的内容格式。
- 文章的模板,采用了一定的自动化(例如自动取当前日期的UTC时间)。由于新建文章的命令不能传入更多的参数(例如不能传入完整的标题),还是需要新建文章后再修改。
- 文章的文件名,需要配合Blog的管理而决定。这里定的是文件创建日期 + 文章英文标题。
先新建文章的模板,文件为archetypes/post.md
,内容如下。其中[params]
下的项目,是自定义的。
+++
title = "{{ replace (substr .File.ContentBaseName 11) "-" " " | title }}"
draft = false
date = "{{ now.UTC.Format "2006-01-02T15:04:05Z" }}"
categories = [ "" ]
tags = [ ]
slug = "{{ substr .File.ContentBaseName 11 }}"
[params]
title-cn = ""
+++
再新建文章。后面新建文章都会按模板生成初始文件。new-post-title
对应该文章的标题。
hugo new content posts/$(date +%Y-%m-%d)-new-post-title.md -k post
文章的模板,相关函数和方法,参考官方说明:
3.3. 重新构建
按前面提到的命令处理。
3.4. 重新发布
把public
目录上传到服务器即可。自建服务器的,可以使用rsync
命令同步public
目录。