Hugo 和 Gemini

2021-12-05 15:37 +0800

我觉得 Gimini 是为终端用户而生的协议。

如果您是 GUI 用户,大概会对 Gemini 协议无感。 但是如果平时经常用 CLI ,大概会觉得 Gemini 是一个简洁优雅的协议。

Gemini 协议简单,语法比较像 Markdown 的缘故,像是 Hugo 这样用 Markdown 写作的博客平台,可以比较容易的生成 gemini 站点所需的文件。

本文参考 Gemini and HugoUsing Hugo to Launch a Gemini Capsule ,在 hermit 主题的基础上修改。但其他主题也很容易效仿,也可以不修改 Hugo 的主题,只针对单个站点修改。

由于此文援引 Parisian 的 Gemini and Hugo ,需要与其协议兼容,故遵守其 BY-NC-SA 3.0 协议,而不是底部脚注的 CC BY-NC 4.0 协议,在此说明。

此教程只适用于服务器搭建的 Hugo 博客站点,不适用于使用 Github Page 等服务搭建的博客站点

修改 config 文件

先添加 gmi 模板文件的支持,Hugo 的开发者已经想到了会有这样的需求。可以用不同的文件后缀,来区分不同的协议模板文件。

除了已定义好的 AMP ,CSV,JSON 等各种格式,也可以在 config 中自定义其他的格式。

下面是定义使用 gmi 作为模板文件后缀,并添加订阅链接的配置文件,以 yaml 格式为例,添加到原有配置文件结尾即可。

mediaTypes:
  text/gemini:
    suffixes:
      - "gmi"
  application/atom:
    suffixes:
      - "xml"

outputFormats:
  GEMINI:
    name: "GEMINI"
    isPlainText: true
    isHTML: false
    mediaType: "text/gemini"
    protocol: "gemini://"
    permalinkable: true
    # path: "gemini/"
    path: ""
  ATOM:
    name: "ATOM"
    isPlainText: false
    isHTML: true
    protocol: "gemini://"
    path: "/gemini/"
    permalinkable: true
    mediaType: "application/atom"
    baseName: "atom"
outputs:
  home:
    - "HTML"
    - "RSS"
    - "ATOM"
    - "GEMINI"
  page:
    - "HTML"
    - "GEMINI"
  section:
    - "HTML"
    - "GEMINI"
    - "RSS"

配置文件的详细意义,可以参考 官方文档

此配置有别于 Using Hugo to Launch a Gemini Capsule ,没有将 gemini 生成内容单独放一个子文件夹,以便更好的利用 Hugo 的 section 功能。

配置模板文件

模板文件皆在 layouts 文件夹下。可以是主题包内的 layouts 文件夹,如不愿意动主题包,也可以放在博客主目录的 layouts 文件夹中。

index.gmi

首先写首页

# {{ .Site.Title }}

## Subcription  
=> gemini/atom.xml Gemini Atom Feed

## Pages
{{ range .Site.Menus.main }}
=> {{ .URL| }} - {{ .Name }}
{{- if .Params.Subtitle }}  {{ .Params.Subtitle }}{{- end}}
{{- end}}

## Social
{{ range .Site.Params.social }}
=> {{ .url | safeURL }} {{ .name | humanize }}
{{ end }}

=> {{ .Site.BaseURL }}{{ replace .RelPermalink "index.gmi" "" }} View {{ .Site.Title }} on the WWW

配置参考 Hermit 主题,如下。

menu:
  main:
    - name: Posts
      url: posts/
    - name: Now
      url: now/
    - name: About
      url: about/
    - name: Links
      url: links/
params:
  social:
    - name: email
      url: 'mailto://i@lin.moe'
    - name: telegram
      url: 'https://t.me/LindsayZhou'
    - name: gitea
      url: 'https://git.lin.moe/Lindsay'

_default/list.gmi

# Posts
  
{{- range .Pages.GroupByDate "2006-01" }}  
  
### {{ .Key }}
{{- range .Pages }}
=> {{.Permalink}} {{.Title}}
{{- end }}{{- end }}

