Migrating Blog From Typecho to Hugo

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安装

参考:

使用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)后,会自动创建publicresources两个目录,详见后文。

进入content目录,建立两个目录,pageposts。把上一步导出的文章文件,复制到posts目录下。在page目录下存放自定义页面(其格式跟文章的文本格式类似)。

其中pageposts两个目录的名称可以自定义,并修改配置文件的对应配置项,详见后文。

2.5. 选择主题(Theme)

选择所需主题,并安装到所创建的Blog。官方提供的主题列表:Complete List | Hugo Themes

我选择了Stack主题,相关介绍参考:

选择该主题的理由:

  • 喜欢其选用颜色和页面布局设计。
  • 自带全文搜索功能。跟Fuse.js类似,是通过浏览器端js实现的。
  • 首页能显示按年份、分类、标签归档。
  • 支持暗黑(Dark)模式。

安装主题:

  • 参考官方文档:Installation | Getting Started | Stack
  • 我选择了官方不推荐的方式,从Github发布中下载最新版,并解压到Blog的themes目录下。
  • 主题所存放的目录名称,要记住,后面配置要用。比如我命名该目录为hugo-theme-stack。即相关主题文件放在themes/hugo-theme-stack目录。

2.6. 配置Blog

配置Blog是最麻烦的,除了需要了解Hugo的配置,还要学习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目录。

My Family Journey Through Chengdu

过年前,一直计划着春节假期去个旅游。经过对多个目的地的人流、天气、抢票情况、交通等问题的考虑,最后选择了四川省成都市。能给孩子带来初次坐飞机和看雪景的体验。

1. 概述

成都的景点,其实挺多亮点:带有三国历史的武侯寺、著名商业步行街的宽窄巷和春熙路、满眼大熊猫的大熊猫繁育研究基地、4千多年历史的三星堆遗址、历史文化深厚的四川省博物馆、美丽的九寨沟、教科书上的都江堰、宏伟的乐山大佛、美丽但折磨的峨眉山等等。综合了历史文化、自然风光、国宝、特色美食、热闹商圈等要素。

但是,由于是春节假期,很多推荐的小店都每开,导致吃得不太好。晚上的居民区,基本都是大门紧闭,没体会到地方特色。

另外,推荐参考“小红书”的攻略,比较详细和实用(当然,还是建议参考)。有些景点(例如峨眉山)还有每天发布景点情况。

2. 交通

抵达成都,一般选择高铁和飞机。如果能订到特价机票(几乎与高铁票等价),肯定推荐推荐飞机。

成都市内一般推荐地铁。“支付宝”或“微信”能申请“成都公交卡”,票价打折,扫码即可进站和出站。

成都市外的景点,例如“大熊猫繁育研究基地”、“三星堆博物馆”等,有旅游专线(大巴车)直达,价格不贵。一般景点都有对应车站。

再远一点的景区,例如乐山、峨眉山等,最好是提前预定动车票。不过像峨眉山,动车也就1个小时,实在买不到坐票,站一下也可以。

3. 行程

兼顾体力与时间,确定行程如下:

  • 1月30日
    • 下午,飞往成都双流机场。
  • 1月31日
    • 上午,大熊猫繁育研究基地。
    • 下午,成都市博物馆。
  • 2月1日
    • 上午,人民公园、宽窄巷。
    • 下午,三星堆博物馆。
  • 2月2日
    • 上午,前往峨眉山。
    • 下午,游玩峨眉山。
  • 2月3日
    • 上午,睡到自然醒,再回成都市区。
    • 下午,芳草街与玉林路一带闲逛。
    • 晚上,前往成都双流机场,回程。

订票顺序是,先订景点票,再订交通票(飞机和高铁),最后订住宿(酒店或旅馆)。

比较遗憾的是,选了“成都市博物馆”,主要是想着在市中心,会比较方便。要选的话,还是“四川省博物馆”的内容比较丰富。

