<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://jiang1997.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jiang1997.github.io/" rel="alternate" type="text/html" /><updated>2026-05-13T07:27:56+08:00</updated><id>https://jiang1997.github.io/feed.xml</id><title type="html">Jiang’s Blog</title><subtitle>Personal blog. Notes on software, projects, and whatever else.</subtitle><author><name>jiang1997</name></author><entry><title type="html">从一次 quickjs PR 看 ES 规范：赋值时，extensible 检查发生在哪一步？</title><link href="https://jiang1997.github.io/programming/2026/05/13/es-spec-extensible-check.html" rel="alternate" type="text/html" title="从一次 quickjs PR 看 ES 规范：赋值时，extensible 检查发生在哪一步？" /><published>2026-05-13T00:00:00+08:00</published><updated>2026-05-13T00:00:00+08:00</updated><id>https://jiang1997.github.io/programming/2026/05/13/es-spec-extensible-check</id><content type="html" xml:base="https://jiang1997.github.io/programming/2026/05/13/es-spec-extensible-check.html"><![CDATA[<blockquote>
  <p>最近 quickjs-ng 的一个 <a href="https://github.com/quickjs-ng/quickjs/commit/a7f0b8a4c294e15c69e67dae55b2ad2e2e3e6f76">PR</a> 修复了一个属性赋值相关的 bug。排查过程中涉及到 ES 规范里 <a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinary-object-internal-methods-and-internal-slots-set-p-v-receiver"><code class="language-plaintext highlighter-rouge">[[Set]]</code></a> 的一条关键分支——<strong>“修改已有属性”和”创建新属性”走的是两条不同的路径</strong>。本文借这个 PR 的由头，聊聊规范里 extensible 检查到底发生在什么时候。</p>
</blockquote>

<hr />

<h2 id="一个常见的失败场景">一个常见的失败场景</h2>

<p>先看一段简单的代码：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">use strict</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">var</span> <span class="nx">obj</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nb">Object</span><span class="p">.</span><span class="nx">preventExtensions</span><span class="p">(</span><span class="nx">obj</span><span class="p">);</span>

<span class="nx">obj</span><span class="p">.</span><span class="nx">x</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">// TypeError</span>
</code></pre></div></div>

<p>对象被 <code class="language-plaintext highlighter-rouge">preventExtensions</code> 之后就不能再添加新属性了。规范是如何定义这个行为的呢？我们顺着 ES 规范的内部流程走一遍，就知道<strong>“不可扩展”这个检查到底发生在哪一步</strong>了。</p>

<h2 id="规范里的执行流程">规范里的执行流程</h2>

<p>当我们写 <code class="language-plaintext highlighter-rouge">obj.x = 1</code> 时，规范里会依次进入这些”过程”（你可以把它们理解为引擎内部的函数调用链）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PutValue(lref, 1)
  ↓
baseObj.[[Set]]("x", 1, baseObj)
  ↓
OrdinarySet(baseObj, "x", 1, baseObj)
  ↓
  // baseObj 上没有 "x"（ownDesc = undefined）
  ↓
OrdinarySetWithOwnDescriptor(baseObj, "x", 1, baseObj, undefined)
  ↓
  // ownDesc is undefined，查原型链
  ↓
Object.prototype.[[Set]]("x", 1, baseObj)
  ↓
OrdinarySet(Object.prototype, "x", 1, baseObj)
  ↓
OrdinarySetWithOwnDescriptor(Object.prototype, "x", 1, baseObj, undefined)
  ↓
  // Object.prototype 上也没有 "x"（ownDesc = undefined）
  // 原型链到头（parent is null）
  // ownDesc 被设为默认值 { [[Value]]: undefined, [[Writable]]: true, ... }
  ↓
  // IsDataDescriptor(ownDesc) is true
  // ownDesc.[[Writable]] is true
  // Receiver（baseObj）是 Object
  ↓
existingDescriptor = Receiver.[[GetOwnProperty]]("x")  // undefined
  ↓
  // Receiver 上无此属性 → 走"创建新属性"分支
  ↓
