<?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>Bblog</title>
    <link>https://www.alrgin.cn</link>
    <description>A personal technical blog.</description>
    <language>zh-CN</language>
    <lastBuildDate>Thu, 04 Sep 2025 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://www.alrgin.cn/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Welcome to Bblog</title>
      <link>https://www.alrgin.cn/blog/welcome-to-bblog</link>
      <guid isPermaLink="true">https://www.alrgin.cn/blog/welcome-to-bblog</guid>
      <description>A from-scratch, opinionated technical blog built with Next.js 14, Tailwind, shadcn/ui, and a local MDX pipeline. Here is what it is and why it exists.</description>
      <pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate>
      <category>meta</category>
      <category>nextjs</category>
      <content:encoded><![CDATA[<p>There are a hundred blog starters on the internet. Most of them are fine. Few of them feel like <em>yours</em>. This one is built so the code, the typography, and the color palette all argue for the same thing: <strong>sharp, calm, fast writing about software.</strong></p>
<p>This post is the orientation. It walks through the stack, the design language, and the things I deliberately left out.</p>
<h2>What this is</h2>
<p>Bblog is a statically generated personal blog. Pages are rendered at build time, shipped to the edge, and read like a printed essay — narrow column, generous line height, no sidebar shouting at you.</p>
<p>A few constraints shaped every decision:</p>
<ul>
<li>The reading column never exceeds 680px.</li>
<li>The accent color appears only where it earns it.</li>
<li>Every interaction is 300ms or faster.</li>
</ul>
<h2>The stack</h2>
<pre><code class="language-bash">npm install next react react-dom
npm install next-mdx-remote rehype-pretty-code shiki
npm install tailwindcss @tailwindcss/typography
</code></pre>
<p>The interesting part is not the list — it is how they fit together. The MDX pipeline is the subject of its own article, so I will keep this one short and point you there.</p>
<h2>What I left out</h2>
<blockquote>
<p>Restraint is a feature.</p>
</blockquote>
<ul>
<li>No comment system. Use the replies on the social link instead.</li>
<li>No analytics by default. Add the one line you trust, when you trust it.</li>
<li>No theme toggle button that flashes. The system preference is respected, instantly.</li>
</ul>
<p>That is the whole pitch. The rest is reading.</p>]]></content:encoded>
    </item>
    <item>
      <title>Shipping on the edge with Next.js 14</title>
      <link>https://www.alrgin.cn/blog/shipping-on-the-edge</link>
      <guid isPermaLink="true">https://www.alrgin.cn/blog/shipping-on-the-edge</guid>
      <description>How to think about static generation, generateStaticParams, and edge rendering so your blog is fast everywhere it is read.</description>
      <pubDate>Thu, 21 Aug 2025 00:00:00 GMT</pubDate>
      <category>nextjs</category>
      <category>performance</category>
      <category>ssg</category>
      <content:encoded><![CDATA[<p>A blog that renders fast is a blog that gets read. Next.js 14 gives you several ways to render, and the defaults are quietly excellent — but only if you understand which one you are using.</p>
<h2>Static by default</h2>
<p>Every route in the <code>app</code> directory is statically rendered unless you opt out. That is the right default for a blog. The one thing you must remember is to enumerate your paths.</p>
<pre><code class="language-tsx">


  return getPostSlugs().map((slug) => ({ slug }));
}


  // rendered at build time, one HTML file per slug
  return ;
}
</code></pre>
<p>The <code>generateStaticParams</code> call is what turns a dynamic route into a folder of static HTML. Forget it and you get on-demand rendering — fine for an app, wasteful for a blog.</p>
<h2>When to reach for the edge</h2>
<p>Static files already serve from the nearest CDN PoP. Edge rendering earns its keep when your page needs <em>a little</em> dynamism — a draft preview, a per-request theme, a footer with the current build hash.</p>
<pre><code class="language-ts">

