<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>我的胶囊旅馆开张了 · Cytrogen 的个人博客</title><meta name="description" content="此胶囊旅馆非彼胶囊旅馆。虽说我对现实中的胶囊旅馆很感兴趣,但至今都没有睡过一次,去开张一家真正的胶囊旅馆更是无稽之谈。标题中的「胶囊旅馆」指的是通过 Gemini 协议提供内容的独立信息集合。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/d823.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/d823.html">永久链接</a><div class="p-summary visually-hidden"><p>此胶囊旅馆非彼胶囊旅馆。虽说我对现实中的胶囊旅馆很感兴趣,但至今都没有睡过一次,去开张一家真正的胶囊旅馆更是无稽之谈。标题中的「胶囊旅馆」指的是通过 Gemini 协议提供内容的独立信息集合。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/Gemini/">Gemini</a></div><a class="u-syndication visually-hidden" target="_blank" rel="noopener" href="https://m.otter.homes/@Cytrogen/116135105291668258">https://m.otter.homes/@Cytrogen/116135105291668258</a><h1 class="post-title p-name">我的胶囊旅馆开张了</h1><div class="post-info"><time class="post-date dt-published" datetime="2026-02-25T05:00:00.000Z">2/25/2026</time><time class="dt-updated visually-hidden" datetime="2026-02-26T04:44:36.523Z"></time></div><div class="post-content e-content"><html><head></head><body><p>此胶囊旅馆非彼胶囊旅馆。虽说我对现实中的胶囊旅馆很感兴趣,但至今都没有睡过一次,去开张一家真正的胶囊旅馆更是无稽之谈。标题中的「胶囊旅馆」指的是通过 Gemini 协议提供内容的独立信息集合。</p>
<span id="more"></span>
<p>想必有些读者看到 Gemini,第一时间想到的是同名的,由谷歌开发的多模态大型语言模型,亦或者是同由谷歌开发的生成式 AI 聊天机器人了吧。实则不然,本篇文章所说的 <em> Gemini</em> 指的都是 <u>Gemini 协议</u>,一个轻量级的互联网应用层通信协议。</p>
<p>一个常见的误解是,将「互联网」和「万维网」混为一谈。很多人认为,浏览器就是互联网,或者说互联网只有 HTTPS。这些都是认知盲区,但也怪不了谁。现代万维网极度繁荣,承载了普通用户所有的线上活动,但也掩盖了底层网络架构的多样性。</p>
<p>互联网是由全球无数计算机网络相互连接而成的庞大基础设施,它的核心职责是数据的路由和传输,其基石是 TCP / IP 协议族。</p>
<p>在计算机网络的体系结构中,网络通信被严密地划分为不同的层级。从下往上分别是「链接层」、「网络层」、「传输层」、「应用层」。</p>
<p>底层协议负责处理物理信号、IP 寻址和数据包在路由器之间的接力,而最顶层的应用层协议定义了运行在不同设备上的应用程序如何相互传递报文、直接决定了数据在终端用户面前的呈现和交互方式。</p>
<p>为了让同一台服务器能够同时处理多种不同的网络服务,传输层引入了「端口」的概念。当我们在浏览器中输入 <code>https://</code> 时,实际上要求了操作系统使用超文本传输协议安全版(HTTP over TLS,也就是 HTTPS),去连接目标服务器的 443 端口。</p>
<p>HTTP 协议最初仅仅是为了在研究人员之间共享简单的学术超文本文档而设计的。但随着数十年的发展,为了满足复杂的现代 Web 应用程序的需求,它被不断追加了 Cookie 会话管理、CORS、Service Worker 等庞杂的机制,最终造就了今天功能强大但也极其臃肿的 Web 生态。</p>
<p>不过应用层是一个相当宽广的领域,HTTP 仅仅是其中的一个居民。互联网上时刻运行着大量与 HTTP 平级的应用层协议,它们各自服务于高度专一的通信需求。例如:构建全球电子邮件系统的核心是 SMTP 和 IMAP / POP3 协议;远程管理 Linux 服务器时,系统管理员会依赖运行在 22 端口的 SSH 协议;服务器之间进行时间同步,依赖于 NTP 协议。这些协议支撑着互联网的底层运转,却鲜为人知,很大缘故是它们通常在后台静默工作,或者需要特定的客户端而非通用浏览器来访问。</p>
<p>今天的主角 Gemini 协议也是与 HTTP 完全平行的应用层协议。它拥有着自己专属的通信规则、状态码体系和数据格式,并且默认运行在 TCP 1965 端口(纪念 1965 年的「双子星号」载人航天任务)。</p>
<p>在深入 Gemini 的技术基层之前,有必要了解它的精神前身:<u>Gopher 协议</u>。</p>
<p>在万维网尚未一统天下的二十世纪,Gopher 是互联网上最流行的信息检索系统。它采用严格的层级菜单结构来组织纯文本信息:</p>
<figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Welcome to Gopherspace</span><br><span class="line"></span><br><span class="line"> 1. About this server.</span><br><span class="line"> 2. Articles and Musings/</span><br><span class="line"> 3. Links to other Gopher holes/</span><br><span class="line"> 4. Search this server <?></span><br><span class="line"> 5. A plain text document.</span><br><span class="line"></span><br><span class="line">Press ? for Help, q to Quit, u to go up a menu, or enter a number: _</span><br></pre></td></tr></tbody></table></figure>
<p>大概就是这样吧,是不是看着很像早些年的文字游戏呢?</p>
<p>其实我最初学习编程语言,写的第一个项目就是一个类似于这样的文字冒险游戏 —— 可惜源代码被我删除了,之后考虑使用 Python 重构却没有写下去。</p>
<p>Gopher 的客户端界面之所以是这么个列表形态,是因为服务端返回的原始数据本身就是一种以制表符分割的结构化纯文本。每一行代表菜单中的一项,行首的单个字符定义了该项的资源类型。</p>
<p>然而随着带有图形界面、支持内嵌图像和复杂排版的早期 Web 浏览器的出现,加之 Gopher 的发源地明尼苏达大学曾短暂尝试其服务端软件收取许可费,Gopher 迅速走向了衰落。</p>
<p>Gemini 正是诞生于对现代 Web 日益臃肿和商业化,以及对 Gopher 历史教训的深度反思之中。其核心设计理念被概括为「比 Gopher 重,比 HTTP 轻」。很聪明地,它并不打算取代现有的 Web,而是想要构建一个被称为 <em> Geminispace</em> 的独立、纯粹的文本网络生态。对于偏好纯文本与自托管服务的极客们而言,Gemini 是个相当具备吸引力的信息分发与获取方案。</p>
<p>与 HTTP 不同的是,Gemini 强制要求使用 TLS 加密连接,不允许任何明文传输,确保了基础的通信安全。同时它在实践中广泛接受 TOFU(Trust on First Use,首次信任)模式或自签发证书,大幅降低了个人站长维护基础设施的门槛。</p>
<p>其请求和响应模型被设计为一次性的单向事务,连接在响应完成后立即关闭。服务端的响应头部极简,仅包含一个两位数的状态码、一个空格、一段元数据(在请求成功时通常是 MIME 类型,如 <code>text/gemini</code>),从根本上抛弃了 HTTP 中复杂的头部字段。</p>
<p>更为彻底的是,Gemini 规范中没有 Cookie,没有 User-Agent 嗅探,不支持任何形式的客户端脚本执行。如果服务端应用需要维持会话状态或进行身份验证,协议规定直接使用 TLS 客户端证书来实现。这意味着在体系结构上,任何人都无法在 Gemini 页面中嵌入追踪探针、广告代码或第三方分析工具。</p>
<p>与该协议深度绑定的是一种名为 Gemtext 的专属轻量级标记语言(文件后缀通常为 <code>.gmi</code>)。Gemtext 的语法相较于 Markdown,仅支持三级标题、无序列表、引用块、预格式化文本以及链接。</p>
<p>三级标题对我而言,不是什么大问题,因为我现在使用的主题的原作者 <a target="_blank" rel="noopener" href="https://github.com/WhoKnowInfinity">Infinity</a> 这么认为:</p>
<blockquote>
<p>实际上,Hexo-theme-apollo 只支持两种标题:<code>h1~h3</code> 大标题,<code>h4~h6</code> 小标题,也就是说,<code># 和 ###</code> 的样式是一样的。之所以这么处理,是因为就个人感觉而言,我们不应该为文章设置过多的层级消耗读者的阅读精力。这相当于强制使用 Hexo-theme-apollo 的用户在写文章时注意文章结构,最多只能使用两层结构。</p>
</blockquote>
<p>不过我印象里,这个「两种标题」并不准确,实际上是三种标题:<code>h1</code> 一个,<code>h2~h3</code> 一个,<code>h4~h6</code> 一个才对。我并不记得我特意修改了这一点,也有可能是我记错了。总之,我在写博客的时候就已经慢慢培养出来了不滥用标题层级的习惯。</p>
<p>特殊的是,Gemtext 还严格禁止在段落文本中内嵌链接,所有超链接必须作为独立的代码行存在。</p>
<p>我个人算是半个脚注的反对者。脚注在实体书籍上出现没问题,但展示在互联网的文章上,阅读起来相当难受。如果可以的话,我会希望文章都尽量是从头到尾、瀑布般的排版,不需要让读者去别的地方才能收获完整的阅读体验。不过为了漂亮,我还是会在文本内嵌入链接,给它们附加文本。这个做法我会深思熟虑一下,是否应当完全参照 Gemini 协议希望的那样。</p>
<p>先前说过,Capsule 是通过 Gemini 协议提供内容的独立信息集合。它的概念完全等同于万维网中的 Website。正如一个 Website 是由多个通过 HTTP 协议传输的 HTML 页面和资源组成,一个 Capsule 则是通过 Gemini 协议传输的 Gemtext 文件及其他媒体文件的集合。</p>
<p>虽然它的命名出处来自于阿波罗计划中的「双子星号」太空舱,但结合我给个人网络附加的设定,最终将我自己的 Capsule 其称为「胶囊旅馆」。莫要搞混了,它的意思是太空舱,而不是我二次创作出来的胶囊旅馆!</p>
<p>因为 Capsule 等同于 Website,所以通过 Gemini 客户端访问他人搭建的服务器时,最好不要称其为「Gemini 站点」,而是说「Gemini 胶囊」会更好些。因为「站」这个概念属于 HTTP 协议,胶囊严格来说也不属于「网站」—— 网站是依附于万维网生态、基于 HTTP / HTTPS 协议和 HTML 标记语言构建的产物。</p>
<h3 id="访问-gemini-地址"><a class="markdownIt-Anchor" href="#访问-gemini-地址"></a> 访问 Gemini 地址</h3>
<p>由于 Gemini 协议从根本上切断了与 HTTP 的联系,常规的现代 Web 浏览器是无法撬开这些太空舱的。我们需要借助专门支持 Gemini 协议的客户端工具。</p>
<p>在终端环境中,Amfora 是一款功能完备的命令行客户端。如果依然偏好图形界面的便捷性,Lagrange 则是目前生态中非常优秀的桌面端选择。</p>
<p>如果你要问我,我选了哪个的话。莫要忘了,我是一个 <em> typical Emacs user</em>(经典 Emacs 用户)—— 其实只要使用 Elpher 包就可以访问 Gemini 胶囊啦,太方便了!</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">M-x package-install RET elpher RET</span><br></pre></td></tr></tbody></table></figure>
<p>接着你可以在 Elpher 界面上,使用 <code>g</code> 命令,然后输入我的胶囊旅馆地址 <code>gemini://cytrogen.icu</code> 便可以访问了。</p>
<h3 id="博文生成和部署"><a class="markdownIt-Anchor" href="#博文生成和部署"></a> 博文生成和部署</h3>
<p>我的写作流程因为相当私人化,所以仅供参考了。</p>
<p>Org Mode 文件是所有文章的上游。写好 Org Mode 文件,我会转换它为 Markdown 和 Gemtext 文件。这里只说 Gemtext 的转换:我引用的是 <code>ox-gemini</code> 这个导出后端,不过我有很多额外的定制化需求,因此进行了一些魔改。比方说在我的月刊内,有个「日记片段」板块,那里理论上来说是 Hexo 框架在构建文件时,自动从 Fediverse 应用上获取数据、嵌入 HTML 文件内。这意味着 Org Mode 文件里没有这些日记内容,转换成的 Gemtext 文件也不会有。但我目前并不希望 Gemtext 文件在构建时进行过多的操作,导致流程愈来愈复杂,最终决定只放一个 Mastodon 主页链接。</p>
<p>接着我会将 Gemini 项目 <code>git push</code> 到 VPS 的 bare repo,并触发它的构建命令。说是构建命令,其实也就是帮我部署到 Gemini 服务器,然后镜像到 Sourcehut 仓库。</p>
<p>我选用的是 Agate 这个由 Rust 写的 Gemini 协议服务器。它会自动生成并管理 TLS 证书,不需要额外配置 Let's Encrypt,运行内存要求很低,我自己搭建后实际用的连 500KB 都不到。</p>
<p>不过它仅适用于纯静态 gemtext 文件 capsule。想要动态内容支持的话,可以考虑一下 Gemserv 或者 Molly Brown 这些服务器。</p>
<p>在 VPS 上安装了 Agate 之后,所需要的命令相当简单:</p>
<figure class="highlight zsh"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">agate --content /gemtext/文件目录 --hostname 域名.后缀 --lang zh</span><br></pre></td></tr></tbody></table></figure>
</body></html></div></article></div></main><footer><div class="paginator"><a class="next" href="fbd.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/d823.html" data-full-url="https://cytrogen.icu/posts/d823.html" data-syndication-url="https://m.otter.homes/@Cytrogen/116135105291668258" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<span>暂无 Webmentions</span>
<div class="webmention-respond">
<a class="webmention-respond-btn" href="https://m.otter.homes/@Cytrogen/116135105291668258" target="_blank" rel="noopener syndication">在 Mastodon 上回应本文</a>
</div>
<p class="webmention-hint">本站支持 <a href="https://www.w3.org/TR/webmention/" target="_blank" rel="noopener">Webmention</a>。你可以在 Fediverse(如 Mastodon)上回应本文的联合链接,互动会自动出现在此处。<a href="/colophon/#webmention">详情见营造记</a>。</p>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>