手记2:从本地博客到Github Pages的最佳指南及工具

现在你来到了Ghost的世界(或别的什么本地博客系统),怎么写博客以及格式等等问题都是这个博客系统的问题了,你应该去搜搜相关的教程。就我来说,Ghost挺好用的,偶尔还有些很便利使用的小惊喜。

如果你成功的导入了历史博客(或者你只是随手写了几篇),那么现在你面临的问题是:怎么把它放到网上去。当然可以是某个免费主页空间(只需要静态的就可以了),也可以是——比如我选择的就是Github Pages。这样一来,就可以有一个名为xxxxxx.github.io的博客地址了,而且博客文章和维护等等都使用Github的后台或Git命令行,又熟悉又自然,对吧。

4. Github开工

你得先有Github账号。然后,创建一个名为xxxxxx.github.io的仓库。最后,你进入这个仓库,在仓库的Settings(注意不是你的帐户的Settings)中找到GitHub Pages,检查一下,他可能已经缺省写着:

Your site is published at https://xxxxxx.github.io/

OK。建议你将Features中的WikisRestrict editing to collaborators only设为关闭,而Issues建议开启——我相信你用得到它。

最后,你还可以在Theme chooser中选一个Theme。不过对我来说没什么意义,因为我将使用Ghost中的Theme。不过,如果你使用了Github的Theme,那么你的主页根目录——也就是xxxxxx.github.io这个仓库的根目录下就会多一个_config.yml文件。

此外,还总是会有一个README.md文件,知道Github的人都知道这个文件用来做什么,建议你留着——尽管删除掉也没什么影响。

除了上述两个文件,Github什么都没给你,一切都得自己来干。

当然,只要你创建了这个仓库,那么https://xxxxxx.github.io就已经可以访问了;如果你更新这个仓库,那么网站也就自动更新了。

很酷。

5. 从本地博客到Github Pages

尽管几乎所有的Ghost用户都被推荐使用Buster来生成静态页(generate static pages),但这个工具确实相当不好用,按某博主所说『是个大坑』,不过我也建议你看看他的博客文章(在这里),写得很细致了。不过如果你不怎么用Python的话,我建议你不要安装pyenv和多版本的python,直接用MacOSX原装的就好——或者只用Brew安装Python 2.x。

参考上面这位博主(Kris Cotter)的文章,我写了一个名为makesite.sh的脚本,它配合Buster使用,并主要用来填Buster的一些坑。下面说说这个工具。

5.1 使用makesite.sh的标准过程

参考下面的控制台命令:

## 取一份你的Github Pages仓库,例如(YOURNAME/YOURNAME.github.io)
> git clone https://github.com/YOURNAME/YOURNAME.github.io
> cd ./YOURNAME.github.io

## 定制一下git exclude files
> echo -e '\nstatic/\nmakesite.sh\npatchme.sh\nbuster.log’ >> .git/info/exclude

## 下载makesite.sh文件到本地仓库
> wget -nv 'https://github.com/aimingoo/ghost-utils/raw/master/makesite.sh'

## 使用帮助
> bash makesite.sh --help
###########################################################################
 Usage:
  > bash makesite.sh --generate --reset-domain --short-path --deploy-now
 ...

上面列举了makesite.sh对Ghost -> Github Pages的一个标准过程的理解,亦即是分成四步:

  • generate: 调用Buster生成静态页
  • reset-domain: 重置本地博客的domain到远端(YOURNAME.github.io)
  • short-path: 生成简短的网页路径
  • deploy-now: 部署到Github仓库

你可以使用参数来开启或关闭任何一个步骤,或者从任何一个步骤开始(而无视于其它步骤)。缺省情况下所有的步骤都是关闭的,只检查一下当前目录下是否存在./static子目录。——注意这个目录是被保留,不被提交到Github仓库中去的(所以前面的脚本中我将它添加到了.git/info/exclude文件中)。

整个过程需要依赖gitbuster两个工具,另外需要wgetgrep和MacOSX版本的sed。如果你是在别的系统上运行这个脚本,你"可能"需要改一下sed的命令行参数。

如果你只是生成静态页而不部署它,那么你不需要使用--deploy-now参数,这种情况下没有安装git也是可以的。但我总是建议你先安装buster:

