























「想在互联网上拥有自己的一隅。」
这是我一以贯之的念想,便这么做了。对于一个笨拙的人而言,学习新事物难免有些困难,好在热情还是足够的,总是能坚持下来。兜兜转转,几经迭代,迎来了如今这个站点。
开发设计之初的目标只有一个:不嫌弃就行,那么一切都需要从简。
毫不违心地说,现在的样式也好,细节也罢,很难说让自己满意。只是恰好不沾染任何华丽的外观,不会出现原则上的错误,因此勉勉强强看得过去。
似有天意,这回站点公开之后,鲜有不满和疑惑的声音,多是夸赞,称是「有特点」。这何尝不是一种歪打正着。更时有建议开源发布出去,或许互联网上还有同样偏好的开发者乐于使用。
大概是为了收获更多认同,也想得到他人的维护支持,我想,值得一试。
不幸中的万幸,我是有些精神洁癖的完美主义者。
早在站点迁移之初,就考虑到了开源的可能性,为了让 Git Tree 看上去清爽,自初始化起就使用了约定式提交,且通过英语撰写提交信息。只是,即使格式是固定的,内容也仅能凭着主观臆断煞有介事地填充上去,至今没有定下非常满意的方案。
基本开发工作完成之后,便需要确定版本,选择 SemanticVersioning 是最自然也最实际的,各个工具链的支持也相当完备,无需多想,按照规范正常推进就是。
最初,我用 npm version 命令手动更新版本,自动更新 package.json 版本号并添加 git tag,一切都合适,只是缺少了 GitHub 上的 Release 信息,显得纯粹的标签十分空洞。
于是基于 googleapis/release-please-action@v4 写了一个 GitHub Actions 工作流,识别 git log 自动创建升级版本,弊端是会疯狂创建 Pull Request。
值得一提的是 SemVer 的第 4 条:
主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。
侥幸于没有着急发布 1.0.0,这成为了我发布 BREAKING CHANGE 的免死金牌。
理论上说,主题已经投入了生产环境中,应该发布稳定版本了,但依旧有许多想法随时可能变更……
例如前不久为了增强配置文件的约束,从
JSON Schema的方式换成了TypeScript的类型检查。未来还计划更改数据库的字段使其更具泛用性。
在 ZeroVer 下,所有的 BREAKING CHANGE 都淡化为了一次次小小的次版本号迭代。更多的是困惑与惭愧,怨自己能力不足,亦不太愿意为这么个小项目花额外的精力编写迁移脚本…
即便不去变更主版本,我认为也应该在提交中体现,例如按照约定式提交的规范使用 ! 字符和 BREAKING CHANGE 脚注。一开始也确实这么做了,但 release-please 直接将版本号迭代到了 更新到了 1.0.0,给我吓得不轻,连忙手动修改了那条 PR。之后就没再这么做,只能简单地在提交的正文的弱弱地加入一句「This is a breaking change.」
好在就结果来看,Git Tree 保持得比较干净,长成了自己期待的模样,自觉赏心悦目。