另外,考虑到峨眉山会比较累,所以放弃了只有一座大佛的乐山。在峨眉山与九寨沟之间,选择了前者,因为峨眉山会下雪,并且有雪景(适合没看过雪的娃)。

市区的住宿选在人民公园最南端,为了省钱,离地铁站比较远,不推荐。也因为春节,即使附近有个菜市场,也没多少餐饮店开门。

4. 景点

4.1. 成都大熊猫繁育研究基地

该园区的特点是,可以看到很多大熊猫,但也只有大熊猫,甚至会看到视觉疲劳。记得提前预约买票。

总体来看,有几个点是选对了:

  • 冬天上午参观,大熊猫都在户外活动,很活泼。
  • 上午8点,从西门进入,南门离开,避开大部分拥堵的人流。
  • 放弃去看“花花”,时间都留给其它熊猫。

详细攻略可以参考“小红书”攻略。个人比较随缘,熊猫看够了、走累了,就可以满足地离开。

注意的点:

  • 地铁站“军区总医院(熊猫基地西大门)”,B出口,接驳车(摆渡车)直达西大门,票价两元。
  • 地铁站“熊猫大道(熊猫基地南大门)”,A或D出口,接驳车(摆渡车)直达南大门。
  • 整个园区超大,如果不是心态随缘,一定要做好路线规划。
  • 想省钱,可以自备水和干粮。冬天可以带保温杯,园区免费提供热水。
  • 熊猫步行街有比较集中的餐饮店(价格还好),可以考虑安排午饭,不过位置几乎在园区中间。
  • 园区内的纪念品比较贵,款式超多,最好适可而止。因为整个成都都在卖类似的东西。
  • 出了南大门,找旅游专线,可直达春熙路。

4.2. 成都博物馆

由于其地理位置在市中心,就作为一个景点。对于游客,相比“四川省博物馆”,更推荐后者。“成都博物馆”的展览,主要针对成都市。虽然不收门票费,但需要提前预约。

另外,旁边有家“蕃坊酥小蒙牛肉饼”,其牛肉饼很推荐。

4.3. 天府广场

在成都市中心的“天府广场”,除了是个人流超大的地铁站,还是个很大的地下商场。其中有个“二次元”区域,孩子很喜欢。“天府广场”的地面,是个比较有历史气息的广场(空地)。

4.4. 人民公园

个人觉得人民公园最大特点是,里面开了家“鹤鸣茶社”。在里面悠闲喝茶、采耳,被认为是体验成都“慢生活”。

人民公园有两个纪念碑:“辛亥秋保路死事纪念碑”和“川军抗日阵亡将士纪念碑”。路过的话,值得停下脚步,瞻仰和默哀一下。

另外,人民公园有个奶茶摊,在小茶壶里放个干冰,模仿水烧开的样子,挺有意思。

4.5. 宽窄巷

两条古色古香的商业步行街。高情商的说法,这里浓缩了成都的特色,一次逛个够。缺点是太有名,导致人太多,商业化也太浓。记得十年前左右,刚好没什么人,古朴与休闲的气息,还特意在“星巴克”里喝个咖啡。现在就是走一下,打个卡而已。惊喜的是,“星巴克”还在,不过他家的咖啡,已经不是我的菜了。

4.6. 三星堆博物馆

三星堆遗址其实一直都在挖掘,其历史最早能追溯到公元前4500年左右。出土众多的青铜器,在公元前1600年左右(大概对应商朝初期)。由于2021年最新发掘成果陆续发布,导致“三星堆博物馆”火了一把。最震撼的是,一大堆用作祭祀的精美青铜器,包括大型青铜树、青铜立人等。建议参观前,先了解一下相关历史和介绍,看的时候主要感受一下实物。

我们在“宽窄巷”坐旅游专线直达,比较方便。博物馆场地较大,两个主要展厅也较大,需要安排好休息。

4.7. 峨眉山