## 使用Python的PIP安装buster
> pip install buster

## 如果你没有wget或git
> brew install wget git

5.2 makesite.sh的配置与参数

如果你读makesite.sh的源代码,你会发现它处理参数的代码非常漂亮(嘿~嘿~)。

这些参数可以有两种格式:

  • --reset-domain=false:指定reset-domain参数值为false。

  • --reset-domain:这种省略掉值的情况,等效于--reset-domain=true

也可以指定字符串值,例如设置你的domain(缺省值是我的"aimingoo.github.io"):

--domain='YOURNAME.github.io'

所有通过命令行参数传入的参数配置,都有相对应的bash变量(全大写字符),所以你也可以直接修改.sh脚本中的变量缺省值,以避免每次都要通过命令行指定(例如,尤其是--domain--generate-info这样的参数)。

由于通常你都需要配置自己的domain参数,所以我没有将makesite.sh写成通用脚本,而是建议你每个Github本地仓库下载一份独立的,并修改其中DOMAIN变量的缺省值。

也正是因此,它也被加入了git的排除文件列表。

6. 填了哪些坑?

前面说过makesite.sh用来填Buster的一些坑的。但填了哪些坑呢?下面讲讲技术问题。

6.1 Buster调用wget时的参数问题

6.1.1 Buster其实只帮你抓5页Pages

如果你的博客很多,有很多的分页(比如我的就有五十多页),那么你很容易就发现其实Buster只帮你抓了其中的5页——很郁闷吧。

这是因为Buster调用wget来抓取页面,而它在递归下载时默认搜索的递归尝试就是5层,这个需要修改--level参数。

6.1.2 你可能需要忽略掉一些抓取页面

Ghost默认会帮你生成很多东西,有SEO用的amp文件、sitemap文件或rss文件,又例如最新的ld+json数据。然而你不见得都需要用到,其中最严重的就是amp,因为这相当于你生成了两份博客,所以我们需要用--reject-regex来忽略掉它。

还有一种情况非常特殊。在Ghost博客里,tag页可能有两种url(事实上所有的页面都会有这两种页):/tags/XXXtags/XXX/。别小看多出来的这个斜杠/:当wget访问/XXX页时,它是将url理解为文件的,因此将要写入的是文件XXX(如果加上--adjust-extension参数那么就写入XXX.html);而访问/XXX/时,wget认为它访问的是目录,所以也就会尝试创建XXX这个目录并写入./XXX/index.html

然而我们设想一个问题:如果一个网站中既有/tags/XXX又有tags/XXX/,那么当wget先找到前者的时候,就会在当前目录下写一个XXX文件,而下一次它找到后一种的时候,会发生什么呢?

这有三种可能性:

  • 如果有XXX目录,而又要创建XXX文件的话,wget会创建一个名为XXX.1的文件;
  • 如果反过来是有XXX文件的情况下,
    • 需要再创建./XXX/目录,那么wget直接覆盖XXX文件,于是旧的XXX文件丢失,多出来一个新的XXX目录;
    • 需要创建./XXX/yyy/index.html这样的子级目录中的文件的话,那么很不幸,wget抛出一个异常./XXX/yyy : Not a directory,写文件不成功。

所以,我们需要忽略掉一些抓取页面,因为他们可能先于一个目录创建之前要写入,又或者与已写入的文件冲突,再或者根本就是多余的、重复的页。

6.1.3 不修改buster.py的做法

为了不修改Buster的源代码,我在makesite.sh中定义了一个名为wget的函数,并且在当前进程中导出它。这样一来,由于Buster是由makesite.sh这个shell脚本launch起来的,那么当它调用wget下载的时候,就调用了我们在程序中修改过的版本。

——这是一种在当前脚本中打patch的方法(可以不修改buster.py的源代码)。基本的代码如下:

##
## 参见makesite.sh源代码
##

## 声明函数并追加参数
function wget { $RAW_WGET --level=0 inf --reject-regex=... }

## 取原始的wget的路径并导出到当前环境中
export RAW_WGET=`which wget`

## 将wget函数导出到当前环境中
export -f wget

