Ohhnews

分类导航

$ cd ..
foojay原文

Foojay全新改版:从WordPress迁移至基于Hugo的静态站点

#foojay#hugo#静态网站#开源社区#java

Foojay 已经迁移,但地址仍完全停留在原处,就像你收藏、链接或引用过的每一个 URL 一样。那个地址背后的一切都是新的。Foojay 现在是一个静态站点,其全部内容都放在一个公共 Git 仓库中,而发布一篇文章就是提交一个 PR。

如果你只读一段:你写过的任何东西都没有移动,你需要做的事情也没有变化,除非你想贡献内容

为什么要做这个改变?

Foojay 曾是一个 WordPress 站点,六年来、2,000 多篇文章,它完成了任务。但有几件事一直在悄悄地碍事。

发布存在与写作无关的摩擦。 一个有话要说的作者必须注册账号、学习编辑器,并希望格式在发布后还能保留下来。例如,Markdown 是可用的,但代码块在发布后有时看起来一团糟。与此同时,我们的大多数作者本来整天都在 IDE 或文本编辑器以及 Git 客户端中工作。

随着文章数量增长,WordPress 变得越来越慢。 但不只是页面加载时间增加了。添加新功能、改进样式、修复 bug、不断更新插件和 WordPress 本身,整个流程开始耗费太多时间、精力和金钱,对一个社区驱动的项目来说难以承受。

AI 出现了。 很长一段时间里,WordPress 是构建 Foojay 这类东西的少数选项之一。但有了 Hugo 和 AI 辅助编码,把我们原来的东西替换成对访客、作者和维护者都更快、更友好的东西,就变得可能——用非常小的预算,在几周内构建出你现在看到的东西。

一天晚上,我对反复出现的问题感到“受够了”,决定看看自己能不能做得更好。我有几个想法,想看看它们是否可行。我开始了小实验,那天凌晨 03:00,我确信自己能让这件事成功,并把所有现有内容迁移到一个 Hugo 网站,加上额外功能,并进行一次彻底清理。

为了给你现在看到的这个 Foojay 版本的“用非常小的预算,在几周内完成”给出真实数字,经过大量轮次的测试、重构、改进,以及把内容从 WordPress 导入 GitHub:

  • 两个月内大约 100 小时的工作,其中很多是在正式工作时间之外,因为我被卷入了一项使命 ;-)
  • 按公布的 token 价格计算,大约 $1000 的 AI 模型使用量。这个数字是 Claude 自己给我的,所以不知道它有多准确,也不知道对支付订阅的公司来说真实成本是多少……

由 Hugo 生成、由 Java 维护的新 Foojay

Foojay.io 现在是一个 Hugo 站点,由一个公共仓库构建,只要有任何内容进入 main 分支,就会通过 GitHub Actions 自动部署到 GitHub Pages。具体来说:

以前现在
发布CMS 账号 + 网页编辑器提交 PR
搜索服务器端静态索引,在浏览器中运行
评论插件GitHub Discussions,旧的评论作为存档保留
阅读量插件我们运行的一个计数器,在 Cloudflare 上存储 slug 和数字
分析通过 360 KB 的 Tag Manager 加载 Google Analytics同样的 Google Analytics,但直接加载
播客节目按下播放并收听同上,并在页面上提供文字稿
所有内容在数据库中可在 GitHub 上阅读的 Markdown 文件

所有东西都搬过来了:2250+ 篇文章350+ 位作者资料50+ 条 JVM 术语解释JUG(Java 用户组)目录日历赞助商……每条旧路径仍然可以解析,包括那些没人会想到去检查的:旧的 /blog/ 方案、已停用的 slug,以及多年前文章改名时变更过的 URL。

Hugo?没有 Java?

Foojay.io 是面向 OpenJDK 之友的网站,所以你会期待这里使用 Java。确实用了,但不是用于发布部分。

我确实看过那些 Java 生成器。我以前试过 JBake,当时它看起来像个已经沉寂的项目。从 2023 年初到 2025 年底都没有发布版本,这个间隔太长,没法把网站押在上面。(该表扬的还是要表扬:它后来发布了 2.7.0。)当我在 Foojay Slack 上提到这个项目时,有人向我推荐了 Roq,一个基于 Quarkus 的静态网站生成器。它是 Quarkus 之上的一层薄封装,通过 Qute 模板渲染 Markdown,具备类型安全模板和代码补全。它看起来确实不错,如果你也希望生成器用 Java 写,可以从那里开始。

