Gemini で個人サイト (カプセル) を作った

昨日、YouTube を見ていたら、申し訳程度に登録していた DistroTube の下記の動画がたまたま目に留まりました。

https://youtu.be/aklF1v8v2Mk

Gemini? Capsule? なんだそれ？って感じだったのですが、視聴してみると、Gemini というプロトコルがあって、そのプロトコルで通信する世界には別の世界が広がっていることを知り、自分でもその世界に飛び込んでみようと思い、実際に個人サイト (カプセル) を作ってみた次第です。

## Gemini とは？

任意のファイルを配布するためのアプリケーションレベルの新しいプロトコルで、2019 年に生まれた、割と最近の作られたばかりプロトコルです。

Gopher (Go のマスコットじゃなく) を知ってる人だったら、Gopher を少しいい感じにした [^1] ものという説明だけで、Gemini が何者なのかすぐにわかるっぽいのですが、私は Go のマスコットの方しか知りません。

これも教養がないので知らなかったのですが、アポロ計画に先駆けて実施された有人宇宙飛行計画をジェミニ計画と言うそうです。Gemini という名称はそこから取っていて、マーキュリー計画が Gopher で、アポロ計画が Web ならば、Gemini はその中間であるといった意味合いが含まれているらしいです。

Gemini 上のサイトはカプセルと呼ぶらしいのですが、その理由がわかりました。

今の Web の世界にうんざりしていたり、もっと低速なネットワーク、低消費電力なコンピューティングに興味がある人におすすめで、プロトコル部分はほぼ完成しているとのこと。

生まれた背景から言って、今後仕様をいろいろ拡張していったり、普及するように宣伝したりはないと思いますし、商業的に盛り上がることも皆無でしょう。

以下の FAQ を見ると Gemini が何者で、どんなことができるかがザッと把握できます。

https://gemini.circumlunar.space/docs/faq.gmi

## Gemini の世界

冒頭で紹介した DistroTube の Gemini カプセルは、Gemini クライアントで閲覧すると下記のような見た目になります。

![distro.tube](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8478/f70d7def-47f1-cd70-36b0-f2a417cba969.png)

**しっぶ、めっちゃ渋っ**。

URL は **gemini://distro.tube/** のように、プロトコル (スキーム) 名が `gemini:` になります。

ちなみに CSS のような仕組みはなく、何のメタ情報も持てない Markdown のような (後述の Gemitext) 単なるテキストファイルを、サーバからそのまま返すので、その見た目をどうこうするのはクライアントの責務です。

## Gemini クライアント

いくつか Gemini クライアントがあって、私は下記の 3 点を試しました。