这次旅程,最值得记录的是峨眉山。冬天的峨眉山,漫山遍野都是白皑皑的雪,树木枝头挂上冰花,非常漂亮,仿似《冰雪奇缘》的世界。看过雪景的我,也感受到震撼。虽然到达金顶没有放晴,但是漫天飞雪,给金色神像增添神秘感。暗自感叹,能上来烧个香的,都是虔诚的信徒。

简要的攻略:

  • 提前订好峨眉山门票。一般建议也订了上下山的旅游大巴、缆车。其中缆车票不坐可退。
  • 高铁站“峨眉山站”才是到峨眉山的。不要买错“峨眉站”的票。
  • 高铁站有摆渡车送去“报国寺”,免费的。“报国寺”是摆渡车与上下山旅游大巴的车站。
  • 住宿可以订“报国寺”到“景区路”一带的酒店或旅馆,强烈建议带温泉或泡澡的。下山回来,泡个澡,非常舒服。我们订的“竹间私汤民宿”,总体很好,就是稍微有点偏僻。
  • “景区路”一带很多餐饮店,离“报国寺”不远。
  • 上山前,除了在“景区路”吃饱喝足,还要买好水、干粮和装备。价格比山上便宜很多。
  • 装备推荐:
    • 不建议带登山杖,景区路买竹竿才2元/根。
    • 鞋子最好防水防滑。不防水不防滑,买防滑鞋套;防水不防滑,可考虑冰爪。最好是出发前准备好。
    • 外套最好放水溅。衣服不防水的,需要雨衣。建议不要打伞,双手忙不过来的。
  • 上山是从“报国寺”坐旅游大巴(约两小时)上去,再坐缆车(约3分钟)上山顶。但是山上人太多,会导致缆车排队两小时。
  • 下山看情况。上山人多的话,下山也会是人多。我们不想在风雪中排队,就直接步行下山,大概两小时,能体验更多的风景。
  • 如果预算充足,可以考虑山上住一晚,能看日落和日出。

4.8. 芳草街与玉林路

成都市区挺多可以逛的区域,选择芳草街与玉林路,一来是上机前闲逛一下,二来找找吃的。

路线大概是:芳草街地铁站,玉林西路,玉林中学,玉林综合市场,芳华街。其中玉林西路的店比较普通,芳华街的店比较个性,玉林综合市场也就是个菜市场。

5. 美食

由于比较随缘,也由于春节,加上广东人怕辣,没有深入四川美食。吃过的,都总结一下。

  • 蹄花
    • 清炖猪蹄。一般炖得很烂,但吃到的出品是,猪气味未能僻除,不能接受。另外,很多“老妈蹄花”店,会有小字前缀,需要注意是否去错了店。
  • 豆花
    • 咸口豆腐花。相比豆腐花,没那么嫩,感觉不算特别好吃。
  • 成都牛肉面
    • 面是圆形细条,牛肉是卤过的。跟家附近菜市场的出品一致,就是我经常吃的“成都牛肉面”了。
  • 抄手
    • 四川云吞。跟广东云吞一样,皮很薄,但包成UFO形状(大概吧)。一般汤里带点紫菜。吃的是不加辣的版本,可以。
  • 凉拌折耳根
    • 吃过几次都是没有腥味的,直到吃到腥味很重,才体会到抗拒折耳根的人的感受。
  • 麻辣兔头
    • 没吃。一来是泡在辣椒汤里,二来太硬核,整个兔头很完整,并且牙齿外露,看着也罢。
  • 冰粉
    • 因为在长沙吃过好吃的冰粉,就想着在原产地吃一下正宗的。有的店,应该是水加多了,口感比较稀,不喜欢。在玉林西路某店,吃到比较符合记忆中的口感。
  • 红糖糍粑
    • 软糯与甜,没毛病。
  • 麻辣火锅
    • 太辣,劝退。另外,在春熙路那边某店吃的,除了贵,牛肉还是“科技嫩牛肉”,留下污点。
  • 糖油果子
    • 甜品都是好吃的,而且串起来卖,吃着也方便。
  • 干噎酸奶
    • 像雪糕吧。不是成都特色,但是第一次在成都吃到。
  • 跷脚牛肉
    • 据说是乐山的美食,但在峨眉山吃的。简单概括的话,就是清汤牛杂。还行,但不惊艳。
  • 麻婆豆腐
    • 辣度被调低,吃起来跟家附件的店,味道一致。不知道是不是有专门的料理包或者调味包。
  • 峨眉山叶儿粑
    • 只吃到咸口版。糯米粉做皮,包着肉馅,再粑叶包着蒸熟。圆柱状,外形挺特别,味道一般。后来才知有甜口版,没吃到。
  • 香椿炒蛋
    • 人生第二次吃香椿,用来炒蛋确实更容易接受。不过香椿的特别香味,还是不太喜欢。
  • 峨眉山竹叶菜
    • 据说峨眉山开发旅游之前,当地人拿来喂猪,也叫“猪草”。厨师用蒜蓉炒,吃起来也算好吃。只是想着“猪草”这名字,心里有点不舒服。

