<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ethan · ikoobee.com</title><description>Ethan 的个人自留地：写代码、造工具、记录把想法变成产品的过程。</description><link>https://example.com/</link><item><title>ikoTheme 开发日志 #1：灯箱、公式与丝滑的页面过渡</title><link>https://example.com/posts/iko-theme-log-1/</link><guid isPermaLink="true">https://example.com/posts/iko-theme-log-1/</guid><description>v1.1 的四个新特性是怎么用零依赖、零常驻脚本的方式落地的——以及为什么块级公式必须独立成段。</description><pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;v1.0 发布之后，我对着同类主题的功能清单做了一次逐项对比，最后圈出四个「低成本、高感知」的候选：图片灯箱、LaTeX 公式、页面过渡、侧边栏配置化。v1.1 把它们全部落地了，这篇记录一下取舍。&lt;/p&gt;
&lt;h2&gt;灯箱：原生 &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; 就够了&lt;/h2&gt;
&lt;p&gt;市面上的灯箱方案几乎都要引库。但 2026 年了，原生 &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; 自带焦点管理、Esc 关闭和 &lt;code&gt;::backdrop&lt;/code&gt;，配上三十行样式就是一个完整灯箱：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;dialog id=&amp;quot;lightbox&amp;quot;&amp;gt;
  &amp;lt;img id=&amp;quot;lightboxImg&amp;quot; alt=&amp;quot;&amp;quot; /&amp;gt;
  &amp;lt;button id=&amp;quot;lightboxClose&amp;quot; aria-label=&amp;quot;关闭预览&amp;quot;&amp;gt;✕&amp;lt;/button&amp;gt;