CreateDataProperty(baseObj, "x", 1)
  ↓
baseObj.[[DefineOwnProperty]]("x", { [[Value]]: 1 })
  ↓
OrdinaryDefineOwnProperty(baseObj, "x", desc)
  ↓
  // 先查属性是否存在 → current = undefined
  // 再查是否 extensible → false
  ↓
ValidateAndApplyPropertyDescriptor(baseObj, "x", false, desc, undefined)
  ↓
  // current is undefined，extensible is false
  // 规范：如果 !extensible 且 current 为 undefined，return false
  ↓
return false
  ↓
一路返回到 PutValue
  ↓
如果赋值语句处于严格模式 → throw TypeError
</code></pre></div></div>

<p>关键一步在 <a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-validateandapplypropertydescriptor"><code class="language-plaintext highlighter-rouge">ValidateAndApplyPropertyDescriptor</code></a>。这个抽象操作在<strong>创建新属性</strong>时会检查对象是否 extensible：如果不是，且属性还不存在，就直接返回 <code class="language-plaintext highlighter-rouge">false</code>。这个 <code class="language-plaintext highlighter-rouge">false</code> 一路传回 <code class="language-plaintext highlighter-rouge">PutValue</code>，最终根据是否严格模式决定是否抛错。</p>

<p><strong>简单说：给不可扩展的对象添加新属性，规范在 ValidateAndApplyPropertyDescriptor 这一步拦住了。</strong></p>

<hr />

<h2 id="那修改已有属性呢">那”修改已有属性”呢？</h2>

<p>现在来看 PR 相关的场景。它要表达的核心问题是：<strong>如果我不是创建新属性，而是修改一个已经存在的属性，还会检查 extensible 吗？</strong></p>

<p>我们把 PR 里的测试改写成一个更常见的版本：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">parent</span> <span class="o">=</span> <span class="p">{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">1</span> <span class="p">};</span>
<span class="kd">var</span> <span class="nx">child</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">create</span><span class="p">(</span><span class="nx">parent</span><span class="p">);</span>
<span class="nx">child</span><span class="p">.</span><span class="nx">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>                      <span class="c1">// child 上已有自己的属性 x</span>
<span class="nb">Object</span><span class="p">.</span><span class="nx">preventExtensions</span><span class="p">(</span><span class="nx">child</span><span class="p">);</span>  <span class="c1">// 让 child 不可扩展</span>

<span class="nx">child</span><span class="p">.</span><span class="nx">x</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>                      <span class="c1">// 修改已有的 x，能成功吗？</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">child</span><span class="p">.</span><span class="nx">x</span><span class="p">);</span>             <span class="c1">// ?</span>
</code></pre></div></div>

<p>结果是：<strong>能成功</strong>，输出 <code class="language-plaintext highlighter-rouge">2</code>。</p>

<p>奇怪，<code class="language-plaintext highlighter-rouge">child</code> 明明不可扩展，为什么赋值还能成功？我们再走一遍规范流程，看看这次走的是哪条分支。</p>

<h2 id="修改已有属性的规范流程">修改已有属性的规范流程</h2>

<p><code class="language-plaintext highlighter-rouge">child.x = 2</code> 进入规范后，同样走到 <a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinarysetwithowndescriptor"><code class="language-plaintext highlighter-rouge">OrdinarySetWithOwnDescriptor</code></a>。但这一次和前面的关键区别是——<strong>receiver（也就是 <code class="language-plaintext highlighter-rouge">child</code>）上已经有属性 <code class="language-plaintext highlighter-rouge">x</code> 了</strong>。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PutValue(lref, 2)
  ↓
child.[[Set]]("x", 2, child)
  ↓
OrdinarySet(child, "x", 2, child)
  ↓
OrdinarySetWithOwnDescriptor(child, "x", 2, child, ownDesc)
  ↓
  // IsDataDescriptor(ownDesc) is true
  // ownDesc.[[Writable]] is true
  // Receiver is Object
  ↓
existingDescriptor = Receiver.[[GetOwnProperty]]("x")
  ↓
  // existingDescriptor is NOT undefined！child 上已有 x
  ↓
  // existingDescriptor.[[Writable]] is true
  ↓