Generate Coffee Roasting Curves Using Frappe Charts

用温度监测模块记录了咖啡烘焙过程中的咖啡豆温度数据,保存为CSV文件。根据该数据生成“烘焙曲线”和“升温曲线”,需要用Excel之类的表格处理软件生成对应的“折线图”,比较麻烦。然后写了个简单的网页,可以选择CSV文件,自动生成两个曲线图。生成图表的JavaScript库,采用了Frappe Charts。记录一下Frappe Charts的使用。

优点:

  1. 简单。根据官方示例代码,即可生成图表。
  2. 支持混合图表。通过多种图表同时显示,更好表达出数据的含义。
  3. 折线图(line chart)支持序列数据,并优化显示。包括:平滑曲线、优化X轴标签显示(避免显示全部而导致太密集)等。

缺点:

  1. 项目太久没更新(起码3年了),很多issue都没有处理。
  2. 扩展性差。几乎没有插入特殊处理代码的地方。
  3. 可生成Y轴标签(以虚线显示),但不能生成X轴标签。目前使用条形图(bar chart)实现X轴标签的效果(指定X轴标签上显示一条竖线)。
  4. 折线图(line chart)数据点的提示框(tooltip),不能根据不同的数据做显示定制。由于提示框是生成图表时,预先生成的,并且按数据顺序执行每个数据点的各组数据的处理。例如有3组数据,那么每次执行数据处理都记录其自增ID,再用该ID除以3求余数,就能确定当次所处理的是第几组数据,并做对应的处理。
  5. 折线图(line chart)不能设置缺失数据,也不能隐藏指定数据点。暂时无解决方案,只能用零值表示缺失数据。

Yashixiang Lemon Tea

某天脑里有坑,想自制“鸭屎香柠檬茶”。参照了视频 鸭屎香柠檬茶保姆级教程!赶紧码起来 做出来,味道不及预期。后来细看,才发现视频有bug。修正后,终于调出满意的味道。可是,夏天已经过去了[苦笑]……

材料

  • 鸭屎香茶叶,5g
  • 热开水,150g
  • 冰块,75g + 220g

    替代:冰水,75g + 220g

  • 冰糖浆,20g(按口味调整)

    替代:糖粉,20g

  • 广东香水柠檬,30g(按酸度调整)

    替代:冻干柠檬,3片,约5g

制作

  1. 制作鸭屎香茶汤。材料配比,茶叶:热开水:冰 = 1:30:15。水烧开后,倒入茶叶,焖泡10分钟。过滤茶叶后倒入75g的冰块中,做成茶汤备用。
  2. 制作柠檬汁。使用广东香水柠檬的话,柠檬切片后加少量冰块(大概30g),锤打出香味。使用冻干柠檬片的话,加40ml热水浸泡,约10分钟。
  3. 混合出品。柠檬汁里倒入冰糖浆(或糖粉)、茶汤、220g冰块,摇匀,然后享用。