&amp;lt;/dialog&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击文章图片时把 &lt;code&gt;currentSrc&lt;/code&gt; 灌进弹窗、&lt;code&gt;showModal()&lt;/code&gt; 一气呵成。图注直接取 &lt;code&gt;alt&lt;/code&gt; 文本，无障碍和懒加载都是白送的。&lt;/p&gt;
&lt;h2&gt;公式：构建期渲染，客户端零成本&lt;/h2&gt;
&lt;p&gt;数学公式走 remark-math + rehype-katex，在构建期就渲染成 HTML 和 CSS，浏览器不跑一行 JS。比如模拟退火的接受概率：&lt;/p&gt;
&lt;p&gt;$$
P(\text{accept}) = \exp\left(-\frac{\Delta E}{T}\right)
$$&lt;/p&gt;
&lt;p&gt;行内公式像 $\alpha = 0.95$ 这样写就行。踩过一个坑：&lt;code&gt;$$公式$$&lt;/code&gt; 写在同一行不会被识别为块级公式，&lt;strong&gt;独立成段&lt;/strong&gt;才是正确姿势——这个约定已经写进 README。&lt;/p&gt;
&lt;h2&gt;页面过渡：最难的不是动画&lt;/h2&gt;
&lt;p&gt;接入 Astro 的 &lt;code&gt;ClientRouter&lt;/code&gt; 只要一行，难的是让全站脚本在「换页不刷新」的世界里继续正常工作。重构原则很简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;常驻行为（主题切换、滚动监听）绑定在 &lt;code&gt;document&lt;/code&gt; 上，用事件委托，永远只绑一次；&lt;/li&gt;
&lt;li&gt;页面级初始化（筛选、打字机、评论区懒加载）全部挂到 &lt;code&gt;astro:page-load&lt;/code&gt;，每次换页重新执行；&lt;/li&gt;
&lt;li&gt;搜索弹窗和灯箱用 &lt;code&gt;transition:persist&lt;/code&gt; 跨页保活，状态不丢。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hero 的粒子动画加了 &lt;code&gt;isConnected&lt;/code&gt; 自检，页面切走后循环自动终止，不留僵尸 rAF。&lt;/p&gt;
&lt;h2&gt;结果&lt;/h2&gt;
&lt;p&gt;本地 Lighthouse 实测：首页性能 99 / 无障碍 92 / 最佳实践 100 / SEO 100，文章页性能满分。四项全过 CI 门禁的 90 分线，「零默认客户端 JS」的底线也守住了。&lt;/p&gt;
&lt;p&gt;下一个大项是 i18n（zh/en），体量够单独发一个版本。在那之前，先用这套主题把文章写起来。&lt;/p&gt;
</content:encoded></item><item><title>ikoTheme 开发日志 #0：为什么我要自己写一套 Astro 主题</title><link>https://example.com/posts/iko-theme-log-0/</link><guid isPermaLink="true">https://example.com/posts/iko-theme-log-0/</guid><description>从 vhAstro 二开到自己造轮子：聊聊我对个人博客的完整想象，以及这套主题到底要解决什么问题。</description><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;用 vhAstro 二开了大半年，我终于决定亲手写一套属于自己的 Astro 主题。这不是一时冲动——当第三次解决「上游更新与本地魔改的合并冲突」时，我就知道该换个思路了。&lt;/p&gt;
&lt;h2&gt;为什么不是继续改 vhAstro&lt;/h2&gt;
&lt;p&gt;vhAstro 是一套完成度很高的主题，视觉到现在我依然喜欢。但「二开」天生有一个结构性问题：&lt;strong&gt;你的每一处修改都是对上游的偏离&lt;/strong&gt;。偏离越多，升级越痛，最后主题被锁死在某个旧版本。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;项目展示是刚需&lt;/strong&gt;：我需要的不只是「文章博客」，而是文章 + 项目集的组合门面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;暗色模式是底线&lt;/strong&gt;：开发者读者一半时间在暗处&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型安全的内容模型&lt;/strong&gt;：frontmatter 写错字段应该在构建期报错，而不是渲染出空白页&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;造轮子的意义不在轮子本身，而在于你终于拥有一辆完全按自己路况调校的车。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;我想要的主题长什么样&lt;/h2&gt;
&lt;h3&gt;项目是一等公民&lt;/h3&gt;
&lt;p&gt;文章页之外，项目要有独立的集合与页面：状态徽章、技术栈、仓库与 Demo 链接。首页的瀑布流里，项目卡片与文章卡片同等露出。&lt;/p&gt;
&lt;h3&gt;性能是底线&lt;/h3&gt;
&lt;p&gt;目标是 Lighthouse 四项全 100：默认零客户端 JS、&lt;code&gt;astro:assets&lt;/code&gt; 管理图片、字体走系统栈起步，交互岛屿按需注水。&lt;/p&gt;
&lt;h3&gt;内容迁移零成本&lt;/h3&gt;
&lt;p&gt;Markdown + frontmatter 的内容组织方式保持不变，从 Hexo / vhAstro 迁移只需要一个脚本批量改写 frontmatter。&lt;/p&gt;
&lt;h2&gt;技术方案速览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;关注点&lt;/th&gt;
&lt;th&gt;选型&lt;/th&gt;
&lt;th&gt;理由&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;框架&lt;/td&gt;
&lt;td&gt;Astro 5&lt;/td&gt;
&lt;td&gt;内容集合 + 零默认 JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;样式&lt;/td&gt;
&lt;td&gt;Tailwind CSS 4&lt;/td&gt;
&lt;td&gt;原子化迭代快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索&lt;/td&gt;
&lt;td&gt;Pagefind&lt;/td&gt;
&lt;td&gt;构建期生成索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;评论&lt;/td&gt;
&lt;td&gt;giscus&lt;/td&gt;
&lt;td&gt;数据存 GitHub Discussions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;下一步&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;M1：仓库脚手架 + 内容集合 schema 定稿&lt;/li&gt;
&lt;li&gt;M2：七个页面全部落地&lt;/li&gt;
&lt;li&gt;M3：Pagefind 搜索、giscus 评论、RSS 全文输出&lt;/li&gt;
&lt;li&gt;M4：Lighthouse 全 100、双语 README、发布 v1.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个系列会随着开发持续更新——你正在读的，就是第一篇。&lt;/p&gt;
</content:encoded></item><item><title>Astro 5 内容集合实战：从 Markdown 到类型安全的站点</title><link>https://example.com/posts/astro5-collections/</link><guid isPermaLink="true">https://example.com/posts/astro5-collections/</guid><description>Content Layer API 入门：定义 schema、glob 加载器、类型推导，以及迁移路上踩过的坑。</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;内容集合（Content Collections）是 Astro 最被低估的能力：它让你的 Markdown 不再是「一堆字符串」，而是一组构建期校验过的数据。&lt;/p&gt;
&lt;h2&gt;定义集合与 schema&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;src/content.config.ts&lt;/code&gt; 里用 zod 声明每个集合的形状：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { defineCollection, z } from &amp;quot;astro:content&amp;quot;;
import { glob } from &amp;quot;astro/loaders&amp;quot;;