我选择了我已经熟悉的东西。我用 Hugo 搭建过许多非常不同的网站:webtechie.becodewriter.bepi4j.comlottie4j.commelodymatrix.rocksvikdelportemusic.be。个人博客、开源项目文档、产品网站……它们都运行良好,而且 Hugo 一直稳定、长寿,并且始终非常活跃地维护着。对于一个十年后仍要站得住的社区项目,“我了解这个工具,而且它不会消失”胜过“这是最有趣的选择”。它还能在 GitHub Actions 上几分钟内构建所有文章、完整站点和搜索索引。

而且生成器本来就是这里面最小的一部分。还有大量不同的 Java 脚本,用于迁移、每日数据同步、对 PR 的检查等。正是这一部分让这一切成为可能。所有脚本都在仓库的 scripts/ 目录中。

所有这些都运行在 JBang 上,所以没有 pom.xml,没有 Gradle,也没有构建步骤。每个脚本都是一个单独文件,在顶部声明自己的依赖并直接运行:

$ java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.jsoup:jsoup:1.17.2
//DEPS com.vladsch.flexmark:flexmark-html2md-converter:0.64.8
//JAVA 21+

例如:jbang scripts/transfer/Posts.java 读取 WordPress 站点上的所有内容,用 jsoup 解析 HTML,flexmark 将其转换为 Markdown,Jackson 和 SnakeYAML 处理 JSON 和 YAML。作为一名 Java 开发者,这些就是我在其他项目中了解和使用的工具。基于这些信息,Claude 生成了脚本,我进行审查,并反复使用,直到切换完成——在新站构建和测试期间,老站一直保持在线。

在这个过程中,内容被清理,成为合适的 Markdown 站点。添加了更简单的图片画廊,代码块变成围栏代码块,链接经过检查和修复,每篇文章顶部的 frontmatter 现在要求六项内容:标题、日期、描述、作者、主图和分类。其他一切都由此派生。

一个潜在问题:GitHub Pages 有 1GB 的限制。很多图片是从旧站导入的,其中一些非常巨大。最大的是一个 52 MB 的动态 GIF,被用作三篇不同文章的页眉图片。仅它就足以把仓库推过限制。于是添加了一个脚本来减小文件大小,我们现在已经低于限制,并为将来留出了一些空间。以后我们可能需要把图片移到 CDN,但目前并不紧急。

我会等几天,看看是否出现问题,然后删除一些仅迁移所需的脚本。留下来的,是让网站保持最新的 Java 脚本:

运行时机作用
每次部署 + 每日从拥有这些数据的社区维护仓库中拉取 JUG 目录和 Java Champions 列表
每日读取每个 JUG 发布的 iCal 订阅源,让聚会出现在日历上,无需任何人手动输入
每天 4 次刷新阅读量
每次 PR验证 frontmatter,这项检查会在人工查看你的文章之前告诉你,例如作者 slug 拼错了

对作者来说重要的是什么

你的文章现在是一个文件夹,结构如下:

draft/
  my-great-article/
    index.md or .adoc  <- the article, in Markdown or AsciiDoc
    a-diagram.png      <- one or more images used in the article

文件夹名称会变成 URL。图片与文本放在一起,所以永远不会丢失。index.mdindex.adoc 顶部的 frontmatter 需要标题、描述、作者、主图和分类。没有要填写的“SEO 部分”,没有要写两遍的摘要,也没有要猜测的标签分类法。

打开一个 PR,自动检查会读取你的 frontmatter 并构建整个站点。如果你把作者 slug 拼错了,或者忘了提交图片,大约一分钟内就会从一个机器人那里知道,而人类还没有为此花任何时间。

已经在自己的博客上发布过这篇文章? 使用 canonical 链接,搜索引擎会继续把你的网站记为原始来源,同时文章也能触达 Foojay 的读者。这里大约有 800 篇文章是通过在 frontmatter 部分添加这一额外行来实现的交叉发布:

$ config
canonical: "https://your-blog.example/the-original/"

关于撰写和提交文章的所有信息都在如何提交你的下一篇文章页面上。

自动检查

自动检查会在 Foojay 团队任何人阅读你的文章之前运行,每一项都能捕获不同类型的错误。

在 PR 上。 除了你的 frontmatter,该检查还会构建所有页面,因此损坏的 shortcode 会在你的 PR 上显示为红叉,而不是在站点上留下一个坏页面。

