我觉得 Gimini 是为终端用户而生的协议。
如果您是 GUI 用户,大概会对 Gemini 协议无感。 但是如果平时经常用 CLI ,大概会觉得 Gemini 是一个简洁优雅的协议。
Gemini 协议简单,语法比较像 Markdown 的缘故,像是 Hugo 这样用 Markdown 写作的博客平台,可以比较容易的生成 gemini 站点所需的文件。
本文参考 Gemini and Hugo 和 Using 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
- Subscription 部分提供 Gemini 的订阅地址
- Pages 部分展示首页的菜单,Social 部分展示的联系方式,在配置文件中给出。
- 最后的链接提供 http 访问。
配置参考 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 ` ` " " -}}
{{ $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 ,修改了少量的内容。
如添加   的替换,用作中文的缩进。使用 .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
放弃了,太麻烦