const posts = defineCollection({
  loader: glob({ pattern: &amp;quot;**/*.md&amp;quot;, base: &amp;quot;./src/content/posts&amp;quot; }),
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
    category: z.enum([&amp;quot;Code&amp;quot;, &amp;quot;Tutorial&amp;quot;, &amp;quot;Essay&amp;quot;, &amp;quot;Daily&amp;quot;]),
    tags: z.array(z.string()).default([]),
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写错字段名或日期格式，构建直接失败并指出是哪个文件——这在二开主题时代是想都不敢想的体验。&lt;/p&gt;
&lt;h2&gt;类型推导直达页面&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import { getCollection } from &amp;quot;astro:content&amp;quot;;
const posts = await getCollection(&amp;quot;posts&amp;quot;, (p) =&amp;gt; !p.data.draft);
---
{posts.map((p) =&amp;gt; &amp;lt;h2&amp;gt;{p.data.title}&amp;lt;/h2&amp;gt;)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑器里 &lt;code&gt;p.data.&lt;/code&gt; 的每一次补全，都来自你的 schema。&lt;/p&gt;
&lt;h2&gt;踩过的坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;z.coerce.date()&lt;/code&gt; 接受字符串日期，但时区按 UTC 解析，展示时注意格式化&lt;/li&gt;
&lt;li&gt;glob 加载器的 &lt;code&gt;base&lt;/code&gt; 是相对项目根目录，不是相对配置文件&lt;/li&gt;
&lt;li&gt;frontmatter-only 的 &lt;code&gt;.md&lt;/code&gt;（没有正文）也是合法条目，&lt;code&gt;entry.body&lt;/code&gt; 为 &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;内容集合换来的不是「又一个 API」，而是整条内容流水线的类型安全。主题作者把 schema 定义好，使用者写内容时就被保护了。&lt;/p&gt;
</content:encoded></item><item><title>爬山算法 × 模拟退火：教室排座工具的寻优之路</title><link>https://example.com/posts/seating-algo/</link><guid isPermaLink="true">https://example.com/posts/seating-algo/</guid><description>如何把「想让全班都满意」翻译成目标函数？一个纯前端小工具里的算法工程实践。</description><pubDate>Sat, 15 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这个项目的起因是当班主任的朋友一句吐槽：「每次换座位都要权衡四十多个学生的关系，比写代码难多了」。我说：那就交给代码。&lt;/p&gt;
&lt;h2&gt;把愿望翻译成目标函数&lt;/h2&gt;
&lt;p&gt;寻优算法的第一步不是选算法，而是定义「好」。我们把老师的直觉拆成可计算的规则，每条规则一个权重：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function score(seats, rules) {
  let total = 0;
  for (const [a, b, w] of rules.pairs) {
    if (isNeighbor(seats, a, b)) total += w; // 想同桌 +w
  }
  return total - penalty(seats, rules.blocks);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;爬山与退火&lt;/h2&gt;
&lt;p&gt;先试了最朴素的爬山法：随机交换两个学生，分数变高就接受。实现只要十几行，收敛飞快——但总停在「还行」的方案上。&lt;/p&gt;
&lt;p&gt;模拟退火的洞察是：在高温阶段以概率接受更差的解，随温度下降，接受概率趋近于零。设新解与当前解的分数差为 $\Delta E$（越低越差），接受劣解的概率：&lt;/p&gt;
&lt;p&gt;$$
P(\text{accept}) = \exp\left(-\frac{\Delta E}{T}\right)
$$&lt;/p&gt;
&lt;p&gt;温度按几何日程衰减：$T_{k+1} = \alpha T_k$，本项目取 $\alpha = 0.95$、初始 $T_0 = 100$。同样是 3 秒预算，爬山法平均分 82，模拟退火 91，且方差明显更小。&lt;/p&gt;
&lt;h2&gt;工程上的取舍&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;决策&lt;/th&gt;
&lt;th&gt;选择&lt;/th&gt;
&lt;th&gt;代价&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;运行位置&lt;/td&gt;
&lt;td&gt;纯前端 WebWorker&lt;/td&gt;
&lt;td&gt;首次加载多一个文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据存储&lt;/td&gt;
&lt;td&gt;localStorage&lt;/td&gt;
&lt;td&gt;换设备需导出导入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可视化&lt;/td&gt;
&lt;td&gt;Canvas 逐帧重绘&lt;/td&gt;
&lt;td&gt;代码量更多&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;上线一学期后，朋友的反馈是：「它给出的方案不总是我心里的最优，但从来不会离谱。」——对算法工程来说，这就是好评。&lt;/p&gt;
</content:encoded></item><item><title>东京五日：一台相机和三卷胶片</title><link>https://example.com/posts/tokyo-film/</link><guid isPermaLink="true">https://example.com/posts/tokyo-film/</guid><description>出差顺带的五天假期，带着一台老胶片机在东京街头乱走，拍下了一些没有用途的照片。</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;出差顺带的五天假期，塞了一台老胶片机和三卷过期胶卷进包，在东京街头乱走。&lt;/p&gt;
&lt;p&gt;没有行程表，也没有必须打卡的店。谷中根津一带的坡道、傍晚的昭和通、凌晨四点的筑地外围——快门按下去的时候大多没在想「这张能不能发」，更多是「这个瞬间好像值得留」。&lt;/p&gt;
&lt;p&gt;三卷胶片冲出来一卷，另外两卷还在防潮箱里。等待的时间也是摄影的一部分。&lt;/p&gt;
&lt;h2&gt;一些没有用途的照片&lt;/h2&gt;
&lt;p&gt;胶片的好处是每按一次快门都有成本，于是你会开始认真看。这座城市值得认真看。&lt;/p&gt;
&lt;p&gt;回头把剩下两卷冲出来，再补一篇。&lt;/p&gt;
</content:encoded></item></channel></rss>