在构建后的站点上、部署之前。 每个源文件都生成页面了吗?损坏的 section 很少抛出错误。它会渲染一个运行正常但什么都匹配不到的模板。该检查还会根据磁盘上的文件解析每个内部链接,约五十万个链接,大约五秒钟完成。

在真实浏览器中。 搜索、两张世界地图、图片灯箱、站点地图中可排序的表格、语法高亮……:在 JavaScript 运行之前,这些都不存在,而且它们都会以同样安静的方式失败。页面返回 200,看起来完整,却不再履行自己的职责。因此大约有四十项检查会点击浏览完成的站点。

有一项检查关注的是安全,而不是错误。Foojay 的 Markdown 允许原始 HTML,而且必须如此,因为两千篇导入的 WordPress 文章带有表格和嵌入内容,没有 Markdown 等价物。因此,一个合并的 PR 可能携带 <script>,而静态站点没有服务器端层来捕获它。所以该检查会拒绝文章文本中的可执行标记。它会跳过围栏代码块,因为 Hugo 反正会转义那里的一切,所以你仍然可以写关于 XSS 的内容,只要把示例放在围栏代码块里。

播客现在可以阅读了

Foojay 播客过去只有一种进入方式:按下播放,然后听下一个小时。但现在在这个新站点上,100 集节目在页面上提供了文字稿,大约 824,000 词的对话,你可以阅读、略读、Ctrl-F 查找或引用。

没有人为了做到这一点而转写任何内容。每一集都在 YouTube 上,那里的语音识别已经抓取出了文本。每集只需几秒钟,而本地计算需要数小时才能得到同等质量的结果。一个 JBang 脚本拉取字幕,并在该集的 index.md 旁边写入 transcript.md。页面渲染它是因为文件在那里,所以不需要在节目上设置标志,也不存在要记得取消设置的标志。它只是页面的一个普通部分,并在“本页内容”中有自己的条目。

但是,别忘了,这是机器生成的文字稿,每一集都会在文本上方明确说明这一点。 自动字幕会把姓名和 Java 词汇弄错。脚本会尝试修复一些反复出现的错误,比如把 fujy 重写为 Foojay。另一方面,它有意不动嘉宾的姓名。识别结果对姓名的破坏比其他任何内容都严重,但没有脚本能知道某人想要的是哪种拼写,而凭空发明一个拼写就是把话塞进他们嘴里。所以,如果你参与过某一集,而你的名字显示错了,每份文字稿都带有一个 建议更正 链接,它会在编辑器中打开那一集的文字稿文件。脚本再也不会覆盖已更正的文字稿。

有一个有意为之的省略:文字稿不进入搜索索引。 因为它们会给搜索结果增加太多“噪音”。搜索索引面向文章,而不是播客中说出的每一个词。

JUG、日历和活动

Foojay 最重要的事情之一,是让 Java 开发者彼此连接。JUG 目录和聚会日历完全自动化,任何人都可以通过 PR 添加会议或活动。

自动化的 JUG 聚会日历

Foojay 的 WordPress 曾有一个系统,可以从它知道的 JUG 导入 Meetup 活动,但它仅限于那个列表,而且 JUG 必须让我们知道他们正在使用 Meetup。现在这已经改为一个完全自动化的系统,分两步:

  • Java 用户组列表不由我们维护,因为已经有一个公共来源! 它来自 GlobalWWJugs,这个由社区运营的目录已经跟踪哪些 JUG 存在以及在哪里,一个 JBang 脚本在每次部署时把它拉取到站点中。想要更正自己条目的 JUG 负责人应针对那个仓库打开 PR,而不是 Foojay 的仓库。
  • Foojay 日历现在完全自动化。它读取每个 JUG 发布的 iCal 订阅源,不在乎该订阅源来自 Meetup、Google Calendar、小组自己网站上的文件,还是任何其他导出 iCal 的平台。每天一次,第二个脚本遍历该列表并读取日历订阅源,以填充我们的日历数据文件。

目录中有 102 个 JUG,其中 70 个发布了我们可以读取的订阅源,而在我写这篇文章时,日历上有 62 场即将举行的聚会。

下面两张截图都展示 2026 年 9 月,拍摄于该月第一天: [LOADING...] 旧日历。当月有 11 条记录,每一条都是手动输入 WordPress 的。 [LOADING...] 新站点上的同一个月。23 条记录,没有一条是手动输入的。

会议和活动