## 调用buster,这时buster将使用我们在代码中声明的wget函数来下载
buster ...

6.2 移除版本号

在新近一些的前端技巧中,为JavaScript和CSS文件加上版本号已经是很流行的做法了。然而这意味着Buster抓取的文件会存成类似shared/ghost-url.js?v=3edb33f1b1这样的名字。

所以在makesite.sh中有两行代码来对assetsshared目录中的资源做更名,以移除这种版本号:

## 移除assets目录中的资源版本号
> find static/assets -name '*\?*' -type f -exec sh -c "echo '{}' | sed 's|\?.*$||' | xargs -I[] mv '{}' '[]'" \;

## 移除shared目录中的资源版本号(通常只有/ghost-url.js这个文件)
> find static/shared -name '*\?*' -type f -exec sh -c "echo '{}' | sed 's|\?.*$||' | xargs -I[] mv '{}' '[]'" \;

6.3 从reset-domain到short-path

reset-domain是Kris Cotter最早版本代码中的功能,简单地说就是将Buster下载的网页文件中残留的localhost:2368再做一次替换,所以我将这个过程叫做reset-domain。

事实上wget的--convert-links参数做过一次高性能的链接转换,但这远远不够——这些转换基于wget的html parser中对"链接"的定义(可以参见tag_attr html_allow这个数据结构,位于wget源码html.c中)。而reset-domain针对的是可能存在的文本替换——而无论对象是否是.html,或者是否是真实的url链接。

经过这两轮的处理(generate和reset-domain)之后,事实上整个./static目录下的文件已经可以作为静态页发布了。确切地说,你已经可以使用下面的命令来部署本地git仓库,将它推到你的主页YOURNAME.github.io中去了:

> bash makesite.sh --deploy-now

然而makesite.sh脚本还在这个操作之前插入了一个--short-path——我想你已经注意到这一点了。

这个short-path的目的是将Ghost生成的类似于/your--post-full-title----as-slug/index.html这样长的“目录名+文件名”变得短一些。它基本的想法就是让主页根目录下不要太多的目录,因此将这些目录中的index.html上移到它们的父目录中——对于所有的posts来说,其实也就是根目录下。

这个过程其实有一个更好的解决方案,就是将所有这些文件移入到/posts/your--post-full-title----as-slug.html,这样一来主页根目录下就只有一个posts目录了。我尝试过,但之后放弃了,因为我写不出一个有效的正则表达式来替换所有“其它”页面中的url——到该页面的新位置。

好吧,总而言之,我们做了一点工作(尽管实际上这是效率最低的一个步骤)。所以你还是可以在部署之前尝试一下:

> bash makesite.sh --short-path

6.3.1 更短的url

我的主页aimingoo.github.io中其实使用的是更短的url地址,而并不是Ghost中缺省地按照文章标题通过拼音转换而来——后面这种文件名生成得太长太难看了。这种『更短的url』其实是通过为每个post设置它独自的slug属性而得到的,这需要为每篇文章在Ghost后台管理界面去编辑Post的属性。

这也是上一篇博客中提到的putrefy.js这个工具(在这里)中会有一个SLUG_FROMID参数的原因。当配置这个参数为true时,你从旧博客中导入的文章就将自动地以author_id作为前缀,并加上post_id作为slug了——我的博客就是用这种方法来导入的。当然,你也可以将SLUG_FROMID配置为别的什么字符串来作为前缀。

但是,你在Ghost后台新添加的文章,就需要你手工地改slug了。这个今后我会发布个小工具来自动化的,这里暂且不提。

6.4 关于patchme.sh

这是在makesite.sh中留下的一小处补丁程序。你可以编写一段shell代码(放在makesite.sh同目录中就可以了),让makesite.sh在正式地deploy-now操作之前,由你自己来对./static目录中的页面做些修补。这偶尔也是必要的,例如我现在这篇文章是在介绍Ghost,那就不可避免地会用到localhost:2368这样的url地址,而按照Buster和makesite.sh的规则,它们就可能被替换成线上Github仓库中的地址了——所以需要one by one地patch。

当然,你也可以写点别的什么代码。

尤其是……程序员总想干点什么黑活之类的啦。

你懂的。