总结

  • 使用“冻干柠檬片”代替“广东香水柠檬”,有柠檬酸味,但是少了柠檬香味。
  • 采用的冰糖浆或糖粉,主要是更易于低温溶解。其用量需要根据口味调整。
  • 没有冰块的话,冰水也行(也方便称取份量),缺点是不够冷。

Egg and Scallion Crepe

这个葱花鸡蛋煎饼,材料简单,容易做,适合作为早餐。

参考小高姐的视频:【小高姐】鸡蛋煎饼 三样食材的美味家常早餐饼

材料

这个份量能煎6个饼,适合两人吃。

  • 鸡蛋,2个
  • 面粉(中筋或高筋),35g
  • 水,125g
  • 盐,2g
  • 葱花,适量

做法

  1. 鸡蛋、面粉和水混合并搅匀。重点要把鸡蛋搅散。可以先把鸡蛋搅散再混合,或者混合后再搅。
  2. 放入盐和葱花,混合均匀。
  3. 不粘锅刷薄油,开小火(约100°C),倒入1/6的份量,煎1~2分钟,翻面再煎1分钟,即可出锅。

补充

  • 葱花增加香味,没有或者不喜的,可不放。
  • 可以作为卷饼的皮。

Chinese Steamed Eggs

蒸水蛋是很常见的家常菜,这里主要记录材料配比和烹饪时间。

材料

  • 鸡蛋,2只
  • 常温清水,150g
  • 盐,小许
  • 酱油,适量
  • 葱花,小许

做法

  1. 鸡蛋与清水的比例是1:1.5。按一个鸡蛋50g,所以这里清水150g。
  2. 鸡蛋、清水、盐,一起搅拌均匀,再倒入碟子。水烧开后,放进去蒸。
  3. 建议去除表面气泡。对口感要求高的,可以过一下筛子。蒸的时候用另一个碟子盖住,可以避免蒸汽水(倒汗水)滴下。
  4. 不同材质的碟子,需要调整蒸的时间。不锈钢碟子大概蒸5~6分钟。
  5. 蒸好后撒上葱花,有条件可以淋上热油激发香味。再淋上酱油调味。

补充

  • 这个份量的材料上,可加入虾米 10g。虾米最好浸泡一下,洗净,再均匀放入打好的蛋液。
  • 可以加入白贝,增加鲜味。但是一定要挑选没有泥沙的。
  • 可以加入瑶柱,增加鲜味。个人不喜。

Sweet Red Bean Soup

红豆是广东乃至全国甜品的常用食材。红豆沙除了是一种甜品馅料,也是一种甜汤的名称。配合刚上市的栗子,就是秋天的甜品了。加入“黑米”,纯粹误以为是“紫米”(黑糯米),不过增加了嚼劲。

材料

这是两人份量。但是我喜欢刚煮好吃一半,留一半第二天再吃。

  • 红豆,100g
  • 栗子,5~10颗
  • 陈皮,一片(约25平方厘米)
  • 冰糖,适量

做法

  1. 红豆清洗后,加水浸泡4个小时以上。红豆比绿豆硬,泡久一点更容易“出沙”(煮烂)。
  2. 红豆沥水后倒入高压锅,加3~4倍的水,加入陈皮,放入60%~70%的冰糖,压40分钟。
  3. 栗子去壳后,最好切粒备用。最好先煮熟(水煮15分钟左右),特别是整颗。如果一起放高压锅里煮,容易煮烂,所以最好分开煮。
  4. 高压锅泄压后倒入另一个锅,加入栗子再煮。大概煮15分钟,调整粘稠度(太稠就加水)。最后调整甜味(加冰糖),即可出锅。

补充

  • 去掉栗子,就是比较传统的陈皮红豆沙了。
  • 我加入黑米 50g增加口感。最好浸泡一下,并与红豆一起放入高压锅。
  • 红豆沙加西米、汤圆等,都是比较常见的吃法。

