跳转到正文

从手机发一条短文

短文是随手记的东西:一个念头、一段引用、一条链接。但这个博客是静态站,内容以 Markdown 文件的形式躺在 git 仓库里,发布意味着写文件、提交、推送。于是每次想记点什么,都要等回到电脑前——等到那时,多半已经不想记了。

先排除掉换系统

最直接的想法是换个带后台的博客系统,手机上打开网页就能发。

但这解决的不是我的问题。为了「手机上能发一条短文」,代价是把已有的东西全部推倒:统一时间线、中文排版的行高段距、字体分包、折叠目录——这些都是自定义组件,搬不过去。工作流也整个变掉,内容从 git 里的 Markdown 变成数据库记录,提交历史、更新记录都无从谈起。

真正该问的是:发布链路上到底缺哪一环?

缺的只是「把文件放进仓库」

推送到 main 后 Cloudflare Pages 会自动构建上线——这一环本来就是通的。短文的 frontmatter 也只有一个日期字段,没有标题、没有标签。

所以缺的仅仅是:在手机上生成一个格式正确的 Markdown 文件,提交到仓库。这件事 GitHub 的 contents API 一次调用就能完成,iOS 快捷指令原生支持发 HTTP 请求,不需要服务器、不需要中间件。

链路是这样的:

手机打字 → 快捷指令 → GitHub API → main → CF Pages 构建 → 上线

快捷指令里十个动作:取当前时间、格式化成文件名和日期、接收输入、清理尾部空白、拼成 Markdown、Base64 编码、拼 URL、PUT 提交、显示结果。

有一个前提让这件事变得可行:我先把 frontmatter 的日期格式改成了和页面显示一致的 "YYYY-MM-DD HH:mm",按站点时区解读。原本是 ISO UTC,手机上生成就得做时区换算;改完之后,写进去的就是页面上显示的值。

三个坑,两个是静默的

链路很快就打通了,真正花时间的是之后的格式细节。

文件名里不能有空格。 iOS 输入法在连字符后面自动补了一个空格,日期格式串从 yyyy-MM-dd-HHmmss 变成了 yyyy- MM-dd-HHmmss。URL 里的裸空格会截断路径,GitHub 于是创建了一个叫 2026- 的文件。

这个失败糟糕在它看起来是成功的:API 返回 201 Created,快捷指令里一切正常。但这个文件没有 .md 后缀,不匹配内容集合的 glob,构建直接忽略它。我以为发出去了,站点上什么都没有。同样的问题换了个空格位置又发生了一次。

结尾必须正好一个换行。 少一个或多一个,prettier 的格式检查都会失败。输入框自带的换行和模板末尾的换行叠加成了两个。解法是在快捷指令里加一步「替换文本」,用正则 \s+$ 清掉输入的尾部空白,再由模板补上唯一那个——这样不依赖打字习惯,结果是确定的。

换行符必须是 LF。 CRLF 同样过不了格式检查。

三个坑里有两个不会报错,只会安静地什么都不做。

补上三道网

问题不在于犯错,而在于犯了错没人告诉你。所以每个坑对应一道防线:

防线拦什么
pubDatetime 的 schema 校验日期格式写错,构建失败并提示正确写法
prettier 的 format:check结尾空行、CRLF
CI 的内容收录检查文件名截断导致的静默失败

第三道是新加的,逻辑很简单:找出 src/content 下所有不以 _ 开头、又不是 .md/.mdx 的文件,有就让构建失败。

还有一件更基础的事:CI 原本只在 pull request 时触发,直推 main 完全没有校验。手机端不会先在本地构建,这是唯一能发现问题的地方,于是给它加上了 push 触发。

加完当天就抓到了一次——那条结尾多空行的短文,CI 直接标红。

加上图片,一次请求就不够了

纯文字发了一阵之后,想发张照片。看上去只是多传一个文件,实际上整条链路的假设都变了。

要两次请求,而且有先后。 contents API 一次只写一个文件,图片和 Markdown 得分两次提交。图必须先走——反过来的话,仓库里会短暂存在一篇引用了不存在图片的短文,而 astro:assets 解析不到相对路径引用的文件会让构建直接失败。两次提交也意味着两次构建。

图得在手机上压。 手机直出的照片三五兆,Base64 编码后再涨三分之一。更要紧的是 git 不会忘事:原图一旦提交,就永远躺在历史里,以后删掉文件也减不回去。所以得先缩到 1600px、转成 JPEG,再编码上传。

加上选照片、缩放、格式转换、第二次 PUT,原来的十个动作变成二十多个。

显示这一端:画廊

短文是内联在时间线里的,一张满宽的竖构图照片能占掉大半屏。所以多图不平铺,做成画廊:大图常驻在正文和缩略图条之间,底下一排 80px 的方图当选择器,点哪张换哪张,默认选中第一张。

不做点开弹全屏的灯箱。短文本来就是列表里扫一眼的东西,覆盖全屏的层打断感太强。

图片放在 src/assets/images/notes/,正文里用相对路径引用。这一点不能省:只有相对路径才会被 astro:assets 接管,自动转 webp、生成 srcset、把宽高写进属性(避免加载时的布局跳动)、补上懒加载。放进 public/ 或者用外链,这些就全没有。

第四个静默的坑

配完之后我以为图片优化已经生效,直到去看生成的 HTML——srcset 是空的。

装了 sharp 不等于有 srcset,这是两件事。Markdown 里的图默认只输出一张原尺寸,要生成 srcset 得显式打开 image: { layout: "constrained" }

构建不报错,页面看着也正常。区别只在手机上:一张 80px 的缩略图,背后下的是 1600px 的原图。

和前面三个坑同一个毛病——看起来是对的。

现在的样子

文字这条链路是通的:在手机上点开快捷指令,打字,完成。剩下的全自动——提交进仓库、CI 校验、Cloudflare Pages 构建上线。构建失败的话站点保持旧版本,不会发出半个坏页面。

图片那条还没接上。显示这一端做完了,卡在发布这一端:二十多个动作要在手机上一格一格搭出来。我试过直接生成一个 .shortcut 文件让它导入,但未签名的快捷指令装不上,而签名只能由苹果设备完成。

也想过绕开快捷指令:拿自建的 memos 当入口,它本来就有现成的编辑器和图片上传,再用 webhook 或者定时任务把内容同步进仓库。方案是成立的,但为了一个月发几张照片去维护一条同步管线,先搁着。

这篇文章想说的其实不是快捷指令怎么配。而是遇到「静态博客不能在手机上发布」这个问题时,第一反应是换一套系统,但拆开看,缺的只是一次 HTTP 请求。

同样的拆解放到图片上,结论就变了:缺的是两次有序的请求、一次端上压缩,和一个能选照片的界面。方法一样,答案不一定一样。

以及:能返回成功却什么都没做的失败,比直接报错的失败危险得多。三道网里,最该有的是第三道。