valueDesc = PropertyDescriptor { [[Value]]: 2 }
  ↓
Receiver.[[DefineOwnProperty]]("x", valueDesc)
  ↓
OrdinaryDefineOwnProperty(child, "x", valueDesc)
  ↓
ValidateAndApplyPropertyDescriptor(child, "x", false, valueDesc, current)
  ↓
  // current 不是 undefined（属性已存在），跳过 extensible 检查
  // 只检查 writable → true，允许修改
  // 更新 [[Value]] 为 2
  ↓
return true
  ↓
一路返回到 PutValue
  ↓
succeeded is true，赋值成功
</code></pre></div></div>

<p>注意这里 <a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-validateandapplypropertydescriptor"><code class="language-plaintext highlighter-rouge">ValidateAndApplyPropertyDescriptor</code></a> 的第 3 个参数 <code class="language-plaintext highlighter-rouge">extensible</code> 仍然是 <code class="language-plaintext highlighter-rouge">false</code>（因为 <code class="language-plaintext highlighter-rouge">preventExtensions</code>），但规范在这个分支的伪代码里写的是：</p>

<blockquote>
  <p>If <strong>current is undefined</strong>, then … if extensible is false, return false.</p>
</blockquote>

<p>也就是说，<strong>只有当属性不存在时，才看 extensible。属性已经存在的话，extensible 直接被忽略，只检查 <code class="language-plaintext highlighter-rouge">writable</code></strong>。</p>

<p>所以 <code class="language-plaintext highlighter-rouge">child</code> 虽然不可扩展，但因为 <code class="language-plaintext highlighter-rouge">x</code> 本来就存在，而且可写，规范允许直接修改它的值。返回 <code class="language-plaintext highlighter-rouge">true</code>，赋值成功。</p>

<hr />

<h2 id="关键的顺序问题为什么-quickjs-会出错">关键的顺序问题：为什么 quickjs 会出错</h2>

<p>我们稍微认真看一遍 <code class="language-plaintext highlighter-rouge">ValidateAndApplyPropertyDescriptor</code></p>

<p><code class="language-plaintext highlighter-rouge">ValidateAndApplyPropertyDescriptor</code> 的 <code class="language-plaintext highlighter-rouge">current</code> 参数是由它的调用方——<a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinarydefineownproperty"><code class="language-plaintext highlighter-rouge">OrdinaryDefineOwnProperty</code></a>——通过 <code class="language-plaintext highlighter-rouge">O.[[GetOwnProperty]](P)</code> 查询后传入的：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OrdinaryDefineOwnProperty(O, P, Desc):
  1. Let current be ? O.[[GetOwnProperty]](P).
  2. Let extensible be ? IsExtensible(O).
  3. Return ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current).
</code></pre></div></div>

<p>而 <code class="language-plaintext highlighter-rouge">ValidateAndApplyPropertyDescriptor</code> 收到 <code class="language-plaintext highlighter-rouge">current</code> 后，关键步骤：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Assert: extensible is true or false.
2. If current is undefined, then
   a. If extensible is false, return false.
   b. ...
   c. ...
   d. Else,
      i. Create an own data property named propertyKey ...
   e. Return true.
3. Assert: current is a fully populated Property Descriptor.
4. ...
5. ...
6. Return true.
</code></pre></div></div>

<p>注意步骤 1 和 2 的顺序：</p>

<ol>
  <li><strong>步骤 1（OrdinaryDefineOwnProperty 中）</strong>：先查询属性是否存在，得到 <code class="language-plaintext highlighter-rouge">current</code></li>
  <li><strong>步骤 2（ValidateAndApplyPropertyDescriptor 中）</strong>：只有在 <code class="language-plaintext highlighter-rouge">current is undefined</code>（属性不存在）时，才去检查 <code class="language-plaintext highlighter-rouge">extensible is false</code></li>
  <li><strong>步骤 3</strong>：如果属性存在，直接断言它是个完整的属性描述符，跳过 extensible 检查</li>