Sweet Mung Bean Soup

臭草海带绿豆沙,是广东人夏天必须吃的甜品。中医认为绿豆性寒,具有清热解毒、消暑止渴的功效,适合夏天食用。但是海带和绿豆常有,臭草不常有(缺少臭草感觉没了“灵魂”)。刚好家里种了几棵,可以煲一锅。另外,近年来喜欢加上夏天特有的苹婆,给这道甜品增加口感。

材料

这是一人份量。刚煮好,热的吃一半,留一半放冰箱冷藏,第二天再吃。

  • 绿豆,100g
  • 干海带,2g
  • 苹婆,5~10颗
  • 广东臭草(芸香)的叶子,若干
  • 冰糖,适量

做法

  1. 绿豆清洗后,加水浸泡2~3个小时。
  2. 绿豆沥水后倒入高压锅,加3~4倍的水,放入60%~70%的冰糖,压40分钟。
  3. 干海带浸泡至柔软(大概10分钟)后,切丝备用。
  4. 苹婆去壳后,最好切粒备用。整颗的话,比较难煮透,最好先煮熟。
  5. 高压锅泄压后倒入另一个锅再煮,主要调整粘稠度(太稠就加水)和加入其它食材。先加入苹婆、臭草叶,煮5分钟。再加入海带煮5分钟。最后调整甜味(加冰糖),即可出锅。

Hong Kong Disneyland

作为孩子的生日礼物,老婆带领全家去香港迪士尼乐园游玩。该乐园从2005年开业至今,差不多20年了,我们仨是第一次进去。但是整个乐园很难察觉20年的岁月痕迹,给人充满活力与快乐的感觉。

1. 总体印象

整个乐园不大,并且得益于香港开通了高铁,能够实现一天游玩不过夜,非常适合周末游。即使看完晚上的烟花,也能坐高铁回去。推荐找个普通周六,游客相对少一点,降低各个项目的排队时间。

对迪士尼的认识,是从迪士尼动画开始。但是迪士尼乐园给我的感觉,不是迪士尼动画乐园,而是具有自己特色的主题游乐场。米奇、唐老鸭,已经不是主角,而是淹没在乐园里众多卡通角色当中。其中“迷离大宅”作为原创故事的园区,利用各种机关和科技手段,展示奇幻的故事。没有熟悉的IP角色,但充满迪士尼的味道。

迪士尼乐园,对比玩过的其它乐园,最大的特点是十分强烈的沉浸感。各个园区主题鲜明,连排队的通道都没有马虎。比如“魔雪奇缘世界”,从户外的街道、城堡,到室内的排队通道、游玩项目,都是“冰雪奇缘”的世界。甚至因为乐园选址在郊外,没有高楼大厦会让游客感到“出戏”。

其游玩项目一般采用轨道小车、小船,以固定的游玩路线,保证游客的体验一致。这个有点西方快餐店的味道,保证各地的出品一致。虽然园区提供了一些刺激的机动游戏,但显然,迪士尼乐园的重点不在此。所以如果只是想玩机动游戏,建议另选其它乐园。

虽然乐园的门票不便宜,但我觉得没必要像“特种兵旅游”那样,一定要玩完所有项目。走到哪玩到哪,好好体会一下这个奇幻的乐园吧。

2. 前期准备

列出需要前期准备的工作。

购买乐园门票

  • 微信小程序“香港迪士尼乐园”可购买门票。
  • 不同时间价格不同。
  • 最好提前截图,便于进园时展示。

预约入园时间

  • 微信小程序“香港迪士尼乐园”可进行预约,包括预约入园时间、餐饮服务、主题酒店等。

开通手机流量

  • 手机开通香港漫游,或者购买香港旅客流量卡。
  • 主要用于移动支付,包括香港地铁刷卡、乐园内外的购物支付等。