目录页面以年月分隔,给出文件跳转地址和链接。

_default/single.gmi

# {{ .Title }}{{ $scratch := newScratch }}
{{ $content := .RawContent -}}
{{ $content := $content | replaceRE `#### ` "### " -}}
{{ $content := $content | replaceRE `\n- (.+?)` "\n* $1" -}}
{{ $content := $content | replaceRE `\n(\d+). (.+?)` "\n* $2" -}}
{{ $content := $content | replaceRE `\[\^(.+?)\]:?` "" -}}
{{ $content := $content | replaceRE `<br/??>` "\n" -}}
{{ $content := $content | replaceRE `<a .*href="(.+?)".*>(.+?)</a>` "[$2]($1)" -}}
{{ $content := $content | replaceRE `\sgemini://(\S*)` " [gemini://$1](gemini://$1)" -}}
{{ $content := $content | replaceRE `{{ < audio "(.+?)" >}}` "=> https://brainbaking.com/$1 Embedded Audio link - $1" -}}
{{ $content := $content | replaceRE `{{ < video "(.+?)" >}}` "=> https://brainbaking.com/$1 Embedded Video link - $1" -}}
{{ $content := $content | replaceRE `{{ < youtube (.+?) >}}` "=> https://www.youtube.com/watch?v=$1 YouTube Video link to $1" -}}
{{ $content := $content | replaceRE `{{ < vimeo (.+?) >}}` "=> https://vimeo.com/$1 Vimeo Video link to $1" -}}
{{ $content := $content | replaceRE "([^`])<.*?>([^`])" "$1$2" -}}
{{ $content := $content | replaceRE `\n\n!\[.*\]\((.+?) \"(.+?)\"\)` "\n\n=> $1 Image: $2" -}}
{{ $content := $content | replaceRE `\n\n!\[.*]\((.+?)\)` "\n\n=> $1 Embedded Image: $1" -}}
{{ $content := $content | replaceRE `&emsp;` "  " -}}
{{ $tmpcontent := $content | replaceRE "(?ms:^```.*?```$)" "" -}}
{{ $links := findRE `\n=> ` $tmpcontent }}{{ $scratch.Set "ref" (add (len $links) 1) }}
{{ $refs := findRE `\[.+?\]\(.+?\)` $tmpcontent }}
{{ $scratch.Set "content" $content }}{{ range $refs }}{{ $ref := $scratch.Get "ref" }}{{ $contentInLoop := $scratch.Get "content" }}{{$linkmark := . |replaceRE `\[(.+?)\]\((.+?)\)` "[$1]-($2)" }}{{ $url := (printf "%s #%d" $linkmark $ref) }}{{ $contentInLoop := replace $contentInLoop . $url 1 -}}{{ $scratch.Set "content" $contentInLoop }}{{ $scratch.Set "ref" (add $ref 1) }}{{ end }}{{ $content := $scratch.Get "content" | replaceRE `\[(.+?)\]-\((.+?)\) #(\d+)` "$1 [#$3]" -}}

{{ $content | safeHTML }}
---
Written by {{ .Site.Author.name }} on {{ .Lastmod.Format (.Site.Params.dateFormat | default "2 January 2006") }}.

## References
{{ $scratch.Set "ref" (add (len $links) 1) }}{{ range $refs }}{{ $ref := $scratch.Get "ref" }}{{ $url := (printf "%s #%d" . $ref) }}{{ $base_url := $url | replaceRE `\[.+?\]\((.+?)\) #\d+` "$1" | absURL }}
=> {{ $base_url }} {{ $url | replaceRE `\[(.+?)\]\((.+?)\) #(\d+)` "$1 ($2)" -}}
{{ $scratch.Set "ref" (add $ref 1) }}{{ end}}
{{ $related := first 3 (where (where .Site.RegularPages.ByDate.Reverse ".Params.tags" "intersect" .Params.tags) "Permalink" "!=" .Permalink) }}
{{ if $related }}
## Related articles
{{ range $related }}
=> {{ .RelPermalink | absURL |replaceRE `https?://(.+?)` "gemini://$1" }} {{ .Title }}{{ if .Params.Subtitle }}: {{ .Params.Subtitle }}{{ end }}{{ end }}
{{ end }}
---

