blog-site

git clone git://git.lin.moe/blog-site.git

  1---
  2title: "Hugo 和 Gemini"
  3date: 2021-12-05T15:37:09+08:00
  4---
  5
  6* [Gemini](https://en.wikipedia.org/wiki/Gemini_%28protocol%29) 是什么
  7* [Gemini 官网](https://gemini.circumlunar.space/)
  8
  9我觉得 Gimini 是为终端用户而生的协议。  
 10
 11如果您是 GUI 用户,大概会对 Gemini 协议无感。  但是如果平时经常用 CLI ,大概会觉得 Gemini 是一个简洁优雅的协议。    
 12
 13Gemini 协议简单,语法比较像 Markdown 的缘故,像是 Hugo 这样用 Markdown 写作的博客平台,可以比较容易的生成 gemini 站点所需的文件。  
 14
 15本文参考 [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 协议,在此说明。**  
 18
 19**此教程只适用于服务器搭建的 Hugo 博客站点,不适用于使用 Github Page 等服务搭建的博客站点**  
 20
 21## 修改  config 文件
 22
 23先添加 gmi 模板文件的支持,Hugo 的开发者已经想到了会有这样的需求。可以用不同的文件后缀,来区分不同的协议模板文件。  
 24
 25除了已定义好的 AMP ,CSV,JSON 等各种格式,也可以在 config 中自定义其他的格式。  
 26
 27下面是定义使用 gmi 作为模板文件后缀,并添加订阅链接的配置文件,以 yaml 格式为例,添加到原有配置文件结尾即可。  
 28
 29
 30
 31```
 32mediaTypes:
 33  text/gemini:
 34    suffixes:
 35      - "gmi"
 36  application/atom:
 37    suffixes:
 38      - "xml"
 39
 40outputFormats:
 41  GEMINI:
 42    name: "GEMINI"
 43    isPlainText: true
 44    isHTML: false
 45    mediaType: "text/gemini"
 46    protocol: "gemini://"
 47    permalinkable: true
 48    # path: "gemini/"
 49    path: ""
 50  ATOM:
 51    name: "ATOM"
 52    isPlainText: false
 53    isHTML: true
 54    protocol: "gemini://"
 55    path: "/gemini/"
 56    permalinkable: true
 57    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"
 72
 73```
 74
 75  
 76
 77配置文件的详细意义,可以参考 [官方文档](https://gohugo.io/templates/output-formats/)   、
 78
 79此配置有别于  [Using Hugo to Launch a Gemini Capsule](https://brainbaking.com/post/2021/04/using-hugo-to-launch-a-gemini-capsule/) ,没有将 gemini 生成内容单独放一个子文件夹,以便更好的利用 Hugo 的 section 功能。  
 80
 81## 配置模板文件  
 82
 83模板文件皆在 layouts 文件夹下。可以是主题包内的 layouts 文件夹,如不愿意动主题包,也可以放在博客主目录的 layouts 文件夹中。  
 84
 85### index.gmi  
 86
 87首先写首页      
 88
 89```
 90# {{ .Site.Title }}
 91
 92## Subcription  
 93=> gemini/atom.xml Gemini Atom Feed
 94
 95## Pages
 96{{ range .Site.Menus.main }}
 97=> {{ .URL| }} - {{ .Name }}
 98{{- if .Params.Subtitle }}  {{ .Params.Subtitle }}{{- end}}
 99{{- end}}
100
101## Social
102{{ range .Site.Params.social }}
103=> {{ .url | safeURL }} {{ .name | humanize }}
104{{ end }}
105
106=> {{ .Site.BaseURL }}{{ replace .RelPermalink "index.gmi" "" }} View {{ .Site.Title }} on the WWW
107
108```
109
110* Subscription 部分提供 Gemini 的订阅地址  
111* Pages 部分展示首页的菜单,Social 部分展示的联系方式,在配置文件中给出。  
112* 最后的链接提供 http 访问。  
113
114
115配置参考 Hermit 主题,如下。  
116
117
118```
119menu:
120  main:
121    - name: Posts
122      url: posts/
123    - name: Now
124      url: now/
125    - name: About
126      url: about/
127    - name: Links
128      url: links/
129params:
130  social:
131    - name: email
132      url: 'mailto://i@lin.moe'
133    - name: telegram
134      url: 'https://t.me/LindsayZhou'
135    - name: gitea
136      url: 'https://git.lin.moe/Lindsay'
137```
138
139
140### _default/list.gmi  
141
142```
143# Posts
144  
145{{- range .Pages.GroupByDate "2006-01" }}  
146  
147### {{ .Key }}
148{{- range .Pages }}
149=> {{.Permalink}} {{.Title}}
150{{- end }}{{- end }}
151```
152
153目录页面以年月分隔,给出文件跳转地址和链接。  
154
155### _default/single.gmi    
156
157```
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 `&emsp;` "  " -}}
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]" -}}
179
180{{ $content | safeHTML }}
181---
182Written by {{ .Site.Author.name }} on {{ .Lastmod.Format (.Site.Params.dateFormat | default "2 January 2006") }}.
183
184## References
185{{ $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 articles
191{{ range $related }}
192=> {{ .RelPermalink | absURL |replaceRE `https?://(.+?)` "gemini://$1" }} {{ .Title }}{{ if .Params.Subtitle }}: {{ .Params.Subtitle }}{{ end }}{{ end }}
193{{ end }}
194---
195
196=> / Back to the Index
197=> {{ .Site.BaseURL }}{{ replace .RelPermalink "index.gmi" "" }} View this article on the WWW
198```
199
200大部分参考 Parisian 的 [Gemini and Hugo](https://sylvaindurand.org/gemini-and-hugo/)  ,修改了少量的内容。  
201
202如添加 \&emsp; 的替换,用作中文的缩进。使用 .Site.Author.name 等变量, 替换原来硬编码的作者名等。  
203
204此规则还不太完善,不代表最终版本。  
205
206
207### _default/index.atom.xml  
208
209```
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```
250
251
252
253这部分没有太多好说的,与一般的 atom 模板大致相似。  
254
255只是使用正则,将 http 协议头,替换成了 gemini 的协议头。  
256
257
258## 文件树  
259最后,涉及到的文件大致如下  
260
261```
262.
263├── config.yaml
264└── layouts
265    ├── _default
266    │   ├── index.atom.xml
267    │   ├── list.gmi
268    │   └── single.gmi
269    └── index.gmi
270```
271
272
273
274运行 hugo 命令,生成静态文件后,可以看到 public 文件夹中生成了 gmi 后缀的文件。    
275
276这些文件可以用 agate 之类的服务器提供访问。  
277
278您现在可以用 amfora 之类的 Gemini 浏览器,通过 gemini://lin.moe/posts/hugo_and_gemini/ 访问本文  
279
280可惜 Hugo 不能提供 server 之类对 gemini 进行 debug 的功能。  
281
282## 另
283除了使用 hugo 本身的功能,也可利用 [md2gmi](https://github.com/n0x1m/md2gmi), [gmnhg](https://github.com/tdemin/gmnhg) 等工具。笔者暂未尝试,效果未知,故不再此详细说明。
284
285## Update
286#### 2022-01-18  
287single.gmi: 修复重复链接匹配混乱和匹配到代码块中链接的问题  
288
289如果需要分离 gmi 文件,可以用下面的命令  
290```
291# 复制 *.gmi 文件和 atom.xml 文件复制到 gemini 文件夹
292rsync -zarv --include "*/" --include="*.gmi" --include="atom.xml" --exclude="*" --prune-empty-dirs ./public/* ./gemini 
293# 其他文件复制到 www 文件夹
294rsync -zarv --include "*/"  --exclude="*.gmi" --exclude="atom.xml" --prune-empty-dirs ./public/* ./www
295```   
296  
297
298#### 2022-05-28  
299放弃了,太麻烦  
300