- [Amfora](https://github.com/makeworld-the-better-one/amfora)
  - Go で書かれた Gemini クライアント。バイナリを入手して実行するだけ。Vim キーバインドで操作できる。ちなみに DistroTube ではこれを使っていた。
- [Elpher](https://github.com/emacsmirror/elpher)
  - Emacs の Gopher クライアントで Gemini にも対応している。
- [Ariane](https://oppen.digital/software/ariane/)
  - Gemini の Android 用クライアント。

Amfora は見た目もよく、Vim キーバインドで操作性も良いので広範囲の人におすすめできそうでした。リンク先に数字キーでジャンプできる機能、フィードの購読機能、テーマ機能、ブックマーク機能などがあります。

私は Emacser なので、PC では操作体系も慣れていて、必要に応じてカスタマイズもできる Elpher をメインに使うと思います。

Android に入れた Ariane も閲覧しやすく、Gemini はワンカラムレイアウト強制になりますし、余計な情報も一切入ってこないので、テキストをじっくりと読めます。Gemini はスマホでの閲覧に向いているのかもしれません。 

## Gemitext Markup Language

Gemini で主にページの記述に使用する言語は Gemitext という Markdown の劣化版 (失礼) みたいな言語で、Gemitext で書かれたテキストファイルに、拡張子 `.gmi` を付けて、非公式な MIME タイプである `text/gemini` でサーバから応答するようにすると、クライアントがいい感じ (クライアントに依るかもしれませんが) にレンダリングしてくれます。 

https://gemini.circumlunar.space/docs/gemtext.gmi

参考までに gemini://distro.tube/ のトップページ 
(`index.gmi`) のソースコードは下記のようになってます。リンクの仕様が少し特徴的なくらいで、ほぼ Markdown ですね。

    ```
         __ __       __                 __         __          
     .--|  |__.-----|  |_.----.-----.  |  |_.--.--|  |--.-----.
     |  _  |  |__ --|   _|   _|  _  |__|   _|  |  |  _  |  -__|
     |_____|__|_____|____|__| |_____|__|____|_____|_____|_____|
    
    ```
    # WELCOME TO DISTRO.TUBE
    Distro.tube is a site created by Derek Taylor (DT), creator of the DistroTube (DT) channel on YouTube and LBRY.  I create videos about GNU/Linux, free software, open source software and related interests.  This site is best viewed using the Gemini protocol using a Gemini browser such as Amfora (terminal-based client), Lagrange (GUI client) or using the Geminize plugin for Firefox.
    
    => https://youtube.com/DistroTube DistroTube on YouTube
    => https://odysee.com/@DistroTube:2 DistroTube on Odysee (LBRY)
    => https://patreon.com/distrotube/ Support DT on Patreon
    => https://gitlab.com/dwt1/ DT's GitLab
    => https://gitlab.com/dwt1/distro.tube Submit Guest Articles On GitLab
    
    ## VIDEO LIBRARY
    Here are links to my videos hosted on Odysee (LBRY), a free and open source alternative to YouTube.
    
    => videos/2021-videos.gmi Videos from 2021
    => videos/2020-videos.gmi Videos from 2020
    => videos/2019-videos.gmi Videos from 2019
    => videos/2018-videos.gmi Videos from 2018
    => videos/2017-videos.gmi Videos from 2017
    
    ## ARTICLES
    Here are some articles written by me that some may find useful.  They are mostly Linux and software-related.
    
    => articles/social-media-implements-new-policies.gmi Social Media Sites Implement New Policies To Combat Wrong-Think (07-20-2020)
    => articles/fish-is-the-better-shell.gmi Fish Is The Better Shell For More Modern Times (04-09-2020)
    => articles/give-new-linux-users-ubuntu.gmi Give New Linux Users Ubuntu, Not Choice (03-28-2020)
    => articles/standardized-keybindings-tiling.gmi Standardized Keybindings Across All Tiling Window Managers (03-12-2020)
    => articles/why-linux-users-are-elitist.gmi Why Do Most Linux Users Have An Elitist Attitude? (03-07-2020)
    => articles/file-globbing-in-linux.gmi File Globbing In Linux (02-26-2020)
    => articles/securing-the-firefox-browser.gmi Securing The Firefox Web Browser (02-25-2020)
    => articles/move-your-home-folder-to-second-drive.gmi Move Your Home Folder To A Second Drive (02-20-2020)
    => articles/the-foss-code-of-conduct.gmi The FOSS Code Of Conduct (01-20-2020)
    => articles/seven-things-to-avoid-on-linux.gmi Seven Things To Avoid On Linux (01-14-2020)
    => articles/installing-lets-encrypt-on-ubuntu.gmi Installing Let's Encrypt On Your Ubuntu Server (01-04-2020)
    
    ## GUEST ARTICLES
    These are guest articles submitted by members of the community.  The source of this site and all of its content is availble on my GitLab.  Thus, anyone that wants to submit an article can do so with a pull request.  Please, submit your articles in "gemtext" and not HTML or markdown.
    
    => guest-articles/interactive-dotfile-management-dotbare.gmi Interactive Dotfile Management With Dotbare (05-25-2020) [Kevin Zhuang]
    => guest-articles/managing-dotfiles-with-rcm.gmi Managing Dotfiles With Style With rcm (04-23-2020) [Ronnie Nissan]
    => guest-articles/installing-manjaro-dual-boot.gmi Installing Manjaro in Dual Booting Environment (04-22-2020) [Primož Ajdišek "Bigpod"]
    => guest-articles/installing-and-using-sxhkd.gmi Installing And Using sxhkd (04-21-2020) [Ronnie Nissan]
    => guest-articles/using-dwmblocks-with-dwms-bar.gmi Using dwmblocks With dwm's Bar (04-20-2020) [Ronnie Nissan]
    => guest-articles/how-to-install-suckless-tools.gmi How to Install, Customize and Backup Suckless Tools (04-18-2020) [Ronnie Nissan]
    => guest-articles/its-time-for-new-text-editor.gmi It's Time For a New Text Editor (03-20-2020) [Klaus-Dieter Schmatz]
    => guest-articles/vim-plugins-without-manager.gmi Vim Plugins Without a Plugin Manager (03-15-2020) [Klaus-Dieter Schmatz]
    
    ## PROJECT GEMINI
    This site uses a new Internet protocol called Gemini which can be seen as a middle ground between the old gopher protocol of the early 1990s and the modern web.  Gemini attempts to address the weaknesses of gopher while avoiding the pitfalls of the web.
    
    I have built this site (or "capsule") using Gemini in an effort to promote the project in the hope that more people will turn away from the dumpster fire that is the modern web.  Gemini is plain text written in a markdown-like syntax.  Links can only be written and displayed one per line (similar to gopher).  There are no images, no video and no advertisements!
    
    Gemini should be of particular interest to people who are:
    * Opposed to the web's ubiquitous user tracking
    * Tired of obnoxious adverts, autoplaying videos and other misfeatures
    * Interested in low-power computing and/or low-speed networks
    * Opposed to the big tech monopoly that dominates the web
    
    If you want to help promote a simpler and more efficient alternative to the web, please consider making your next "web" site a Gemini capsule instead.
    
    => gemini://gemini.circumlunar.space Project Gemini
    => gemini://geminispace.info Gemini Search Engine
    
    ## INTERESTING GEMINI CAPSULES
    => gemini://chriswere.uk Chris Were
    => gemini://gemini.barca.mi.it fnt400
    => gemini://friendo.monster Friendo (uoou)
    => gemini://hexdsl.co.uk Hex DSL
    => gemini://gemini.tunerapp.org Internet Radio Stations Directory
    => gemini://medusae.space medusae.space Gemini directory

https://gitlab.com/dwt1/distro.tube/-/blob/7406d63233383d11ca1d5edf740fc89ed5f3d4c9/index.gmi

## Gemini-to-web proxy

Gemini には上記のクライアントの他、Web で Gemini カプセルを閲覧できるようにするための Gemini-to-web proxy がいくつかあるようです。

https://proxy.vulpes.one/ などがその一例で、例えば DistroTube のカプセルは下記の URL で Gemini-to-web proxy 越しに閲覧できます。

https://proxy.vulpes.one/gemini/distro.tube

## Gemini カプセルのホスティング

Gemini サーバを自分で管理したくはなかったので、無料のホスティングができないか調べていたところ、FAQ にいくつか方法が載っていて、そのうちの 1 つに、[sourcehut](https://sourcehut.org/) でのホスティングがありました。sourcehut は、以前アカウント作って放置していたこともあって、これを活用することにしました。

https://proxy.vulpes.one/gemini/srht.site/

上記のページの [Quick start](https://proxy.vulpes.one/gemini/srht.site/quickstart.gmi) の説明にあるとおり、`.gmi` ファイルを `.tar.gz` にまとめ、あらかじめ発行しておいたアクセストークンを使って、作った `.tar.gz` を sourcehut に POST するだけで公開できます。GitHub Pages みたいなものです。めちゃ簡単。

下記のようなシェルスクリプトで公開しています。

```bash
#!/usr/bin/env bash

# export env
set -a
source .env
set +a

# bundle
find . -iname "*.gmi" -print0 | tar --null -T - -cvzf site.tar.gz

# publish
curl --oauth2-bearer $token \
    -Fcontent=@site.tar.gz \
    -Fprotocol=GEMINI \
    https://pages.sr.ht/publish/gemini.taiju.info
```

ちなみに独自ドメインが設定できるので、ドキュメントに書いてある通りに CNAME を設定してみたのですが、DNS の設定は正しくできているのに、Not Found [^2] になってしまってうまく表示できずしばらくハマりました。幸い、sourcehut は各種機能 (サービス) が AGPL でライセンスされているので、Go で書かれた下記のサーバ実装を読んで自己解決しました。

```go
func ServeGemini(conf ini.File, db *sql.DB, mc *minio.Client) *gemini.Server {
	bucket, _ := conf.Get("pages.sr.ht", "s3-bucket")
	prefix, _ := conf.Get("pages.sr.ht", "s3-prefix")
	certdir, _ := conf.Get("pages.sr.ht", "gemini-certs")

	certificates := &certificate.Store{
		CreateCertificate: func(hostname string) (tls.Certificate, error) {
			return certificate.Create(certificate.CreateOptions{
				Subject: pkix.Name{
					CommonName: hostname,
				},
				DNSNames: []string{hostname},
				Duration: 100 * 365 * 24 * time.Hour,
			})
		},
	}
	if err := certificates.Load(certdir); err != nil {
		log.Fatal(err)
	}

	mux := &gemini.ServeMux{}
	mux.HandleFunc("/", func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
		var version string
		ctx = database.Context(ctx, db)
		if err := database.WithTx(ctx, &sql.TxOptions{
			ReadOnly:  true,
			Isolation: 0,
		}, func(tx *sql.Tx) error {
			row := tx.QueryRowContext(ctx, `
				SELECT version
				FROM sites
				WHERE domain = $1 AND protocol = 'gemini';
			`, r.URL.Host)
			if err := row.Scan(&version); err != nil {
				return err
			}
			return nil
		}); err == sql.ErrNoRows {
			w.WriteHeader(gemini.StatusNotFound, "Site not found")
			return
		} else if err != nil {
			panic(err)
		}

		paths := []string{
			r.URL.Path,
			path.Join(r.URL.Path, "index.gmi"),
		}

		var object *minio.Object
		for _, cand := range paths {
			s3path := path.Join(prefix, "sites", r.URL.Host, version, cand)
			_, err := mc.StatObject(ctx, bucket, s3path, minio.StatObjectOptions{})
			if err != nil {
				continue
			}
			object, err = mc.GetObject(ctx, bucket, s3path, minio.GetObjectOptions{})
			if err != nil {
				panic(err)
			}
			break
		}

		if object == nil {
			w.WriteHeader(gemini.StatusNotFound, "File not found")
			return
		}

		defer object.Close()
		ext := path.Ext(r.URL.Path)
		if ext == ".gmi" {
			w.SetMediaType("text/gemini")
		} else {
			w.SetMediaType(mime.TypeByExtension(ext))
		}
		if _, err := io.Copy(w, object); err != nil {
			w.WriteHeader(gemini.StatusTemporaryFailure, err.Error())
			return
		}
	})

	srv := &gemini.Server{
		Handler:        mux,
		ReadTimeout:    30 * time.Second,
		WriteTimeout:   1 * time.Minute,
		GetCertificate: func(scope string) (*tls.Certificate, error) {
			if _, ok := certificates.Lookup(scope); !ok {
				certificates.Register(scope)
			}
			return certificates.GetCertificate(scope)
		},
	}

	return srv
}
```

https://git.sr.ht/~sircmpwn/pages.sr.ht/tree/0.4.4/item/server.go#L238-332

ドメインとプロトコルが DB で管理されているようで、sites テーブルに独自ドメイン用のレコードが INSERT されるように、`.tar.gz` の POST 先 URL に含まれるドメインも、独自ドメインに変更しておかないといけないというオチでした。AGPL バンザイ。

作った個人サイト (カプセル) は、Gemini クライアントで **gemini://gemini.taiju.info/** にアクセスすると閲覧できます。

独自ドメインを設定しない場合は、**gemini://taiju.srht.site/** のような URL になります。

ソースは下記に置きました。

https://git.sr.ht/~taiju/gemini.taiju.info

sourcehut にも [builds.sr.ht](https://man.sr.ht/builds.sr.ht/) という CI/CD のサービスがありますが、それを使った公開の自動化はまだしていません。

## Gemini プロトコルの仕様

ちゃんと読んでないです :bow:

## まとめ

シンプルに書いたテキストを公開したいのであれば、Gemini を使うのもありだなと思いました。誰も見てくれない文書になりそうだけど。

クライアントも Info っぽくて、各ページをスムーズに巡回できるので、技術文書とかだったらいい感じに読み進められそうです。

[Awesome リスト](https://github.com/kr1sp1n/awesome-gemini)を見るといろんなツールやリソースも見つかります。DistroTube では [gmi2html](https://github.com/shtanton/gmi2html) というツールで、Web 上にも HTML として公開することについても言及していました。個人的にはそこまでやると Gemini である意義もなくなる気がして微妙かもと思いました。[^3]

インラインイメージに対応していなかったりしますが、フィードを公開して[アグリゲータ](https://gemini.circumlunar.space/capcom/)に収集させたり、クライアントに依ると思いますが、それを購読できたりもするので、ブログにも使えるかもしれません。[ロボット型検索エンジン](https://proxy.vulpes.one/gemini/geminispace.info/)や、[カテゴリ型検索エンジン](https://proxy.vulpes.one/gemini/medusae.space/)[^4] もあるみたいです。ノスタルジー。

何にせよ非常にシンプルなプレーンテキストで文書などを公開でき、書き殴った簡単なシェルスクリプト程度で個人サイトを運用できるので、魅力に感じる人も少しはいるんじゃないかなと思いました。

自己紹介のコンテンツで、個人サイトの URL として `gemini://` から始まるテキストが書いてあったら、「こいつ、Tech Geek だな！」と評価してもらえるかもしれませんし、逆に変な人認定されて距離を置かれるかもしれませんが、現状の知名度や普及度からして後者でしょう。ありがとうございました。

Gemini の世界にようこそ。

以上。

[^1]: Gopher は非 ASCII 文字が使えないという日本人的に致命的な欠陥があったらしいが、その問題も解決している。 絵文字も使える。
[^2]: ページが見つからない時のステータスコードは HTTP と異なり `51 NOT FOUND` になっていて、ステータスコード体系も HTTP と異なる。
[^3]: とは言え、Gemini の世界だけにしかないコンテンツは誰も見に来ないというジレンマはある。
[^4]: Gemini クライアントでしかうまく画面遷移できなかった。