会议和其他活动的工作方式不同。 没有人会像订阅 JUG 那样在日历应用中订阅 Devoxx,所以这些条目以每场活动一个小型 YAML 文件的形式存放在仓库中,任何人都可以通过 PR 添加一个。复制 template/event.yaml,填写名称、URL 和日期,然后打开一个 PR。frontmatter 检查会在审查时读取它,并在遇到无法识别的键时失败。每场活动一个文件也意味着,两个人在同一周添加两个会议时永远不会碰到同一份数据,因此永远不会冲突。

事后也不需要删除任何东西。页面会在活动结束后的第二天将其移除。

两种途径都在日历页面本身上。添加活动 会在此仓库中打开 events 文件夹。添加你的 JUG 会打开上游仓库。

评论已迁移到 GitHub Discussions

六年来,Foojay 在 270 篇文章中收集了 580 条评论,其中许多都很有价值。

新评论由 giscus 处理,它把每个讨论串作为 GitHub Discussion 保存在与文章相同的公共仓库中。你用 GitHub 登录,讨论串有一个可以链接的 URL,团队用他们每天已经在使用的工具进行管理。

那 580 条旧评论需要另一种方案,而第一次尝试失败了……计划是用 Foojay 账号把它们发布到同样的 Discussions 中,让旧对话和新对话看起来一模一样。GitHub 在几篇文章之后阻止了迁移,因为它看起来像垃圾邮件机器人……

所以旧评论现在改为存放在仓库中。每篇文章的文本旁边都有一个小型 JSON 文件,页面会在实时讨论下方把它渲染为 旧 Foojay 站点上的讨论。## 无障碍

似乎没有法律要求 Foojay 必须实现无障碍。《欧洲无障碍法案》覆盖的是所列行业中的消费者服务,而不是社区博客。但这不是跳过它的充分理由。Java 开发者正是那些会用键盘、以 200% 缩放、在深色配色方案下,或者用屏幕阅读器浏览的受众。

因此本站以 WCAG 2.2 AA 为目标,现在也有了一份无障碍声明,说明它实际处于什么水平。已加入的内容:

  • 一个跳转链接、每页一个 <h1>,以及真正的地标区域。
  • 一切都应该无需鼠标即可使用:菜单、搜索框、图片查看器、活动日历、可排序表格、赞助商横幅。有关使用键盘浏览本站的更多信息,请阅读无障碍页面
  • 我们会测量对比度,样式表会在每种颜色旁记录数值。 我们正是通过这种方式发现,Foojay 自己的 logo 蓝色在白色背景上的对比度为 2.02:1。作为填充色没问题,作为文字则不可用。所以在这里它从不用于文字,而焦点指示器也有了自己的颜色。正文中的链接带有下划线也是同样的原因:与正文文字的对比度为 1.2:1,仅靠颜色根本说明不了什么。
  • 没有你不能停止的动态效果,而且如果你的系统要求减少动态效果,就完全没有动态效果。
  • 移动端菜单移到了屏幕外,但仍留在 Tab 键顺序中,所以用键盘会让你遍历一个你看不见的菜单。
  • 图片查看器把点击处理程序绑定到了 <img> 上,所以对于键盘来说,点击放大功能根本不存在。

有一件事无法轻易修复:存档中大约 3,000 张图片没有 alt 文本。 它们来的时候就是这样,任何脚本都不能也不应该凭空编造描述。当你打开拉取请求时,检查会读取新文章,并给出警告而不是失败,因为一张图片是否承载意义是一个需要判断的问题。对硬性失败可预见的反应是 alt="image",这对屏幕阅读器来说比什么都没有还糟。如果你在 WordPress 上写过其中一篇文章,你就是世上最适合描述其截图的人。

访客统计与隐私

两个计数器,原因不同。一个是 Google Analytics,营销团队用它来了解流量模式。另一个是我们自己运行的计数器,用来统计实际有多少人阅读了一篇文章。

1. Google Analytics,因为营销团队喜欢它

Foojay 仍然上报到它一直使用的同一个 Google Analytics 媒体资源。这一点没有改变,我们也不会假装不是这样。Ketch 仍然是同意管理器,而 Google Consent Mode 现在会在加载前将所有类别默认设为拒绝。因此,在你真正同意某项之前,GA 不设置 Cookie,并发送无 Cookie 的 ping。如果你拒绝,它就会一直保持这样。

你使用广告拦截器并拒绝 Cookie?没问题,我也是。 这正是第二个计数器存在的原因。

2. 阅读计数器,因为我们想知道人们实际读了什么

