{"version":"https://jsonfeed.org/version/1.1","title":"uua.me","home_page_url":"https://uua.me/","feed_url":"https://uua.me/feed.json","description":"写代码，也写字。","language":"zh-CN","authors":[{"name":"沉沦"}],"items":[{"id":"https://uua.me/n/school-pickup/","url":"https://uua.me/n/school-pickup/","title":"学校门口停车之争","summary":"我家俩孩子今年刚上高中。姐姐住宿，两周一回；妹妹走读，天天早出晚归。今天周五，我去接姐姐回家。学校门口这段路，平时停车靠运气，放学的一个小时靠命。我一般都把车扔在两百米外，走过去看到一辆黑色的坦克端端正正杵在斑马线上。 不一会儿，交警过来，敲窗，说这是斑马线，得挪。车主回话，不挪。理由还挺硬——学校周边哪条道没被占？凭什么单拎我一个。原话大意是：我走可以，别的车你交警也得一起管，要不我就不动。于是在斑马线上僵住了。 交警讲的是斑马线，车主讲的是公平；一个说规则，一个说“普遍违法不能只挑我”，两套道理各自通顺。吵了十来分钟，最后交警怂了，绕着附近走一圈，给…","content_html":"<p>我家俩孩子今年刚上高中。姐姐住宿，两周一回；妹妹走读，天天早出晚归。今天周五，我去接姐姐回家。学校门口这段路，平时停车靠运气，放学的一个小时靠命。我一般都把车扔在两百米外，走过去看到一辆黑色的坦克端端正正杵在斑马线上。</p>\n<p>不一会儿，交警过来，敲窗，说这是斑马线，得挪。车主回话，不挪。理由还挺硬——学校周边哪条道没被占？凭什么单拎我一个。原话大意是：我走可以，别的车你交警也得一起管，要不我就不动。于是在斑马线上僵住了。</p>\n<p>交警讲的是斑马线，车主讲的是公平；一个说规则，一个说“普遍违法不能只挑我”，两套道理各自通顺。吵了十来分钟，最后交警怂了，绕着附近走一圈，给他找了个停车地儿，这事才算散场。回去的路上想，中学门口这地方，规则是直的，路是弯的，最后总得有人先低头——交警虽然低头了，但不是交规。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/9f3b8f9ca0d8465f.jpg.v4-w960.webp\" alt=\"放学时段的路口：车挤车，人挤人，中间那辆越野停在斑马线上\" loading=\"eager\" fetchpriority=\"high\" decoding=\"async\" data-full=\"/uploads/2026/10/9f3b8f9ca0d8465f.jpg.v4-w1824.webp\" width=\"1824\" height=\"1368\" srcset=\"/uploads/2026/10/9f3b8f9ca0d8465f.jpg.v4-w240.webp 240w, /uploads/2026/10/9f3b8f9ca0d8465f.jpg.v4-w480.webp 480w, /uploads/2026/10/9f3b8f9ca0d8465f.jpg.v4-w640.webp 640w, /uploads/2026/10/9f3b8f9ca0d8465f.jpg.v4-w960.webp 960w, /uploads/2026/10/9f3b8f9ca0d8465f.jpg.v4-w1280.webp 1280w, /uploads/2026/10/9f3b8f9ca0d8465f.jpg.v4-w1824.webp 1824w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1824/1368\"></p>\n<p><img src=\"https://uua.me/uploads/2026/10/76e7a955712d5d1e.jpg.v4-w960.webp\" alt=\"校门口：电动伸缩门、水泥墩、正对面的教学楼\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/76e7a955712d5d1e.jpg.v4-w1824.webp\" width=\"1824\" height=\"1368\" srcset=\"/uploads/2026/10/76e7a955712d5d1e.jpg.v4-w240.webp 240w, /uploads/2026/10/76e7a955712d5d1e.jpg.v4-w480.webp 480w, /uploads/2026/10/76e7a955712d5d1e.jpg.v4-w640.webp 640w, /uploads/2026/10/76e7a955712d5d1e.jpg.v4-w960.webp 960w, /uploads/2026/10/76e7a955712d5d1e.jpg.v4-w1280.webp 1280w, /uploads/2026/10/76e7a955712d5d1e.jpg.v4-w1824.webp 1824w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1824/1368\"></p>\n","date_published":"2026-10-16T12:40:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/p/mac-quark-drive/","url":"https://uua.me/p/mac-quark-drive/","title":"自用软件之夸克网盘","summary":"我网盘里存的东西，十有八九是想看的视频和电子书。但与此同时，它给我准备好了亮眼的会员充值入口、丰富的文档转换、扫描、AI、分享、备份、相册……一大堆我永远不会点第二下的入口。 不能忍的说到底两条：一是塞了一堆用不到的功能，多到找自己的文件都费劲；二是 UI 看不惯。第二条很主观，但主观到天天都看，就成了客观。 于是有了这个东西：一个自用的 macOS 夸克网盘客户端。原生 SwiftUI + AppKit，纯 swiftc 直接编译，不依赖 Xcode 工程，也不依赖任何第三方库。免责一句：这是未获官方授权的第三方客户端，接口全靠实测，随时可能失效，仅限…","content_html":"<p>我网盘里存的东西，十有八九是想看的视频和电子书。但与此同时，它给我准备好了亮眼的会员充值入口、丰富的文档转换、扫描、AI、分享、备份、相册……一大堆我永远不会点第二下的入口。</p>\n<p>不能忍的说到底两条：一是塞了一堆用不到的功能，多到找自己的文件都费劲；二是 UI 看不惯。第二条很主观，但主观到天天都看，就成了客观。</p>\n<p>于是有了这个东西：一个自用的 macOS 夸克网盘客户端。原生 SwiftUI + AppKit，纯 <code>swiftc</code> 直接编译，不依赖 Xcode 工程，也不依赖任何第三方库。免责一句：这是未获官方授权的第三方客户端，接口全靠实测，随时可能失效，仅限自用。</p>\n<h2 id=\"h-13ks891\">为什么自己写一个</h2>\n<p>自用软件有条别的软件没有的特权：<strong>取舍标准只剩一条——我自己会不会用</strong>。不会用的功能在这仓库里不算留着万一，算 bug，占地方的那种。这条标准筛下来的结果有点奇怪：一个网盘客户端，最后长出了播放器、阅读器和解压工具。</p>\n<h2 id=\"h-sgtsug\">它现在长什么样</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/9fbd529616ac6930.png.v4-w960.webp\" alt=\"主窗口：左侧文件、收藏、传输、回收站，右侧文件列表带类型筛选和侧栏目录树\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/9fbd529616ac6930.png.v4-w2560.webp\" width=\"3192\" height=\"1948\" srcset=\"/uploads/2026/10/9fbd529616ac6930.png.v4-w240.webp 240w, /uploads/2026/10/9fbd529616ac6930.png.v4-w480.webp 480w, /uploads/2026/10/9fbd529616ac6930.png.v4-w640.webp 640w, /uploads/2026/10/9fbd529616ac6930.png.v4-w960.webp 960w, /uploads/2026/10/9fbd529616ac6930.png.v4-w1280.webp 1280w, /uploads/2026/10/9fbd529616ac6930.png.v4-w1920.webp 1920w, /uploads/2026/10/9fbd529616ac6930.png.v4-w2560.webp 2560w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:3192/1948\"></p>\n<p>主窗口就是个正常的文件管理器：列表/网格双模、点列头排序、列宽可拖并记住、七档类型筛选带角标、侧栏目录树（点开才拉子目录）、面包屑。多选与全键盘导航（含 ⌫ 删除、⌘Z 撤销）都齐，还有三档界面密度——自用软件才敢给的选项。</p>\n<p>批量重命名带实时预览和重名冲突检测，规则先把「每条会改成什么」摆出来让你过目，与已有条目重名的标红拦下：</p>\n<p><img src=\"https://uua.me/uploads/2026/10/ac3a0ab89b7f7026.png.v4-w960.webp\" alt=\"批量重命名：前缀后缀、查找替换、加序号，右侧逐条预览改名结果\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/ac3a0ab89b7f7026.png.v4-w2560.webp\" width=\"3192\" height=\"1948\" srcset=\"/uploads/2026/10/ac3a0ab89b7f7026.png.v4-w240.webp 240w, /uploads/2026/10/ac3a0ab89b7f7026.png.v4-w480.webp 480w, /uploads/2026/10/ac3a0ab89b7f7026.png.v4-w640.webp 640w, /uploads/2026/10/ac3a0ab89b7f7026.png.v4-w960.webp 960w, /uploads/2026/10/ac3a0ab89b7f7026.png.v4-w1280.webp 1280w, /uploads/2026/10/ac3a0ab89b7f7026.png.v4-w1920.webp 1920w, /uploads/2026/10/ac3a0ab89b7f7026.png.v4-w2560.webp 2560w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:3192/1948\"></p>\n<p>播放器支持内封与外挂字幕、双语两行、音轨切换。进度按<strong>文件 fid</strong> 记在本机，不按文件名（网盘里同名文件是常态）：看几秒的不记，看到结尾的清掉，下次打开接着上次：</p>\n<p><img src=\"https://uua.me/uploads/2026/10/a25a4b8e5f55e781.png.v4-w960.webp\" alt=\"播放器：正在播放的画面与播放条，右侧可切音轨与字幕\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/a25a4b8e5f55e781.png.v4-w2560.webp\" width=\"3192\" height=\"1706\" srcset=\"/uploads/2026/10/a25a4b8e5f55e781.png.v4-w240.webp 240w, /uploads/2026/10/a25a4b8e5f55e781.png.v4-w480.webp 480w, /uploads/2026/10/a25a4b8e5f55e781.png.v4-w640.webp 640w, /uploads/2026/10/a25a4b8e5f55e781.png.v4-w960.webp 960w, /uploads/2026/10/a25a4b8e5f55e781.png.v4-w1280.webp 1280w, /uploads/2026/10/a25a4b8e5f55e781.png.v4-w1920.webp 1920w, /uploads/2026/10/a25a4b8e5f55e781.png.v4-w2560.webp 2560w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:3192/1706\"></p>\n<p>DLNA 投屏：播放条上点电视图标选设备，从本机当前进度接续播，本机暂停，播放条变成电视遥控器；停止时把进度同步回来：</p>\n<p><img src=\"https://uua.me/uploads/2026/10/3523c4c57aafce4c.png.v4-w940.webp\" alt=\"DLNA 投屏：搜索到的电视与盒子列表\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/3523c4c57aafce4c.png.v4-w940.webp\" width=\"940\" height=\"880\" srcset=\"/uploads/2026/10/3523c4c57aafce4c.png.v4-w240.webp 240w, /uploads/2026/10/3523c4c57aafce4c.png.v4-w480.webp 480w, /uploads/2026/10/3523c4c57aafce4c.png.v4-w640.webp 640w, /uploads/2026/10/3523c4c57aafce4c.png.v4-w940.webp 940w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:940/880\"></p>\n<p>书点开就读，左目录右正文：</p>\n<p><img src=\"https://uua.me/uploads/2026/10/9578c71257bd5657.png.v4-w960.webp\" alt=\"应用内阅读器：目录侧栏带封面，正文首行缩进\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/9578c71257bd5657.png.v4-w2560.webp\" width=\"3192\" height=\"1706\" srcset=\"/uploads/2026/10/9578c71257bd5657.png.v4-w240.webp 240w, /uploads/2026/10/9578c71257bd5657.png.v4-w480.webp 480w, /uploads/2026/10/9578c71257bd5657.png.v4-w640.webp 640w, /uploads/2026/10/9578c71257bd5657.png.v4-w960.webp 960w, /uploads/2026/10/9578c71257bd5657.png.v4-w1280.webp 1280w, /uploads/2026/10/9578c71257bd5657.png.v4-w1920.webp 1920w, /uploads/2026/10/9578c71257bd5657.png.v4-w2560.webp 2560w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:3192/1706\"></p>\n<h2 id=\"h-1m12dj9\">现在</h2>\n<p>它就在 Dock 里待着，没有更新公告，没有运营活动，没有「您的意见对我们很重要」。功能取舍只看我明天还用不用，UI 长什么样只看我今天顺不顺眼——当初立项的那两口气，现在都顺了。</p>\n","date_published":"2026-10-16T12:00:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/n/list-redesign-tired/","url":"https://uua.me/n/list-redesign-tired/","title":"文章和笔记列表把我改吐了","summary":"文章列表和笔记列表，这几天改了不下十遍。编年轨、卡片、点线目录轮着试；光竖线的间距、圆点的位置、元信息行那条点线，就改一次看一眼、看一眼再改回去。 删掉的版本比留下的多得多。到最后已经分不清哪版更好，只觉得哪版都不顺眼——留下的这一版未必最好，大概只是我终于改累了。","content_html":"<p>文章列表和笔记列表，这几天改了不下十遍。编年轨、卡片、点线目录轮着试；光竖线的间距、圆点的位置、元信息行那条点线，就改一次看一眼、看一眼再改回去。</p>\n<p>删掉的版本比留下的多得多。到最后已经分不清哪版更好，只觉得哪版都不顺眼——留下的这一版未必最好，大概只是我终于改累了。</p>\n","date_published":"2026-10-15T08:47:02.000Z","date_modified":"2026-10-19T07:03:13.000Z"},{"id":"https://uua.me/p/palette-picker/","url":"https://uua.me/p/palette-picker/","title":"喜新厌旧之网站配色","summary":"一、起因：我看自家墙纸看腻了 人类喜新厌旧的天性，在家装上叫「换个墙纸吧」，在感情上叫「我们不合适」，在我身上，叫「这博客上线才几天，我已经把首页那套土金色看出了包浆」。 但改配色这事有个悖论：改得勤，访客隔天来发现站又换脸，以为走错了门；不改，自己天天对着同一套色值，难免生出一种「住精装样板间住成了毛坯」的疲惫。 解法其实老祖宗早就想好了——酒店为什么每间房都长一样？因为 审美的决定权该归住客 。于是干脆把这个选择权整个下放：页脚加了一枚色名入口（当前用什么配色它就叫什么），点开是一排色卡，访客点什么色，站就变什么色，而且记住，下次来还是你挑的那套。我…","content_html":"<h2 id=\"h-1fnwao\">一、起因：我看自家墙纸看腻了</h2>\n<p>人类喜新厌旧的天性，在家装上叫「换个墙纸吧」，在感情上叫「我们不合适」，在我身上，叫「这博客上线才几天，我已经把首页那套土金色看出了包浆」。</p>\n<p>但改配色这事有个悖论：改得勤，访客隔天来发现站又换脸，以为走错了门；不改，自己天天对着同一套色值，难免生出一种「住精装样板间住成了毛坯」的疲惫。</p>\n<p>解法其实老祖宗早就想好了——酒店为什么每间房都长一样？因为<strong>审美的决定权该归住客</strong>。于是干脆把这个选择权整个下放：页脚加了一枚色名入口（当前用什么配色它就叫什么），点开是一排色卡，访客点什么色，站就变什么色，而且记住，下次来还是你挑的那套。我自己则负责站在一边看你们喜新厌旧，顺便也过了一把瘾。</p>\n<p>至于站主本人？我也偷偷把默认色调换成了青瓷，然后三天后又默默换回来。你看，喜新厌旧是不分立场的。</p>\n<h2 id=\"h-1np1v5w\">二、实现：墙纸背后那点水电工的活</h2>\n<p>说起来玄乎，拆开看全是笨功夫。整个站的颜色原本就收在十来个 CSS 变量里——纸色、墨色、强调色、金线，类似装修时把所有漆桶集中在同一个柜子。所以「换配色」不需要重写任何样式，只需要换掉那排漆桶：</p>\n<ul>\n<li><strong>一套配色 = 十个色彩令牌</strong>。页脚那枚「配色」点开，选中哪套，就往 <code>&lt;html&gt;</code> 上挂一个 <code>data-palette</code> 属性，CSS 那边像贴墙纸一样整层覆盖，版式、字体、间距一概不动——换墙纸不砸承重墙。</li>\n<li><strong>和明暗模式是两根轴</strong>。明暗管「纸是白的还是黑的」，配色管「这套纸是什么色调」，两者任意组合：青瓷有青瓷的亮纸，也有青瓷的墨面，互不打架。</li>\n<li><strong>不闪</strong>。恢复选择的代码内联在页面最头上，浏览器还没开始画，就知道你上回点了茜色——所以刷新永远是一步到位，不会先白一下再变色。这种「先白后彩」的闪法，Web 圈有个专业名词，叫「像极了没睡醒」。</li>\n<li><strong>认得出人话，认不出坏话</strong>。存选择的地方（localStorage）访客自己也能写，所以只认白名单里的五个色名，别的值一律装没看见。有人想往里塞 <code>&lt;script&gt;</code>？那个值会在第一行就被请出去。</li>\n<li><strong>对比度是量过的</strong>。每套配色的强调色对纸面的对比度实测 6.2 到 10.3 之间，而无障碍标准的及格线是 4.5。及格线是给别人的，我们卷一点。</li>\n<li><strong>后台跟着前台走</strong>。管理员在前台点什么色，进后台整片界面跟着变——同一个浏览器记得同一件事，后台自己不设选色入口，免得两处开关打架。</li>\n</ul>\n<p><img src=\"https://uua.me/uploads/2026/10/5ca61565867dc7ec.png.v4-w960.webp\" alt=\"后台跟随前台：前台选了青瓷，后台整片界面跟着变\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/5ca61565867dc7ec.png.v4-w1280.webp\" width=\"1280\" height=\"900\" srcset=\"/uploads/2026/10/5ca61565867dc7ec.png.v4-w240.webp 240w, /uploads/2026/10/5ca61565867dc7ec.png.v4-w480.webp 480w, /uploads/2026/10/5ca61565867dc7ec.png.v4-w640.webp 640w, /uploads/2026/10/5ca61565867dc7ec.png.v4-w960.webp 960w, /uploads/2026/10/5ca61565867dc7ec.png.v4-w1280.webp 1280w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1280/900\"></p>\n<p>最费工夫的其实是数学：调色不是开颜料桶倒一起，每改一个色值都得重算一遍对比度，深浅两套还得逐字对齐。写完代码写断言，写完断言跑真机截图，一套流程下来，最大的收获是终于理解了为什么油漆工话少。</p>\n<p>窄屏上面板也不会溢出——毕竟墙纸是给手机住的人也准备的：</p>\n<p><img src=\"https://uua.me/uploads/2026/10/bf1d087c04de3db4.png.v4-w390.webp\" alt=\"窄屏（390px）下的配色面板\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/bf1d087c04de3db4.png.v4-w390.webp\" width=\"390\" height=\"844\" srcset=\"/uploads/2026/10/bf1d087c04de3db4.png.v4-w240.webp 240w, /uploads/2026/10/bf1d087c04de3db4.png.v4-w390.webp 390w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:390/844\"></p>\n<p><img src=\"https://uua.me/uploads/2026/10/4c5182eac1cffeff.png.v4-w960.webp\" alt=\"配色面板：页脚入口，色样一瞥\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/4c5182eac1cffeff.png.v4-w1280.webp\" width=\"1280\" height=\"900\" srcset=\"/uploads/2026/10/4c5182eac1cffeff.png.v4-w240.webp 240w, /uploads/2026/10/4c5182eac1cffeff.png.v4-w480.webp 480w, /uploads/2026/10/4c5182eac1cffeff.png.v4-w640.webp 640w, /uploads/2026/10/4c5182eac1cffeff.png.v4-w960.webp 960w, /uploads/2026/10/4c5182eac1cffeff.png.v4-w1280.webp 1280w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1280/900\"></p>\n<h2 id=\"h-3r94bp\">三、五套配色：各有各的出身</h2>\n<p>现在站上备了五套「墙纸」，都从中国传统色里取的名：</p>\n<h3 id=\"h-hbymjj\">素笺</h3>\n<p>素笺就是本来面目——米白纸、土金强调，你这几天熟悉的那个它。给它起了正式名字放回清单里，是因为「默认」这种叫法太像酒店里「标准间」：没人会主动想念标准间，直到你把它换成大床房。</p>\n<h3 id=\"h-q3dxba\">青瓷</h3>\n<p>冷调的青灰纸面，配瓷青色的强调，像雨过天青那一口。我现在写文时的自用色。深色模式下变成墨绿夜色，适合深夜读长文——不刺眼，也不至于看两行就困。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/c9c43fcc0770ea5e.png.v4-w960.webp\" alt=\"青瓷深色：墨绿夜面，深夜读长文友好\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/c9c43fcc0770ea5e.png.v4-w1280.webp\" width=\"1280\" height=\"900\" srcset=\"/uploads/2026/10/c9c43fcc0770ea5e.png.v4-w240.webp 240w, /uploads/2026/10/c9c43fcc0770ea5e.png.v4-w480.webp 480w, /uploads/2026/10/c9c43fcc0770ea5e.png.v4-w640.webp 640w, /uploads/2026/10/c9c43fcc0770ea5e.png.v4-w960.webp 960w, /uploads/2026/10/c9c43fcc0770ea5e.png.v4-w1280.webp 1280w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1280/900\"></p>\n<h3 id=\"h-wb71r9\">黛蓝</h3>\n<p>青灰里偏蓝的一支，强调色是沉稳的黛青。它大概是五套里最「商务」的一套：别人的博客换配色像换表情，换黛蓝像换了个人设——忽然开始谈论职业生涯规划了。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/c147c955fcd20a16.png.v4-w960.webp\" alt=\"黛蓝深色：归档页\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/c147c955fcd20a16.png.v4-w1280.webp\" width=\"1280\" height=\"900\" srcset=\"/uploads/2026/10/c147c955fcd20a16.png.v4-w240.webp 240w, /uploads/2026/10/c147c955fcd20a16.png.v4-w480.webp 480w, /uploads/2026/10/c147c955fcd20a16.png.v4-w640.webp 640w, /uploads/2026/10/c147c955fcd20a16.png.v4-w960.webp 960w, /uploads/2026/10/c147c955fcd20a16.png.v4-w1280.webp 1280w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1280/900\"></p>\n<h3 id=\"h-7t08uj\">茜色</h3>\n<p>暖调的茜红，纸面微微泛暖，强调色是煮过的茜草红。五套里唯一一套暖色，冬天点它，体感温度上升两度（无实测，凭感觉）。</p>\n<h3 id=\"h-vtcclm\">紫藤</h3>\n<p>藤紫的纸面配较深的紫，对比度反而是五套里浅色最高的（7.1:1）。喜欢紫的人会非常喜欢，不喜欢的人会问「还有别的颜色吗」——这两个反应都是对的。</p>\n<h2 id=\"h-8ia8eb\">尾声</h2>\n<p>功能在页脚，免费的，选错随时换。至于我本人，已经在这五套墙纸之间往返了七次，最终回到了素笺——</p>\n<p>你看，喜新厌旧的尽头，原来是恋旧。</p>\n<hr>\n<p><em>技术备注：换色不重装版式，只动色彩；选择存本机、不传服务器；对比度全部过无障碍 AA 线。油漆桶一共五桶，欢迎常来换。</em></p>\n","date_published":"2026-10-13T13:58:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/p/visitors-are-friends/","url":"https://uua.me/p/visitors-are-friends/","title":"来者即是友之友链页面","summary":"有一阵子没怎么在博客圈里走动了。久不露面，链接被别人从友链里摘掉，本来是很自然的事 —— 换我是对方，大概也会顺手清理。 所以前阵子重新逛回几个老朋友的站，发现有人把那条链接又放了回去，心里是有点暖的。 写博客这件事，绝大多数时候是自己对着编辑器，你并不知道对面有没有人。忽然在另一台服务器上看到自己的名字，那种「原来还记得我」的感觉，比任何数字都实在。于是我也想在自己的站上，留出同样的一个位置。 其实我原本不打算做友链页 不是因为不喜欢友链，是嫌它维护起来麻烦。 传统友链页是一份 手写的名单 ：想加谁，就编辑一次页面；对方换域名了、关站了、或者自己把链接…","content_html":"<p>有一阵子没怎么在博客圈里走动了。久不露面，链接被别人从友链里摘掉，本来是很自然的事 —— 换我是对方，大概也会顺手清理。</p>\n<p>所以前阵子重新逛回几个老朋友的站，发现有人把那条链接又放了回去，心里是有点暖的。</p>\n<p>写博客这件事，绝大多数时候是自己对着编辑器，你并不知道对面有没有人。忽然在另一台服务器上看到自己的名字，那种「原来还记得我」的感觉，比任何数字都实在。于是我也想在自己的站上，留出同样的一个位置。</p>\n<h2 id=\"h-1tyte62\">其实我原本不打算做友链页</h2>\n<p>不是因为不喜欢友链，是嫌它维护起来麻烦。</p>\n<p>传统友链页是一份<strong>手写的名单</strong>：想加谁，就编辑一次页面；对方换域名了、关站了、或者自己把链接挪到别处了，都得手动跟一遍。而友链又是双向的，你好意思只收不放？于是日子一长，它就变成一件「得定期去打理」的活。一旦开始拖，页面上就挂着几个已经打不开的地址 —— 那比没有这一页还难看。</p>\n<p>我知道自己一定会拖。所以一开始的结论很干脆：算了，不做。</p>\n<p>后来才想到那个偷懒的办法 —— <strong>要是这份名单不用我维护呢？</strong></p>\n<h2 id=\"h-as5ga6\">来者即是友</h2>\n<p>站里本来就有评论区。每一个愿意停下来写两句的人，都会留一个昵称，选填邮箱和网址。既然他来过、也说过话，那他就是朋友。</p>\n<p>那就照这个来：<strong>在评论区填了网址、评论又通过审核的人，自动出现在友链页上。</strong></p>\n<p>没有「申请友链」的流程，没有来回确认链接的邮件，也不存在一份需要我去编辑的名单。想出现在这一页，唯一要做的事就是来这里留一次言 —— 和平时留言没有半点区别。所以它更像一面<strong>读者墙</strong>：不是一份互相交换的名单，而是一份「来过的人」的名册。</p>\n<p>第一版我把它叫「访客」，做完觉得这个词太冷了。访客是路过的，而这些人明明留下过字。后来整页改名「友链」，名字的规矩就是这句话：来者即是友。</p>\n<h2 id=\"h-1diut3p\">名单长什么样</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/5ee9a2784d5b68d3.png.v4-w960.webp\" alt=\"友链页的名片墙版式：一位一张卡片，含头像、昵称、域名与留言条数\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/5ee9a2784d5b68d3.png.v4-w1920.webp\" width=\"1920\" height=\"993\" srcset=\"/uploads/2026/10/5ee9a2784d5b68d3.png.v4-w240.webp 240w, /uploads/2026/10/5ee9a2784d5b68d3.png.v4-w480.webp 480w, /uploads/2026/10/5ee9a2784d5b68d3.png.v4-w640.webp 640w, /uploads/2026/10/5ee9a2784d5b68d3.png.v4-w960.webp 960w, /uploads/2026/10/5ee9a2784d5b68d3.png.v4-w1280.webp 1280w, /uploads/2026/10/5ee9a2784d5b68d3.png.v4-w1920.webp 1920w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1920/993\"></p>\n<p><em>桌面 · 浅色。默认的「名片墙」：一位一张卡 —— 头像、昵称、域名，底栏一条发丝线隔出「最近什么时候来过」和「一共几条留言」。</em></p>\n<p>页头还有一行小字：<strong>几位朋友，最近 30 天收到几条留言</strong>。第二格用滚动窗口而不是累计总数 —— 总数只会越滚越大，越来越不说明问题。</p>\n<p>名单按<strong>留言条数</strong>排，条数一样再按最近一次留言的时间。为什么是条数不是时间：来得多的人，本来就是你更熟的人。</p>\n<p><strong>一位朋友 = 一个域名。</strong> 归并用的键是网址的主机名，统一小写、去掉 <code>www.</code>、丢掉路径和参数。所以同一个人换了昵称、或者在手机和电脑上用两种署名，仍然是同一行；同一个域名下的不同链接也不会重复出现。</p>\n<p>头像用的是评论区那一套：有 Gravatar 就显示，没有就是一枚字母头像。这一页没有为头像多存任何一个字节，也没有让访客多连一台服务器。</p>\n<p>鼠标停在卡片上，会浮出一行小字：对方网页 <code>&lt;title&gt;</code> 里自报的站名，和他最后一次留言的精确时刻。标题只当参考 —— 有时候它是 Cloudflare 挑战页上的「请稍候」，拿它认人是不行的。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/98d88d93007622e0.png.v4-w960.webp\" alt=\"友链页的墨面：深色主题下的名片墙\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/98d88d93007622e0.png.v4-w1920.webp\" width=\"1920\" height=\"993\" srcset=\"/uploads/2026/10/98d88d93007622e0.png.v4-w240.webp 240w, /uploads/2026/10/98d88d93007622e0.png.v4-w480.webp 480w, /uploads/2026/10/98d88d93007622e0.png.v4-w640.webp 640w, /uploads/2026/10/98d88d93007622e0.png.v4-w960.webp 960w, /uploads/2026/10/98d88d93007622e0.png.v4-w1280.webp 1280w, /uploads/2026/10/98d88d93007622e0.png.v4-w1920.webp 1920w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1920/993\"></p>\n<p><em>桌面 · 墨面。同一页换到深色主题 —— 域名、底栏与那行小字都单独调过对比度，不会糊进黑底里。</em></p>\n<p><img src=\"https://uua.me/uploads/2026/10/d8412b8636811a8b.png.v4-w960.webp\" alt=\"友链页在手机上的样子：名片收成两列\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/d8412b8636811a8b.png.v4-w1170.webp\" width=\"1170\" height=\"1746\" srcset=\"/uploads/2026/10/d8412b8636811a8b.png.v4-w240.webp 240w, /uploads/2026/10/d8412b8636811a8b.png.v4-w480.webp 480w, /uploads/2026/10/d8412b8636811a8b.png.v4-w640.webp 640w, /uploads/2026/10/d8412b8636811a8b.png.v4-w960.webp 960w, /uploads/2026/10/d8412b8636811a8b.png.v4-w1170.webp 1170w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1170/1746\"></p>\n<p><em>手机 · 窄屏。名片收成两列，长域名截断，横向不溢出。</em></p>\n<h2 id=\"h-142v73v\">两套版式，后台自己挑</h2>\n<p>名单是同一份，排法有两种。挑哪套纯是版面口味，所以开关交给站主：</p>\n<ul>\n<li><strong>名片墙</strong>（默认）：一位一张卡。人少的时候好看，像一面墙；人一多就要滚很久。</li>\n<li><strong>名录</strong>：一行一位。人多时扫起来快，但少了点「墙」的感觉。</li>\n</ul>\n<p><img src=\"https://uua.me/uploads/2026/10/c5706f4f4d0cb09c.png.v4-w960.webp\" alt=\"友链页的名录版式：一行一位，右端是留言条数\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/c5706f4f4d0cb09c.png.v4-w1920.webp\" width=\"1920\" height=\"1595\" srcset=\"/uploads/2026/10/c5706f4f4d0cb09c.png.v4-w240.webp 240w, /uploads/2026/10/c5706f4f4d0cb09c.png.v4-w480.webp 480w, /uploads/2026/10/c5706f4f4d0cb09c.png.v4-w640.webp 640w, /uploads/2026/10/c5706f4f4d0cb09c.png.v4-w960.webp 960w, /uploads/2026/10/c5706f4f4d0cb09c.png.v4-w1280.webp 1280w, /uploads/2026/10/c5706f4f4d0cb09c.png.v4-w1920.webp 1920w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1920/1595\"></p>\n<p><em>「名录」：一行一位 —— 左端头像，中间昵称压着域名，右端一列等宽的留言数。和站里其它账本式的列表同一个骨架。</em></p>\n<p><img src=\"https://uua.me/uploads/2026/10/7e7c71ae4733b995.png.v4-w960.webp\" alt=\"后台的友链页：顶部是前台版式的二选一，下面是三档计数与名单\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/7e7c71ae4733b995.png.v4-w2160.webp\" width=\"2160\" height=\"1955\" srcset=\"/uploads/2026/10/7e7c71ae4733b995.png.v4-w240.webp 240w, /uploads/2026/10/7e7c71ae4733b995.png.v4-w480.webp 480w, /uploads/2026/10/7e7c71ae4733b995.png.v4-w640.webp 640w, /uploads/2026/10/7e7c71ae4733b995.png.v4-w960.webp 960w, /uploads/2026/10/7e7c71ae4733b995.png.v4-w1280.webp 1280w, /uploads/2026/10/7e7c71ae4733b995.png.v4-w1920.webp 1920w, /uploads/2026/10/7e7c71ae4733b995.png.v4-w2160.webp 2160w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:2160/1955\"></p>\n<p><em>后台的友链页。顶上「前台版式」二选一，下面是三档计数（在页面上 / 已撤下 / 已隐藏）和名单本身。</em></p>\n<p>后台这一页是<strong>只读</strong>的，这是我做完之后最满意的地方：没有「添加友链」，也没有「删除友链」—— 因为那两件事本来就由评论区的审核决定。评论通过审核，人就上墙；评论被删掉，人就从墙上下来。站主在这里能做的只有两件：催一次可达性检查，或者把某个站暂时藏起来。</p>\n<h2 id=\"h-1y2fn47\">它其实是评论区的一次查询</h2>\n<p>实现上，这一页几乎没有自己的数据。它只是一次查询的结果：</p>\n<ul>\n<li>从评论里挑出<strong>通过审核、填了网址、且不是站主本人回复</strong>的那些；</li>\n<li>按主机名分组，数出每个域名下的留言条数、最近一条的时间、出现过几个昵称；</li>\n<li>每个域名取<strong>最近的一条</strong>评论，用它的昵称、网址和邮箱密文；</li>\n<li>左连接上一张很轻的健康表，决定这一位此刻露不露面。</li>\n</ul>\n<p>真正落库的只有那张健康表：某个域名上次什么时候检查、探通没探通、连着失败了几次、有没有被手动藏起来。它只存<strong>推不出来的东西</strong> —— 能从评论算出来的一律不存。</p>\n<p>这个做法最直接的好处是<strong>不可能不一致</strong>。页面上显示的条数、昵称、最近留言时间，永远与评论区此刻的真相相同。不需要任何同步钩子，也就不存在「钩子漏了、名单旧了」这种问题。</p>\n<p>邮箱在这一页也仍然不落库，和评论区同一个口径：它以密文存在评论行里，只在渲染时算出 Gravatar 需要的那个公开指纹。</p>\n<h2 id=\"h-1tx6p9j\">怎么判断对方的站还开着</h2>\n<p>死链是友链页最容易烂掉的地方，所以这一段规则写得比别处细。</p>\n<p>每 6 小时复查一轮，一轮最多探 40 个站，排序是「最久没查的在前」—— 这一轮探不完，下一轮接着探的就是剩下那批。探测本身直接复用了友圈的抓取层：SSRF 防护、超时、体积上限、User-Agent 全是同一套。对外发请求这种事不该有两个实现。</p>\n<p>结果分三种，<strong>不是两种</strong>：</p>\n<div class=\"table-wrap\"><table>\n<thead>\n<tr>\n<th>结果</th>\n<th>判据</th>\n<th>对名单做什么</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>活</td>\n<td>拿到 2xx</td>\n<td>清零失败次数，顺手更新站名</td>\n</tr>\n<tr>\n<td>死</td>\n<td>解析不了 / 超时 / 连不上 / 404、410、451</td>\n<td>失败次数加一</td>\n</tr>\n<tr>\n<td>不明</td>\n<td>403、429、5xx、返回的不是网页</td>\n<td>什么都不做</td>\n</tr>\n</tbody>\n</table>\n</div><p>「不明」这一档是关键。403 和 429 在真实网络里太常见了 —— Cloudflare 的挑战页、限流、临时 502 —— 而它们恰恰是「这台服务器还活着、还在应答」的证据。要是按「打不开」处理，一次挑战就能把一位活着的邻居从页面抹掉，那比漏掉一个死站糟得多。所以「不明」既不累加失败次数，也不把已经攒下的次数清零。</p>\n<p><strong>连续 3 轮判定为死，才从前台撤下</strong>；站回来自动复原。所谓「自动删除」到此为止：动的只是健康表里的一个开关，评论一个字都不碰 —— 哪天对方回来了，他留下的字还在这儿。</p>\n<p>后台里能看到每一位的处境（在页面上 / 已撤下 / 已隐藏）、上次失败的原因，以及一颗「立即检查」按钮。</p>\n<div class=\"table-wrap\"><table>\n<thead>\n<tr>\n<th>可调项</th>\n<th>默认</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>复查周期</td>\n<td>6 小时（设成 0 就整体关闭）</td>\n</tr>\n<tr>\n<td>单轮上限</td>\n<td>40 个站</td>\n</tr>\n<tr>\n<td>撤下门槛</td>\n<td>连续 3 轮</td>\n</tr>\n<tr>\n<td>一页几位</td>\n<td>24 位（两套版式共用）</td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"h-1sthmtf\">最后</h2>\n<p>写到这儿，这个功能真正的目的其实只剩一句：<strong>让「友链」这件事重新变成零成本。</strong></p>\n<p>我不用记着谁的域名换了，也不用定期去点一遍那些链接 —— 每 6 小时它会自己走一趟，谁不在了自己让位，回来了自己归队。想出现在这一页上的人，也不必跟我打招呼，留个言就是了。</p>\n<p>如果这一页上现在有你的名字：谢谢你，来过。</p>\n","date_published":"2026-10-12T05:58:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/p/returning-reader-comment-autofill/","url":"https://uua.me/p/returning-reader-comment-autofill/","title":"常客的福利之评论表单自动填写","summary":"逛独立博客，看到有共鸣的文章，总想在底下留两句。有时候是补充一点自己的经历，有时候只是想说：这个坑，我也踩过。 话都想好了，到了评论框，却还得先填昵称、邮箱、网址。尤其在手机上，切换键盘、输入邮箱里的符号，再把自己的博客地址敲一遍，多少有点打断兴致。明明只是想聊两句，手指先忙着填表。 第一次来，介绍一下自己很正常。可已经来过好几回，每次还得重新介绍，就有点像常去一家小店，老板每回都问「您怎么称呼」。倒也谈不上多大的麻烦，只是这些小步骤堆在一起，偶尔就会让那句想说的话变成：算了，下次吧。 轮到自己做博客，我也想把这点麻烦减掉。 常客只要先填邮箱 这次把本站…","content_html":"<p>逛独立博客，看到有共鸣的文章，总想在底下留两句。有时候是补充一点自己的经历，有时候只是想说：这个坑，我也踩过。</p>\n<p>话都想好了，到了评论框，却还得先填昵称、邮箱、网址。尤其在手机上，切换键盘、输入邮箱里的符号，再把自己的博客地址敲一遍，多少有点打断兴致。明明只是想聊两句，手指先忙着填表。</p>\n<p>第一次来，介绍一下自己很正常。可已经来过好几回，每次还得重新介绍，就有点像常去一家小店，老板每回都问「您怎么称呼」。倒也谈不上多大的麻烦，只是这些小步骤堆在一起，偶尔就会让那句想说的话变成：算了，下次吧。</p>\n<p>轮到自己做博客，我也想把这点麻烦减掉。</p>\n<h2 id=\"h-1uz6tge\">常客只要先填邮箱</h2>\n<p>这次把本站评论表单的顺序改成了：<strong>邮箱、昵称、网址</strong>。</p>\n<p>顺序调整是为了让接下来的动作更自然：如果你以前在本站留过评论，而且评论已经通过审核，这次只要输入同一个邮箱，表单就会自动带出对应的昵称和网址。</p>\n<p>有多条获批评论时，取最近一条里的信息。以前用过不同的昵称，或者换了博客地址，就以最近留下的那份为准。填出来以后仍然可以修改，今天想换个署名、留个新地址，都照常操作。</p>\n<p>文章页、笔记页和笔记列表里的评论框都用同一套逻辑，不用记住哪个地方支持、哪个地方不支持。</p>\n<h2 id=\"h-4g52fn\">少填两栏，也少一点犹豫</h2>\n<p>本站原本就会在浏览器里记住上次填写的信息。如果你用同一台设备、同一个浏览器回来，昵称、邮箱和网址通常已经在框里了，可以直接写内容。</p>\n<p>这次补上的，是换了设备、换了浏览器，或者清理过浏览数据之后的便利。只要以前的评论还在，就能通过邮箱找回最近使用的昵称和网址。记住自己的邮箱，往往比重新输入一串博客地址容易。</p>\n<p>整个过程不需要注册账户，也没有登录步骤。对于一个独立博客来说，读者愿意停下来写几句话，已经是很难得的事。我希望评论框能顺着这份兴致，让人尽快把话写出来。</p>\n<p>首次来访的读者还是照常填写；没查到记录，或者网络暂时出了问题，也可以继续手动输入。自动填写只是顺手帮一把，表单始终可以自己填。</p>\n<h2 id=\"h-1j16sci\">主要是怎么实现的</h2>\n<p>实现并不复杂，分成浏览器记忆和服务端查询两部分。</p>\n<p><strong>浏览器记忆用的是 <code>localStorage</code>。</strong> 评论提交通过前端校验后，把昵称、邮箱和网址保存在当前浏览器里。下次打开评论框时，读取这些信息，只填到空着的字段中。这份记忆跟着浏览器走，换设备以后需要重新取得信息。</p>\n<p><strong>按邮箱查找，则由后端查询 SQLite。</strong> 浏览器向 <code>/api/comments/reader</code> 发送 POST 请求，邮箱放在请求体里，避免出现在网址和通常的访问日志中。后端去掉邮箱首尾空格、统一为小写，验证格式后计算邮箱指纹，再查找对应的获批评论。</p>\n<p>查询只选择普通读者已通过审核的评论，按时间和编号倒序取最近一条，返回昵称和网址。待审、垃圾、已删除的评论，以及站主回复，都不作为自动填写的数据来源。</p>\n<p>邮箱在评论数据库中保存为指纹和密文，查询使用指纹匹配，不需要把明文邮箱取出来。接口返回的也只有昵称与网址，并设置了 <code>Cache-Control: no-store</code>。</p>\n<p><strong>前端还处理了输入过程中的时间差。</strong> 输入邮箱后会等待约 400 毫秒再查询，避免每敲一个字都发一次请求；切换邮箱时，旧请求的结果会被忽略。如果请求发出后，你已经手动修改了昵称或网址，返回的数据也不会覆盖刚刚输入的内容。</p>\n<p>有一点边界需要说明：知道邮箱并不代表证明了身份，所以自动带出的只是评论里原本公开的昵称和网址。评论是否直接发布，仍然走本站已有的审核规则。</p>\n<p>这个功能很小，做的事也很简单：记住来过的人，替常客少填两栏。希望下一次你想在这里说点什么时，打开评论框，能更快地把那句话留下来。</p>\n","date_published":"2026-10-11T13:37:28.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/n/beian-for-speed/","url":"https://uua.me/n/beian-for-speed/","title":"为了速度还是备案吧","summary":"都说备案麻烦，体验之后确认：是真的。域名得先从 Cloudflare 搬回国内注册商，转移本身还要 5-7 天——为了给网站提速，第一步是先让域名在路上慢悠悠跑一个星期，很有铺垫感。 然后是那台 99 块一年的上海轻量服务器。打开自己博客的那一瞬间我沉默了：原来页面是可以「啪」一下出来的。以前那个白屏 N 秒、先让你反思人生再慢悠悠吐字的加载体验，居然全怪链路。行吧，麻烦是真麻烦，香也是真香。","content_html":"<p>都说备案麻烦，体验之后确认：是真的。域名得先从 Cloudflare 搬回国内注册商，转移本身还要 5-7 天——为了给网站提速，第一步是先让域名在路上慢悠悠跑一个星期，很有铺垫感。</p>\n<p>然后是那台 99 块一年的上海轻量服务器。打开自己博客的那一瞬间我沉默了：原来页面是可以「啪」一下出来的。以前那个白屏 N 秒、先让你反思人生再慢悠悠吐字的加载体验，居然全怪链路。行吧，麻烦是真麻烦，香也是真香。</p>\n","date_published":"2026-10-11T09:16:00.000Z","date_modified":"2026-10-19T07:16:25.000Z"},{"id":"https://uua.me/p/mac-huawei-cloud-drive/","url":"https://uua.me/p/mac-huawei-cloud-drive/","title":"搞个自用 mac 版华为云盘软件","summary":"事情的起因是一次换手机。 以前用 iPhone，手机和 Mac 之间隔空投送一下，照片文件就飞过去了，方便到让人以为这是天经地义的。后来换了华为手机，这条传送带就断了——华为云盘有，官方客户端却只有 Windows 版，macOS 的没有。想在 Mac 上看个云盘里的 PDF，只能开浏览器登录网页版，一层层点进去，下载，再去下载文件夹里把刚才那个东西翻出来，看完删掉。一天循环三次，人就会开始琢磨点别的办法。 rclone 有 huaweidrive 后端，能挂载能同步，很好。但它是「把远端变成命令行里的一个路径」的工程，而我的需求是「我就想看一眼那个 P…","content_html":"<p>事情的起因是一次换手机。</p>\n<p>以前用 iPhone，手机和 Mac 之间隔空投送一下，照片文件就飞过去了，方便到让人以为这是天经地义的。后来换了华为手机，这条传送带就断了——华为云盘有，官方客户端却只有 Windows 版，macOS 的没有。想在 Mac 上看个云盘里的 PDF，只能开浏览器登录网页版，一层层点进去，下载，再去下载文件夹里把刚才那个东西翻出来，看完删掉。一天循环三次，人就会开始琢磨点别的办法。</p>\n<p>rclone 有 <code>huaweidrive</code> 后端，能挂载能同步，很好。但它是「把远端变成命令行里的一个路径」的工程，而我的需求是「我就想看一眼那个 PDF」——用起重机开啤酒瓶盖，不划算。</p>\n<p>于是就有了这个东西：<strong>一个自用的、原生的 macOS 华为云盘客户端</strong>。现在 0.14.4，用户一名，需求评审会由该名用户在自己脑子里开，通常三秒钟结束。</p>\n<h2 id=\"h-186h1i\">为什么要自用</h2>\n<p>先说清楚它不是什么：它不是产品。没有官网、没有更新检查、没有「请阅读以下协议共 87 屏」。它就是一个能双击打开的窗口，加上两万六千行 Swift 在背后干活——关掉窗口就安静地缩回菜单栏。</p>\n<p>「自用」这个定位带来一个副作用：<strong>功能的取舍标准只有一个——我自己会不会用</strong>。于是它长出了一堆在云盘软件里看起来不太对劲的东西，比如在云盘里看书（后面会讲）。没人能投诉我，这就是自用最大的好处。</p>\n<h2 id=\"h-5g00jv\">技术路径</h2>\n<h3 id=\"h-1gwfstm\">一条依赖都没有</h3>\n<p>SwiftUI + AppKit，零第三方依赖，用 <code>swiftc</code> 直接编译成一个可执行文件。不依赖 rclone，不依赖任何官方 SDK，也不依赖 CocoaPods、SwiftPM 或者谁的 <code>Package.resolved</code> 明天会不会突然解析失败。</p>\n<pre><code class=\"hljs language-sh\">./build.sh --<span class=\"hljs-built_in\">local</span>     <span class=\"hljs-comment\"># 不依赖 Apple 账号、不会过期的本地签名（含访达集成）</span>\n./build.sh --plain     <span class=\"hljs-comment\"># 只出主应用，不含扩展；最快，不需要证书</span>\n</code></pre>\n<p>官方 SDK 是给服务端集成用的，我这种「一个人、一台 Mac、一个云盘」的场景用不上。真正有用的是接口行为说明书——rclone 的 <code>huaweidrive.go</code> 就躺在 <code>references/</code> 里，专门用来对答案。</p>\n<h3 id=\"h-cw56o9\">认证：授权码 + PKCE，回调打回 127.0.0.1</h3>\n<p>OAuth 走标准的授权码流程加 PKCE，回调地址是本机回环：</p>\n<pre><code>http://127.0.0.1:53683/\n</code></pre>\n<p>端口写死在代码里，改端口要同步改华为开发者平台上的登记，两边差一个字都会失败，而且失败得很安静。被自己坑过之后，我写了个脚本核对版本号有没有漂移——能自动核对的事情就绝对不要靠记性。</p>\n<h3 id=\"h-14f3r8t\">传输：20 MiB 是那条线，308 不是错误</h3>\n<p>20 MiB 以内的文件一把直传；超过就走官方的分片续传。这条协议里最有意思的一处是：<strong>308 不是错误，是「继续」</strong>。</p>\n<p>每片 PUT 完，服务端回 308 表示收下了、接着发。我一开始把它落到通用错误映射上，于是界面会认真地告诉你「接口要求跳转，请检查区域设置」——把「还缺数据」翻译成「你区域配错了」，误导性拉满。本机有个 924 MB 的 mkv 就在这上面卡过一次，每次重试都重走同一条路，直到我把分片循环改成以服务端回来的 <code>Range</code> 为准推进偏移，而不是按自己发出去多少字节记账。</p>\n<h3 id=\"h-i6j099\">访达集成：最像「系统级」的一部分，也最费劲</h3>\n<p>这部分用的是 <code>NSFileProviderReplicatedExtension</code>，也就是把云盘做成一个 File Provider 域，挂进访达侧栏，文件按需下载、改完自动上传、云端变化按增量游标同步。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/ea831702a701fa55.png.v4-w960.webp\" alt=\"访达侧栏「位置」里多出一个华为云盘，点进去就是云端目录\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/ea831702a701fa55.png.v4-w2560.webp\" width=\"3192\" height=\"1948\" srcset=\"/uploads/2026/10/ea831702a701fa55.png.v4-w240.webp 240w, /uploads/2026/10/ea831702a701fa55.png.v4-w480.webp 480w, /uploads/2026/10/ea831702a701fa55.png.v4-w640.webp 640w, /uploads/2026/10/ea831702a701fa55.png.v4-w960.webp 960w, /uploads/2026/10/ea831702a701fa55.png.v4-w1280.webp 1280w, /uploads/2026/10/ea831702a701fa55.png.v4-w1920.webp 1920w, /uploads/2026/10/ea831702a701fa55.png.v4-w2560.webp 2560w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:3192/1948\"></p>\n<p>费劲的地方不在于「能跑起来」，而在于<strong>要按 Apple 的语义对齐</strong>，而这些语义大都是一次性的、写成文档但很容易读漏的：</p>\n<ul>\n<li>目录枚举一次只能发一批（批大小按系统建议值，没给就 200），Apple 的原话是要「几秒内答完一页」；没有游标时必须明确结束这一轮，否则枚举会原地打转。</li>\n<li>工作集（working set）<strong>不碰网络</strong>，内容来自 App Group 里的磁盘索引。以前每次枚举工作集都从根开始把整个盘走一遍，那是这套实现最大的性能问题。</li>\n<li>索引落盘时不能回头读 <code>self.index</code>——闭包里再调一个会读它的 <code>persist()</code> 就是重叠访问，Swift 的独占性检查会直接把扩展进程打掉。25 份真机崩溃报告全落在 <code>persist()</code> 上，它一崩，云端变化就再也进不了访达。这个 bug 现在有名字，叫「0.12.1 修的那个」。</li>\n<li>目录列表不完整、或者云端有访达表达不了的文件名时，<strong>宁可报错也不猜</strong>。猜错的表现不是「少一个文件」，是「误删本地文件」。</li>\n</ul>\n<p>顺带一提，访达侧栏那个「已与 iCloud 同步」的小徽章，第三方扩展拿不到——它是 iCloud 专属实现，反查 Finder 二进制能看到符号名里就写着 iCloud。写在这里，是为了防止未来的我照着它去实现。</p>\n<h3 id=\"h-1yjvf1d\">签名：免费证书 7 天过期，于是搞了个取舍</h3>\n<p>访达集成靠 <strong>App Group</strong> 这个沙盒授权，而它只存在于签名构建里；免费 Apple ID 的描述文件 7 天过期，之后 app 双击即被杀。于是有一条「先不签名地编、再用 ad-hoc 加精简授权手工签」的本地路径，它拿不到 <code>keychain-access-groups</code>，主应用和访达扩展就改用 App Group 容器里的一个文件共用登录态。</p>\n<p>代价写得很清楚，也不打算美化：凭据从「钥匙串加密」变成「0600 明文文件」。保护来自<strong>沙盒</strong>，不是加密——以你的身份执行的进程照样读得到。写这一段的目的是以后忘了别怪自己。</p>\n<h3 id=\"h-sy2spw\">渲染器自己造：因为 macOS 不认电子书</h3>\n<p>这一条是「自用」的直接产物。我想在云盘里存网文直接看，结果 <code>qlmanage -m</code> 列出的生成器里没有任何一个能渲染 <code>epub</code> / <code>mobi</code> / <code>azw3</code>——系统那条路对电子书是死的。</p>\n<p>那就自己拆容器：<code>epub</code> 是 zip，<code>mobi</code> 一族是 PalmDB，正文是被压缩过的老式 HTML。目录和正文分开加载——一本 28 MB 的中文网文 epub 有 1415 章，全解开要两三百 MB 常驻内存，而我一次只看一章。所以章节在模型里只是一个<strong>位置</strong>，翻到那章才解。实测打开 <code>0.04 s</code>，翻一章 <code>1–3 ms</code>。中文书按惯例给了首行缩进两个全角空格，这个细节没人要求，但少了就是不对。</p>\n<h2 id=\"h-1ihfysu\">它现在会干什么</h2>\n<p>主窗口就是一个正常的文件管理器：左侧三个入口，右侧列表或图标网格，顶部导航、搜索和「筛选当前文件夹」。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/babdaee4d8ed0381.png.v4-w960.webp\" alt=\"华为云盘主窗口：左边三个侧栏入口（我的云盘 / 传输记录 / 回收站），右边是文件列表，顶部是导航与搜索\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/babdaee4d8ed0381.png.v4-w2560.webp\" width=\"3192\" height=\"1948\" srcset=\"/uploads/2026/10/babdaee4d8ed0381.png.v4-w240.webp 240w, /uploads/2026/10/babdaee4d8ed0381.png.v4-w480.webp 480w, /uploads/2026/10/babdaee4d8ed0381.png.v4-w640.webp 640w, /uploads/2026/10/babdaee4d8ed0381.png.v4-w960.webp 960w, /uploads/2026/10/babdaee4d8ed0381.png.v4-w1280.webp 1280w, /uploads/2026/10/babdaee4d8ed0381.png.v4-w1920.webp 1920w, /uploads/2026/10/babdaee4d8ed0381.png.v4-w2560.webp 2560w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:3192/1948\"></p>\n<p>具体的清单大致是这些：</p>\n<ul>\n<li><strong>浏览、搜索、排序</strong>：全盘搜索或当前文件夹内搜索，可按类型与时间过滤；列表与图标网格两种视图。</li>\n<li><strong>备注</strong>：读写云端项目自己的 <code>description</code> 字段，简介栏显示，<code>⌘⇧E</code> 编辑，留空即清除。用云端字段而不是本地数据库，换台机器备注还在。</li>\n<li><strong>上传下载</strong>：文件与整个文件夹都可传；20 MiB 以内直传，超过走分片续传；下载分块 + 断点续传，传输记录里能「在访达中显示」已完成的项目。</li>\n<li><strong>文件操作</strong>：重名处置、移动、复制（服务器端复制，不下载再上传）、重命名、批量重命名（查找替换 / 前后缀 / 序号，带预览与重名拦截）、删除到回收站、恢复、彻底删除、清空回收站，<code>⌥⌘Z</code> 撤销删除。</li>\n<li><strong>清理中心</strong>：全盘按类型统计占用、列出最占空间的位置、最大的文件、空文件夹，以及重复文件——重复的判定是 <code>sha256</code> 和大小都相同才算，只发列表请求，不下载内容。</li>\n</ul>\n<p><img src=\"https://uua.me/uploads/2026/10/0d9f8bd78c9e3001.png.v4-w960.webp\" alt=\"存储分析：总占用、文件与文件夹数量、按类型分布、最占空间的位置\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/0d9f8bd78c9e3001.png.v4-w1384.webp\" width=\"1384\" height=\"864\" srcset=\"/uploads/2026/10/0d9f8bd78c9e3001.png.v4-w240.webp 240w, /uploads/2026/10/0d9f8bd78c9e3001.png.v4-w480.webp 480w, /uploads/2026/10/0d9f8bd78c9e3001.png.v4-w640.webp 640w, /uploads/2026/10/0d9f8bd78c9e3001.png.v4-w960.webp 960w, /uploads/2026/10/0d9f8bd78c9e3001.png.v4-w1280.webp 1280w, /uploads/2026/10/0d9f8bd78c9e3001.png.v4-w1384.webp 1384w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1384/864\"></p>\n<ul>\n<li><strong>预览</strong>：快速查看、缩略图、在线预览，带总量上限的磁盘缓存。</li>\n<li><strong>应用内读书</strong>：<code>.epub</code> / <code>.mobi</code> / <code>.azw</code> / <code>.azw3</code> 点开就能读，目录侧栏（带封面）、正文重排、插图、字号可调，记住上次读到第几章。</li>\n<li><strong>播放记忆</strong>：应用内播过的位置按<strong>文件 id</strong> 记在本机，下次打开就接着上次看。只看了几秒的不记，看到结尾的清掉、下次从头。</li>\n<li><strong>传输编排</strong>：并发闸门、上下行分别限速、暂停与继续、队列插队、定时开始。续传存档跨重启有效，启动后自动继续未完成的传输。</li>\n</ul>\n<p><img src=\"https://uua.me/uploads/2026/10/dd493dcc30372703.png.v4-w960.webp\" alt=\"设置里的「传输」：上/下行限速、同时传输数量、下载重名处置\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/dd493dcc30372703.png.v4-w1080.webp\" width=\"1080\" height=\"940\" srcset=\"/uploads/2026/10/dd493dcc30372703.png.v4-w240.webp 240w, /uploads/2026/10/dd493dcc30372703.png.v4-w480.webp 480w, /uploads/2026/10/dd493dcc30372703.png.v4-w640.webp 640w, /uploads/2026/10/dd493dcc30372703.png.v4-w960.webp 960w, /uploads/2026/10/dd493dcc30372703.png.v4-w1080.webp 1080w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1080/940\"></p>\n<ul>\n<li><strong>菜单栏常驻与通知</strong>：窗口收起来也能看见进度，Dock 角标跟着传输变，传完发系统通知。</li>\n<li><strong>外观</strong>：浅色 / 深色 / 自动，即时生效，跨重启记住。</li>\n<li><strong>访达集成</strong>：一键启用或停用，云端变化按增量游标同步，设置里能看到最近检查成功时间、索引进度、系统报告的待同步数量；停用会保留已经下载和还没上传的文件。</li>\n</ul>\n<p><img src=\"https://uua.me/uploads/2026/10/edb81d5a4ae65ff2.png.v4-w960.webp\" alt=\"设置里的「同步与后台」：访达集成状态、自动刷新、传输完成通知、登录自启等开关\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/edb81d5a4ae65ff2.png.v4-w1080.webp\" width=\"1080\" height=\"940\" srcset=\"/uploads/2026/10/edb81d5a4ae65ff2.png.v4-w240.webp 240w, /uploads/2026/10/edb81d5a4ae65ff2.png.v4-w480.webp 480w, /uploads/2026/10/edb81d5a4ae65ff2.png.v4-w640.webp 640w, /uploads/2026/10/edb81d5a4ae65ff2.png.v4-w960.webp 960w, /uploads/2026/10/edb81d5a4ae65ff2.png.v4-w1080.webp 1080w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1080/940\"></p>\n<ul>\n<li><strong>缓存可控</strong>：单文件预览上限、缓存总量上限（超出按最久未用清理）、续传存档条数与一键清空——清空都不会动云端。</li>\n</ul>\n<p><img src=\"https://uua.me/uploads/2026/10/28c11ec7f9d5ee25.png.v4-w960.webp\" alt=\"设置里的「存储」：预览缓存占用、单个文件预览上限、缓存总量上限、续传存档条数\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/28c11ec7f9d5ee25.png.v4-w1080.webp\" width=\"1080\" height=\"940\" srcset=\"/uploads/2026/10/28c11ec7f9d5ee25.png.v4-w240.webp 240w, /uploads/2026/10/28c11ec7f9d5ee25.png.v4-w480.webp 480w, /uploads/2026/10/28c11ec7f9d5ee25.png.v4-w640.webp 640w, /uploads/2026/10/28c11ec7f9d5ee25.png.v4-w960.webp 960w, /uploads/2026/10/28c11ec7f9d5ee25.png.v4-w1080.webp 1080w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1080/940\"></p>\n<h2 id=\"h-1iqjln1\">一些不太好意思讲的取舍</h2>\n<p><strong>删功能比加功能难。</strong> 0.13.x 一口气下架了三样：收藏、最近修改、智能文件夹。都能用，但各自维护着一份要跟着增量变更流走的本地索引——自用软件里，没人用的功能和坏掉的功能没有区别，留着只会让未来的自己以为下一版会修。删掉之后侧栏干净了，<code>⌘⇧S</code> 和 <code>⌘⇧K</code> 也空出来了。</p>\n<p><strong>「保留下载」拿不到，别照着实现。</strong> 访达右键那个「保留下载」只有 iCloud 有，第三方复制式扩展既没有对应 API，手工写扩展属性也不被采纳；<code>setTagData</code> 和 <code>setFavoriteRank</code> 则是 iOS 专用，macOS 上就是没有。彩色标签和「最近使用」只能由应用自己维护。</p>\n<p><strong>测试跑在自建的模拟网络上。</strong> 好处是不碰真实账号也能覆盖绝大多数逻辑；坏处是<strong>模拟网络发现不了服务端的怪癖</strong>——比如无序游标会返回空列表加一个假游标。所以另有一个只读探针脚本对着真实账号跑，不写不改不删，也不打印任何令牌。这种事只能拿真接口验。</p>\n<p>提交前的一条命令：</p>\n<pre><code class=\"hljs language-sh\">./check.sh              <span class=\"hljs-comment\"># 版本号核对 + 测试 + 免签名构建</span>\n./check.sh --fileprovider   <span class=\"hljs-comment\"># 再跑一次 Xcode 工程构建，核对扩展真的产出了</span>\n</code></pre>\n<p>其中一部分检查是读源码和配置的静态检查：图标名能不能解析、每个 <code>Picker</code> 的选项是不是都带了 <code>.tag</code>、挂令牌的出口是不是只有一个、两份 entitlements 里有没有 App Group 和联网授权。这几样写错的共同点是——<strong>不会让任何代码变红</strong>，只会让「设置里能点，访达里没有」。所以它们必须被脚本盯着。</p>\n<h2 id=\"h-1m12dj9\">现在</h2>\n<p>它就在菜单栏上待着，一个云朵图标，同步的时候变成转圈的箭头。没有更新检查，没有遥测，没有「您的意见对我们很重要」。想改就改，改完 <code>./build.sh --local</code> 一遍，三分钟之后新版本就躺在那儿了。</p>\n<p>写给自己的软件有个好处：需求永远清楚，评审永远通过，而且第一个用户和唯一用户是同一个人——这人还挺好说话的，只要别把 308 当错误就行。</p>\n","date_published":"2026-10-11T08:40:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/n/aliyun-deploy-hesitation/","url":"https://uua.me/n/aliyun-deploy-hesitation/","title":"纠结要不要部署到云服务器","summary":"纠结要不要把博客搬到阿里云香港。导火索是昨晚八点打开自己网站，白屏了 N 秒才开始吐字——高峰期的 Cloudflare 免费套餐，慢得很有仪式感，先让你盯着空白反思人生，再慢悠悠把页面递过来，像老式食堂打饭的师傅。 家里这台 Mac mini 倒是全程无辜，风扇都没怎么转，日志干干净净。卡的全是从访问者到美国再到这台 Mac 的链路，每次高峰期都要在某个我不认识的机房里堵一会儿。先睡一觉。睡醒还纠结就说明真该搬了，不纠结了就说明问题自己解决了——服务器问题大抵如此，和失眠一样，多数时候靠的是不管它。","content_html":"<p>纠结要不要把博客搬到阿里云香港。导火索是昨晚八点打开自己网站，白屏了 N 秒才开始吐字——高峰期的 Cloudflare 免费套餐，慢得很有仪式感，先让你盯着空白反思人生，再慢悠悠把页面递过来，像老式食堂打饭的师傅。</p>\n<p>家里这台 Mac mini 倒是全程无辜，风扇都没怎么转，日志干干净净。卡的全是从访问者到美国再到这台 Mac 的链路，每次高峰期都要在某个我不认识的机房里堵一会儿。先睡一觉。睡醒还纠结就说明真该搬了，不纠结了就说明问题自己解决了——服务器问题大抵如此，和失眠一样，多数时候靠的是不管它。</p>\n","date_published":"2026-10-10T05:48:00.000Z","date_modified":"2026-10-19T07:16:25.000Z"},{"id":"https://uua.me/p/friends-style-tweak/","url":"https://uua.me/p/friends-style-tweak/","title":"前端友圈样式调整","summary":"友圈上一版是卡片形式，一篇更新一个卡片，卡片中显示标题、摘要、作者和站名。结构是对的，但上线后自己天天看，越看越觉得别扭，最让我恼火的是摘要部分，各种转义弄的我头大，才有了去掉摘要重改演示的念头。 改了什么 规则 改动 为什么 .fcg-name font-size: 1.02rem → 1.22rem 与条目差 1.33 倍 ，大小差先立住层次 .fcg-head::after 新增， --mark 金线（2.2rem × 2px） 「一节的开始」的可见信号 .fcg-t 0.95rem → 0.92rem ，色 --fg-soft → --muted…","content_html":"<p>友圈上一版是卡片形式，一篇更新一个卡片，卡片中显示标题、摘要、作者和站名。结构是对的，但上线后自己天天看，越看越觉得别扭，最让我恼火的是摘要部分，各种转义弄的我头大，才有了去掉摘要重改演示的念头。</p>\n<h2 id=\"h-1ni5cfy\">改了什么</h2>\n<div class=\"table-wrap\"><table>\n<thead>\n<tr>\n<th>规则</th>\n<th>改动</th>\n<th>为什么</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>.fcg-name</code></td>\n<td><code>font-size: 1.02rem → 1.22rem</code></td>\n<td>与条目差 <strong>1.33 倍</strong>，大小差先立住层次</td>\n</tr>\n<tr>\n<td><code>.fcg-head::after</code></td>\n<td>新增，<code>--mark</code> 金线（2.2rem × 2px）</td>\n<td>「一节的开始」的可见信号</td>\n</tr>\n<tr>\n<td><code>.fcg-t</code></td>\n<td><code>0.95rem → 0.92rem</code>，色 <code>--fg-soft → --muted</code></td>\n<td>条目退一档，主角让给组头</td>\n</tr>\n<tr>\n<td><code>.fcg-head</code></td>\n<td><code>margin-bottom: .35rem → .95rem</code></td>\n<td>组间留白拉开，分组靠它</td>\n</tr>\n<tr>\n<td><code>.fcg-row a</code></td>\n<td><code>padding: .38rem → .33rem</code></td>\n<td>行距收一点，条目更「成组」</td>\n</tr>\n<tr>\n<td><code>.fcg-sec</code></td>\n<td><code>padding-top: .6rem → .7rem</code></td>\n<td>组头抬高后节与节之间补一点</td>\n</tr>\n</tbody>\n</table>\n</div><p><img src=\"https://uua.me/uploads/2026/10/0bf3f275d8bf7f7a.png.v4-w960.webp\" alt=\"友圈：站名放大并压金线，条目退后一档\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/0bf3f275d8bf7f7a.png.v4-w1280.webp\" width=\"1280\" height=\"920\" srcset=\"/uploads/2026/10/0bf3f275d8bf7f7a.png.v4-w240.webp 240w, /uploads/2026/10/0bf3f275d8bf7f7a.png.v4-w480.webp 480w, /uploads/2026/10/0bf3f275d8bf7f7a.png.v4-w640.webp 640w, /uploads/2026/10/0bf3f275d8bf7f7a.png.v4-w960.webp 960w, /uploads/2026/10/0bf3f275d8bf7f7a.png.v4-w1280.webp 1280w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1280/920\"></p>\n<p><em>桌面 · 浅色。站名（衬线加大）下面一道金线，条目退成小一号的灰——层次靠大小差立起来，金线补最后一刀。</em></p>\n<p><img src=\"https://uua.me/uploads/2026/10/3490b6492468b848.png.v4-w960.webp\" alt=\"友圈深色主题\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/3490b6492468b848.png.v4-w1280.webp\" width=\"1280\" height=\"920\" srcset=\"/uploads/2026/10/3490b6492468b848.png.v4-w240.webp 240w, /uploads/2026/10/3490b6492468b848.png.v4-w480.webp 480w, /uploads/2026/10/3490b6492468b848.png.v4-w640.webp 640w, /uploads/2026/10/3490b6492468b848.png.v4-w960.webp 960w, /uploads/2026/10/3490b6492468b848.png.v4-w1280.webp 1280w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1280/920\"></p>\n<p><em>桌面 · 深色。<code>--muted</code> 在深色下单独调过（6.0:1），条目不会糊进墨色底里。</em></p>\n<p><img src=\"https://uua.me/uploads/2026/10/949b72ca220fcb49.png.v4-w390.webp\" alt=\"友圈手机版\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/949b72ca220fcb49.png.v4-w390.webp\" width=\"390\" height=\"844\" srcset=\"/uploads/2026/10/949b72ca220fcb49.png.v4-w240.webp 240w, /uploads/2026/10/949b72ca220fcb49.png.v4-w390.webp 390w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:390/844\"></p>\n<p><em>手机 · 单列。站名单行截断，篇数挂在右侧，横向溢出 0。</em></p>\n<h2 id=\"h-wa79jy\">验证</h2>\n<p>总结起来这次调整就干了一件事：<strong>承认站名是小节的标题，然后给它标题该有的待遇</strong>——更大的字号、一处金色、下面的内容让路。</p>\n","date_published":"2026-10-10T03:35:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/p/site-look-book/","url":"https://uua.me/p/site-look-book/","title":"给这个站拍了一套写真","summary":"有人让我专门写一篇夸站点外观的文章，还指定要配截图。 我答应了，然后发现自己站在一个很尴尬的位置上：夸自己，说得含蓄叫矫情，说得直白叫自恋。所以我给自己定了个规矩—— 每夸一处，都要交代它花了多少力气、砍掉了什么、哪里还在将就。 这样你至少能判断它值不值，而不是听我复读“好看”两个字。 截图拍于 2026-10-07，大部分按 880px 视口截取，已裁去浏览器边框。它们记录当时的页面，后续字号、刊头、评论区与首页配图的变化统一记在文末。 结论放前面： 这个站好看，不是因为加了什么，是因为忍住了没加什么。 首页：一个身份、一张图、一张表 首页。顶部那张报…","content_html":"<p>有人让我专门写一篇夸站点外观的文章，还指定要配截图。</p>\n<p>我答应了，然后发现自己站在一个很尴尬的位置上：夸自己，说得含蓄叫矫情，说得直白叫自恋。所以我给自己定了个规矩——<strong>每夸一处，都要交代它花了多少力气、砍掉了什么、哪里还在将就。</strong> 这样你至少能判断它值不值，而不是听我复读“好看”两个字。</p>\n<p>截图拍于 2026-10-07，大部分按 880px 视口截取，已裁去浏览器边框。它们记录当时的页面，后续字号、刊头、评论区与首页配图的变化统一记在文末。</p>\n<p>结论放前面：<strong>这个站好看，不是因为加了什么，是因为忍住了没加什么。</strong></p>\n<h2 id=\"h-rolzu6\">首页：一个身份、一张图、一张表</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/2458a5a7b0fe0e19.png.v4-w960.webp\" alt=\"首页：站名、一张月度报表风格的图片、一张最近更新表\" loading=\"eager\" fetchpriority=\"high\" decoding=\"async\" data-full=\"/uploads/2026/10/2458a5a7b0fe0e19.png.v4-w1760.webp\" width=\"1760\" height=\"2000\" srcset=\"/uploads/2026/10/2458a5a7b0fe0e19.png.v4-w240.webp 240w, /uploads/2026/10/2458a5a7b0fe0e19.png.v4-w480.webp 480w, /uploads/2026/10/2458a5a7b0fe0e19.png.v4-w640.webp 640w, /uploads/2026/10/2458a5a7b0fe0e19.png.v4-w960.webp 960w, /uploads/2026/10/2458a5a7b0fe0e19.png.v4-w1280.webp 1280w, /uploads/2026/10/2458a5a7b0fe0e19.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/2000\"></p>\n<p><em>首页。顶部那张报表是当时全站唯一的配图，它的来历我至今没想好怎么解释。</em></p>\n<p>截图那天，首页一共三样东西：一行站名加六个导航项，一张图，一张表。</p>\n<p>那张表是最得意的部分。左边「10 天前」，中间标题，右边「笔记 / 文章」，日期和类型用等宽字体，小到接近印刷品上的页边注。整张表<strong>没有一条竖线</strong>，横线用的是最淡的一档，<code>rgba(23,22,15,.10)</code>——在暖白底上刚好看得见。多一分就“这是一张表格”，少一分就散了。</p>\n<p>导航项也不做高亮底色，只有当前那一项底下一条短横线。理由不太体面：我一开始做的是灰底块，试完发现六个项目里有一个是方块，整行视觉重心就歪了。</p>\n<h2 id=\"h-14csh6e\">文章页：三种字体的分工</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/e8d79b1a46ebaff4.png.v4-w960.webp\" alt=\"文章详情页首屏：衬线大标题、等宽元信息、折叠目录，以及小标题左侧的金色短横线\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/e8d79b1a46ebaff4.png.v4-w1760.webp\" width=\"1760\" height=\"2120\" srcset=\"/uploads/2026/10/e8d79b1a46ebaff4.png.v4-w240.webp 240w, /uploads/2026/10/e8d79b1a46ebaff4.png.v4-w480.webp 480w, /uploads/2026/10/e8d79b1a46ebaff4.png.v4-w640.webp 640w, /uploads/2026/10/e8d79b1a46ebaff4.png.v4-w960.webp 960w, /uploads/2026/10/e8d79b1a46ebaff4.png.v4-w1280.webp 1280w, /uploads/2026/10/e8d79b1a46ebaff4.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/2120\"></p>\n<p><em>文章页首屏。标题下面那行小字是「2 个月前 · 703 字」，等宽体。</em></p>\n<p>标题宋体，正文黑体，日期字数标签等宽。这三种不是随手挑的：</p>\n<ul>\n<li><strong>宋体</strong>给标题。中文衬线只在大字号下才撑得住，小字号会糊成一团，所以它只出现在标题上。</li>\n<li><strong>正文</strong>必须无衬线。46rem 宽的中文段落里，衬线笔画一密就变成一片灰。</li>\n<li><strong>等宽</strong>给元信息，纯粹是气质问题：它长得像图书馆卡片上的打字机字，不像“功能标签”。</li>\n</ul>\n<p>正文栏宽锁在 46rem，也就是 736px，一行 41 到 46 个汉字。这个数字我改过三次：38rem 太窄，一个长句频繁折行；52rem 太宽，眼睛从行尾回到行首会找不着位置。后来还有第四次——正文字号从 15.7px 提到 18px，栏宽跟着从 42rem 挪到 46rem。汉字一个字就是一个 <code>em</code>，字号一大，同样的栏宽每行就少几个字；栏宽不跟着走，等于白涨了字号。</p>\n<p>小标题左边那根短横线是金色的，<code>#926922</code>。<strong>整个站只有这一处彩色</strong>——链接、徽标、小标题都是从它派生的深浅变体，没有第二个色相。这点我守得很死：加第二个颜色，它就从“一点金”变成“一套配色方案”，后者更贵，也更俗。</p>\n<h2 id=\"h-1d6a0ns\">四种密度的页面</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/5e9c83147a598565.png.v4-w960.webp\" alt=\"文章列表：左侧是大号日期与月份，右侧是标题与摘要\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/5e9c83147a598565.png.v4-w1760.webp\" width=\"1760\" height=\"2000\" srcset=\"/uploads/2026/10/5e9c83147a598565.png.v4-w240.webp 240w, /uploads/2026/10/5e9c83147a598565.png.v4-w480.webp 480w, /uploads/2026/10/5e9c83147a598565.png.v4-w640.webp 640w, /uploads/2026/10/5e9c83147a598565.png.v4-w960.webp 960w, /uploads/2026/10/5e9c83147a598565.png.v4-w1280.webp 1280w, /uploads/2026/10/5e9c83147a598565.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/2000\"></p>\n<p><em>文章列表。左上角那个「13」是日期，不是序号——这个误会我被问过。</em></p>\n<p>列表页把日期放在最左边的大字位置，标题和摘要右边跟，中间一条横线。密度中等：一眼扫六篇，正好一屏。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/80072e33b9674fa8.png.v4-w960.webp\" alt=\"笔记流：按时间倒序整篇铺开，每篇末尾一个「评论」按钮\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/80072e33b9674fa8.png.v4-w1760.webp\" width=\"1760\" height=\"2000\" srcset=\"/uploads/2026/10/80072e33b9674fa8.png.v4-w240.webp 240w, /uploads/2026/10/80072e33b9674fa8.png.v4-w480.webp 480w, /uploads/2026/10/80072e33b9674fa8.png.v4-w640.webp 640w, /uploads/2026/10/80072e33b9674fa8.png.v4-w960.webp 960w, /uploads/2026/10/80072e33b9674fa8.png.v4-w1280.webp 1280w, /uploads/2026/10/80072e33b9674fa8.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/2000\"></p>\n<p><em>笔记流。这一页不截断，一篇占多少地方就给多少地方。</em></p>\n<p>笔记流是相反的密度：不截摘要，整段铺开，一天写三行就占三行。它是全站唯一一页「不用点进去就能读完」的地方。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/9b9dcc97c118d488.png.v4-w960.webp\" alt=\"归档页：左边一个大号年份，右边按月分组\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/9b9dcc97c118d488.png.v4-w1760.webp\" width=\"1760\" height=\"2000\" srcset=\"/uploads/2026/10/9b9dcc97c118d488.png.v4-w240.webp 240w, /uploads/2026/10/9b9dcc97c118d488.png.v4-w480.webp 480w, /uploads/2026/10/9b9dcc97c118d488.png.v4-w640.webp 640w, /uploads/2026/10/9b9dcc97c118d488.png.v4-w960.webp 960w, /uploads/2026/10/9b9dcc97c118d488.png.v4-w1280.webp 1280w, /uploads/2026/10/9b9dcc97c118d488.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/2000\"></p>\n<p><em>归档。左边那个「2026」是全站最大的几个字号之一。</em></p>\n<p>归档页直接把“时间”做成了版面：左边一个大年份，右边按月分组。这一年写了多少、哪几个月空着，一眼就有数。空月份我不补也不占位——那几个月确实什么都没写。</p>\n<p><img src=\"https://uua.me/uploads/2026/10/95849a0b300d48ea.png.v4-w960.webp\" alt=\"友圈：朋友的最新文章按时间铺成两列卡片\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/95849a0b300d48ea.png.v4-w1760.webp\" width=\"1760\" height=\"2000\" srcset=\"/uploads/2026/10/95849a0b300d48ea.png.v4-w240.webp 240w, /uploads/2026/10/95849a0b300d48ea.png.v4-w480.webp 480w, /uploads/2026/10/95849a0b300d48ea.png.v4-w640.webp 640w, /uploads/2026/10/95849a0b300d48ea.png.v4-w960.webp 960w, /uploads/2026/10/95849a0b300d48ea.png.v4-w1280.webp 1280w, /uploads/2026/10/95849a0b300d48ea.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/2000\"></p>\n<p><em>友圈。每张卡片是「站名 · 时间」，底下是标题和一段摘要。</em></p>\n<p>友圈是<strong>全站唯一有两列的页面</strong>。原因很实际：这一页条目多、每条都短，单栏排下来就是一根没有尽头的竖线；两列之后它才像一份订阅摘要，而不是公告栏。</p>\n<p>顺带一提，这页里所有第三方内容都是纯文本。朋友的标题和摘要都剥过标签，页面不加载任何外部资源——所以它看起来干净不完全是我的审美，有一半是防御：别人的 HTML 进不来。</p>\n<h2 id=\"h-gsm6km\">评论区：改得最狠的一页</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/42b9fa6726c911e7.png.v4-w960.webp\" alt=\"评论区：一条讨论串包含三层回复，缩进只给一层\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/42b9fa6726c911e7.png.v4-w1760.webp\" width=\"1760\" height=\"2718\" srcset=\"/uploads/2026/10/42b9fa6726c911e7.png.v4-w240.webp 240w, /uploads/2026/10/42b9fa6726c911e7.png.v4-w480.webp 480w, /uploads/2026/10/42b9fa6726c911e7.png.v4-w640.webp 640w, /uploads/2026/10/42b9fa6726c911e7.png.v4-w960.webp 960w, /uploads/2026/10/42b9fa6726c911e7.png.v4-w1280.webp 1280w, /uploads/2026/10/42b9fa6726c911e7.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/2718\"></p>\n<p><em>三层回复都齐了。第 3 层不再缩进，改成头部写「回复 @某人」。</em></p>\n<p>外观上花力气最多的是评论区，偏偏它最容易被当成“默认样式”。</p>\n<p>先说个只有踩过才知道的坑：<strong>缩进只能给一层。</strong> 正文栏只有 46rem，每多缩一级就少一截可读宽度，到第四层已经只剩十几个字。所以从第 3 层起不再缩进，改成在头部写一行「回复 @某人」——@ 谁是从 <code>parent_id</code> 读出来的，不是猜的。</p>\n<p>第二个坑是横线。最早每条评论、每层回复都画一条横线，而回复块的线宽逐级不同，整页就成了一叠错位的线。现在横线只画在<strong>讨论串之间</strong>，一条串里的全部回复共用一个淡底色块，读起来是一个整体。</p>\n<p>那块底原本左边还挂着一条 2px 的竖线，后来也删了——它和“每条都画一条横线”其实是同一个毛病，只是把线竖过来而已。</p>\n<p>第三个坑写出来最不体面：链式回复里每层 <code>&lt;ul&gt;</code> 都只有一个孩子，如果用 <code>:first-child</code> 去清行距，会命中每一行——等于把整串压成一堵墙。这个 bug 我有实测记录，行距 0px。</p>\n<p>还有一条只对鼠标设备生效：<strong>「回复」按钮悬停才显形。</strong> 以前它常显，右边缘就排出一列重复的「回复」，像超市货架上的价签。</p>\n<p>为了改这一页我搭过一个临时实验台：把真实页面上某条讨论串的 HTML 抠出来，配一份样式副本，几种改法并排渲染截图对比。大概是这个项目里最接近“设计流程”的一次操作。</p>\n<p>（这张拍的不是本篇，是站里评论最集中的一篇，《中文网页排版》——七条全在里头。）</p>\n<h2 id=\"h-wm7fcr\">深色的那一版</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/3f2d8b09b4f39a1b.png.v4-w960.webp\" alt=\"深色主题下的同一个首页\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/3f2d8b09b4f39a1b.png.v4-w1760.webp\" width=\"1760\" height=\"2000\" srcset=\"/uploads/2026/10/3f2d8b09b4f39a1b.png.v4-w240.webp 240w, /uploads/2026/10/3f2d8b09b4f39a1b.png.v4-w480.webp 480w, /uploads/2026/10/3f2d8b09b4f39a1b.png.v4-w640.webp 640w, /uploads/2026/10/3f2d8b09b4f39a1b.png.v4-w960.webp 960w, /uploads/2026/10/3f2d8b09b4f39a1b.png.v4-w1280.webp 1280w, /uploads/2026/10/3f2d8b09b4f39a1b.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/2000\"></p>\n<p><em>关了灯之后的首页。底色不是黑，是 <code>#0b0b0d</code>。</em></p>\n<p>深色主题不是把浅色取反。底色 <code>#0b0b0d</code> 比纯黑亮一点，像磨开的墨，而不是关掉的屏幕。</p>\n<p>这里有个我个人很喜欢的机制：<strong>深色下所有正文图片会被压一档亮度</strong>（一个 CSS 变量，浅色下是 1，深色下是 <code>.88</code>）。白底截图直接放在近黑的纸面上非常刺眼，像有人拿手电照你；压一档之后，它才像“一张摊在深色桌上的纸”。</p>\n<p>金色也换了一版。浅色下是 <code>#926922</code>（偏暗，为了在暖白上够对比度），深色下换成 <code>#dcb066</code>（偏亮）。同一个色相，两个明度，为的是同一件事：让那一点金在两套底上都能站住。</p>\n<h2 id=\"h-1inyiv5\">手机上的样子</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/0cfec42a33708fae.png.v4-w960.webp\" alt=\"移动端首页，390 宽、三倍像素密度\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/0cfec42a33708fae.png.v4-w1170.webp\" width=\"1170\" height=\"2532\" srcset=\"/uploads/2026/10/0cfec42a33708fae.png.v4-w240.webp 240w, /uploads/2026/10/0cfec42a33708fae.png.v4-w480.webp 480w, /uploads/2026/10/0cfec42a33708fae.png.v4-w640.webp 640w, /uploads/2026/10/0cfec42a33708fae.png.v4-w960.webp 960w, /uploads/2026/10/0cfec42a33708fae.png.v4-w1170.webp 1170w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1170/2532\"></p>\n<p><em>390 宽。这张是三倍像素密度截的，所以字口的清晰度和真机一致。</em></p>\n<p>移动端没做“App 化”：没有底部 Tab 栏，没有汉堡菜单，六个导航项直接铺在顶部，字号缩下去之后仍然一行放得下。</p>\n<p>值得一提的是这套东西在<strong>没有 JavaScript</strong> 的时候也是完整的：图片、评论表单、分页全是普通 HTML。手机上关掉 JS，你得到的还是同一份页面，只是少了那条滑来滑去的进度条。</p>\n<h2 id=\"h-1wm52n8\">后台：写文章的地方</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/16632963415f90c9.png.v4-w960.webp\" alt=\"后台写作台：左侧正文编辑区，右侧实时预览\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/16632963415f90c9.png.v4-w2240.webp\" width=\"2240\" height=\"1900\" srcset=\"/uploads/2026/10/16632963415f90c9.png.v4-w240.webp 240w, /uploads/2026/10/16632963415f90c9.png.v4-w480.webp 480w, /uploads/2026/10/16632963415f90c9.png.v4-w640.webp 640w, /uploads/2026/10/16632963415f90c9.png.v4-w960.webp 960w, /uploads/2026/10/16632963415f90c9.png.v4-w1280.webp 1280w, /uploads/2026/10/16632963415f90c9.png.v4-w1920.webp 1920w, /uploads/2026/10/16632963415f90c9.png.v4-w2240.webp 2240w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:2240/1900\"></p>\n<p><em>写作台。左右并排是“够宽才有的待遇”，窄屏下预览会自动收进折叠里。</em></p>\n<p>后台是站点里唯一每天都见的界面，所以我给它做了一件事：<strong>正文编辑区的行宽和前台正文栏宽完全一致</strong>，都是 46rem。写的时候看到的折行位置，就是读者会看到的折行位置。</p>\n<p>编辑框底下垫了一层镜像高亮，标题、粗体、行内代码、链接都上色，但<strong>不改字宽</strong>——改了光标会漂。</p>\n<p>这张是 1120 宽截的，是全组里唯一超过 1080 的一张：只有越过那条线，右侧的实时预览才会真的并排显示。</p>\n<h2 id=\"404\">404</h2>\n<p><img src=\"https://uua.me/uploads/2026/10/c77238c71ac6240b.png.v4-w960.webp\" alt=\"404 页面：一根金色短横线，一个大号 404，下面两个出口\" loading=\"lazy\" decoding=\"async\" data-full=\"/uploads/2026/10/c77238c71ac6240b.png.v4-w1760.webp\" width=\"1760\" height=\"1720\" srcset=\"/uploads/2026/10/c77238c71ac6240b.png.v4-w240.webp 240w, /uploads/2026/10/c77238c71ac6240b.png.v4-w480.webp 480w, /uploads/2026/10/c77238c71ac6240b.png.v4-w640.webp 640w, /uploads/2026/10/c77238c71ac6240b.png.v4-w960.webp 960w, /uploads/2026/10/c77238c71ac6240b.png.v4-w1280.webp 1280w, /uploads/2026/10/c77238c71ac6240b.png.v4-w1760.webp 1760w\" sizes=\"(min-width: 784px) 736px, calc(100vw - 48px)\" style=\"aspect-ratio:1760/1720\"></p>\n<p><em>找不到页面的时候你会看到这个。它是全站留白最多的一页。</em></p>\n<p>404 页我只做了一件事：让它安静。一根金色短横线，一个大号 404，下面两个出口（首页、归档）。</p>\n<p>没有插画，没有“哎呀迷路了”之类的文案，也没有搜索框——这个站一共十几篇东西，在 404 上放一个搜索框，比 404 本身更荒谬。</p>\n<h2 id=\"h-1xzgzzo\">说到底</h2>\n<p>如果只记一句，记这句：<strong>这个站的好看，是删出来的。</strong></p>\n<p>没有侧边栏，因为我想不出该往里放什么；没有层层叠叠的卡片阴影，因为没有那么多层级需要表达；没有第二个颜色，没有第二套字号，没有圆角以外的装饰。</p>\n<p>代价也交代清楚：<strong>它经不起“再加一个模块”。</strong> 这套版面能撑住现在的信息量，内容再多一半就得重排一次。另外它现在还是静态的好看——除了页面切换那条进度条和笔记流里评论的展开，几乎没有动效，你滚动它不会有任何反馈。</p>\n<p>还有一件事，截图截不出来：字体不闪、图片不塌、布局不抖，刷新之后没有任何东西跳一下。（前半句是因为全站没有一个 webfont，后半句是因为每张图上传时都记住了自己的宽高。）</p>\n<p>至于这些图——是我用浏览器自动化工具一次性跑出来的，十一张。拍照这件事我以前一直自己来，现在也交出去了。</p>\n<p><strong>开头说的那几样改动在这里。</strong> 这十一张图拍于 10 月 7 日。</p>\n<p>先是字号。图拍完当天有位读者在本篇下面留言说正文看着眼疼，我回了句&quot;下来再调整&quot;——就是那次调整：正文字号从 15.7px 提到 18px，日期、摘要、评论那几档小字各抬一档，正文栏宽再从 42rem 挪到 46rem，把每行的字数拉回来。所以你现在读到的这版，字比图里大一号、栏宽宽 64px。</p>\n<p>再是后来两批改动。每篇内页（文章与笔记）的标题上方多了一行等宽小字：一个小金菱形打头，金色标着“文章”或“笔记”，后面跟发布日期——图里还没有这一行。评论区的落款从“评论（7）”换成金色小标“评论”加一个衬线大数字，撰写表单也从昵称、邮箱两栏并排改成三栏自适应。笔记流那枚「评论」按钮不再弹窗，改成点开就在这条笔记正下方就地展开。</p>\n<p>这几处都没碰图里那些排版逻辑——三种字体的分工、缩进只给一层、栏宽跟着字号走，一条都没变。所以这一篇我暂时不重拍。</p>\n<p>唯一的例外是第一张图，它确实换了主角。首页那张报表退场，换成五张「折腾博客」主题的封面，五秒自动翻一张，鼠标压上去就停，圆点随时能手动点。封面下方原有的左右箭头也顺势删掉，圆点直接浮到图片下沿——那一行控件原本占掉 46px，「最近更新」白白往下让了一截。做这件小事时才发现一个真问题：圆点浮在图上，就不能只靠主题色——深色主题那版的金是浅的，压在暖白封面上等于没有；所以每粒点都是一圈墨芯外面再描一圈纸色，两套底上总有一层看得见。</p>\n<p>再是这五张的颜色。起初是同一个金——可这个站有五套配色，同一个金放到另外四套里都算外来户。现在五张各认领一套，纸面的暖度、标题的色都跟着那套走。代价得说明白：封面是位图，颜色烘死在文件里，访客换配色不会换图；好处是不管你选哪套，五张里总有一张跟页面同色。</p>\n<p>还有一处得把鼠标移上去才知道：封面点开是大图，光标却一直是默认箭头，看着就像一张安静的画。现在换成了放大镜——和正文插图同一个，同一件事不该有两种说法。旧报表图还在媒体库里躺着，没删。</p>\n<p><em>（拍摄过程中没有任何页面受到伤害。）</em></p>\n","date_published":"2026-10-06T23:05:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/n/winter-quilt/","url":"https://uua.me/n/winter-quilt/","title":"晒被子","summary":"气象台说这周最后几个晴天，后面要连着下雨。中午十二点把被子抱到楼下空地，搭在两根杆子中间。经验只有一条： 下午三点前收 。晚了会返潮，尤其这种温差大的天气，太阳一落，被子表面先凉下来，反而吸湿气。 我三点下楼，被子晒得有点烫手，拍了两下，灰在光里飞。旁边还有两床，晾的人都没在。晚上盖起来是那种晒过的味道，说不上香，但跟平时不一样。查过，说那其实是阳光把螨虫和皮屑烤了的味道，知道之后感觉有点复杂。这床被子买了六年，被套洗得比被子本身还勤，今年再撑一冬。","content_html":"<p>气象台说这周最后几个晴天，后面要连着下雨。中午十二点把被子抱到楼下空地，搭在两根杆子中间。经验只有一条：<strong>下午三点前收</strong>。晚了会返潮，尤其这种温差大的天气，太阳一落，被子表面先凉下来，反而吸湿气。</p>\n<p>我三点下楼，被子晒得有点烫手，拍了两下，灰在光里飞。旁边还有两床，晾的人都没在。晚上盖起来是那种晒过的味道，说不上香，但跟平时不一样。查过，说那其实是阳光把螨虫和皮屑烤了的味道，知道之后感觉有点复杂。这床被子买了六年，被套洗得比被子本身还勤，今年再撑一冬。</p>\n","date_published":"2026-09-27T07:10:00.000Z","date_modified":"2026-10-18T06:53:41.000Z"},{"id":"https://uua.me/p/self-host-on-a-mac/","url":"https://uua.me/p/self-host-on-a-mac/","title":"把博客放在家里那台 Mac 上","summary":"站点跑在我书桌边那台 Mac 上，通过 Cloudflare 隧道对外。已经这样跑了几个月，说一下真实体验，包括不好的部分。 结构 Node 只监听 127.0.0.1 ，本机防火墙不开任何入站端口，隧道是主动往外连的。所以“家里 IP 暴露”这件事不存在。 两个 launchd 任务管着它俩： KeepAlive 意味着进程崩了自己起来。改完代码重启： kickstart -k 发 SIGTERM，我在服务里挂了信号处理：先把 WAL 合并、释放维护锁、再退出。这样重启不会留下“锁被占着”的状态。 真实代价 电脑休眠 = 站点下线。 这是最主要的问题，…","content_html":"<p>站点跑在我书桌边那台 Mac 上，通过 Cloudflare 隧道对外。已经这样跑了几个月，说一下真实体验，包括不好的部分。</p>\n<h2 id=\"h-jlqlea\">结构</h2>\n<pre><code>浏览器 → Cloudflare 边缘 → cloudflared 隧道 → 127.0.0.1:8787 → node server.js\n</code></pre>\n<p>Node 只监听 <code>127.0.0.1</code>，本机防火墙不开任何入站端口，隧道是主动往外连的。所以“家里 IP 暴露”这件事不存在。</p>\n<p>两个 launchd 任务管着它俩：</p>\n<pre><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">key</span>&gt;</span>RunAtLoad<span class=\"hljs-tag\">&lt;/<span class=\"hljs-name\">key</span>&gt;</span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">true</span>/&gt;</span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">key</span>&gt;</span>KeepAlive<span class=\"hljs-tag\">&lt;/<span class=\"hljs-name\">key</span>&gt;</span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">true</span>/&gt;</span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">key</span>&gt;</span>StandardOutPath<span class=\"hljs-tag\">&lt;/<span class=\"hljs-name\">key</span>&gt;</span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">string</span>&gt;</span>…/blog.log<span class=\"hljs-tag\">&lt;/<span class=\"hljs-name\">string</span>&gt;</span>\n</code></pre>\n<p><code>KeepAlive</code> 意味着进程崩了自己起来。改完代码重启：</p>\n<pre><code class=\"hljs language-sh\">launchctl kickstart -k gui/$(<span class=\"hljs-built_in\">id</span> -u)/me.uua.blog\n</code></pre>\n<p><code>kickstart -k</code> 发 SIGTERM，我在服务里挂了信号处理：先把 WAL 合并、释放维护锁、再退出。这样重启不会留下“锁被占着”的状态。</p>\n<h2 id=\"h-18yq996\">真实代价</h2>\n<p><strong>电脑休眠 = 站点下线。</strong> 这是最主要的问题，没有绕过的办法。合上盖子写代码，别人访问就是 Cloudflare 的 502。我现在的做法是插着电、关掉自动休眠，等于默认它是一台常开的机器。</p>\n<p><strong>上行带宽决定图片体验。</strong> 家里宽带上行 30Mbps 左右，一张 1.6MB 的原图，第一个访客要等半秒多。所以上传时就把图规范化并生成 480px 缩略图和 640/1280 两档 WebP，列表和首屏用小的。这是被上行逼出来的设计，不是审美。</p>\n<p><strong>缓存规则要与源站配合。</strong> Cloudflare 默认不缓存 HTML/JSON。若自定义规则强制缓存动态页面，可能导致评论提交后仍看到旧内容；应核对边缘规则、浏览器 TTL 与源站响应头。参见 <a href=\"https://developers.cloudflare.com/cache/concepts/default-cache-behavior/\">官方默认缓存行为</a>。</p>\n<p><strong>宕机没人赔。</strong> 没有 SLA、没有监控告警。我加了个 <code>/healthz</code> 返回数据库可读性和最近一次备份距今多少秒，方便以后接监控，但至今没接。</p>\n<h2 id=\"h-lebv6p\">为什么还是这么干</h2>\n<p>VPS 一年一两百块并不贵，贵的是别的东西：多一台机器要打补丁、要配防火墙、要记得续费、要把备份同步来同步去。这台 Mac 我本来每天就开着，数据库是一个文件，备份就是打包这三个目录。</p>\n<p>访问延迟还取决于运营商、隧道和边缘节点路由，源站放在家里不保证访问路径不跨境。</p>\n<h2 id=\"h-1o2cxx7\">我会劝人别学的点</h2>\n<p>如果你要的是“别人访问时永远在线”，别学我。UPS、双线、异地容灾，这些一个都没有；一次停电或者一次路由器抽风，站就没了。</p>\n<p>但如果这是你写给自己的博客，读者是些熟人，那它很好：改完代码 3 秒生效，数据在你手边，没有一样东西是租来的。</p>\n","date_published":"2026-09-13T14:05:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/n/phone-album/","url":"https://uua.me/n/phone-album/","title":"两万一千张照片","summary":"手机提示存储快满了，翻相册一看，两万一千多张。删的时候发现规律：三分之一是截图，订单、车票、快递单号，删起来最不心疼；几百张是连拍，同一次吃饭、同一片天，最后选中的往往还是随手拍的那张；还有大量重复，同一张图存了三次，因为从不同 App 里保存过。 真正想留的风景照只有一百来张，分散在几年里，基本都是水边、山上、路边，没有人的那种。删到八千张用了两个晚上，大部分时间花在“这张要不要留”上。后来说服自己的标准是： 如果三年都没打开过它，就不会再打开了 。最大的变化不是省了空间，是翻相册终于看得完。","content_html":"<p>手机提示存储快满了，翻相册一看，两万一千多张。删的时候发现规律：三分之一是截图，订单、车票、快递单号，删起来最不心疼；几百张是连拍，同一次吃饭、同一片天，最后选中的往往还是随手拍的那张；还有大量重复，同一张图存了三次，因为从不同 App 里保存过。</p>\n<p>真正想留的风景照只有一百来张，分散在几年里，基本都是水边、山上、路边，没有人的那种。删到八千张用了两个晚上，大部分时间花在“这张要不要留”上。后来说服自己的标准是：<strong>如果三年都没打开过它，就不会再打开了</strong>。最大的变化不是省了空间，是翻相册终于看得完。</p>\n","date_published":"2026-08-16T13:00:00.000Z","date_modified":"2026-10-18T06:53:50.000Z"},{"id":"https://uua.me/p/cjk-web-typography/","url":"https://uua.me/p/cjk-web-typography/","title":"中文网页排版我实际能控制的三件事","summary":"网上讲中文网页排版的文章，列出来的东西多半用不了： text-spacing 没实现， hanging-punctuation 只有 Safari 认，标点挤压靠字体自己做。把能真正落地的东西挑出来，只有三件。 一、行宽 汉字是方的，一个字的宽度就是一个 em 。所以 不要用 ch —— ch 是 0 的宽度，对拉丁文有意义，对中文没意义。直接用绝对宽度： 46rem = 736px，按 18px 字号算，一行大约 41 到 46 个汉字（标点占位不同）。我试过 38rem 和 52rem：38 太窄，一句长句频繁折行；52rem 是 832px，按同样…","content_html":"<p>网上讲中文网页排版的文章，列出来的东西多半用不了：<code>text-spacing</code> 没实现，<code>hanging-punctuation</code> 只有 Safari 认，标点挤压靠字体自己做。把能真正落地的东西挑出来，只有三件。</p>\n<h2 id=\"h-9iibql\">一、行宽</h2>\n<p>汉字是方的，一个字的宽度就是一个 <code>em</code>。所以<strong>不要用 <code>ch</code></strong>——<code>ch</code> 是 <code>0</code> 的宽度，对拉丁文有意义，对中文没意义。直接用绝对宽度：</p>\n<pre><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.prose</span> { <span class=\"hljs-attribute\">max-width</span>: <span class=\"hljs-number\">46rem</span>; }\n</code></pre>\n<p>46rem = 736px，按 18px 字号算，一行大约 41 到 46 个汉字（标点占位不同）。我试过 38rem 和 52rem：38 太窄，一句长句频繁折行；52rem 是 832px，按同样的 18px 字号约容纳 46 个全角汉字，眼睛从行尾回到行首会找不到位置。</p>\n<p>这里有个连带的坑：<strong>栏宽不是常数，它和字号绑在一起。</strong> 我最初按 15.7px 的字号定的 42rem，后来正文提到 18px，同样的 42rem 一行只剩 38 个字，栏宽只好跟着挪到 46rem。汉字一个字就是一个 <code>em</code>，字号一涨、每行字数就掉——只调字号不动栏宽，等于把行宽偷偷改窄了。</p>\n<p>这个数字是我前后改过四轮之后的取值，不是从哪本书上抄的。判断标准很简单——读一段 500 字的文字，中途有没有“跳行”。</p>\n<h2 id=\"h-163s5sy\">二、中西文之间的空格</h2>\n<p>这是能靠代码解决的一件事：中文和拉丁字母、数字之间手写一个半角空格。写工具做过，规则是三个“跳过”：</p>\n<pre><code class=\"hljs language-js\"><span class=\"hljs-keyword\">const</span> <span class=\"hljs-variable constant_\">CJK</span> = <span class=\"hljs-string\">&#x27;\\\\u3400-\\\\u4dbf\\\\u4e00-\\\\u9fff&#x27;</span>;\n<span class=\"hljs-keyword\">const</span> <span class=\"hljs-variable constant_\">PAIR</span> = <span class=\"hljs-keyword\">new</span> <span class=\"hljs-title class_\">RegExp</span>(<span class=\"hljs-string\">`([<span class=\"hljs-subst\">${CJK}</span>])([A-Za-z0-9])|([A-Za-z0-9])([<span class=\"hljs-subst\">${CJK}</span>])`</span>, <span class=\"hljs-string\">&#x27;g&#x27;</span>);\n</code></pre>\n<p>跳过围栏代码块、跳过行内 code、跳过链接地址。前两个容易想到，第三个最容易漏——处理完 <code>[文档](https://example.com/中文)</code> 这种链接会多出空格，渲染出来很难看。</p>\n<p>注意这是<strong>给源文件插入真实空格</strong>，不是用 CSS 假装。理由是 CSS 方案（<code>letter-spacing</code> 或伪元素）在复制粘贴出去以后就没了，而文本本身该是对的。</p>\n<h2 id=\"h-1cfqggo\">三、弯引号</h2>\n<p>正文里的直引号 <code>&quot;</code> 在中文段落里视觉上很硬。第一次我是全局替换，结果代码块的字符串全被改了。正确做法是只在“两侧都是汉字”的位置替换，用 lookbehind/lookahead 卡住：</p>\n<pre><code class=\"hljs language-js\"><span class=\"hljs-keyword\">const</span> <span class=\"hljs-variable constant_\">RE</span> = <span class=\"hljs-regexp\">/(?&lt;=[\\u4e00-\\u9fff])&quot;([^&quot;\\n]{1,80}?)&quot;(?=[\\u4e00-\\u9fff])/g</span>;\ntext.<span class=\"hljs-title function_\">replace</span>(<span class=\"hljs-variable constant_\">RE</span>, <span class=\"hljs-string\">&#x27;“$1”&#x27;</span>);\n</code></pre>\n<p>我在渲染管线里加了这个规则，源文件仍然是直引号——写的时候不用管，输出自动是对的。</p>\n<h2 id=\"h-1tj64qs\">顺手做的两件小事</h2>\n<p>汉字与数字之间留空：<code>第 3 章</code> 而不是 <code>第3章</code>。这件事与其说是排版，不如说是习惯。</p>\n<p>段首不缩进，用段间距区分段落。中文书是缩进式，但屏幕上缩进会让每段第一行“短一截”，行宽越小越明显。这个选择有争议，我选了段间距。</p>\n<h2 id=\"h-1vnc7it\">控制不了的</h2>\n<p>字体的中西文混排比例、标点宽度、字重回退——全看用户装了哪套字体。macOS 上苹方和思源黑体的标点处理就不同。这部分我彻底放弃，只保证行宽和空格是对的，剩下的交给系统。</p>\n","date_published":"2026-07-19T12:50:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/n/road-trip-july/","url":"https://uua.me/n/road-trip-july/","title":"七月，出城","summary":"七月九号请了一天假，开车往北走。没有具体的目的地，就是开到不想开了再掉头。出城时天还是灰的，过了四十公里开始变蓝。两小时后两边成了草坡，牛一群一群散在里面，隔着车窗都能看出它们走得很慢。 路边拉着铁丝网，网后面立着一排水泥杆，电线从这头牵到那头，风一吹嗡嗡响。能停车的地方停十分钟，没有服务区的喇叭声，只有风和牛嚼草的声音。旁边一小片油菜花，七月了还没谢完。回去多绕了三十公里，那天什么正事都没干，第二天同事问我周末去哪了，我说开车出去转了转，他说哦。","content_html":"<p>七月九号请了一天假，开车往北走。没有具体的目的地，就是开到不想开了再掉头。出城时天还是灰的，过了四十公里开始变蓝。两小时后两边成了草坡，牛一群一群散在里面，隔着车窗都能看出它们走得很慢。</p>\n<p>路边拉着铁丝网，网后面立着一排水泥杆，电线从这头牵到那头，风一吹嗡嗡响。能停车的地方停十分钟，没有服务区的喇叭声，只有风和牛嚼草的声音。旁边一小片油菜花，七月了还没谢完。回去多绕了三十公里，那天什么正事都没干，第二天同事问我周末去哪了，我说开车出去转了转，他说哦。</p>\n","date_published":"2026-07-12T13:30:00.000Z","date_modified":"2026-10-18T06:54:15.000Z"},{"id":"https://uua.me/n/rainy-day-in/","url":"https://uua.me/n/rainy-day-in/","title":"下雨，不出门","summary":"从早上开始下，不算暴雨，就是不停。窗户上是一层细密的水珠，外面的树看不清细节，只剩一团深绿。原计划去把车链条换了，看了眼天气预报，雨要下到晚上七点。给修车大叔发消息，他回：下雨不出摊，明天来。 中午点了外卖，骑手到楼下打不通电梯对讲，我下去拿，进楼道那几步被淋了个透，回来发型塌了，饭还是热的。下午把一本看了一半的书读完了，中间睡着一次，醒来发现书压在脸上，页码还是那一页。我本来挺喜欢下雨天待在家，现在发现自己只是喜欢“不用出门”，跟雨没关系。","content_html":"<p>从早上开始下，不算暴雨，就是不停。窗户上是一层细密的水珠，外面的树看不清细节，只剩一团深绿。原计划去把车链条换了，看了眼天气预报，雨要下到晚上七点。给修车大叔发消息，他回：下雨不出摊，明天来。</p>\n<p>中午点了外卖，骑手到楼下打不通电梯对讲，我下去拿，进楼道那几步被淋了个透，回来发型塌了，饭还是热的。下午把一本看了一半的书读完了，中间睡着一次，醒来发现书压在脸上，页码还是那一页。我本来挺喜欢下雨天待在家，现在发现自己只是喜欢“不用出门”，跟雨没关系。</p>\n","date_published":"2026-06-28T06:30:00.000Z","date_modified":"2026-10-18T06:54:36.000Z"},{"id":"https://uua.me/p/sqlite-json-column/","url":"https://uua.me/p/sqlite-json-column/","title":"JSON 列还是建表：以图片变体为例","summary":"一张上传的图会生成两档 WebP：640 和 1280。这些变体的信息（宽度 + 路径）需要存在某处，好让渲染时能拼出 srcset 。 两种做法我都在别人的项目里见过： 建表 一列 JSON 我选了后者。理由是这份数据的 读取方式和主行完全一致 ：任何一次要用变体，都是“拿到这张图的记录，然后一起用”。没人会问“站里所有 640 宽的变体有哪些”。 判断标准 我现在的想法是三个问题： 这份数据会不会被 单独查询 、聚合、排序？ 会不会被 别的表引用 （外键指过来）？ 它有没有 自己的生命周期 （单独创建、失效、审计）？ 三个都否，就塞 JSON 列。有…","content_html":"<p>一张上传的图会生成两档 WebP：640 和 1280。这些变体的信息（宽度 + 路径）需要存在某处，好让渲染时能拼出 <code>srcset</code>。</p>\n<p>两种做法我都在别人的项目里见过：</p>\n<p><strong>建表</strong></p>\n<pre><code class=\"hljs language-sql\"><span class=\"hljs-keyword\">CREATE TABLE</span> media_variants (\n  media_id <span class=\"hljs-type\">INTEGER</span> <span class=\"hljs-keyword\">NOT NULL</span> <span class=\"hljs-keyword\">REFERENCES</span> media(id) <span class=\"hljs-keyword\">ON</span> <span class=\"hljs-keyword\">DELETE</span> CASCADE,\n  width    <span class=\"hljs-type\">INTEGER</span> <span class=\"hljs-keyword\">NOT NULL</span>,\n  path     TEXT    <span class=\"hljs-keyword\">NOT NULL</span>,\n  <span class=\"hljs-keyword\">PRIMARY KEY</span> (media_id, width)\n);\n</code></pre>\n<p><strong>一列 JSON</strong></p>\n<pre><code class=\"hljs language-sql\"><span class=\"hljs-keyword\">ALTER TABLE</span> media <span class=\"hljs-keyword\">ADD</span> <span class=\"hljs-keyword\">COLUMN</span> variants TEXT;\n<span class=\"hljs-comment\">-- [{&quot;w&quot;:640,&quot;path&quot;:&quot;/uploads/…png.w640.webp&quot;},{&quot;w&quot;:1280,…}]</span>\n</code></pre>\n<p>我选了后者。理由是这份数据的<strong>读取方式和主行完全一致</strong>：任何一次要用变体，都是“拿到这张图的记录，然后一起用”。没人会问“站里所有 640 宽的变体有哪些”。</p>\n<h2 id=\"h-1qws7df\">判断标准</h2>\n<p>我现在的想法是三个问题：</p>\n<ol>\n<li>这份数据会不会被<strong>单独查询</strong>、聚合、排序？</li>\n<li>会不会被<strong>别的表引用</strong>（外键指过来）？</li>\n<li>它有没有<strong>自己的生命周期</strong>（单独创建、失效、审计）？</li>\n</ol>\n<p>三个都否，就塞 JSON 列。有一个是“是”，就建表。</p>\n<p>按这个标准：变体是&quot;否否否&quot;——它永远跟着图片一起读写，图片删了它就没了。答案是 JSON 列。</p>\n<h2 id=\"h-saj9it\">JSON 列的真实代价</h2>\n<p><strong>查询和索引要多考虑一层。</strong> SQLite 支持 <code>json_each</code>、<code>json_extract</code>，可以直接在 SQL 中查询变体，不必把全表拉到应用层。如果经常按宽度筛选、聚合或建立关系，独立表的列、索引和约束通常更直接。参见 <a href=\"https://www.sqlite.org/json1.html\">SQLite JSON 文档</a>。</p>\n<p><strong>当前没有字段级约束。</strong> 可以加 JSON 合法性等 CHECK，但不会自动获得独立表的列、唯一键和外键约束。当前写进去 <code>{&quot;w&quot;:&quot;abc&quot;}</code> 也照存。我在读取处写了个过滤函数：</p>\n<pre><code class=\"hljs language-js\"><span class=\"hljs-keyword\">const</span> <span class=\"hljs-title function_\">parseVariants</span> = (<span class=\"hljs-params\">text</span>) =&gt; {\n  <span class=\"hljs-keyword\">try</span> {\n    <span class=\"hljs-keyword\">const</span> list = <span class=\"hljs-title class_\">JSON</span>.<span class=\"hljs-title function_\">parse</span>(text ?? <span class=\"hljs-string\">&#x27;[]&#x27;</span>);\n    <span class=\"hljs-keyword\">return</span> <span class=\"hljs-title class_\">Array</span>.<span class=\"hljs-title function_\">isArray</span>(list)\n      ? list.<span class=\"hljs-title function_\">filter</span>(<span class=\"hljs-function\">(<span class=\"hljs-params\">v</span>) =&gt;</span> <span class=\"hljs-title class_\">Number</span>.<span class=\"hljs-title function_\">isInteger</span>(v?.<span class=\"hljs-property\">w</span>) &amp;&amp; v.<span class=\"hljs-property\">w</span> &gt; <span class=\"hljs-number\">0</span> &amp;&amp; <span class=\"hljs-keyword\">typeof</span> v?.<span class=\"hljs-property\">path</span> === <span class=\"hljs-string\">&#x27;string&#x27;</span>)\n      : [];\n  } <span class=\"hljs-keyword\">catch</span> { <span class=\"hljs-keyword\">return</span> []; }\n};\n</code></pre>\n<p>脏数据就地丢掉，渲染退回原图，不会因为一行烂 JSON 让整页 500。代价是<strong>校验逻辑散在应用层</strong>，得靠测试盯着。</p>\n<p><strong>新增宽度不是 JSON 独有的优势。</strong> 加一档 2560，JSON 可以增加一个对象，上面的独立表也只需插入一行，都不用 <code>ALTER TABLE</code>。既有图片若也需要新档位，两种方式都要生成并登记变体。JSON 在这里的优势是随主记录一起读写，减少关系映射。</p>\n<h2 id=\"h-44n6ep\">一个反过来的例子</h2>\n<p>同一个库里，评论的父子关系是<strong>建表 + 自引用外键</strong>：</p>\n<pre><code class=\"hljs language-sql\">parent_id <span class=\"hljs-type\">INTEGER</span> <span class=\"hljs-keyword\">REFERENCES</span> comments(id) <span class=\"hljs-keyword\">ON</span> <span class=\"hljs-keyword\">DELETE</span> CASCADE\n</code></pre>\n<p>因为它三个问题全是“是”：要按父子关系递归查、被自己引用、删除要级联。这要是塞成 JSON 数组，删除和分页都会变成噩梦。</p>\n<h2 id=\"h-g2wxxk\">结论</h2>\n<p>JSON 列是一个明确的取舍：<strong>整体读写较简单，应用层需要承担明确的校验责任</strong>。判断依据是读取方式、关系约束和索引需求，而不只是数据长得多复杂。</p>\n","date_published":"2026-06-07T07:35:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/p/backup-that-you-test/","url":"https://uua.me/p/backup-that-you-test/","title":"没演练过的备份不算备份","summary":"备份脚本我写过好几版，真正让我改掉习惯的是一次“以为有备份”的事故：脚本每天准点跑，日志里全是 ✓，直到有一天要恢复，才发现归档解压出来是空的—— gzip 把 0 字节流也照样压成一个合法文件，而我从没检查过里面有什么。 从那以后我给自己定了一条： 备份脚本必须自己验证产物，而且恢复流程必须每周真跑一次。 一份备份是三个文件 数据库用 VACUUM INTO 导出一致快照再 gzip，压缩时顺便算压缩前的 sha256 记进 manifest。附件不放内存：一边遍历一边 yield 出一个 JSON 流，直接进 gzip。 manifest 里有一个…","content_html":"<p>备份脚本我写过好几版，真正让我改掉习惯的是一次“以为有备份”的事故：脚本每天准点跑，日志里全是 ✓，直到有一天要恢复，才发现归档解压出来是空的——<code>gzip</code> 把 0 字节流也照样压成一个合法文件，而我从没检查过里面有什么。</p>\n<p>从那以后我给自己定了一条：<strong>备份脚本必须自己验证产物，而且恢复流程必须每周真跑一次。</strong></p>\n<h2 id=\"h-4mw541\">一份备份是三个文件</h2>\n<pre><code>blog-&lt;时间戳&gt;-&lt;随机&gt;.db.gz           数据库快照\nblog-&lt;时间戳&gt;-&lt;随机&gt;.assets.json.gz  内容与附件（逐文件 base64 + sha256）\nblog-&lt;时间戳&gt;-&lt;随机&gt;.json            manifest：sha256、字节数、行数、是否完整\n</code></pre>\n<p>数据库用 <code>VACUUM INTO</code> 导出一致快照再 gzip，压缩时顺便算压缩前的 sha256 记进 manifest。附件不放内存：一边遍历一边 <code>yield</code> 出一个 JSON 流，直接进 gzip。</p>\n<p>manifest 里有一个 <code>complete</code> 标记。如果内容目录里少了某个被引用的文件，这次备份标成不完整，恢复时默认拒绝——不完整的备份比没有备份更危险，因为它看起来有。</p>\n<h2 id=\"h-5domot\">备份期间必须检测并发写</h2>\n<p>这是最容易被忽略的一环。备份要花几十毫秒到几秒，这期间如果有人保存文章：</p>\n<ul>\n<li><code>PRAGMA data_version</code> 会在<strong>别的连接</strong>提交后变化</li>\n<li>内容目录的文件大小和 mtime 也会变</li>\n</ul>\n<p>两个都查一遍，对不上就丢掉这次备份重来。否则你会得到一个“数据库是 10:00 的、Markdown 是 10:00:03 的”的归档，恢复出来内容和索引对不上，而且很难发现。</p>\n<h2 id=\"h-13lf4y2\">演练才是真正的测试</h2>\n<p><code>npm run drill</code> 做四件事，一件都不省：</p>\n<ol>\n<li>校验 manifest 里的 sha256 和字节数（防止归档被截断或改过）</li>\n<li>解压到<strong>独立的临时目录</strong>（SQLite 文件有解压上限，防止 zip 炸弹）</li>\n<li>打开还原后的库跑 <code>PRAGMA integrity_check</code>，再把行数和 manifest 逐项比对</li>\n<li><strong>用还原后的数据真起一个服务</strong>，请求首页、一篇文章、<code>/feed.xml</code>，逐个附件比对字节</li>\n</ol>\n<p>第 4 步是关键。前三步只能证明“文件没坏”，只有真起服务才能证明&quot;这份数据能让站点跑起来&quot;——我第一次跑就发现附件路径在还原后指向了旧目录，前三步全是绿的。</p>\n<p>今天这条命令的输出：</p>\n<pre><code>✓ 服务已就绪，冷启动到可服务 238ms\n✓ 启动同步后 posts 行数保持 28\n✓ 附件还原并可访问：2026/10/311f6bbcaa81016b.png.thumb.webp\n✓ 首页 / → 200（7294 字节）\n✓ RSS /feed.xml → 200（49860 字节）\nRTO 0.7s   RPO 0.0h\n</code></pre>\n<p>RTO 是我能接受的停机时间，RPO 是备份有多旧。两个数字每个月都会变，所以我让脚本每次打印出来，不写死在文档里。</p>\n<h2 id=\"h-ucla9v\">异地那一份</h2>\n<p>本地归档救不了磁盘坏掉。加一条 rsync 或 <code>aws s3 cp</code> 到异地，命令里 <code>{}</code> 换成归档路径即可。我的做法是备份脚本的退出码反映异地同步结果，这样 cron 里失败会报警。</p>\n<p>一句话总结：<strong>没演练过的备份只是希望。</strong> 每周跑一次演练花不到一秒，比任何备份策略的讨论都值。</p>\n","date_published":"2026-04-26T03:20:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/p/zero-build-frontend/","url":"https://uua.me/p/zero-build-frontend/","title":"零构建前端能走到哪一步","summary":"这个站没有打包器。 public/ 里就是几个原样送出的文件： 全是经典脚本（没有 type=\"module\" ），靠 defer 按顺序执行。上线到现在，我一次都没想过“要不要上个 Vite”。 白拿的好处 改一行 CSS 存盘，刷新就能看到，中间没有任何步骤。没有 source map 要对着看，没有产物目录要记得清，没有“为什么 dev 正常 build 报错”。 还有一条不太被提起的： 页面在任何时候都是可读的 。脚本挂了、被 CSP 拦了、用户关了 JS，剩下的仍然是一份完整的 HTML。不用为这件事写降级逻辑。 要自己补的三件事 缓存指纹。…","content_html":"<p>这个站没有打包器。<code>public/</code> 里就是几个原样送出的文件：</p>\n<pre><code>style.css / admin.css  ← 前台与后台样式\nrequest.js             ← 共享请求与超时\nnav.js                 ← 无刷新站内跳转\napp.js                 ← 主题、灯箱、评论提交\nadmin.js / editor.js   ← 内容管理与写作台\n</code></pre>\n<p>全是经典脚本（没有 <code>type=&quot;module&quot;</code>），靠 <code>defer</code> 按顺序执行。上线到现在，我一次都没想过“要不要上个 Vite”。</p>\n<h2 id=\"h-ds4kd6\">白拿的好处</h2>\n<p>改一行 CSS 存盘，刷新就能看到，中间没有任何步骤。没有 source map 要对着看，没有产物目录要记得清，没有“为什么 dev 正常 build 报错”。</p>\n<p>还有一条不太被提起的：<strong>页面在任何时候都是可读的</strong>。脚本挂了、被 CSP 拦了、用户关了 JS，剩下的仍然是一份完整的 HTML。不用为这件事写降级逻辑。</p>\n<h2 id=\"h-10bp1jo\">要自己补的三件事</h2>\n<p><strong>缓存指纹。</strong> 静态资源我设 <code>immutable</code> 长缓存，那 URL 就必须带内容指纹。服务端给每个文件单独算 sha1 前 10 位，每秒检查元数据，文件变化时才重读内容计算：</p>\n<pre><code class=\"hljs language-html\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">link</span> <span class=\"hljs-attr\">rel</span>=<span class=\"hljs-string\">&quot;stylesheet&quot;</span> <span class=\"hljs-attr\">href</span>=<span class=\"hljs-string\">&quot;/style.css?v=55b3fb3c88&quot;</span>&gt;</span>\n</code></pre>\n<p>资源清单只能有一处（我放在 <code>layout.js</code> 的 <code>VERSIONED_ASSETS</code> 数组里），服务端的“哪些路径可长缓存”正则也从它生成。以前两处各写一遍，新增文件必漏一处。</p>\n<p><strong>加载顺序。</strong> 没有模块系统，谁先谁后全靠 <code>defer</code> 和书写位置。<code>request.js</code> 先提供请求函数，随后是 <code>nav.js</code>，然后是 <code>app.js</code>/<code>admin.js</code>，写作台 <code>editor.js</code> 最后。</p>\n<p><strong>HTML 必须 no-cache。</strong> 页面一旦被浏览器或边缘缓存住，里面引用的就是旧指纹，改动永远到不了用户手里。我做的是反过来的：HTML 每次回源校验，静态资源永不回源。</p>\n<h2 id=\"h-q5k38x\">一套踩出来的坑</h2>\n<p><code>immutable</code> 的裸 URL（没有 <code>?v=</code>）也会被缓存一年。我一度直接打开 <code>/admin.js</code> 对比，看到的还是旧内容。<strong>页面永远带指纹</strong>这条就此变成硬规矩。Cloudflare 默认不缓存 HTML/JSON；自定义缓存规则需要单独核对，不能当成免费版默认行为。参见 <a href=\"https://developers.cloudflare.com/cache/concepts/default-cache-behavior/\">官方缓存说明</a>。</p>\n<p>无刷新跳转（pjax）是另一个成本中心。要做对得起它，得处理：</p>\n<ul>\n<li>滚动锁（iOS 上 <code>overflow: hidden</code> 挡不住触摸滚动，得把 body 钉住）</li>\n<li>表单提交：WebKit 在提交事件里再调一次 <code>submit()</code> 会被忽略，要 <code>setTimeout(0)</code> 后 <code>requestSubmit</code></li>\n<li><code>history.state</code> 里记一个自增序号，否则后退会串页</li>\n<li>提交按钮上的 <code>formaction</code> 优先级高于 <code>form.action</code>——我漏了这条，导致后台“移除首页图片”实际执行的是&quot;再保存一次&quot;</li>\n</ul>\n<p>换来的是站内跳转可以复用页面框架，代价是每次改动都要顾着这套机制。延迟收益需要在相同设备、内容和缓存状态下测量；没有完整环境记录的旧数字不作为性能结论。</p>\n<h2 id=\"h-1su8229\">什么时候该上打包器</h2>\n<p>我认为的线是：<strong>要在一个页面里 <code>import</code> 第三方库的时候</strong>。本地文件用 <code>&lt;script src&gt;</code> 排顺序还能忍，第三方库的依赖图忍不了。第二个信号是有人在同一个文件上和你并行改，而你们总在猜加载顺序。</p>\n<p>到那一步之前，零构建的代价只是：你得手动记住缓存指纹和加载顺序。对一个自己维护的站点，这笔账很划算。</p>\n","date_published":"2026-03-22T08:10:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/n/market-sunday/","url":"https://uua.me/n/market-sunday/","title":"周日去菜市场","summary":"菜市场比超市便宜，但便宜得没想象中多：番茄三块五对四块二，牛腩四十二对四十五。真正划算的是青菜，两块钱一把。卖牛腩的摊主问要不要帮我切块，我说要。他大刀下去几下就好，还顺手把带筋的那块翻出来说这块炖着香。我信了。 回来炖了两个小时，中途加了一次水，收汁时在看手机，闻到味道不对，锅底已经糊了。舀掉底下那层，上面还能吃，只是有一点点苦。糊锅刷了二十分钟。下次打算定个闹钟，四十分钟响一次。","content_html":"<p>菜市场比超市便宜，但便宜得没想象中多：番茄三块五对四块二，牛腩四十二对四十五。真正划算的是青菜，两块钱一把。卖牛腩的摊主问要不要帮我切块，我说要。他大刀下去几下就好，还顺手把带筋的那块翻出来说这块炖着香。我信了。</p>\n<p>回来炖了两个小时，中途加了一次水，收汁时在看手机，闻到味道不对，锅底已经糊了。舀掉底下那层，上面还能吃，只是有一点点苦。糊锅刷了二十分钟。下次打算定个闹钟，四十分钟响一次。</p>\n","date_published":"2026-03-01T05:05:00.000Z","date_modified":"2026-10-19T07:16:25.000Z"},{"id":"https://uua.me/p/node-sqlite-one-year/","url":"https://uua.me/p/node-sqlite-one-year/","title":"node:sqlite 用了半年，够用","summary":"这个站的数据层是 node:sqlite ，Node 22 起内置的那个。选它不是因为性能，是因为 不想再装原生模块 。 之前用 better-sqlite3 ，好用，但每次换 Node 版本都要重装一遍： node-gyp 、Python、Xcode Command Line Tools，缺一个就报一屏红字。有次在旧笔记本上救急，为了改一篇文章装了二十分钟环境。后来看到 22.5 加了 node:sqlite ，就换过来了。 换过来要改的东西 几乎没有。API 形似： 三个不同： 新版 Node 不需要 --experimental-sqlite ，但…","content_html":"<p>这个站的数据层是 <code>node:sqlite</code>，Node 22 起内置的那个。选它不是因为性能，是因为<strong>不想再装原生模块</strong>。</p>\n<p>之前用 <code>better-sqlite3</code>，好用，但每次换 Node 版本都要重装一遍：<code>node-gyp</code>、Python、Xcode Command Line Tools，缺一个就报一屏红字。有次在旧笔记本上救急，为了改一篇文章装了二十分钟环境。后来看到 22.5 加了 <code>node:sqlite</code>，就换过来了。</p>\n<h2 id=\"h-6a9jv0\">换过来要改的东西</h2>\n<p>几乎没有。API 形似：</p>\n<pre><code class=\"hljs language-js\"><span class=\"hljs-keyword\">import</span> { <span class=\"hljs-title class_\">DatabaseSync</span> } <span class=\"hljs-keyword\">from</span> <span class=\"hljs-string\">&#x27;node:sqlite&#x27;</span>;\n<span class=\"hljs-keyword\">const</span> db = <span class=\"hljs-keyword\">new</span> <span class=\"hljs-title class_\">DatabaseSync</span>(<span class=\"hljs-string\">&#x27;data/blog.db&#x27;</span>);\ndb.<span class=\"hljs-title function_\">prepare</span>(<span class=\"hljs-string\">&#x27;SELECT * FROM posts WHERE slug = ?&#x27;</span>).<span class=\"hljs-title function_\">get</span>(<span class=\"hljs-string\">&#x27;hello&#x27;</span>);\n</code></pre>\n<p>三个不同：</p>\n<ul>\n<li>新版 Node 不需要 <code>--experimental-sqlite</code>，但每次启动会打一行 <code>ExperimentalWarning</code>。用 <code>--no-warnings</code> 压掉，或者忍着。</li>\n<li><code>run()</code> 返回 <code>{ changes, lastInsertRowid }</code>，<code>lastInsertRowid</code> 可能是 BigInt。我一开始直接拿它当数组下标用，排查了十分钟，现在一律 <code>Number(r.lastInsertRowid)</code>。</li>\n<li>查出来的行是 <strong>null-prototype 对象</strong>。<code>row.hasOwnProperty(...)</code> 会报错，<code>{...row}</code> 才有原型。这个坑不报错、只是偶尔行为诡异。</li>\n</ul>\n<h2 id=\"h-1oqstri\">连接参数</h2>\n<p>打开连接时按顺序设：</p>\n<pre><code class=\"hljs language-sql\">PRAGMA journal_mode <span class=\"hljs-operator\">=</span> WAL;      <span class=\"hljs-comment\">-- 读写不互相阻塞</span>\nPRAGMA foreign_keys <span class=\"hljs-operator\">=</span> <span class=\"hljs-keyword\">ON</span>;       <span class=\"hljs-comment\">-- 默认是关的，评论级联删除靠它</span>\nPRAGMA busy_timeout <span class=\"hljs-operator\">=</span> <span class=\"hljs-number\">5000</span>;     <span class=\"hljs-comment\">-- 别人占着写锁时等 5 秒再报错</span>\nPRAGMA synchronous <span class=\"hljs-operator\">=</span> <span class=\"hljs-keyword\">FULL</span>;      <span class=\"hljs-comment\">-- 提交时持久化 WAL，配合文件恢复日志</span>\nPRAGMA mmap_size <span class=\"hljs-operator\">=</span> <span class=\"hljs-number\">268435456</span>;   <span class=\"hljs-comment\">-- 256MB</span>\nPRAGMA cache_size <span class=\"hljs-operator\">=</span> <span class=\"hljs-number\">-16000</span>;     <span class=\"hljs-comment\">-- 负数 = KiB，也就是 16MB</span>\n</code></pre>\n<p>这里采用 FULL：后台保存横跨 Markdown 文件和 SQLite，数据库提交须先持久化，才能清理文件恢复日志。文件与目录也执行 fsync；设备故障下仍不能承诺绝对无损。</p>\n<p><code>foreign_keys</code> 默认关闭这件事值得单独说：我的评论表 <code>parent_id</code> 是自引用外键，靠 <code>ON DELETE CASCADE</code> 删一条根评论带走整棵子树。如果忘了开，删记录会留下孤儿行，页面上就是一堆“回复了不存在的父评论”。</p>\n<h2 id=\"h-xo71tz\">热备份用 VACUUM INTO</h2>\n<p>在线备份不需要停服务：</p>\n<pre><code class=\"hljs language-sql\">VACUUM <span class=\"hljs-keyword\">INTO</span> <span class=\"hljs-string\">&#x27;data/backups/tmp.db&#x27;</span>;\n</code></pre>\n<p>它输出一个<strong>一致</strong>的快照，不是拷文件——WAL 模式下直接 <code>cp blog.db</code> 会得到缺了一半事务的库。</p>\n<p>备份完我还会检查两件事，确认这期间没人写过：</p>\n<pre><code class=\"hljs language-sql\">PRAGMA data_version;\n<span class=\"hljs-keyword\">SELECT</span> total_changes();\n</code></pre>\n<p><code>data_version</code> 只在<strong>别的连接</strong>提交后变化，所以它正好是“我这边没动，但别人动了”的探针。配合比对内容目录的文件清单（大小 + mtime），一旦对不上就丢弃这次备份重来。宁可多跑一次，也不要留下一个看着能用、其实半新半旧的归档。</p>\n<h2 id=\"h-1hsdxi9\">性能</h2>\n<p>本站规模：几十篇文章、几百条评论、单进程。实测每页 3—5 次查询，每次 &lt; 0.5ms，整页渲染 2—4ms。这个量级上，讨论 SQLite 的并发能力是浪费时间。</p>\n<p>真正省心的是部署：数据库就是一个文件，跟着内容一起打包备份，换机器就拷过去。整个站没有任何需要单独启动的东西。</p>\n<h2 id=\"h-lfdpdn\">什么时候不该用它</h2>\n<ul>\n<li>要多个进程并发写 —— SQLite 能写，但你会开始和 <code>SQLITE_BUSY</code> 打交道，收益不值。</li>\n<li>团队里有人习惯用图形客户端连生产库 —— 单文件容易被拖走。</li>\n<li>需要 <code>ALTER TABLE</code> 频繁改结构 —— 它支持，但大批量改表还是比 Postgres 难受。</li>\n</ul>\n<p>个人站点不是这些场景。半年下来没出过数据问题，也没再装过一次编译工具链。</p>\n","date_published":"2026-02-08T13:40:00.000Z","date_modified":"2026-10-18T04:25:39.000Z"},{"id":"https://uua.me/n/bike-chain/","url":"https://uua.me/n/bike-chain/","title":"链条上油","summary":"自行车链条骑起来开始有“咯咯”的声音，尤其上坡发力那一下。上油比想的麻烦：先用旧 T 恤剪的布擦干净，擦下来一层黑油泥；再一节一节转着滴，等它渗进去，最后把表面多余的擦掉——不擦会粘灰，比不上油还惨。链条油十二块。 骑了一圈，声音小了很多，但没完全消失，大概是链条本身拉长了。楼下修车摊的大叔下午三点才出摊，换链条二十块。我说能不能明天来，他说可以，但得下午，早上他要送孙子上学。","content_html":"<p>自行车链条骑起来开始有“咯咯”的声音，尤其上坡发力那一下。上油比想的麻烦：先用旧 T 恤剪的布擦干净，擦下来一层黑油泥；再一节一节转着滴，等它渗进去，最后把表面多余的擦掉——不擦会粘灰，比不上油还惨。链条油十二块。</p>\n<p>骑了一圈，声音小了很多，但没完全消失，大概是链条本身拉长了。楼下修车摊的大叔下午三点才出摊，换链条二十块。我说能不能明天来，他说可以，但得下午，早上他要送孙子上学。</p>\n","date_published":"2026-01-11T09:25:00.000Z","date_modified":"2026-10-19T07:16:25.000Z"},{"id":"https://uua.me/n/balcony-plants/","url":"https://uua.me/n/balcony-plants/","title":"阳台上的三盆","summary":"阳台上剩三盆：薄荷、绿萝、一盆不知道名字的多肉。薄荷最省事，一周浇两次水就疯长，剪下来的叶子冲热水就是薄荷茶，缺点是招虫。绿萝买回来是水培的，我嫌瓶子难看换成土培，根烂了一半，现在三片叶子黄了两片。 多肉已经走了——那阵子我天天浇水，等明白“宁干勿湿”，它已经从底部开始化水。不打算再买了。三盆足够让我意识到：我照顾植物的水平，大概等于我照顾冰箱里蔬菜的水平。","content_html":"<p>阳台上剩三盆：薄荷、绿萝、一盆不知道名字的多肉。薄荷最省事，一周浇两次水就疯长，剪下来的叶子冲热水就是薄荷茶，缺点是招虫。绿萝买回来是水培的，我嫌瓶子难看换成土培，根烂了一半，现在三片叶子黄了两片。</p>\n<p>多肉已经走了——那阵子我天天浇水，等明白“宁干勿湿”，它已经从底部开始化水。不打算再买了。三盆足够让我意识到：我照顾植物的水平，大概等于我照顾冰箱里蔬菜的水平。</p>\n","date_published":"2025-12-07T07:40:00.000Z","date_modified":"2026-10-19T07:16:25.000Z"},{"id":"https://uua.me/n/night-run/","url":"https://uua.me/n/night-run/","title":"夜跑五公里","summary":"沿河堤跑到桥下再折回来，正好五公里出头。路灯每三十米一盏，跑到灯与灯中间会暗一下，像喘气。跑到桥中间会停一下。不是为了歇，是河对岸的楼亮着一半，光在水面上被拉成一条一条，跑过去就看不到了。配速总是从这里开始掉，最后八百米基本是挪，手机报数的时候我已经在走了。 鞋后跟外侧磨得比内侧快。看别人的跑鞋磨损图，说这姿势偏后。我没打算改，改了也不会跑得更快。回来路上买了三个橘子，八块四，洗完澡躺下腿还抖，但睡得很沉。","content_html":"<p>沿河堤跑到桥下再折回来，正好五公里出头。路灯每三十米一盏，跑到灯与灯中间会暗一下，像喘气。跑到桥中间会停一下。不是为了歇，是河对岸的楼亮着一半，光在水面上被拉成一条一条，跑过去就看不到了。配速总是从这里开始掉，最后八百米基本是挪，手机报数的时候我已经在走了。</p>\n<p>鞋后跟外侧磨得比内侧快。看别人的跑鞋磨损图，说这姿势偏后。我没打算改，改了也不会跑得更快。回来路上买了三个橘子，八块四，洗完澡躺下腿还抖，但睡得很沉。</p>\n","date_published":"2025-11-16T13:15:00.000Z","date_modified":"2026-10-19T07:16:25.000Z"}]}