=> / Back to the Index
=> {{ .Site.BaseURL }}{{ replace .RelPermalink "index.gmi" "" }} View this article on the WWW

大部分参考 Parisian 的 Gemini and Hugo ,修改了少量的内容。

如添加 &emsp; 的替换,用作中文的缩进。使用 .Site.Author.name 等变量, 替换原来硬编码的作者名等。

此规则还不太完善,不代表最终版本。

_default/index.atom.xml

{{- $allowedRssSections := (slice "post") -}}
{{- $baseurl := .Site.BaseURL -}}
{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = $pctx.RegularPages -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>{{ .Site.Title }}</title>
  {{- $perm := replace .Permalink "/gemini" "" 1 -}}
  {{- $alt := .Site.BaseURL | replaceRE `https?://(.+?)` "gemini://$1" -}}
  {{ printf "<link rel=\"self\" type=\"application/atom+xml\" href=\"%s\"/>" $perm | safeHTML }}
  {{ printf "<link rel=\"alternate\" type=\"text/html\" href=\"%s\"/>" $alt | safeHTML }}
  <updated>{{ .Date.Format "2006-01-02T15:04:05-0700" | safeHTML }}</updated>
  <author>
    <name>{{ .Site.Author.name }}</name>
    <uri>{{ .Site.BaseURL | replaceRE `https?://(.+?)` "gemini://$1" }}</uri>
  </author>
  <id>{{ $perm }}</id>
  {{ range $pages }}
  <entry>
    <title>{{ .Title }}</title>
    {{- $entryperm := .Permalink | replaceRE `https?://(.+?)` "gemini://$1" -}}
    {{ printf "<link rel=\"alternate\" href=\"%s\"/>" $entryperm | safeHTML }}
    <id>{{ $entryperm }}</id>
    <published>{{ .Date.Format "2006-01-02T15:04:05-0700" | safeHTML }}</published>
    <updated>{{ .Lastmod.Format "2006-01-02T15:04:05-0700" | safeHTML }}</updated>
    <summary>{{ if isset .Params "subtitle" }}{{ .Params.subtitle }}{{ else }}{{ .Summary | html }}{{ end }}</summary>
  </entry>
  {{ end }}
</feed>

这部分没有太多好说的,与一般的 atom 模板大致相似。

只是使用正则,将 http 协议头,替换成了 gemini 的协议头。

文件树

最后,涉及到的文件大致如下

.
├── config.yaml
└── layouts
    ├── _default
    │   ├── index.atom.xml
    │   ├── list.gmi
    │   └── single.gmi
    └── index.gmi

运行 hugo 命令,生成静态文件后,可以看到 public 文件夹中生成了 gmi 后缀的文件。

这些文件可以用 agate 之类的服务器提供访问。

您现在可以用 amfora 之类的 Gemini 浏览器,通过 gemini://lin.moe/posts/hugo_and_gemini/ 访问本文

可惜 Hugo 不能提供 server 之类对 gemini 进行 debug 的功能。

除了使用 hugo 本身的功能,也可利用 md2gmi, gmnhg 等工具。笔者暂未尝试,效果未知,故不再此详细说明。

Update

2022-01-18

single.gmi: 修复重复链接匹配混乱和匹配到代码块中链接的问题

如果需要分离 gmi 文件,可以用下面的命令

# 复制 *.gmi 文件和 atom.xml 文件复制到 gemini 文件夹
rsync -zarv --include "*/" --include="*.gmi" --include="atom.xml" --exclude="*" --prune-empty-dirs ./public/* ./gemini 
# 其他文件复制到 www 文件夹
rsync -zarv --include "*/"  --exclude="*.gmi" --exclude="atom.xml" --prune-empty-dirs ./public/* ./www

2022-05-28

放弃了,太麻烦