</code></pre>
<p>Use it when the cost of going fully static is a stale page. Do not use it to feel modern.</p>
<h2>The checklist I run before every deploy</h2>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> <code>generateStaticParams</code> returns every slug.</li>
<li class="task-list-item"><input type="checkbox" disabled> No accidental <code>fetch</code> with dynamic cache on a static page.</li>
<li class="task-list-item"><input type="checkbox" disabled> Images are <code>.webp</code>, sized, and lazy below the fold.</li>
<li class="task-list-item"><input type="checkbox" disabled> Fonts use <code>next/font</code> or <code>display: swap</code>.</li>
</ul>
<p>Run that list and your Lighthouse score stops being a surprise.</p>]]></content:encoded>
    </item>
    <item>
      <title>A warm-gray design system</title>
      <link>https://www.alrgin.cn/blog/warm-gray-design-system</link>
      <guid isPermaLink="true">https://www.alrgin.cn/blog/warm-gray-design-system</guid>
      <description>The exact color and type tokens behind this site, and the reasoning for each one. Cream background, charcoal text, a single burgundy accent.</description>
      <pubDate>Wed, 09 Jul 2025 00:00:00 GMT</pubDate>
      <category>design</category>
      <category>tailwind</category>
      <category>design-system</category>
      <content:encoded><![CDATA[<p>A design system is just a small set of decisions you refuse to relitigate. Here are mine, and why.</p>
<h2>The palette</h2>
<p>The site runs on one background, one text color, and one accent. Everything else is a tint of those.</p>
<pre><code class="language-css">:root {
  --background: 40 33% 98%;   /* #FAF9F6 warm cream */
  --foreground: 240 3% 11%;   /* #1C1C1E charcoal  */
  --accent: 0 75% 60%;        /* #E54B4B burgundy  */
  --code-bg: 20 12% 15%;      /* #2A2421 warm dark  */
}
</code></pre>
<p>The accent is the strictest rule on the whole site. It appears on heading links, on hover, and nowhere else. The moment you paint a border with it, it stops meaning anything.</p>
<h3>Why warm, not neutral</h3>
<p>Pure neutral gray reads as cold and corporate. Add four degrees of hue and the page reads as paper. That single change is the difference between a dashboard and an essay.</p>
<h2>The type scale</h2>
<p>I keep exactly four sizes in regular use, and a display size for the hero.</p>
<table>
<thead>
<tr>
<th>Element</th>
<th>Size</th>
<th>Weight</th>
</tr>
</thead>
<tbody>
<tr>
<td>Body</td>
<td>17px</td>
<td>400</td>
</tr>
<tr>
<td>H3</td>
<td>20px</td>
<td>700</td>
</tr>
<tr>
<td>H2</td>
<td>26px</td>
<td>700</td>
</tr>
<tr>
<td>H1</td>
<td>44px</td>
<td>800</td>
</tr>
</tbody>
</table>
<h2>The 680px rule</h2>
<pre><code class="language-ts">maxWidth: {
  prose: '680px',
}
</code></pre>
<p>Beyond roughly 680px, the eye loses its place on the return sweep. A wider column does not hold more meaning — it only holds more fatigue.</p>]]></content:encoded>
    </item>
    <item>
      <title>A better MDX pipeline</title>
      <link>https://www.alrgin.cn/blog/a-better-mdx-pipeline</link>
      <guid isPermaLink="true">https://www.alrgin.cn/blog/a-better-mdx-pipeline</guid>
      <description>Replacing Contentlayer with next-mdx-remote-rsc and rehype-pretty-code, with line numbers, warm code blocks, and a synced table of contents.</description>
      <pubDate>Fri, 30 May 2025 00:00:00 GMT</pubDate>
      <category>mdx</category>
      <category>nextjs</category>
      <category>syntax-highlighting</category>
      <content:encoded><![CDATA[<p>Contentlayer is a lovely idea that stopped keeping up with the App Router. The good news is that the replacement is three packages and about twenty lines of config.</p>
<h2>The renderer</h2>
<p>The App Router is built on React Server Components, so the renderer has to be RSC-aware. <code>next-mdx-remote</code> ships an <code>/rsc</code> entrypoint for exactly that.</p>
<pre><code class="language-tsx">



  return (
    &#x3C;div className="prose-blog">
      
    &#x3C;/div>
  );
}
</code></pre>
<h2>Syntax highlighting without a runtime</h2>
<p><code>rehype-pretty-code</code> runs Shiki at build time. Zero JavaScript ships to the reader; the colors are baked in as inline styles. One setting does all the heavy lifting:</p>
<pre><code class="language-ts">const options = {
  theme: 'one-dark-pro',
  keepBackground: false, // we set our own #2A2421 in CSS
};
</code></pre>
<p>Setting <code>keepBackground</code> to <code>false</code> is the trick that lets a warm <code>#2A2421</code> background replace Shiki's default theme background, so code blocks belong to the page instead of fighting it.</p>
<h3>Line highlighting</h3>
<p>Lines are highlighted with the <code>{1,3-5}</code> meta syntax on the fence:</p>
<pre><code class="language-ts">function add(a: number, b: number) {
  return a + b;            // highlighted
}
const result = add(2, 3); // highlighted
console.log(result);
</code></pre>
<h2>A table of contents that matches</h2>
<p>The last piece is a TOC whose anchors line up with the rendered headings. The fix is to produce heading ids with the same slugger the renderer uses.</p>
<pre><code class="language-ts">

const slugger = new GithubSlugger();
// slug(text) === the id rehype-slug will produce
</code></pre>
<p>That is the whole system: read a file, strip frontmatter, render with one component, and the headings, anchors, and sidebar all agree.</p>]]></content:encoded>
    </item>
    <item>
      <title>Bilingual typography, done quietly</title>
      <link>https://www.alrgin.cn/blog/bilingual-typography</link>
      <guid isPermaLink="true">https://www.alrgin.cn/blog/bilingual-typography</guid>
      <description>Pairing Geist for Latin with Noto Serif SC for Chinese body text, and the three numbers that make a bilingual column readable.</description>
      <pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate>
      <category>typography</category>
      <category>i18n</category>
      <category>css</category>
      <content:encoded><![CDATA[<p>Most bilingual blogs fail in the same place: the two scripts look like they were never introduced to each other. The fix is mostly font pairing and three numbers.</p>
<h2>The pairing</h2>
<p>Latin text is set in Geist, a grotesk with a wide enough aperture to stay legible at 15px. Chinese body text is set in Noto Serif SC, a serif that carries weight without shouting. Headings switch to Noto Sans SC at 800 weight, where the strokes are dense enough to anchor a section.</p>
<pre><code class="language-css">--font-geist-sans: /* Geist, via next/font */
--font-noto-serif-sc: /* Noto Serif SC, body */
--font-noto-sans-sc: /* Noto Sans SC, headings */
</code></pre>
<h2>The three numbers</h2>
<p>A bilingual column becomes readable the moment these three are right.</p>
<pre><code class="language-css">body { line-height: 1.8; }
article { max-width: 680px; }
p { margin-block: 1.6em; }
</code></pre>
<ul>
<li><strong>1.8</strong> line height. CJK glyphs are taller and denser; 1.5 reads as cramped.</li>
<li><strong>680px</strong> column. Beyond it the return sweep breaks on both scripts.</li>
<li><strong>1.6em</strong> paragraph spacing. White space is how the eye knows a beat passed.</li>
</ul>
<h2>A paragraph in practice</h2>
<p>中英文混排时，最容易出现的问题是基线不齐与字重失衡。解决方法其实并不复杂：英文使用无衬线、中文正文使用衬线，再把行高拉到 1.8，把段落间距留足。剩下的，交给留白。</p>
<p>Read that paragraph next to the one above it. The two scripts are finally on speaking terms — not because they match, but because each is set to its own comfortable measure and given room to breathe.</p>
<h2>What to avoid</h2>
<ul>
<li>Forcing one font for both scripts. They will both look slightly wrong.</li>
<li>Sub-pixel line heights. Use unitless <code>1.8</code>, never <code>1.8em</code> on <code>line-height</code>.</li>
<li>Justifying text on the web. It creates rivers of whitespace in both languages.</li>
</ul>
<p>Get those right and the typography disappears, which is the whole point.</p>]]></content:encoded>
    </item>
    <item>
      <title>A performance budget you can keep</title>
      <link>https://www.alrgin.cn/blog/performance-budget</link>
      <guid isPermaLink="true">https://www.alrgin.cn/blog/performance-budget</guid>
      <description>A tiny, enforceable performance budget for a content site, with the three metrics that actually correlate with reader retention.</description>
      <pubDate>Tue, 18 Feb 2025 00:00:00 GMT</pubDate>
      <category>performance</category>
      <category>web-vitals</category>
      <category>metrics</category>
      <content:encoded><![CDATA[<p>A performance budget that nobody checks is a wish. A budget you can check in two seconds, in your head, while you ship, is a habit. Here is the second kind.</p>
<h2>The three numbers</h2>
<p>For a content site, almost everything that matters is captured by three metrics. Pick ceilings, write them down, and treat them as a contract.</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Ceiling</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>LCP</td>
<td>1.2s</td>
<td>The hero is the whole first impression.</td>
</tr>
<tr>
<td>CLS</td>
<td>0.05</td>
<td>A shifting column is an unreadable column.</td>
</tr>
<tr>
<td>TBT</td>
<td>150ms</td>
<td>Anything more and the page feels sticky.</td>
</tr>
</tbody>
</table>
<h2>Where the budget leaks</h2>
<p>Almost every budget violation on a blog comes from the same three sources: fonts, images, and third-party scripts.</p>
<pre><code class="language-ts">const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
  },
};
</code></pre>
<h3>Fonts</h3>
<p>Reserve the heavy weights for headings only. Body text in four weights is four round trips the reader never asked for.</p>
<h3>Images</h3>
<p>A <code>.webp</code> at the displayed width is cheaper than any responsive framework. The responsive framework exists to serve large images; if you do not have large images, you do not have the problem.</p>
<h2>Enforce it</h2>
<p>A budget you enforce at build time is the only kind that survives a deadline.</p>
<pre><code class="language-ts">const budget = { lcp: 1200, cls: 0.05, tbt: 150 };
const result = await measure(buildOutput);
for (const [k, ceiling] of Object.entries(budget)) {
  if (result[k] > ceiling) throw new Error(`budget breached: ${k}`);
}
</code></pre>
<p>Fail the build when a number crosses its ceiling. Everything else is optional. The reader never opted in to slow.</p>]]></content:encoded>
    </item>
  </channel>
</rss>