图中所示 VSCode 插件为 Git Graph。
我是一个遇到复用陷阱1就自觉跳进去的人。
似乎生来就对重复冗余的事情有着极强的怨念,尤其在代码中,写死的值和两段及以上逻辑相同的代码会另我抓狂。因此即使是自用的项目,我也要留出些个可配置项来让自己的代码看上去是高度可配置的,除非太麻烦。
但事实上,除了基本的站点信息和作者信息,我很少再提供其它配置,变得有些 Opinionated,认为自己已在提供最合适的逻辑和样式方案。
此前的所谓配置,也不过是以某种自己认可的方式定义一个变量。于是,即便这次有了一个冠冕堂皇的理由可以尽情抽象自己的代码,但这意味着我需要把变量都集中到一个文件中,还是令人头大。
第一时间想到也是最简单的方式是定义一个 JSON,极致的便利与灵活。缺点亦然,过于灵活,导致缺乏字段与类型提示,放到逻辑中也难有较好的校验方式,明明都是一眼就能明白的变量类型,却只能无奈定义为 any。
JSON Schema 在一定程度上可以解决问题,不如说丰富的约束条件使其即使不够严格,也能让使用者在配置时理解开发者的意图。不过到了编译期,其效果难说与变为 any 有何异同。
前文提及关于配置文件类型的变动,正是为了加强约束,虽然无法像 JSON Schema 那样精确到对字符串应用正则表达式,但是合理搭配 TypeScript 类型体操,至少还是能达到缓解运行时报错的效果。
如果说样式从简是视觉上的目标,那么 i18n 就是逻辑上的主要发力点。
搭建动态路由只是基础中的基础,其配套建设才是折腾的核心。先是自行实现了翻译函数,开发插件的适配也需要额外完成,翻译文案与命名空间的设计也全靠自己摸索。
当前语言与目标语言的切换并不复杂,只是脑子没转过弯,一心想着单个文件只能对应一个翻译函数,但为每种语言都定义之后,问题就能迎刃而解。此外,翻译文案又怎么不是一种配置文件呢?在各自的文案中加入配置字段的翻译,先前所有被迫重复编写的代码都迎来了复用的曙光。
在 i18n 的用户配置方面,Astro 一直有个恼人的点,defineConfig 中的 i18n 是一个可选字段,这意味着即使面临无法编译的风险,用户也随时可以删除它,而我在代码中却不得不用 i18n! 的断言方式减少大量不必要的判断,这显然是冲突的。
那么,把选项移动到自己的配置文件中并作为必填选项就好了!这么一想自己真是被原教旨主义思想害得不轻,非得用官方的接口定义配置…或许有一个原因是,其通过泛型设计,使得 defaultLocale 的值必须来自 locales,这在当时还是非常令我震惊。但…实际并不复杂,抄过来就好了:
interface SiteConfigOptions {
/** Internationalization Configuration */
i18n: {
/** Supported Locales */
locales: Locales;
/** Default Locale (must be one of the locales) */
defaultLocale: Locales[number];
};
...
}
...
function siteConfig<const Locales extends string[]>(config: SiteConfigOptions<Locales>) {
return config;
}使用 const 关键字保留 locales 的字面量信息,使其类型 Locales 被推断为形如 ["en", "zh-cn"] 的元组而不被泛化为 string[]。那么,Locales[number] 就表示这个元组中所有元素的联合类型,实现 defaultLocale 必须是 locales 数组中的一个值:

那么接下来,我不再需要调用 Astro 提供的 astro:config/client 转而使用自己的接口,但这主要还是为了自己接下来的一个小巧思做铺垫。
这套主题起初的目标就是作为彻底的 i18n 解决方案走向开源,但回头细想,又有多少开发者,多少站长真正用得上这个功能呢,这是否本就是设计上的冗余?
违背部分的初衷,提供单语言模式也可用的配置成为刚需。
权衡之后,我认为比起提供另外的配置项,利用现有配置一样可以做到。所谓单语言,也就是只保留一种语言:i18n: { locales: ["en"], defaultLocale: "en" } 不就是这个意思么?对于逻辑中的处理,只需要暴露一个常量,判断 locales 的元素个数是否为 1 即可。
得益于可选动态路由(/[...locale])的建设,我在访问路径上不需要做任何改变,只要把对内容解析从各个语言文件夹改为其父级目录变大功告成。
README 是一个项目的门面,应当在最短的时间内向他人阐述这是什么、为什么要用、如何安装以及如何使用。我在编写时以 Standard Readme 为基准,同时参考了一些同类型的灵感项目,简单涵盖特性、安装、命令、部署等方面,主要是作为详细文档的入口,为有需要的使用者提供使用思路。其它文档的编写同样花了不少时间,对接口层面的配置内容做了详细说明,同时也提供了一些主题特性的预览。
光有文档和截图不一定能看出个所以然,还要提供在线演示。同样是利用 GitHub Actions,主分支自动部署到 Vercel,带有评论系统的分支自动部署到 Cloudflare Workers。
对于使用者的文档,个人认为还是比较详尽,但对于开发者则只提供了最最基础的流程,或许盲目信任只会导致工作量与复杂度的骤然提升。
故事得从一条 PR#17 谈起。
在仓库公开之前,我就已经提前放置了一些文档,那大多是提供给普通使用者的教程,留给开发者的只有一份浅显的 CONTRIBUTING.md。
作为自己的第一个开源项目,许多准则都拿不定主意,需要在实践中慢慢积累,但什么对自己对社区有利什么有弊,我想自己还是有些最基本的认知。
面对这条 PR,在第一时间的喜悦与惊喜过后,便多是困惑。
新特性、新配置甚至代码格式化跻身同一条请求,令我稍有些束手无策。看来,前期偷的懒在此刻要开始还了…
向提起 PR 的开发者表明想法请求修改重开后,我开始修订文档,描述了 PR 前的前置工作以及其本身的相关要求。这自然还并不完善,但目前作为一些兜底条款大概还是够用了。
至于格式化规范的需求,原本打算对现有方案精挑细选后逐步推进,现在看来不得不将日程提前。
在前端领域,最最主流的必然是 ESLint + Prettier 的方案,好巧不巧,这俩正好都不在我的好球区。于是将目光转向了正在活跃开发中的 Biome,这也可能是 Rust 爱好者的支持方式吧。
与其说是新兴,不如说有些不成熟过头了,上个月才提供了对 Astro 和 Svelte 这类 HTMLish 框架的实验性支持…经测试使用下来,BUG 也是离奇得多,还是默默关闭了这个特性,暂时用着稳定版本的残疾支持。
但依旧给自己留了一条后路,我没有立刻删除 Prettier 的相关配置,倒是做了优化,将其作为回退选项,以备不时之需。
确定 Formatter / Linter 工具后,就试着将其作为所有项目参与者的标准化工具,接入了 Husky 在本地作为 Git Hook,并 GitHub Actions 中也通过 Biome 处理 CI 流程。
内容可能不多,但值得单独拎出一个章节说说。
使用命令创建新文章是博客主题常有的功能,脚本的实现并不算难,但是脱离了核心框架后略显麻烦。
比起直接甩个用户一个写死的模板,我想尽可能提供更好的体验,通过交互的方式进行初步的文章配置。那么需要用到命令行交互库,一共尝试了三款:@inquirer/prompts 和 enquirer 功能尚可,但样式有所欠缺,最后选择了 @clack/prompts,配置方便,样式也比较现代化。
创建 Svelte 项目时,官方用的也是这个库2!
偶像同款~
脚本的内容就是选择内容类型、语言,填写标题、标签等等。读取命令行输入,再创建文件,最后打开。一开始只有英文提示,就连 i18n 也要在脚本里单独配置一次。而前面提到的配置文件重构,就是为了优化这个问题。
astro.config.ts 并非不能读取,只是需要加载整个配置,东西非常多,导致脚本启动需要费非—常—大的劲:

等脚本加载好了,文件也都创建好了…
那么,我把 i18n 的配置也挪到主题配置中不就好了么,一来脚本可以直接加载,二来也能优化主要文件,一举两得,可惜是个伤敌一千自损八百的做法。
这时,既然与主项目同步的语言选项都到手了,那主项目的翻译文件也可以考虑一下了…于是乎,我给脚本也做了 i18n,顺带扩展了翻译函数的命名空间支持。



虽然不一定有人用得上,但自己的成就感已经满溢而出。
首先,必须感谢科技的发展,如今的 AI 虽然仍有非常多的缺陷,但作为助手已然绰绰有余。特别是针对纯文本的工作,翻译根本不在话下,基本不太需要检查。如果用上 Deep Research,那么甚至写一个直接可用的 Astro 集成都是没有问题的。
然后说一下自己的愿景吧,一直待在 ZeroVer 的舒适区肯定是不行的,但现在正式发布仍为时尚早。
Biome 没有稳定,甚至会给自己的开发添堵;如果要增强 i18n,那必须挑战适配阿拉伯语;最重头的依旧是字体,我需要等待 Astro 的实验性字体 API,也需要规划怎么合理加入 Sans-serif 字体来提升阅读体验。
最后,我想自己可能是尝到了些甜头,有些飘飘然,掉了一些 Star 就开始失落。
但这到底是一个满足自己癖好的业余项目,积累些经验已是谢天谢地,在追求社会认可之前还有很长的路要走。
Footnotes
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。