</ol>

<p>这个顺序非常关键：<strong>先判断”有没有”，再决定”查不查 extensible”</strong>。</p>

<p>而 quickjs 在这个 PR 之前的实现，顺序正好搞反了。它在入口处就先检查了 extensible，发现对象不可扩展就直接拦截，完全没去判断属性是否已经存在。于是遇到下面这种情况：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">proto</span> <span class="o">=</span> <span class="p">{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">1</span> <span class="p">};</span>
<span class="kd">var</span> <span class="nx">receiver</span> <span class="o">=</span> <span class="p">{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">0</span> <span class="p">};</span>
<span class="nb">Object</span><span class="p">.</span><span class="nx">preventExtensions</span><span class="p">(</span><span class="nx">receiver</span><span class="p">);</span>

<span class="nx">receiver</span><span class="p">.</span><span class="nx">x</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>  <span class="c1">// 规范：应该成功（已有属性可写）</span>
</code></pre></div></div>

<p>quickjs 错误地先把 “<code class="language-plaintext highlighter-rouge">receiver</code> 不可扩展” 当成了拒绝理由，抛出了 <code class="language-plaintext highlighter-rouge">TypeError</code>（或在 <code class="language-plaintext highlighter-rouge">Reflect.set</code> 场景下返回 <code class="language-plaintext highlighter-rouge">false</code>）。规范的正确行为是：虽然 <code class="language-plaintext highlighter-rouge">receiver</code> 不可扩展，但 <code class="language-plaintext highlighter-rouge">x</code> 本来就存在且可写，赋值应当成功。</p>

<p>这个 PR 本质上就是<strong>把检查的先后顺序调回到规范定义的样子</strong>。</p>

<hr />

<h2 id="小结">小结</h2>

<table>
  <thead>
    <tr>
      <th>场景</th>
      <th>属性是否存在</th>
      <th>规范走的分支</th>
      <th>是否检查 extensible</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">obj.x = 1</code>（obj 不可扩展，无此属性）</td>
      <td>不存在</td>
      <td><a href="https://tc39.es/ecma262/multipage/abstract-operations.html#sec-createdataproperty"><code class="language-plaintext highlighter-rouge">CreateDataProperty</code></a> → <a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-validateandapplypropertydescriptor"><code class="language-plaintext highlighter-rouge">ValidateAndApplyPropertyDescriptor</code></a></td>
      <td><strong>是</strong>，不存在时检查</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">child.x = 2</code>（child 不可扩展，但已有 x）</td>
      <td>存在</td>
      <td><a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinary-object-internal-methods-and-internal-slots-defineownproperty-p-desc"><code class="language-plaintext highlighter-rouge">[[DefineOwnProperty]]</code></a> → <a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-validateandapplypropertydescriptor"><code class="language-plaintext highlighter-rouge">ValidateAndApplyPropertyDescriptor</code></a></td>
      <td><strong>否</strong>，只检查 writable</td>
    </tr>
  </tbody>
</table>

<p>ES 规范对每个操作都定义了精确的流程。<code class="language-plaintext highlighter-rouge">extensible</code> 限制的只是<strong>新增属性</strong>，不是<strong>修改已有属性</strong>。更准确地说：规范在 <a href="https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-validateandapplypropertydescriptor"><code class="language-plaintext highlighter-rouge">ValidateAndApplyPropertyDescriptor</code></a> 中<strong>先判断属性是否存在，只在不存在的情况下才检查 extensible</strong>。把这个顺序搞反了，引擎就会错误地拦截合法的赋值操作。</p>]]></content><author><name>jiang1997</name></author><category term="programming" /><category term="javascript" /><category term="ecmascript" /><category term="quickjs" /><summary type="html"><![CDATA[最近 quickjs-ng 的一个 PR 修复了一个属性赋值相关的 bug。排查过程中涉及到 ES 规范里 [[Set]] 的一条关键分支——“修改已有属性”和”创建新属性”走的是两条不同的路径。本文借这个 PR 的由头，聊聊规范里 extensible 检查到底发生在什么时候。]]></summary></entry><entry><title type="html">从一个 VSCode 插件开始：我对认识需求的一点反思</title><link href="https://jiang1997.github.io/reflection/2026/05/05/vscode-plugin-and-understanding-user-needs.html" rel="alternate" type="text/html" title="从一个 VSCode 插件开始：我对认识需求的一点反思" /><published>2026-05-05T00:00:00+08:00</published><updated>2026-05-05T00:00:00+08:00</updated><id>https://jiang1997.github.io/reflection/2026/05/05/vscode-plugin-and-understanding-user-needs</id><content type="html" xml:base="https://jiang1997.github.io/reflection/2026/05/05/vscode-plugin-and-understanding-user-needs.html"><![CDATA[<p>平时用第三方模型或者中转服务来使用 Claude Code，所以专门备了几个脚本文件，通过 <code class="language-plaintext highlighter-rouge">source</code> 来执行对应的脚本，设置不同提供商的 URL 和 token 等其它环境变量。</p>