如果只以 Google Analytics 为唯一来源,我们发布的每个数字都会出错,而且会朝着可预见的方向出错。很大一部分 Java 开发者和 Foojay 访客使用广告拦截器。一个悄无声息地漏掉很大一部分读者的页面浏览量计数不是统计数据,而是猜测。而 Foojay 文章上的阅读数是公开发布的数字,就放在页面上作者署名旁边,所以它最好是真的。

所以你看到的 12,345 views 来自我们自己运行的东西:一个位于 https://foojay.io/api/views/all 的小型 Cloudflare Worker,它位于一个正好有两列的表前面。一个像 posts/announcing-the-new-foojay 这样的页面键,以及一个整数。没有访客记录,没有 IP 地址,没有会话,没有任何类型的个人数据。

没有人的文章计数回到零。 我们从 WordPress 转移了数字以保留历史:1380 万次阅读,覆盖 2,147 篇文章、47 个术语条目和 32 个页面。每一个都成为该页面计数的起始值。

这就是整个设计,隐私特性是它的自然结果,而不是作为承诺附加在它之上:

  • 没有 Cookie,没有标识符,没有 IP 地址,没有用户代理,没有指纹。 请求只携带一个页面键,别无其他。没有什么需要匿名化,因为关于的任何信息都从未被发送。
  • 它无法跨页面跟踪你, 因为它不存储任何访客概念。两篇文章的两次阅读就是两个数字在增加,它们之间没有任何关联。
  • 广告拦截器不需要拦截它, 因为没有什么可拦截的。它是第一方,与你正在阅读的文章位于同一域名。这不是漏洞。这正是这个数字准确的原因。
  • 构建过程会把计数直接内嵌到页面中,因此显示它完全不需要请求。没有 JavaScript,也没有会在一秒后变成数字的占位符。

你机器上唯一的状态是一个 sessionStorage 标志,用于防止页面刷新被计数两次,它会在你关闭标签页时消失。

而负责计数的东西也在本仓库中:这个 Worker 不到两百行,包括注释,你可以阅读它对那个表执行的每一条查询。

正式版本请参见隐私政策

来写吧

关于 Foojay 有一点从未改变:Foojay 值得阅读,因为这个社区里的人愿意花时间把事情写下来。

如果你曾经想过应该有人把这个写下来,那个人可以是你。一个为你节省了一周的 JVM 标志,一次出了岔子的迁移,一个没人知道的库,一份会议报告,一件你终于弄懂的事。你不需要是 Java Champion,也不需要写 3,000 字。

如果你在这篇或任何其他文章或页面中发现错别字,请滚动到页面底部,找到 在 GitHub 上编辑此页面 链接。它会在编辑器中打开本页对应的确切文件,并将你的修正变成一个拉取请求。GitHub 会为你 fork 仓库,所以只需点击三次,无需本地配置。网站上的每篇文章、每个页面和每个术语条目都带有该链接。

我们希望你喜欢新的 Foojay,也期待你的贡献。发现了问题或有可以改进之处?打开一个拉取请求,或者提交 issue

发现了错误,或者有要补充的内容?在 GitHub 上编辑此页面 [LOADING...]
作者:

Frank Delporte

Frank Delporte 是 Java Champion、Java 开发者、Azul 高级技术文档工程师、博主、《Java Programming for Raspberry Pi - A Hands-On Guide to Electronics and IoT Projects》的作者,以及 Pi4J、Lottie4J、Sheetmusic4J 等项目的开源贡献者,……

相关文章

Foojay [LOADING...]
3 位作者 2022年5月3日 15,260 次浏览

如何向 Foojay.io 提交你的下一篇文章

Foojay Foojay [LOADING...]
Geertjan Wielenga 2020年9月11日 2,940 次浏览

Foojay 主页的改版与重新设计 | foojat

Foojay Foojay [LOADING...]
2 位作者 2026年7月6日 1,559 次浏览

整理周:Foojay.io 发生了什么变化

Foojay

加入讨论

Foojay [LOADING...]
3 位作者 2022年5月3日 15,260 次浏览

如何向 Foojay.io 提交你的下一篇文章

Foojay Foojay [LOADING...]
Geertjan Wielenga 2020年9月11日 2,940 次浏览

Foojay 主页的改版与重新设计 | foojat

Foojay Foojay [LOADING...]
2 位作者 2026年7月6日 1,559 次浏览

整理周:Foojay.io 发生了什么变化

Foojay