<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Blog — Mojtaba Norouzi</title>
    <link>https://mojtaba.tech/blog/</link>
    <description>Notes, essays and build logs on backend engineering, distributed systems, AI — and the occasional detour into Persian music.</description>
    <language>en</language>
    <lastBuildDate>Wed, 05 Aug 2026 00:00:00 GMT</lastBuildDate>
    <managingEditor>mojtaba.norouzie@gmail.com (Mojtaba Norouzi)</managingEditor>
    <atom:link href="https://mojtaba.tech/blog/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How this blog is built</title>
      <link>https://mojtaba.tech/blog/how-this-blog-is-built/</link>
      <guid isPermaLink="true">https://mojtaba.tech/blog/how-this-blog-is-built/</guid>
      <pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate>
      <description>A Markdown folder, one Node script, and no framework. The build log for the publishing system you are currently reading.</description>
      <content:encoded><![CDATA[<p>The rest of this site is three files: <code>index.html</code>, <code>styles.css</code>, <code>script.js</code>. No build
step, no framework, no <code>node_modules</code> in sight. I liked it that way, and I did not want
a blog to be the thing that dragged in a bundler.</p>
<p>So the constraint I set myself was: <strong>adding a post must not make the site heavier for
the reader, and must not make the repository harder to understand.</strong></p>
<h2 id="what-it-does">What it does<a class="heading-anchor" href="#what-it-does" aria-label="Permalink: What it does"><span aria-hidden="true">#</span></a></h2>
<p>Every post is a Markdown file in <code>content/blog/</code>. A Node script reads them at build
time and writes plain HTML into <code>blog/</code>. The browser receives no Markdown, no parser,
and no JavaScript that is not already on the homepage.</p>
<div class="code-block" data-lang="text"><pre dir="ltr" tabindex="0" role="region" aria-label="Code sample, text"><code class="language-text">content/blog/my-post.md   →   blog/my-post/index.html
                          →   blog/index.html
                          →   blog/tags/&lt;tag&gt;/index.html
                          →   blog/rss.xml, blog/feed.json
                          →   sitemap.xml, robots.txt</code></pre></div>
<p>The generated directory is gitignored. It is rebuilt in CI on every push, immediately
before the GitHub Pages artifact is uploaded — which is exactly how the résumé PDF on
the homepage already worked. I did not invent a pipeline; I added a step to one that
existed.</p>
<h2 id="frontmatter">Frontmatter<a class="heading-anchor" href="#frontmatter" aria-label="Permalink: Frontmatter"><span aria-hidden="true">#</span></a></h2>
<p>Each file opens with a small metadata block:</p>
<div class="code-block" data-lang="yaml"><pre dir="ltr" tabindex="0" role="region" aria-label="Code sample, yaml"><code class="language-yaml">---
title: Ordering is a local property
description: Message ordering is not something a broker hands you globally.
date: 2026-08-05
updated: 2026-08-09
type: article
tags: [Kafka, distributed-systems]
featured: true
draft: false
---</code></pre></div>
<p><code>type</code> is the interesting field. I write four different kinds of thing — long articles,
short notes, reactions to something I read, and build logs like this one — and my first
instinct was four separate systems. That was wrong. They differ in <em>presentation</em>, not
in structure: same title, same date, same body. One content model with a <code>type</code> field
does the whole job, and the templates decide that a note does not need a reading-time
badge.</p>
<h2 id="the-parts-worth-mentioning">The parts worth mentioning<a class="heading-anchor" href="#the-parts-worth-mentioning" aria-label="Permalink: The parts worth mentioning"><span aria-hidden="true">#</span></a></h2>
<p><strong>One dependency.</strong> <code>marked</code>, for Markdown. It has no transitive dependencies and it
runs only at build time. I parse the frontmatter myself — it is eight known keys, and
pulling a full YAML engine in to read eight known keys is not a trade I wanted.</p>
<p><strong>Headings become anchors.</strong> Every <code>##</code> gets a stable, Unicode-safe id and a permalink,
so you can link someone to the paragraph that matters instead of to the top of a
two-thousand-word page.</p>
<p><strong>Direction is per block, not per page.</strong> I write in English and in Persian, sometimes
in the same paragraph. The build tags each block with the direction that most of its
characters actually want, rather than trusting the first-strong-character rule that
<code>dir=&quot;auto&quot;</code> uses — which gets a Persian sentence wrong the moment it opens with an
English technical term.</p>
<p><strong>Drafts are not built.</strong> A post with <code>draft: true</code> produces no file at all. There is no
page to leak, no URL to guess, and nothing in the sitemap or feeds. Locally I run:</p>
<div class="code-block" data-lang="bash"><pre dir="ltr" tabindex="0" role="region" aria-label="Code sample, bash"><code class="language-bash">BLOG_DRAFTS=1 node tools/blog/build.mjs</code></pre></div>
<p>which is the only way a draft ever becomes HTML.</p>
<h2 id="what-i-deliberately-left-out">What I deliberately left out<a class="heading-anchor" href="#what-i-deliberately-left-out" aria-label="Permalink: What I deliberately left out"><span aria-hidden="true">#</span></a></h2>
<p>Syntax highlighting, for now. Every option costs either a large build-time grammar
bundle or client-side JavaScript, and monospaced code with real contrast and generous
line height reads fine. If I start posting more code than prose I will revisit it — the
hook is a single function in <code>tools/blog/markdown.mjs</code>.</p>
<p>Comments, analytics, a newsletter, and search. There are a handful of posts here. A
search box would be decoration.</p>
<p>Pagination. At some point a chronological index gets silly, but &quot;some point&quot; is a long
way from here, and building for it now would mean guessing at a problem I do not have.</p>
<h2 id="the-whole-publishing-workflow">The whole publishing workflow<a class="heading-anchor" href="#the-whole-publishing-workflow" aria-label="Permalink: The whole publishing workflow"><span aria-hidden="true">#</span></a></h2>
<div class="code-block" data-lang="bash"><pre dir="ltr" tabindex="0" role="region" aria-label="Code sample, bash"><code class="language-bash"># 1. write
$EDITOR content/blog/something-i-learned.md

# 2. preview
node tools/blog/build.mjs &amp;&amp; python -m http.server 8123

# 3. publish
git add content/blog/something-i-learned.md &amp;&amp; git commit &amp;&amp; git push</code></pre></div>
<p>Step three triggers CI, which rebuilds the site and the PDF and deploys. There is no
step four, and there is no admin panel to log into — which was the entire objective.</p>
]]></content:encoded>
      <category>meta</category>
      <category>static-sites</category>
      <category>tooling</category>
    </item>
    <item>
      <title>یادداشتی دربارهٔ idempotency</title>
      <link>https://mojtaba.tech/blog/fa-idempotency/</link>
      <guid isPermaLink="true">https://mojtaba.tech/blog/fa-idempotency/</guid>
      <pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate>
      <description>چرا در سیستم‌های پیام‌محور، «دوباره اجرا شدن» یک استثنا نیست و باید از ابتدا برایش طراحی کنیم.</description>
      <content:encoded><![CDATA[<p>هر سیستم پیام‌محوری که با تضمین at-least-once کار می‌کند، دیر یا زود یک پیام را دوبار
تحویل می‌دهد. این باگ نیست؛ بخشی از قرارداد است. اگر broker مطمئن نباشد که پیام را
پردازش کرده‌اید، دوباره می‌فرستد — و این دقیقاً همان رفتاری است که از او خواسته‌ایم.</p>
<p>مشکل وقتی جدی می‌شود که <code>handler</code> ما فرض کرده باشد هر پیام دقیقاً یک بار می‌آید. آن وقت
یک <code>retry</code> ساده می‌تواند موجودی انبار را دوبار کم کند یا برای یک سفارش دو بار فاکتور
بزند.</p>
<p>راه‌حل معمولاً پیچیده نیست. کافی است هر پیام شناسهٔ یکتا داشته باشد و پیش از اعمال،
بررسی کنیم که قبلاً آن را دیده‌ایم یا نه:</p>
<div class="code-block" data-lang="csharp"><pre dir="ltr" tabindex="0" role="region" aria-label="Code sample, csharp"><code class="language-csharp">if (await _processed.ExistsAsync(message.Id))
{
    return HandlerResult.Skip;
}

await using var tx = await _db.BeginTransactionAsync();
await _inventory.DecreaseAsync(message.Sku, message.Quantity);
await _processed.AddAsync(message.Id);
await tx.CommitAsync();</code></pre></div>
<p>نکتهٔ ظریف این است که ثبت شناسه و خودِ تغییر وضعیت باید در یک تراکنش انجام شوند. اگر
آن‌ها را جدا کنیم، باز هم به همان مشکل برمی‌گردیم — فقط با پنجرهٔ زمانی کوچک‌تری که
پیدا کردنش سخت‌تر است.</p>
<p dir="ltr">The short version in English: at-least-once delivery makes idempotency a requirement,
not an optimisation. Design for the duplicate on day one and you never have to hunt it
down at two in the morning.</p>
]]></content:encoded>
      <category>Kafka</category>
      <category>event-driven</category>
      <category>Persian</category>
    </item>
  </channel>
</rss>