<p>最近有在 VSCode 里面用 Claude Code 插件。相比终端，GUI 交互是会更方便直观一些。这个插件支持在 VSCode 的 <code class="language-plaintext highlighter-rouge">settings.json</code> 里配置特定的字段，然后根据这些字段接入第三方模型。</p>

<p>但问题来了，每次都要在 VSCode 里直接编辑 <code class="language-plaintext highlighter-rouge">settings.json</code> 来切换 provider，太繁琐了。于是突发奇想：是不是可以写一个插件来自动修改 <code class="language-plaintext highlighter-rouge">settings.json</code>？于是花了一点时间 vibe 了一个插件。</p>

<p>个人觉得这个插件还是有一点点亮点的。比如它在首次运行的时候，如果你之前在 VSCode 的 <code class="language-plaintext highlighter-rouge">settings.json</code> 里配置过模型接入相关字段，它能解析出来并自动生成一个默认配置。此外，这个插件还支持从 bash 脚本片解析出配置。</p>

<p>初步搞完这个插件之后，把它分享到了 V2EX 上。收到的评论大概四五条，普遍认为这个插件的价值不高——这点确实没法否认。然后发现，大家使用 Claude Code 接入第三方模型的方式和我都不太一样。也有人提到 CC-Switch 这类已有工具，言下之意是这个 VSCode 插件其实意义不大。</p>

<p>于是在 v2ex 的帖子下简单解释了一下我的这个插件和 CC-Switch 的差异。比如说，我的插件不会有系统级别的影响，只在 VSCode 里面生效，从某种程度上来说会比较”干净”一些。如果你同时在终端和 VSCode 里使用 Claude Code，两者可以使用不同的 provider，因为它们的配置方式是独立的——这也算是一个小小的优点，虽然可能只是我个人比较小众的需求。</p>

<p>不过，V2EX 上这些热心网友的反馈也给了我一些反思的空间。确实，市面上已经存在一些 VSCode 插件或工具能够起到切换 Claude Code provider 的功能，虽然实现方式和我的不太一样，但对大部分人来说，这确实不是大家所在意的痛点。</p>

<p>现在在想，是不是可以继续做一个专门管理 <code class="language-plaintext highlighter-rouge">settings.json</code> 的插件，切换 Claude Code 可以只是这个插件的某个具体的使用场景 （有一个麻烦的点是，插件能够在 <code class="language-plaintext highlighter-rouge">settings.json</code> 修改的字段是需要提前申明）。这样的一个插件可能更有需求空间。</p>

<p>以上大概就是我这两天的一些心路历程，也是对 AI 辅助编程的一次探索，以及对于”如何发现需求、确定需求”的一点认识。</p>

<p>最后感谢 v2ex 的热心网友，促成了后续的一些思考。</p>

<p>PS: 以上内容为语音转录后， 人工 + deepseek-v4-pro 整理所得</p>

<hr />

<p>相关讨论：<a href="https://www.v2ex.com/t/1210275">https://www.v2ex.com/t/1210275</a></p>