规划行程路线

  • 确定交通工具和路线。
  • 建议内地高铁直达香港,再转香港地铁直达乐园。

安装迪士尼乐园手机App

  • 主要查看乐园地图和各个项目的排队等候时长。

3. 开通手机流量

在香港使用手机的移动流量,有两个方案:一是内地手机卡开通香港漫游,二是购买香港旅客流量卡。

3.1 内地SIM卡开启香港漫游

一般流量使用不多,选这个方案即可。主要是方便,开通即可使用,不用换SIM卡。但是内地的电信运营商的收费不一样,开通前最好咨询清楚。

中国联通的计费规则如下(来自联通的短信):

欢迎您抵达中国香港,中国联通伴您出境漫游放心用:
【上网】漫游流量费每天25元封顶,无需办理即可畅享3GB/天高速流量,达量限速。当天使用不足25MB按5MB/5元累加计费(更多优惠请点击https://u.10010.cn/uAbd2,或回复KTGM,根据提示进行更多产品订购)
【通话】接听电话、拨打中国大陆及漫游地均为0.96元/分钟,拨打澳台及其它国家和地区3.86元/分钟(境外拨打中国大陆请加拨+86,键盘长按“0”可拨出“+”号)
【短信】发短信回内地0.36元/条,发港澳台及其他国家和地区1.06元/条
【全球免费客服热线】+8618618610010
精品网络,漫游全球,贴心服务,联通世界。

3.2 购买香港手机卡

我选择了这个方案。淘宝买个“香港旅客流量卡”即可,一般是4G网络的5GB流量(超出流量限制后,应该只是限速),手机插上后,可以通过无线热点分享给同行的其它手机使用。

由于香港也要求手机卡实名制,最好抵达深圳后,在过关前1小时内进行登记。在香港过关后,插上该卡,拨打*103*900#进行开通启用。具体的详细操作,咨询淘宝客服,或查看相关提示短信。

4. 规划行程路线

我们从“广州南站”直达“香港西九龙站”,再转香港地铁(需要转线)到乐园。晚上沿路返回。

乘坐香港地铁时,如果带未成年的小孩,可以分别用“支付宝”和“微信”的乘车卡,一个给自己支付,另一个给孩子支付。

  • 高铁,广州南站 -> 香港西九龙站,约1小时,票价215 RMB。
  • 地铁,香港西九龙站 -> 香港迪士尼乐园,约38分钟,票价约20 RMB。
    • 2号线(屯马线),屯门方向,柯士甸 -> 南昌
    • 4号线(东涌线),东涌方向,南昌 -> 欣澳
    • 迪士尼线,迪士尼方向,欣澳 -> 迪士尼

5. 关于乐园

推荐园区及项目:

  • 魔雪奇缘世界,魔雪奇幻之旅,小船游览,有惊险
  • 迷离庄园,迷离大宅,小车游览
  • 幻想世界,米奇幻想曲,4D电影

午饭:

  • 微信小程序“香港迪士尼乐园”可进行预约相关餐饮。
  • 园区餐厅比较贵,嫌贵可自带干粮。

Deploy Typecho using Docker

部署过才发现,使用Docker部署PHP网站,没想象中的简单。顺便总结一下使用Docker Compose部署Typecho的经验。

1. 概述

重点注意:

  • 一般PHP网站的Docker镜像,只提供了网站的PHP源码和php-fpm服务,在生产环境不能单独部署。
  • 生产环境需要前端搭配Nginx或Apache等接收请求,再交给php-fpm服务处理。一般还需要后端连上MySQL、MariaDB、PostgreSQL等数据库。
  • 建议使用Docker Compose部署。比Docker run更好管理,比K8S更省资源。

2. 建立网络

建议先建立一个Docker网络,把相关服务部署到该网络。

  • 所有服务使用service名称作为host名称,配置相互访问时不用填IP。
  • 同一网络内的服务,不用在宿主机暴露端口,各个服务之间也相互访问。一般只需暴露Nginx的端口。

建立Docker网络的参考命令如下。其中网络名称为docker-net

docker network create --driver "bridge" docker-net

3. Nginx服务

Nginx的docker-compose.yaml,路径在/opt/docker_deploy/nginx/,内容如下:

services:
  nginx:
    image: nginx:1.27.0-alpine
    restart: always
    ports:
      - 80:80
      - 443:443
    environment:
      - NGINX_PORT=80
    volumes:
      - ./etc/nginx.conf/:/etc/nginx/nginx.conf:ro
      - ./etc/conf.d:/etc/nginx/conf.d
      - /etc/letsencrypt/archive:/etc/nginx/cert:ro
      - ./logs:/var/log/nginx
      - ./www:/var/www
      - /opt/docker_deploy/typecho/app:/var/www/typecho:ro
    networks:
      - docker-net

networks:
  docker-net:
    external: true

Type的Nginx配置文件,在/opt/docker_deploy/nginx/etc/conf.d/typecho.conf,内容如下:

server {
  server_name blog.xxxx.com;
  listen 80;

  rewrite ^(.*)$  https://$host$1 permanent;
}

server {
  server_name blog.xxxx.com;
  listen 443 ssl;
  http2 on;
  index index.php index.htm index.html;
  root /var/www/typecho;

  ssl_certificate /etc/nginx/cert/blog.xxxx.com/fullchain8.pem;
  ssl_certificate_key /etc/nginx/cert/blog.xxxx.com/privkey8.pem;

  add_header X-Frame-Options SAMEORIGIN;
  add_header Content-Security-Policy "frame-ancestors 'self';";

  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    include fastcgi_params;
    #fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param SCRIPT_FILENAME /app$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_pass typecho:9000;
    fastcgi_buffers 8 16k;
    fastcgi_buffer_size 32k;
  }

  location / {
    try_files $uri $uri/ /index.php$is_args$query_string;
  }
}

