1---2title: "Hugo 和 Gemini"3date: 2021-12-05T15:37:09+08:004---56* [Gemini](https://en.wikipedia.org/wiki/Gemini_%28protocol%29) 是什么7* [Gemini 官网](https://gemini.circumlunar.space/)89我觉得 Gimini 是为终端用户而生的协议。1011如果您是 GUI 用户,大概会对 Gemini 协议无感。 但是如果平时经常用 CLI ,大概会觉得 Gemini 是一个简洁优雅的协议。1213Gemini 协议简单,语法比较像 Markdown 的缘故,像是 Hugo 这样用 Markdown 写作的博客平台,可以比较容易的生成 gemini 站点所需的文件。1415本文参考 [Gemini and Hugo](https://sylvaindurand.org/gemini-and-hugo/) 和 [Using Hugo to Launch a Gemini Capsule](https://brainbaking.com/post/2021/04/using-hugo-to-launch-a-gemini-capsule/) ,在 [hermit](https://github.com/Track3/hermit) 主题的基础上修改。但其他主题也很容易效仿,也可以不修改 Hugo 的主题,只针对单个站点修改。16<!-- more -->17**由于此文援引 Parisian 的 [Gemini and Hugo](https://sylvaindurand.org/gemini-and-hugo/) ,需要与其协议兼容,故遵守其 [BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/fr/deed.zh) 协议,而不是底部脚注的 CC BY-NC 4.0 协议,在此说明。**1819**此教程只适用于服务器搭建的 Hugo 博客站点,不适用于使用 Github Page 等服务搭建的博客站点**2021## 修改 config 文件2223先添加 gmi 模板文件的支持,Hugo 的开发者已经想到了会有这样的需求。可以用不同的文件后缀,来区分不同的协议模板文件。2425除了已定义好的 AMP ,CSV,JSON 等各种格式,也可以在 config 中自定义其他的格式。2627下面是定义使用 gmi 作为模板文件后缀,并添加订阅链接的配置文件,以 yaml 格式为例,添加到原有配置文件结尾即可。28293031```32mediaTypes:33 text/gemini:34 suffixes:35 - "gmi"36 application/atom:37 suffixes:38 - "xml"3940outputFormats:41 GEMINI:42 name: "GEMINI"43 isPlainText: true44 isHTML: false45 mediaType: "text/gemini"46 protocol: "gemini://"47 permalinkable: true48 # path: "gemini/"49 path: ""50 ATOM:51 name: "ATOM"52 isPlainText: false53 isHTML: true54 protocol: "gemini://"55 path: "/gemini/"56 permalinkable: true57 mediaType: "application/atom"58 baseName: "atom"59outputs:60 home:61 - "HTML"62 - "RSS"63 - "ATOM"64 - "GEMINI"65 page:66 - "HTML"67 - "GEMINI"68 section:69 - "HTML"70 - "GEMINI"71 - "RSS"7273```74757677配置文件的详细意义,可以参考 [官方文档](https://gohugo.io/templates/output-formats/) 、7879此配置有别于 [Using Hugo to Launch a Gemini Capsule](https://brainbaking.com/post/2021/04/using-hugo-to-launch-a-gemini-capsule/) ,没有将 gemini 生成内容单独放一个子文件夹,以便更好的利用 Hugo 的 section 功能。8081## 配置模板文件8283模板文件皆在 layouts 文件夹下。可以是主题包内的 layouts 文件夹,如不愿意动主题包,也可以放在博客主目录的 layouts 文件夹中。8485### index.gmi8687首先写首页8889```90# {{ .Site.Title }}9192## Subcription93=> gemini/atom.xml Gemini Atom Feed9495## Pages96{{ range .Site.Menus.main }}97=> {{ .URL| }} - {{ .Name }}98{{- if .Params.Subtitle }} {{ .Params.Subtitle }}{{- end}}99{{- end}}100101## Social102{{ range .Site.Params.social }}103=> {{ .url | safeURL }} {{ .name | humanize }}104{{ end }}105106=> {{ .Site.BaseURL }}{{ replace .RelPermalink "index.gmi" "" }} View {{ .Site.Title }} on the WWW107108```109110* Subscription 部分提供 Gemini 的订阅地址111* Pages 部分展示首页的菜单,Social 部分展示的联系方式,在配置文件中给出。112* 最后的链接提供 http 访问。113114115配置参考 Hermit 主题,如下。116117118```119menu:120 main:121 - name: Posts122 url: posts/123 - name: Now124 url: now/125 - name: About126 url: about/127 - name: Links128 url: links/129params:130 social:131 - name: email132 url: 'mailto://i@lin.moe'133 - name: telegram134 url: 'https://t.me/LindsayZhou'135 - name: gitea136 url: 'https://git.lin.moe/Lindsay'137```138139140### _default/list.gmi141142```143# Posts144145{{- range .Pages.GroupByDate "2006-01" }}146147### {{ .Key }}148{{- range .Pages }}149=> {{.Permalink}} {{.Title}}150{{- end }}{{- end }}151```152153目录页面以年月分隔,给出文件跳转地址和链接。154155### _default/single.gmi156157```158# {{ .Title }}{{ $scratch := newScratch }}159{{ $content := .RawContent -}}160{{ $content := $content | replaceRE `#### ` "### " -}}161{{ $content := $content | replaceRE `\n- (.+?)` "\n* $1" -}}162{{ $content := $content | replaceRE `\n(\d+). (.+?)` "\n* $2" -}}163{{ $content := $content | replaceRE `\[\^(.+?)\]:?` "" -}}164{{ $content := $content | replaceRE `<br/??>` "\n" -}}165{{ $content := $content | replaceRE `<a .*href="(.+?)".*>(.+?)</a>` "[$2]($1)" -}}166{{ $content := $content | replaceRE `\sgemini://(\S*)` " [gemini://$1](gemini://$1)" -}}167{{ $content := $content | replaceRE `{{ < audio "(.+?)" >}}` "=> https://brainbaking.com/$1 Embedded Audio link - $1" -}}168{{ $content := $content | replaceRE `{{ < video "(.+?)" >}}` "=> https://brainbaking.com/$1 Embedded Video link - $1" -}}169{{ $content := $content | replaceRE `{{ < youtube (.+?) >}}` "=> https://www.youtube.com/watch?v=$1 YouTube Video link to $1" -}}170{{ $content := $content | replaceRE `{{ < vimeo (.+?) >}}` "=> https://vimeo.com/$1 Vimeo Video link to $1" -}}171{{ $content := $content | replaceRE "([^`])<.*?>([^`])" "$1$2" -}}172{{ $content := $content | replaceRE `\n\n!\[.*\]\((.+?) \"(.+?)\"\)` "\n\n=> $1 Image: $2" -}}173{{ $content := $content | replaceRE `\n\n!\[.*]\((.+?)\)` "\n\n=> $1 Embedded Image: $1" -}}174{{ $content := $content | replaceRE ` ` " " -}}175{{ $tmpcontent := $content | replaceRE "(?ms:^```.*?```$)" "" -}}176{{ $links := findRE `\n=> ` $tmpcontent }}{{ $scratch.Set "ref" (add (len $links) 1) }}177{{ $refs := findRE `\[.+?\]\(.+?\)` $tmpcontent }}178{{ $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]" -}}179180{{ $content | safeHTML }}181---182Written by {{ .Site.Author.name }} on {{ .Lastmod.Format (.Site.Params.dateFormat | default "2 January 2006") }}.183184## References185{{ $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 }}186=> {{ $base_url }} {{ $url | replaceRE `\[(.+?)\]\((.+?)\) #(\d+)` "$1 ($2)" -}}187{{ $scratch.Set "ref" (add $ref 1) }}{{ end}}188{{ $related := first 3 (where (where .Site.RegularPages.ByDate.Reverse ".Params.tags" "intersect" .Params.tags) "Permalink" "!=" .Permalink) }}189{{ if $related }}190## Related articles191{{ range $related }}192=> {{ .RelPermalink | absURL |replaceRE `https?://(.+?)` "gemini://$1" }} {{ .Title }}{{ if .Params.Subtitle }}: {{ .Params.Subtitle }}{{ end }}{{ end }}193{{ end }}194---195196=> / Back to the Index197=> {{ .Site.BaseURL }}{{ replace .RelPermalink "index.gmi" "" }} View this article on the WWW198```199200大部分参考 Parisian 的 [Gemini and Hugo](https://sylvaindurand.org/gemini-and-hugo/) ,修改了少量的内容。201202如添加 \  的替换,用作中文的缩进。使用 .Site.Author.name 等变量, 替换原来硬编码的作者名等。203204此规则还不太完善,不代表最终版本。205206207### _default/index.atom.xml208209```210{{- $allowedRssSections := (slice "post") -}}211{{- $baseurl := .Site.BaseURL -}}212{{- $pctx := . -}}213{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}214{{- $pages := slice -}}215{{- if or $.IsHome $.IsSection -}}216{{- $pages = $pctx.RegularPages -}}217{{- else -}}218{{- $pages = $pctx.Pages -}}219{{- end -}}220{{- $limit := .Site.Config.Services.RSS.Limit -}}221{{- if ge $limit 1 -}}222{{- $pages = $pages | first $limit -}}223{{- end -}}224{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}225<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">226 <title>{{ .Site.Title }}</title>227 {{- $perm := replace .Permalink "/gemini" "" 1 -}}228 {{- $alt := .Site.BaseURL | replaceRE `https?://(.+?)` "gemini://$1" -}}229 {{ printf "<link rel=\"self\" type=\"application/atom+xml\" href=\"%s\"/>" $perm | safeHTML }}230 {{ printf "<link rel=\"alternate\" type=\"text/html\" href=\"%s\"/>" $alt | safeHTML }}231 <updated>{{ .Date.Format "2006-01-02T15:04:05-0700" | safeHTML }}</updated>232 <author>233 <name>{{ .Site.Author.name }}</name>234 <uri>{{ .Site.BaseURL | replaceRE `https?://(.+?)` "gemini://$1" }}</uri>235 </author>236 <id>{{ $perm }}</id>237 {{ range $pages }}238 <entry>239 <title>{{ .Title }}</title>240 {{- $entryperm := .Permalink | replaceRE `https?://(.+?)` "gemini://$1" -}}241 {{ printf "<link rel=\"alternate\" href=\"%s\"/>" $entryperm | safeHTML }}242 <id>{{ $entryperm }}</id>243 <published>{{ .Date.Format "2006-01-02T15:04:05-0700" | safeHTML }}</published>244 <updated>{{ .Lastmod.Format "2006-01-02T15:04:05-0700" | safeHTML }}</updated>245 <summary>{{ if isset .Params "subtitle" }}{{ .Params.subtitle }}{{ else }}{{ .Summary | html }}{{ end }}</summary>246 </entry>247 {{ end }}248</feed>249```250251252253这部分没有太多好说的,与一般的 atom 模板大致相似。254255只是使用正则,将 http 协议头,替换成了 gemini 的协议头。256257258## 文件树259最后,涉及到的文件大致如下260261```262.263├── config.yaml264└── layouts265 ├── _default266 │ ├── index.atom.xml267 │ ├── list.gmi268 │ └── single.gmi269 └── index.gmi270```271272273274运行 hugo 命令,生成静态文件后,可以看到 public 文件夹中生成了 gmi 后缀的文件。275276这些文件可以用 agate 之类的服务器提供访问。277278您现在可以用 amfora 之类的 Gemini 浏览器,通过 gemini://lin.moe/posts/hugo_and_gemini/ 访问本文279280可惜 Hugo 不能提供 server 之类对 gemini 进行 debug 的功能。281282## 另283除了使用 hugo 本身的功能,也可利用 [md2gmi](https://github.com/n0x1m/md2gmi), [gmnhg](https://github.com/tdemin/gmnhg) 等工具。笔者暂未尝试,效果未知,故不再此详细说明。284285## Update286#### 2022-01-18287single.gmi: 修复重复链接匹配混乱和匹配到代码块中链接的问题288289如果需要分离 gmi 文件,可以用下面的命令290```291# 复制 *.gmi 文件和 atom.xml 文件复制到 gemini 文件夹292rsync -zarv --include "*/" --include="*.gmi" --include="atom.xml" --exclude="*" --prune-empty-dirs ./public/* ./gemini293# 其他文件复制到 www 文件夹294rsync -zarv --include "*/" --exclude="*.gmi" --exclude="atom.xml" --prune-empty-dirs ./public/* ./www295```296297298#### 2022-05-28299放弃了,太麻烦300