<p>插件仓库：<a href="https://github.com/jiang1997/claude-code-profile-manager">https://github.com/jiang1997/claude-code-profile-manager</a></p>]]></content><author><name>jiang1997</name></author><category term="reflection" /><category term="vscode" /><category term="claude-code" /><summary type="html"><![CDATA[平时用第三方模型或者中转服务来使用 Claude Code，所以专门备了几个脚本文件，通过 source 来执行对应的脚本，设置不同提供商的 URL 和 token 等其它环境变量。]]></summary></entry><entry><title type="html">How to write a new post</title><link href="https://jiang1997.github.io/meta/2026/05/03/welcome-to-my-blog.html" rel="alternate" type="text/html" title="How to write a new post" /><published>2026-05-03T00:00:00+08:00</published><updated>2026-05-03T00:00:00+08:00</updated><id>https://jiang1997.github.io/meta/2026/05/03/welcome-to-my-blog</id><content type="html" xml:base="https://jiang1997.github.io/meta/2026/05/03/welcome-to-my-blog.html"><![CDATA[<p>The standard loop for adding a post to a Jekyll + Minima blog.</p>

<h2 id="the-4-step-loop">The 4-step loop</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Create the file. Name = date + URL slug.</span>
<span class="nv">$EDITOR</span> _posts/2026-05-04-thoughts-on-rust.markdown
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 2. Top of the file — YAML "frontmatter" between two --- lines:</span>
<span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span>  <span class="s2">"</span><span class="s">Thoughts</span><span class="nv"> </span><span class="s">on</span><span class="nv"> </span><span class="s">Rust"</span>
<span class="na">date</span><span class="pi">:</span>   <span class="s">2026-05-04 14:30:00 +0800</span>
<span class="na">categories</span><span class="pi">:</span> <span class="s">programming</span>
<span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">rust</span><span class="pi">,</span> <span class="nv">language-design</span><span class="pi">]</span>
<span class="nn">---</span>

<span class="s">The post body in **Markdown** starts here.</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 3. Save. With `jekyll serve --livereload` running on :4000,</span>
<span class="c">#    the site rebuilds in ~200 ms and the browser auto-refreshes.</span>

<span class="c"># 4. Ship it.</span>
git add _posts/2026-05-04-thoughts-on-rust.markdown
git commit <span class="nt">-m</span> <span class="s2">"Add post: Thoughts on Rust"</span>
git push
</code></pre></div></div>

<p>GitHub Pages picks up the push and rebuilds in 30–90 s.</p>