4. Typecho

部署要点:

  • Typecho的Docker部署说明,可参考:https://github.com/typecho/Dockerfile
  • 由于服务器上还有其它网站或系统,需要Nginx做反向代理,所以Typecho的Docker镜像选择了fpm版本。
  • 需要把PHP源码文件映射出来,提供给Nginx访问。

Typecho的docker-compose.yaml,路径在/opt/docker_deploy/typecho/,内容如下:

ervices:
  typecho:
    image: joyqi/typecho:1.2.1-php8.0-fpm-alpine
    restart: always
    #ports:
    #  - "9000:9000"
    environment:
      - "TYPECHO_SITE_URL=https://blog.xxxx.com"
      - "TYPECHO_DB_ADAPTER=Pdo_Mysql"
      - "TYPECHO_DB_HOST=mariadb"
      - "TYPECHO_DB_PORT=3306"
      - "TYPECHO_DB_USER=typecho"
      - "TYPECHO_DB_PASSWORD=123456"
      - "TYPECHO_DB_DATABASE=typecho"
    volumes:
      - ./app:/app
    networks:
      - docker-net

networks:
  docker-net:
    external: true

要注意,如果Nginx的Typecho没有开启SSL,但需要全局https访问Typecho,可以在配置文件config.inc.php添加以下配置:

/** 全站开启https */
define('__TYPECHO_SECURE__', true);

5. MariaDB

MariaDB的docker-compose.yaml,路径在/opt/docker_deploy/mariadb/,内容如下:

ervices:
  mariadb:
    image: mariadb:11.4.2
    restart: always
    #ports:
    #  - 3306:3306
    volumes:
      - ./dbdata:/var/lib/mysql
    environment:
      MARIADB_ROOT_PASSWORD: root123456
      #MYSQL_USER: ${DB_USER}
      #MYSQL_PASSWORD: ${DB_PASSWORD}
      #MYSQL_DATABASE: ${DB_DATABASE}
    networks:
      - docker-net

networks:
  docker-net:
    external: true