<blockquote>
  <p><strong>Things worth knowing</strong></p>

  <ul>
    <li><strong>The filename is load-bearing.</strong> <code class="language-plaintext highlighter-rouge">YYYY-MM-DD-slug.markdown</code> isn’t a convention — Jekyll <em>parses it</em>. The date becomes <code class="language-plaintext highlighter-rouge">page.date</code> (overridable via the frontmatter <code class="language-plaintext highlighter-rouge">date:</code> field), the slug becomes <code class="language-plaintext highlighter-rouge">page.slug</code> and the URL fragment. A file named <code class="language-plaintext highlighter-rouge">notes.md</code> in <code class="language-plaintext highlighter-rouge">_posts/</code> is silently skipped, no error.</li>
    <li><strong>Frontmatter is YAML, with all of YAML’s quirks.</strong> Quote titles containing <code class="language-plaintext highlighter-rouge">:</code> or starting with <code class="language-plaintext highlighter-rouge">[</code>, <code class="language-plaintext highlighter-rouge">&amp;</code>, <code class="language-plaintext highlighter-rouge">*</code>, <code class="language-plaintext highlighter-rouge">?</code>. The <code class="language-plaintext highlighter-rouge">date:</code> value needs the timezone offset, set globally via <code class="language-plaintext highlighter-rouge">_config.yml</code>’s <code class="language-plaintext highlighter-rouge">timezone:</code>. If parsing fails, Jekyll prints a quiet warning and skips the post.</li>
    <li><strong><code class="language-plaintext highlighter-rouge">layout: post</code></strong> wraps the body in Minima’s <code class="language-plaintext highlighter-rouge">_layouts/post.html</code> (title header, date, prev/next nav). Without it, you get raw content with no styling.</li>
  </ul>
</blockquote>

<h2 id="drafts-work-in-progress">Drafts (work in progress)</h2>

<p>Posts not yet ready to publish go in <code class="language-plaintext highlighter-rouge">_drafts/</code> instead of <code class="language-plaintext highlighter-rouge">_posts/</code>, and don’t need a date in the filename:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> _drafts
<span class="nv">$EDITOR</span> _drafts/half-baked-idea.markdown
</code></pre></div></div>

<p>Preview drafts locally — restart the server with <code class="language-plaintext highlighter-rouge">--drafts</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>jekyll serve <span class="nt">--livereload</span> <span class="nt">--drafts</span>
</code></pre></div></div>

<p>When ready, move and rename:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mv </span>_drafts/half-baked-idea.markdown _posts/2026-05-10-half-baked-idea.markdown
</code></pre></div></div>

<blockquote>
  <p>Drafts get <code class="language-plaintext highlighter-rouge">page.date</code> set to the file’s mtime, so they appear at the top of the local index until renamed. Without <code class="language-plaintext highlighter-rouge">--drafts</code>, Jekyll ignores <code class="language-plaintext highlighter-rouge">_drafts/</code> entirely — both locally and on GitHub Pages — so it’s safe to commit drafts as backups, though most people gitignore <code class="language-plaintext highlighter-rouge">_drafts/</code>.</p>
</blockquote>

<h2 id="images-and-other-assets">Images and other assets</h2>

<p>Drop them in any <strong>non-underscored</strong> directory. The convention is <code class="language-plaintext highlighter-rouge">assets/img/</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> assets/img
<span class="nb">cp</span> ~/Pictures/diagram.png assets/img/
</code></pre></div></div>

<p>Reference in a post:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">![</span><span class="nv">Architecture diagram</span><span class="p">](</span><span class="sx">{{</span> <span class="nn">"/assets/img/diagram.png"</span> | relative_url }})
</code></pre></div></div>

<blockquote>
  <p>The <code class="language-plaintext highlighter-rouge">relative_url</code> Liquid filter prepends <code class="language-plaintext highlighter-rouge">site.baseurl</code>. For a <em>user site</em> the baseurl is empty, so <code class="language-plaintext highlighter-rouge">/assets/img/diagram.png</code> resolves directly. But using the filter anyway means the same code works if the post is copied to a <em>project site</em> (where <code class="language-plaintext highlighter-rouge">baseurl</code> is <code class="language-plaintext highlighter-rouge">/repo-name</code>). Cheap insurance.</p>

  <p><strong>Underscored directories are reserved by Jekyll</strong> (<code class="language-plaintext highlighter-rouge">_posts</code>, <code class="language-plaintext highlighter-rouge">_drafts</code>, <code class="language-plaintext highlighter-rouge">_layouts</code>, <code class="language-plaintext highlighter-rouge">_includes</code>, <code class="language-plaintext highlighter-rouge">_data</code>, <code class="language-plaintext highlighter-rouge">_site</code>). Don’t create your own — they’ll be either consumed or excluded.</p>
</blockquote>

<h2 id="categories-vs-tags--the-practical-difference">Categories vs. tags — the practical difference</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Affects URL?</th>
      <th>Generates archive page?</th>
      <th>Typical use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">categories</code></td>
      <td><strong>Yes</strong> — prepended to permalink</td>
      <td>No (in Minima)</td>
      <td>High-level groupings: <em>programming</em>, <em>journal</em>, <em>meta</em></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tags</code></td>
      <td>No</td>
      <td>No (in Minima)</td>
      <td>Cross-cutting topics: <em>rust</em>, <em>kubernetes</em>, <em>book-review</em></td>
    </tr>
  </tbody>
</table>

<p>Because categories show in the URL, <strong>changing them later breaks links</strong>. Tags are cheap to revise.</p>

<p>To get clean URLs without category prefixes, override the permalink globally in <code class="language-plaintext highlighter-rouge">_config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">defaults</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">scope</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">type</span><span class="pi">:</span> <span class="nv">posts</span> <span class="pi">}</span>
    <span class="na">values</span><span class="pi">:</span>
      <span class="na">permalink</span><span class="pi">:</span> <span class="s">/:year/:month/:day/:title/</span>
</code></pre></div></div>

<p>Or per-post via frontmatter <code class="language-plaintext highlighter-rouge">permalink: /thoughts-on-rust/</code>.</p>

<h2 id="common-pitfalls">Common pitfalls</h2>

<table>
  <thead>
    <tr>
      <th>Symptom</th>
      <th>Cause</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Post doesn’t appear</td>
      <td>Filename missing <code class="language-plaintext highlighter-rouge">YYYY-MM-DD-</code> prefix, or date is in the future</td>
    </tr>
    <tr>
      <td>“Future” post hidden</td>
      <td>Add <code class="language-plaintext highlighter-rouge">--future</code> to <code class="language-plaintext highlighter-rouge">jekyll serve</code>, or move the date back</td>
    </tr>
    <tr>
      <td>Page renders without styling</td>
      <td>Missing <code class="language-plaintext highlighter-rouge">layout: post</code></td>
    </tr>
    <tr>
      <td>Build error: <code class="language-plaintext highlighter-rouge">did not find expected key</code></td>
      <td>YAML frontmatter has an unquoted <code class="language-plaintext highlighter-rouge">:</code> or stray indentation</td>
    </tr>
    <tr>
      <td>Site live but local doesn’t update</td>
      <td>LiveReload connection dropped — hard-refresh once</td>
    </tr>
    <tr>
      <td>Push succeeds but site stale</td>
      <td>Bad Liquid syntax silently fails the build; check the latest Pages build status with <code class="language-plaintext highlighter-rouge">gh api repos/&lt;owner&gt;/&lt;repo&gt;/pages/builds/latest</code></td>
    </tr>
  </tbody>
</table>

<h2 id="a-helper-if-posts-come-often">A helper, if posts come often</h2>

<p>Drop this in <code class="language-plaintext highlighter-rouge">~/.zshrc</code> (or <code class="language-plaintext highlighter-rouge">~/.bashrc</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>newpost<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span><span class="nv">slug</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">1</span>:?usage:<span class="p"> newpost slug-words [title]</span><span class="k">}</span><span class="s2">"</span>
  <span class="nb">local </span><span class="nv">title</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">2</span><span class="k">:-${</span><span class="nv">slug</span><span class="p">//-/ </span><span class="k">}}</span><span class="s2">"</span>
  <span class="nb">local date</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">date</span> +%Y-%m-%d<span class="si">)</span><span class="s2">"</span>
  <span class="nb">local </span><span class="nv">file</span><span class="o">=</span><span class="s2">"_posts/</span><span class="k">${</span><span class="nv">date</span><span class="k">}</span><span class="s2">-</span><span class="k">${</span><span class="nv">slug</span><span class="k">}</span><span class="s2">.markdown"</span>
  <span class="nb">cat</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="nv">$file</span><span class="s2">"</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
---
layout: post
title: "</span><span class="k">${</span><span class="nv">title</span><span class="k">}</span><span class="sh">"
date: </span><span class="si">$(</span><span class="nb">date</span> <span class="s1">'+%Y-%m-%d %H:%M:%S %z'</span><span class="si">)</span><span class="sh">
categories: meta
---
</span><span class="no">
EOF
</span>  <span class="k">${</span><span class="nv">EDITOR</span><span class="k">:-</span><span class="nv">vi</span><span class="k">}</span> <span class="s2">"</span><span class="nv">$file</span><span class="s2">"</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Then <code class="language-plaintext highlighter-rouge">newpost thoughts-on-rust "Thoughts on Rust"</code> scaffolds the file with the right date and timezone, and opens it in your editor.</p>]]></content><author><name>jiang1997</name></author><category term="meta" /><category term="jekyll" /><category term="workflow" /><summary type="html"><![CDATA[The standard loop for adding a post to a Jekyll + Minima blog.]]></summary></entry></feed>