<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><id>https://oni.sqzer.com/blog/atom.xml</id><title>blog</title><subtitle>Personal research notes. blog.sqzer.com</subtitle><link rel="self" href="https://oni.sqzer.com/blog/atom.xml"/><updated>2026-11-12T17:14:11+09:00</updated><entry><id>urn:git:82a642367a47612cdb102b6a1554eb7172f470f6</id><title>Link the About page GitHub reference to oni</title><published>2026-11-12T17:14:11+09:00</published><updated>2026-11-12T17:14:11+09:00</updated><author><name>debuman</name><email>110329244+debuman@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/82a642367a47612cdb102b6a1554eb7172f470f6.html"/><content type="text">Link the About page GitHub reference to oni
</content></entry>
<entry><id>urn:git:c268631555139c3973f5530928bf2526c3134529</id><title>Link the GitHub footer to oni</title><published>2026-11-12T17:11:09+09:00</published><updated>2026-11-12T17:11:09+09:00</updated><author><name>debuman</name><email>110329244+debuman@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/c268631555139c3973f5530928bf2526c3134529.html"/><content type="text">Link the GitHub footer to oni
</content></entry>
<entry><id>urn:git:3db53f7fbf1ef917c1e99137b1e483cdabe9e72e</id><title>Update GitHub links and use sqzer.com as canonical domain</title><published>2026-11-12T11:32:26+09:00</published><updated>2026-11-12T11:32:26+09:00</updated><author><name>debuman</name><email>110329244+debuman@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/3db53f7fbf1ef917c1e99137b1e483cdabe9e72e.html"/><content type="text">Update GitHub links and use sqzer.com as canonical domain
</content></entry>
<entry><id>urn:git:0764ea44cdcf13b6db1daf94f4a84ad1781b66a1</id><title>Rename site to debuman and update bio</title><published>2026-11-12T11:12:20+09:00</published><updated>2026-11-12T11:12:20+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/0764ea44cdcf13b6db1daf94f4a84ad1781b66a1.html"/><content type="text">Rename site to debuman and update bio
</content></entry>
<entry><id>urn:git:b2bdb6159fd0ac3231c426a6c2daf9729915e9ba</id><title>delete image</title><published>2026-10-23T14:29:41+09:00</published><updated>2026-10-23T14:29:41+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/b2bdb6159fd0ac3231c426a6c2daf9729915e9ba.html"/><content type="text">delete image
</content></entry>
<entry><id>urn:git:c2999c6653292ecc246d7f90123b089873dd05a7</id><title>change file name</title><published>2026-10-23T14:26:44+09:00</published><updated>2026-10-23T14:26:44+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/c2999c6653292ecc246d7f90123b089873dd05a7.html"/><content type="text">change file name
</content></entry>
<entry><id>urn:git:cd9638173474145cb2fb5c66289a638fcc8c654a</id><title>add PHP2Shell (CVE-2026-109618 / CVE-2026-109619) Vulnerability Analysis &amp; Technical Report</title><published>2026-10-23T14:22:14+09:00</published><updated>2026-10-23T14:22:14+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/cd9638173474145cb2fb5c66289a638fcc8c654a.html"/><content type="text">add PHP2Shell (CVE-2026-109618 / CVE-2026-109619) Vulnerability Analysis &amp; Technical Report
</content></entry>
<entry><id>urn:git:30ad87499411011735a85d01713553ce0e0d496a</id><title>Add a CV link under the portrait on the about page</title><published>2026-10-19T15:06:43+09:00</published><updated>2026-10-19T15:06:43+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/30ad87499411011735a85d01713553ce0e0d496a.html"/><content type="text">Add a CV link under the portrait on the about page
</content></entry>
<entry><id>urn:git:78e059a97da4679890a9c78edabc63dea7030da4</id><title>Add a Goodreads reading list to the about page</title><published>2026-10-19T11:03:38+09:00</published><updated>2026-10-19T11:03:38+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/78e059a97da4679890a9c78edabc63dea7030da4.html"/><content type="text">Add a Goodreads reading list to the about page
</content></entry>
<entry><id>urn:git:8c2e541c9ca3ce1af5dc2a65449a833deb931f30</id><title>Publish the English versions of cognitive-dissonance-theory_kr.md, i-want-to-work-with-someone-who-brings-out-the-genuine-side-of-me_kr.md and resignation-review_kr.md, and add _KR to two Korean titles</title><published>2026-10-19T10:29:44+09:00</published><updated>2026-10-19T10:29:44+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/8c2e541c9ca3ce1af5dc2a65449a833deb931f30.html"/><content type="text">Publish the English versions of cognitive-dissonance-theory_kr.md, i-want-to-work-with-someone-who-brings-out-the-genuine-side-of-me_kr.md and resignation-review_kr.md, and add _KR to two Korean titles
</content></entry>
<entry><id>urn:git:a199544528885a966d09f4da48ee0c176a33cc8e</id><title>Change the titles of cognitive-dissonance-theory_kr.md and hitchhikers-guide-to-a-safe-and-free-internet_KR.md to English</title><published>2026-10-19T10:17:52+09:00</published><updated>2026-10-19T10:17:52+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/a199544528885a966d09f4da48ee0c176a33cc8e.html"/><content type="text">Change the titles of cognitive-dissonance-theory_kr.md and hitchhikers-guide-to-a-safe-and-free-internet_KR.md to English
</content></entry>
<entry><id>urn:git:74bca88e8cc457150133da33785be3092b0b0b1a</id><title>Rename 난나를진심으로만드는사람이랑일하고싶어.md, 인지부조화이론.md and 퇴사리뷰.md to English _kr file names and change two titles to English</title><published>2026-10-19T10:13:06+09:00</published><updated>2026-10-19T10:13:06+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/74bca88e8cc457150133da33785be3092b0b0b1a.html"/><content type="text">Rename 난나를진심으로만드는사람이랑일하고싶어.md, 인지부조화이론.md and 퇴사리뷰.md to English _kr file names and change two titles to English
</content></entry>
<entry><id>urn:git:1eb837e1cda8f2afc5460d8d1c8b8469156c2d84</id><title>Rename how-to-win-best-paper-ko.md to Methodology-for-Writing-a-Research-Paper_KR.md and change its title</title><published>2026-10-19T10:04:14+09:00</published><updated>2026-10-19T10:04:14+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/1eb837e1cda8f2afc5460d8d1c8b8469156c2d84.html"/><content type="text">Rename how-to-win-best-paper-ko.md to Methodology-for-Writing-a-Research-Paper_KR.md and change its title
</content></entry>
<entry><id>urn:git:8a74fb4231c8e812e238b78a7dbad728f569887e</id><title>Rename the Korean hitchhiker&#39;s guide post to hitchhikers-guide-to-a-safe-and-free-internet_KR.md and delete 서버리스환경맛보기수육.md</title><published>2026-10-19T09:59:38+09:00</published><updated>2026-10-19T09:59:38+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/8a74fb4231c8e812e238b78a7dbad728f569887e.html"/><content type="text">Rename the Korean hitchhiker&#39;s guide post to hitchhikers-guide-to-a-safe-and-free-internet_KR.md and delete 서버리스환경맛보기수육.md
</content></entry>
<entry><id>urn:git:3b70b1107a7a182e9a8f1031042928d0f28f4a82</id><title>Publish the English version of 안전하고자유로운인터넷을여행하는히치하이커를위한안내서.md</title><published>2026-10-19T09:46:27+09:00</published><updated>2026-10-19T09:46:27+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/3b70b1107a7a182e9a8f1031042928d0f28f4a82.html"/><content type="text">Publish the English version of 안전하고자유로운인터넷을여행하는히치하이커를위한안내서.md
</content></entry>
<entry><id>urn:git:c0333ee5a4e2b40b4956bf9e22dcca7106b1901f</id><title>update post</title><published>2026-10-06T23:26:28+09:00</published><updated>2026-10-06T23:26:28+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/c0333ee5a4e2b40b4956bf9e22dcca7106b1901f.html"/><content type="text">update post
</content></entry>
<entry><id>urn:git:35f35afbe47d1cd4750b0dbf87404be60cfe9475</id><title>Create Technology That Has Become a Religion.md</title><published>2026-10-06T23:21:10+09:00</published><updated>2026-10-06T23:21:10+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/35f35afbe47d1cd4750b0dbf87404be60cfe9475.html"/><content type="text">Create Technology That Has Become a Religion.md

new post
</content></entry>
<entry><id>urn:git:c6bae878d545bf040fdf4d91a68ff75cf91e5ccc</id><title>docs: make the article stylesheet&#39;s comments describe what ships</title><published>2026-09-25T21:26:40+09:00</published><updated>2026-09-25T21:26:40+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/c6bae878d545bf040fdf4d91a68ff75cf91e5ccc.html"/><content type="text">docs: make the article stylesheet&#39;s comments describe what ships

Comments only, in the &lt;style is:global&gt; block of the article template;
the built site is byte-identical.

- Stale figures re-measured in the built pages and corrected: the rail
  table now covers all 136 entries on the 14 posts with contents
  (clamped and documents rows unchanged, one-line row and segment
  widths updated), the 16px body replaces the 19px one in the Korean
  leading and staccato notes, the header is 59.70/59.88px tall, the
  --sp-80 comparison is about 138/133px, image, blockquote, &lt;em&gt; and
  staccato counts match the corpus, and two contrast ratios and a
  print size were recomputed.
- Claims that were false are fixed: prose links are coloured, not
  underlined; Lora has no U+21A9, so no subset can supply the backref
  arrow; &lt;mark&gt; does not use --ppc-wash; the forced-colours checkbox
  keeps its size and does not hang in the gutter; the hairline stops at
  the site footer; the section indexes&#39; &lt;h1&gt;s are visually hidden; whether an engine
  drops a stable scrollbar gutter from 100vw is no longer stated as a fact.
- References to things that no longer exist are gone: the hover-wipe
  rule, the &lt;span&gt; inside rail links, the header-padding override, the
  nav&#39;s current-item border, &quot;four&quot; rules reading the rail geometry.
- Notes that told the history of the file (&quot;used to&quot;, &quot;no longer&quot;,
  &quot;at the author&#39;s instruction&quot;, &quot;reported from a screenshot&quot;,
  &quot;whoever owns&quot;) now give the present-tense reason, and &quot;370 lines up&quot;
  and &quot;two rules down&quot; point at the blocks by name.
</content></entry>
<entry><id>urn:git:45cb039fdefe15e9818f2374064cee2909bfc784</id><title>docs: correct the post template&#39;s comments, and stop emitting them after &lt;/html&gt;</title><published>2026-09-25T21:25:06+09:00</published><updated>2026-09-25T21:25:06+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/45cb039fdefe15e9818f2374064cee2909bfc784.html"/><content type="text">docs: correct the post template&#39;s comments, and stop emitting them after &lt;/html&gt;

The two &lt;script&gt; blocks sat after &lt;/Base&gt;, so every post shipped them,
and the HTML comment above the stylesheet, after &lt;/html&gt;. The scripts
now close the &lt;Base&gt; slot and render inside &lt;main&gt;, just before
&lt;/main&gt;; module scripts are deferred, so they run at the same point.
The comment is a {/* */} comment and is no longer emitted. Each post
page differs only by that move, the other 46 files build identical,
and check-dist passes.

The comments above the stylesheet:
- Stale counts. 28 of the 30 posts declare lang=&quot;ko&quot;, not 21; the
  front matter has seven keys; We_need_to_talk paints 753 Hangul
  syllables; 20 of the 55 fenced blocks (52 plates, 3 diagrams)
  contain Hangul; the 7 uncaptioned plates are the fences with no
  language or plaintext; the tallest document is under 17,000px at
  1440px.
  Every number kept was recounted on the corpus or the built pages,
  and the ones that could not be (a 175px rail offset, 8 of 14 posts)
  are stated as the rule instead.
- Things that no longer exist: the &lt;span&gt; inside rail links, the
  header wordmark the byline was said to repeat (an article&#39;s header
  reads &quot;Writing&quot;; the owner&#39;s name is the copyright line), the
  six-entry gate and the genre test, `right: 50% + ...`.
- Notes on how the code came to be, rewritten as the present reason.
- The JSON-LD note had lost its escape sequence, and &quot;the line above&quot;
  now names the statement it means.
</content></entry>
<entry><id>urn:git:ab516d6a4f6338bd28a107eda58ab9fed62355a3</id><title>docs: bring the stylesheet comments in line with the code they sit beside</title><published>2026-09-25T21:24:19+09:00</published><updated>2026-09-25T21:24:19+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/ab516d6a4f6338bd28a107eda58ab9fed62355a3.html"/><content type="text">docs: bring the stylesheet comments in line with the code they sit beside

Comments only; the built site is byte-identical.

- Claims the code no longer matches. The link notes said prose links
  carry weight and an underline and that there is no hover rule; they
  are crimson at inherited weight with a colour step on hover. The
  header row is calc() against --fs-nav, not --fs-body (26.70px at a
  1440px window, a 59.70px bar). The code line-height is 1.5 x
  --fs-code, not 24px. --wp-large is a 16-&gt;19 ramp, not --fs-body.
  --fs-small and --fs-medium no longer drive the nav. The footnote
  label is an h2. Changing KoBody&#39;s factor moves two numbers, not one.
  .toc-rail__link restates its own text-decoration, so it is not why
  `nav a` stays unscoped. The header rule is full-bleed, so --root-pad
  moves no hairline. The canonical padding ramp would cost 6px, not 12.
  The footer rule&#39;s one-pixel lift leaves it two pixels under an auto
  underline, not on it. A post with an h1 renders its #### as h5, so h5
  is not only the index entry title.
- Things that no longer exist: .hover-wipe and the seven sites it was
  written for, the rail on &quot;nine articles&quot;, the 19px body, the 64/88px
  headings, KoDisplay, the three KoFallback rules.
- Counts re-measured on the current build and corpus: 52 plates and
  2,230 token spans, 14 &lt;em&gt;, 49 fi and 46 fl with diagram labels, $ x22, inline code
  median 13.5, 387 B / 176 B of diagram rules, the header and nav-gap
  sizes, the nav rule&#39;s offset under the baseline. The type ladder
  table is given against the 16px body. The measure table is re-run at
  16px with the same greedy wrap (122.2 English cpl, 71.2 syllables at
  928px). The 132, 701 and 1,639-column lines are named for what they
  are: a wget command, sysmon XML config, a syslog line. Counts that
  drift with every post (pages that paint Hangul, headings per level)
  are stated as rules instead.
- Process narration rewritten as present-tense reasons: &quot;An earlier note
  here claimed&quot;, &quot;What stood here&quot;, &quot;used to&quot;, &quot;The reader said so&quot;,
  &quot;Two corrections against the earlier draft&quot;, the Korean quotation in
  the --fs-body note, and the probe history in the hover note.
- Location by line distance (&quot;two rules up&quot;) replaced with the section
  name, and a truncated sentence (&quot;What closes the gap&quot;) completed.
</content></entry>
<entry><id>urn:git:081303f45cb37c39022d82747768c6d75aeb2eaa</id><title>chore: correct script comments, rewrite diagrams.mjs on re-subset, ship the OFL</title><published>2026-09-25T21:22:55+09:00</published><updated>2026-09-25T21:22:55+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/081303f45cb37c39022d82747768c6d75aeb2eaa.html"/><content type="text">chore: correct script comments, rewrite diagrams.mjs on re-subset, ship the OFL

subset-fonts.py rewrote the hashed font names in fonts.css, Base.astro
and tokens.css but not in src/lib/diagrams.mjs, whose DIAGRAM_FONTS is
both what the diagram renderer loads and part of every diagram&#39;s cache
key. After a re-subset the keys stayed put on deleted files and the next
render stopped on a missing font. diagrams.mjs is now a target (the
existing pattern matches its public/fonts/ spelling), files are written
with LF instead of the platform default, and the script ends by saying to
run npm run diagrams and commit src/lib/diagrams/. The note in
diagrams.mjs that said the list had to be edited by hand now says so, and
the docstring states the Python 3.10 that write_text(newline=) needs.

Pretendard is SIL OFL 1.1 and was redistributed without its licence. The
upstream LICENSE is added verbatim as fonts-src/OFL-Pretendard.txt, and
both licences now sit beside the served subsets in public/fonts/, so
/fonts/OFL-Lora.txt and /fonts/OFL-Pretendard.txt are published. They
are the only change to dist/.

.gitignore gains Thumbs.db, desktop.ini, .env* and *.log.

Comments, in all five scripts:
- False or stale claims: the &quot;_ skip&quot; that does not exist, the template&#39;s
  blanks (it ships &quot;&quot; and [], not YAML null), a malformed image path or
  oversize paragraph &quot;shipping the post empty&quot; where the render guard now
  stops the build, &quot;28 to 80 times the largest paragraph&quot; (measured: 6.7x
  the bytes, 36x the marks), the only-safety-net claim, the tags-absent
  case (every post carries the key), the build serving a stale diagram
  (npm run build passes --force; dev and a bare build do not), the old
  deploy-hook story in build-info, and in subset-fonts the weights (links,
  nav and wordmark are 400; 700 is kickers, table headers, the byline and
  strong), the em and ligature counts, the italic claim and the page count
  in the summary line.
- The LATIN note now says the scan read source text and so missed the
  footer&#39;s &amp;copy;, which Lora has and the subset leaves out.
- Process narration (&quot;used to&quot;, &quot;until recently&quot;, &quot;as this did&quot;, &quot;the
  outgoing sans&quot;, the retired faces) rewritten as present-tense reasons.
- &quot;see the section near the end&quot; names the section; &quot;cannot disagree&quot;
  reads &quot;kept in step&quot;.
</content></entry>
<entry><id>urn:git:ff8ae2c50d13d5478eaeda4f91cdd30af1faf337</id><title>docs: correct stale and false comments in the config and src/lib</title><published>2026-09-25T21:21:42+09:00</published><updated>2026-09-25T21:21:42+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/ff8ae2c50d13d5478eaeda4f91cdd30af1faf337.html"/><content type="text">docs: correct stale and false comments in the config and src/lib

Comments only; dist/ builds byte-identical apart from build-info.json.

Counts re-measured on the current corpus and build:
- proseShape&#39;s split is staccato 10, technical 11, flow-essay 9, and
  will-the-next&#39;s median is 46; the staccato note reasons from the 928px
  column at 16px rather than 728px at 20px, and no longer quotes a 24px gap.
- satteri-figure: 11 of 24 image paragraphs carry a caption, all 26 post
  images have an empty or filename-shaped alt, and a captioned image gets an
  empty alt; the caption does not become the alt.
- satteri-lang: AI Odyssey&#39;s KR half holds 1,754 Hangul characters.
- docLang: the 28 Korean posts sit at 0.49 or above, the 0.14/0.29 figures
  are against non-space characters, and keying off the title would mislabel
  the seven Korean posts with Latin titles, not AI Odyssey.
- satteri-prose states the image rule against the 928px column instead of
  a count taken at 728px.

Claims the code or corpus no longer matches:
- The subscript note names satteri-korean; satteri-tilde does not exist.
- Math: no post has a `$...$` pair, so the &quot;2 hits&quot; are gone from the note.
- The feed and the Writing index share byHugoOrder; the sitemap does not,
  and a code-point sort would put TSO ahead of journalctl, which share a
  date.
- excerpt() fills every description; the note said the meta was empty.
- satteri-diagram: a failed render now fails the build through the page&#39;s
  render guard, prebuild only sees fences its line scan finds, and the
  element is &lt;code class=&quot;language-mermaid&quot;&gt;.
- diagrams.mjs: re-subsetting a font does not re-key the diagrams until
  DIAGRAM_FONTS is edited by hand, since subset-fonts.py does not touch it.
- The bind9 `* port *` example sits inside a fence and never reaches the
  emphasis repair; the tilde example is located by content, not line 231.
- A local-time date read shifts every date west of UTC, not half the corpus.
- `type` is published as data-type; no sub-view splits on it yet.
- LANGS includes json beside jsonc; the math exclusion names the file that
  applies it; the bare-pre rule is `.prose pre:not(.code *)`.

Process narration rewritten as present-tense reasons: &quot;An earlier note here
claimed&quot;, &quot;Measured ... before this line/plugin&quot;, &quot;used to&quot;, &quot;Now that&quot;,
&quot;this time&quot;, &quot;no longer&quot;, the readingMinutes tombstone, and past-tense
landmark stories. The code-block wrapper&#39;s doc comment moves back above
codeBlock, which lineCap and ALIAS had separated it from.

Also restores the \u00a0 and \ufffd escapes in prose.ts: an editor had
written them as the literal no-break space and replacement character,
which behave the same but cannot be seen in the source.
</content></entry>
<entry><id>urn:git:1d84d183176964d5f67d9a7396c3b4054b67d5a1</id><title>docs: bring the layout and section-page comments back in line with the code</title><published>2026-09-25T21:19:26+09:00</published><updated>2026-09-25T21:19:26+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/1d84d183176964d5f67d9a7396c3b4054b67d5a1.html"/><content type="text">docs: bring the layout and section-page comments back in line with the code

Stale claims. The Korean-preload notes counted 35 pages, a &quot;30 posts&quot;
line /writing/ does not print, and a Korean tagline about.md no longer
has; the Latin preload note still argued for a display face that was
removed, with the old pair&#39;s sizes. They now give the rule and the pages
it currently skips, and the measured 118,136B and 22,300B. On /writing/
the contents-rail band applies to every article, not nine; the
title-font switch flips nothing while both families map to Lora; and the
token block says which hooks tokens.css and fonts.css actually set, with
both values beside the three it overrides.

Removed things. The hover and forced-colours notes on /code/ and
/writing/ described .hover-wipe, an underline base.css does not draw,
--crimson-dark and a selector base.css no longer has. They now describe
the colour step, the pointer gate, and what the leftover
padding-block-end does. Notes about a visible h1, .index__head and an
orphaned --sp-48 remark are gone or rewritten for the .sr-only h1.

Process narration. &quot;What stood here&quot;, &quot;used to&quot;, &quot;re-measured after the
change&quot; and similar are now present-tense reasons, and corpus counts
that go stale with each new entry are stated without the number.

Two output changes. The HTML comment above the home page&#39;s stylesheet
was emitted after &lt;/html&gt;; it is a JSX comment now. The /writing/
description uses the computed year span instead of &quot;2024–2026&quot;, which
builds the same string today, and with no posts it reads
&quot;Essays and technical notes.&quot; rather than ending on a stray comma.
</content></entry>
<entry><id>urn:git:a98503bae4f1154612c339dfb2bcf66715b4b28d</id><title>fix: mark passages in the other language, and six smaller accessibility gaps</title><published>2026-09-25T20:02:54+09:00</published><updated>2026-09-25T20:02:54+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/a98503bae4f1154612c339dfb2bcf66715b4b28d.html"/><content type="text">fix: mark passages in the other language, and six smaller accessibility gaps

Language (WCAG 3.1.2). The article carries one lang, measured on its
prose, so the KR half of the two bilingual essays was read by an English
voice and the English headings of Korean posts by a Korean one. That
measurement moves to docLang() in hangul.ts, and a new satteriLang hast
plugin, run last, uses it: blocks under a heading that reads exactly EN
or KR take that section&#39;s lang, and a heading in the other script takes
its own. Only values that differ from the article are written, and code
is left alone. The post title and the inline contents list get the same
per-string lang the rail already had. Measured: 17 paragraphs of AI
Odyssey and 5 of We need to talk now carry lang=&quot;ko&quot;; how-to-win&#39;s 25
English headings carry lang=&quot;en&quot;.

- Tables and diagrams were role=&quot;region&quot;, so a post with several listed
  landmarks with the same name. They are groups now, like code plates.
- The primary nav has a name, and aria-current is &quot;page&quot; only on the
  page itself; on an article, Writing is &quot;true&quot;.
- The byline&#39;s name and date were separated only by padding and read
  &quot;Hyukjin AnNovember 5, 2024&quot;; a visually hidden comma fixes the text.
- The copy status region is emptied two seconds after it speaks.
- The rail&#39;s focus ring is drawn inside the link, where the rail&#39;s
  scrollport can no longer clip it.
- On /writing/, Shift+Tab could leave the focused row entirely under the
  sticky year label. The link&#39;s scroll-margin now clears the label, the
  row padding and the ring; worst overlap went from 100% of the row to
  7%, which is on rows already in view that the browser does not scroll.
</content></entry>
<entry><id>urn:git:090cf1c7ea202381a588aff79c675b1d942bcaad</id><title>fix: always build from a cleared content cache locally</title><published>2026-09-25T19:55:06+09:00</published><updated>2026-09-25T19:55:06+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/090cf1c7ea202381a588aff79c675b1d942bcaad.html"/><content type="text">fix: always build from a cleared content cache locally

Astro caches rendered Markdown keyed by the .md file&#39;s digest, so after
an image was replaced the local build kept the old width and height in
the HTML; only a change to the post itself re-rendered it. CI already
builds cold (cache: false). `astro build --force` makes a local build
match it, for about 0.3 s.
</content></entry>
<entry><id>urn:git:7857194024d11f2f27af79021e57664ed4423128</id><title>perf: serve the grid scans and the portrait at twice their display size</title><published>2026-09-25T19:55:05+09:00</published><updated>2026-09-25T19:55:05+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/7857194024d11f2f27af79021e57664ed4423128.html"/><content type="text">perf: serve the grid scans and the portrait at twice their display size

public/uploads bypasses Astro&#39;s image service, so every file ships at
the size it was uploaded. The three book-page scans in the cognitive
dissonance post were 1600x2134 (802 KB together) and are drawn 299px
wide on a desktop and 104px on a phone, where they were that page&#39;s
LCP element. They are now 600x800 WebP, 2x the largest slot and still
legible when zoomed: 215 KB together. The about portrait, drawn at
232x290, goes from 720x900 to 480x600 (42 KB to 25 KB).

Only images whose displayed size CSS fixes were resized: a standalone
image renders at its intrinsic width, so shrinking it would shrink it
on screen. URLs are unchanged and satteriProse reads the new sizes, so
no post changes. Measured before and after in Chrome at 1440 and 375:
every box is within 0.1 px (800 is 2134 x 600/1600 rounded) and page
heights are equal. On a 1.6 Mbps link the post transferred 947 KB
before and 375 KB after.
</content></entry>
<entry><id>urn:git:f333bb27ffe4aa6729cc44c19c5a3772ae5387f9</id><title>perf: keep pathological input from crashing or stalling the renderer</title><published>2026-09-25T19:49:23+09:00</published><updated>2026-09-25T19:49:23+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/f333bb27ffe4aa6729cc44c19c5a3772ae5387f9.html"/><content type="text">perf: keep pathological input from crashing or stalling the renderer

- satteriProse found out whether a post had an h1 or an h6 by walking
  the whole tree in JavaScript, materialising every token span and SVG
  node. A 6,000-deep list overflowed the stack and the post rendered
  empty. A new satteriHeadingScan visits only h1 and h6, natively
  filtered, and runs before satteriProse.
- Shiki&#39;s time limit is checked between scans, not within one, so a long
  bash line was superlinear and a few KB of &lt;&lt; threw RangeError. Lines
  of 2,000 characters or more now render as one uncoloured token; the
  corpus&#39;s longest is 1,639.
- Four body regexes were quadratic on crafted input: an unclosed [ or (
  scanned to the end from each one (1.3 s on 80,000), ^\s*\| rescanned
  blank runs (4.9 s), and \n+$ rescanned newline runs (3.7 s). Each is
  now linear; link destinations may still hold one level of parentheses.

The 30 posts build byte-identical. The list and &lt;&lt; fixtures that crashed
on the previous commit now build with their bodies.
</content></entry>
<entry><id>urn:git:6d017c9ef7bd5200b241b9327186a6e5b312f48f</id><title>fix: stop oversize paragraphs and tables at the gate, and escape YAML keys</title><published>2026-09-25T19:49:23+09:00</published><updated>2026-09-25T19:49:23+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/6d017c9ef7bd5200b241b9327186a6e5b312f48f.html"/><content type="text">fix: stop oversize paragraphs and tables at the gate, and escape YAML keys

Satteri&#39;s smart punctuation costs bytes x marks within one inline run.
Measured: 85 KB took 1.4 s and 2 GB, 88 KB of pasted JSON or 169 KB of
unfenced log made the render throw, and about 41,000 apostrophes in one
run killed Node without naming a file. Satteri also has no table cell
limit; an 18 KB table 3,072 columns wide became a 94 MB page.

The gate now holds each paragraph, list item or heading outside code to
16 KB and 400 quote, &amp; and ... marks, 28 to 80 times the largest
paragraph in the corpus, and each table to 20,000 cells, reporting the
line and what to do. Posts, code entries and about.md are all checked.

A front-matter key went into a RegExp unescaped: `c++:` threw a
SyntaxError that named no file. Keys are now escaped.
</content></entry>
<entry><id>urn:git:37e7f817d75c1833dd543062132b38e4dc5c4de5</id><title>fix: refuse file names, dates and case clashes the build mishandles</title><published>2026-09-25T19:42:14+09:00</published><updated>2026-09-25T19:42:14+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/37e7f817d75c1833dd543062132b38e4dc5c4de5.html"/><content type="text">fix: refuse file names, dates and case clashes the build mishandles

Three inputs passed the gate and then broke the site without an error:
- A file name with #, %, ? or a space. `#` dropped the post from every
  page, the feed and the sitemap; `%` shipped an address GitHub Pages
  answers with 400. File names are now held to letters in any script,
  digits, - and _, for posts and code entries alike.
- A date that matches YYYY-MM-DD but does not exist. 2025-02-30 was
  published on March 2, and 2025-12-32 under the next year&#39;s URL.
- Two files whose names differ only in letter case. They share one
  collection id and the loader keeps one. They cannot coexist on
  Windows, but a commit from Linux or the web editor can add the second.

Tested on fixtures, the clash in a case-sensitive directory; the 30
posts and 7 code entries pass unchanged.
</content></entry>
<entry><id>urn:git:14e9108794c651cb5789d7d6226cc2664742f568</id><title>fix: read an angle-bracket image path whole in the gate</title><published>2026-09-25T05:01:59+09:00</published><updated>2026-09-25T05:01:59+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/14e9108794c651cb5789d7d6226cc2664742f568.html"/><content type="text">fix: read an angle-bracket image path whole in the gate

CommonMark writes a destination containing a space as &lt;/uploads/a b.png&gt;,
which is how default screenshot names come out. The gate stopped at the
space and reported a file that was never named. Angle-bracket
destinations are now read up to the closing &gt;, before the bare form.
</content></entry>
<entry><id>urn:git:28b28decddf08e37d3b72a7d8793d31d47e1cb7b</id><title>fix: build descriptions from the text a reader sees</title><published>2026-09-25T05:01:59+09:00</published><updated>2026-09-25T05:01:59+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/28b28decddf08e37d3b72a7d8793d31d47e1cb7b.html"/><content type="text">fix: build descriptions from the text a reader sees

excerpt() returned Markdown source, so once the feed escaped it as HTML
text, an entity or an allowed inline tag in a first paragraph showed up
in feed readers as written: AT&amp;amp;T, &lt;abbr title=&quot;...&quot;&gt;. It now turns
the paragraph into reader-visible text: code spans stay as written,
allowed inline tags go, &lt;br&gt; becomes a space, emphasis markers go,
backslash escapes resolve and entities decode. The meta description and
JSON-LD get the same text; every sink still escapes what it receives.
Only the first 4,000 characters are read. The current posts&#39; output is
unchanged.
</content></entry>
<entry><id>urn:git:9ac8801e9a85907fe9b52482c7fb296af16cad71</id><title>fix: accept what the guard wrongly refused, and say how to fix the rest</title><published>2026-09-25T05:01:58+09:00</published><updated>2026-09-25T05:01:58+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/9ac8801e9a85907fe9b52482c7fb296af16cad71.html"/><content type="text">fix: accept what the guard wrongly refused, and say how to fix the rest

- An &lt;abbr&gt; title may contain &quot;&gt;&quot;, which is text inside quotes to both
  CommonMark and the browser. Whitespace in allowed tags is now the
  HTML tokenizer&#39;s set rather than \s, which also matched a no-break
  space the browser reads as part of the name.
- Satteri percent-encodes the brackets of an IPv6 host, which no URL
  parser accepts. The brackets are put back in the authority of an
  http(s) link and the repaired address ships; a link that still does
  not parse gets its own message instead of a wrong one about schemes.
- The messages now give the working fix: a backslash for a literal &lt; in
  prose, &lt;kbd&gt; for a key, the escapes that keep braces at the end of a
  heading (each tested; escaping only the opening brace does not work),
  and a second list marker instead of an HTML comment between lists.
</content></entry>
<entry><id>urn:git:bc2768e6bd9dcb505c109caa95e40115f0a80861</id><title>fix: keep a repeated diagram&#39;s colours under the CSP, and gate the policy</title><published>2026-09-25T05:01:58+09:00</published><updated>2026-09-25T05:01:58+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/bc2768e6bd9dcb505c109caa95e40115f0a80861.html"/><content type="text">fix: keep a repeated diagram&#39;s colours under the CSP, and gate the policy

A diagram drawn twice on one page is renamed for its second copy, and
that rename rewrites the copy&#39;s &lt;style&gt;. Only the committed file&#39;s
&lt;style&gt; was hashed, so the browser dropped the copy&#39;s stylesheet and the
diagram lost its palette on a green build. The rename now lives in
diagramCopy() in diagrams.mjs, used by satteriDiagram and by the config,
which hashes each copy up to the most any one document has.

check-dist now also holds every page to its policy: exactly one CSP
&lt;meta&gt;, nothing it governs ahead of it, and every inline &lt;style&gt; and
executable inline &lt;script&gt; listed by hash. A dropped style or script
becomes a failed build instead of a page that quietly looks wrong.
Tested by injecting a style, a script, a second policy and a script
before the policy, and by removing a diagram&#39;s hash: each fails.

The config comment now notes the hashes that are Astro&#39;s, not the
site&#39;s: five client-directive runtimes and the empty stylesheet.
</content></entry>
<entry><id>urn:git:d8520215044afa06771ee574411264247170a757</id><title>fix: escape feed descriptions as HTML text</title><published>2026-09-25T03:54:29+09:00</published><updated>2026-09-25T03:54:29+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/d8520215044afa06771ee574411264247170a757.html"/><content type="text">fix: escape feed descriptions as HTML text

Feed readers XML-decode &lt;description&gt; and render it as HTML, but the
description is a plain-text excerpt. A first paragraph quoting
&lt;img src=x onerror=...&gt; in a code span would come out of a reader as a
live element. &amp; &lt; &gt; are now escaped before @astrojs/rss escapes the XML,
so readers show the text as written. No current description contains
those characters, so today&#39;s feed is unchanged.
</content></entry>
<entry><id>urn:git:0442f99e782c241881da99fa5235f307379d33d7</id><title>feat: refuse unsafe markup at build time and ship a Content-Security-Policy</title><published>2026-09-25T03:54:28+09:00</published><updated>2026-09-25T03:54:28+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/0442f99e782c241881da99fa5235f307379d33d7.html"/><content type="text">feat: refuse unsafe markup at build time and ship a Content-Security-Policy

Satteri emits raw HTML verbatim with rawHtml on or off, headingAttributes
copies any key=value onto a heading, and link destinations are not
filtered, so a post could ship a &lt;script&gt;, an onmouseover or a
javascript: link on a green build. On a blog that quotes payloads, one
missed pair of backticks is enough.

satteriGuard runs first among the hast plugins and throws on:
- raw HTML other than kbd, mark, abbr with a title, sub, sup, ins and br,
  including HTML comments
- heading attributes other than id and class
- links that are not http, https, mailto or a path on this site
- images not served from this site
The loader logs the message against the file and the render guard fails
the build. Tested with 25 hostile posts: 22 fail the build by name, and
the other 3 are not markup to the parser and render as inert text.

Astro&#39;s security.csp now emits a CSP &lt;meta&gt; on every page, since Pages
cannot send headers: default-src &#39;none&#39;, same-origin images and fonts,
script hashes Astro computes itself, the baked diagrams&#39; &lt;style&gt; hashes
computed in the config, and &#39;unsafe-inline&#39; only for style attributes,
which Shiki needs. In Chrome, all 36 pages report no violation and the
copy buttons, contents rail and diagram colours behave as before; on a
page with injected markup, an onerror handler, an inline script, a
javascript: link and remote images are all blocked.

The rawHtml comment no longer claims the flag governs sanitisation.
</content></entry>
<entry><id>urn:git:cb20946600eb02e86b7607c64bb8aae1326b0888</id><title>feat: end each post with the post itself</title><published>2026-09-25T04:18:42+09:00</published><updated>2026-09-25T04:18:42+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/cb20946600eb02e86b7607c64bb8aae1326b0888.html"/><content type="text">feat: end each post with the post itself

The footer under every article, the Older and Newer links and the
&quot;All &lt;year&gt; posts&quot; link, is gone at the author&#39;s request, along with
the neighbour lookup in getStaticPaths and the styles that only served
it.

The contents rail used that footer to know the reader had reached the
end: a final section too short to bring its heading up to the reading
line was marked while the footer was on screen. It now watches a
one-pixel marker appended after everything in &lt;body&gt;, pulled up over
the pixel it adds, which is on screen only at full scroll. The footer
came into view hundreds of pixels early, so this also stops the rail
jumping to the last entry while the heading before it can still be
reached. On the 14 posts with a rail, sweeping top to bottom in Chrome:
every post still marks its first entry at the top and its last at the
bottom, and entries never marked fall from 8 to 6.
</content></entry>
<entry><id>urn:git:d19bd8b59ac60796a90ed3432d63b30edb54a50f</id><title>fix: let render-diagrams exit as soon as it is done</title><published>2026-09-25T03:42:44+09:00</published><updated>2026-09-25T03:42:44+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/d19bd8b59ac60796a90ed3432d63b30edb54a50f.html"/><content type="text">fix: let render-diagrams exit as soon as it is done

Both Promise.race timeouts left their timers pending, so Node stayed
alive until the 120 s render budget ran out after every successful
render. Rendering one diagram took 121 s of wall time for 1 s of work.
The timers are now cleared when the race settles, and the success path
exits explicitly like every other path does, because Chrome on Windows
can hold its pipes open after it is killed. One diagram now takes 1.2 s,
and the rendered SVG is byte-identical.
</content></entry>
<entry><id>urn:git:591166a63d287a4e6f262bed9aef61fd29bb3d05</id><title>ci: pin actions to SHAs, scope permissions per job, refuse lockfile drift</title><published>2026-09-25T03:42:44+09:00</published><updated>2026-09-25T03:42:44+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/591166a63d287a4e6f262bed9aef61fd29bb3d05.html"/><content type="text">ci: pin actions to SHAs, scope permissions per job, refuse lockfile drift

- checkout, withastro/action and deploy-pages are pinned to the commit
  SHAs of v7.0.1, v6.1.3 and v5.0.1. A moved tag can no longer change
  what runs without a change here. Dependabot proposes the bumps.
- Workflow-level permissions are empty. The build job, which runs every
  npm dependency, mermaid and Chrome, gets contents: read. Only the
  deploy job holds pages: write and id-token: write, the minimum
  deploy-pages documents.
- checkout no longer leaves the token in .git/config.
- withastro/action installs with npm install, which quietly resolves new
  versions when package.json and the lockfile disagree. A dry-run npm ci
  before it fails the job instead, without installing or running any
  package script. Checked on npm 10 and 11.
</content></entry>
<entry><id>urn:git:eeb8e14abbc2c2e9b50b9b191d9db8e9bf2f9908</id><title>fix: make a missing image stop the build</title><published>2026-09-25T03:42:44+09:00</published><updated>2026-09-25T03:42:44+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/eeb8e14abbc2c2e9b50b9b191d9db8e9bf2f9908.html"/><content type="text">fix: make a missing image stop the build

The image check reported through req(&#39;uploads&#39;), but the value CI
passes, CONTENT_STRICT=urls,schema, names no key the gate reads, so a
missing image was only ever a warning. It is now always an error, and
an unknown CONTENT_STRICT name stops the gate instead of silently
enabling nothing.

The check is made safe to be strict:
- paths are compared segment by segment against directory listings, so
  a wrong case fails on Windows as it would on Pages
- only references that start a URL count, outside fenced code and code
  spans, so a quoted curl http://host/uploads/... is not an image
- query strings and fragments are dropped, the path is percent-decoded
  as the renderer does, and a malformed escape is reported by name
  instead of emptying the post at render time
- about.md and the code entries are checked as well as posts
</content></entry>
<entry><id>urn:git:507a9ad5ab5ce4706db44607d7af78c5bf9122a4</id><title>perf: one KoFallback face instead of one per platform</title><published>2026-09-25T03:42:44+09:00</published><updated>2026-09-25T03:42:44+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/507a9ad5ab5ce4706db44607d7af78c5bf9122a4.html"/><content type="text">perf: one KoFallback face instead of one per platform

With three same-family @font-face rules Chrome builds the fallback from
every face whose local() resolves and loads each installed one for every
size and weight it sets Hangul at, before first paint. Korean Windows
with both Malgun Gothic and Noto Sans KR installed paid for both. A
single src list loads only the first local() that resolves.

The order keeps each platform&#39;s face: Malgun wherever it is installed,
then Apple SD Gothic Neo, then Noto Sans KR or Nanum Gothic. The
overrides were identical, so no metric moves. Measured on the longest
post at 4x CPU with cache off, alternating: first contentful paint
median 1428 ms before, 1016 ms after. Out-of-subset syllables still
render in Malgun Gothic.
</content></entry>
<entry><id>urn:git:3f716c554ff7c413ee8a0aefc2093621b684c6df</id><title>fix: keep the text between --- rules in excerpts</title><published>2026-09-25T03:42:43+09:00</published><updated>2026-09-25T03:42:43+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/3f716c554ff7c413ee8a0aefc2093621b684c6df.html"/><content type="text">fix: keep the text between --- rules in excerpts

plain() stripped the first span between two --- lines as if it were
front matter, but post.body never carries front matter: Astro removes it
before the page sees the body. What the regex matched was the post&#39;s own
horizontal rules, so a post whose opening paragraph sat between two
rules got its meta description and feed summary from the paragraph
after. The existing posts build byte-identical.
</content></entry>
<entry><id>urn:git:effa6f1ff24968ff0afee7ef30d4196e59951f52</id><title>fix: stop Satteri reading the body&#39;s first line as front matter</title><published>2026-09-25T03:26:22+09:00</published><updated>2026-09-25T03:26:22+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/effa6f1ff24968ff0afee7ef30d4196e59951f52.html"/><content type="text">fix: stop Satteri reading the body&#39;s first line as front matter

Astro splits the front matter off before rendering and hands Satteri the
trimmed body, but Satteri parses front matter by default. A post whose
body opened with a --- rule lost everything up to the next ---, ... or
+++ line, with no log, and the page still had a body so the render guard
could not see it.

Nothing reads what Satteri would parse there: Astro&#39;s metadata
frontmatter is the loader&#39;s own copy. The existing 30 posts build
byte-identical. The comment above the features block no longer claims
every unlisted feature defaults off.
</content></entry>
<entry><id>urn:git:540215720a4ae0693a1b20a3af9f6ad9c2c631e5</id><title>fix: stack the Older/Newer title under its label on narrow screens</title><published>2026-09-24T12:50:49+09:00</published><updated>2026-09-24T12:50:49+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/540215720a4ae0693a1b20a3af9f6ad9c2c631e5.html"/><content type="text">fix: stack the Older/Newer title under its label on narrow screens

Below 36rem the footer nav collapses to a single track, but the title
kept grid-column: 2. It landed in an implicit max-content column, the
1fr track shrank to nothing and the label was painted over the start
of the title: 24 of 58 links at 320px, 8 at 375px, still 2 at 480px.

The title now moves to column 1 inside the container query, under the
label. Widths of 768px and up are unchanged. The hit-area note beside
the card is re-measured for the stacked layout.
</content></entry>
<entry><id>urn:git:ed5883878a7eb2b53db6df22c3c5c9291f5e6560</id><title>fix: fail the build when an entry renders to nothing</title><published>2026-09-24T12:17:41+09:00</published><updated>2026-09-24T12:17:41+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/ed5883878a7eb2b53db6df22c3c5c9291f5e6560.html"/><content type="text">fix: fail the build when an entry renders to nothing

Astro&#39;s glob loader catches any exception a Markdown render throws,
logs one line and stores the entry without HTML. The page around it
still builds and astro build exits 0, so the post ships as a title over
an empty column. A mermaid fence inside a blockquote, a list-indented
fence, or an image URL with a stray % all did this without tripping
the content gate.

Two layers now stop it:
- assertRendered() in src/lib/rendered.ts, called by the post page, /
  and /code/ before rendering, throws for an entry with no HTML or an
  empty body. Its message covers the cached case, where the loader
  does not re-render the file and no error line appears.
- scripts/check-dist.mjs runs as postbuild and checks the output: one
  non-empty .prose per post page, a non-empty about article, and one
  non-empty .project__about per project.
</content></entry>
<entry><id>urn:git:29c40923b2cce5f862de8ba642805499ade4a03e</id><title>fix: stop CI from restoring stale rendered HTML</title><published>2026-09-24T12:17:40+09:00</published><updated>2026-09-24T12:17:40+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/29c40923b2cce5f862de8ba642805499ade4a03e.html"/><content type="text">fix: stop CI from restoring stale rendered HTML

withastro/action caches node_modules/.astro by default, and that store
holds every post&#39;s rendered HTML keyed only by the .md file&#39;s digest.
Restored from the previous run, it ships the old HTML after a change to
a Markdown plugin, a baked diagram or an image, while build-info.json
reports the new commit. It also keeps a failed render failed without
logging the error again.

A cold content build costs under a second.
</content></entry>
<entry><id>urn:git:fcb32e5626e70505eca05774cee7d59ae6c5532d</id><title>chore: give every existing post the template&#39;s blank fields</title><published>2026-09-23T17:30:44+09:00</published><updated>2026-09-23T17:30:44+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/fcb32e5626e70505eca05774cee7d59ae6c5532d.html"/><content type="text">chore: give every existing post the template&#39;s blank fields

The template gained titleKo, deck, type and draft; the 30 posts already in the corpus
did not, so adding a subtitle to one of them meant remembering the field name and typing
it in by hand — and a typo would have been silent, since every one of these is optional
and a misspelled key is simply ignored.

Each post now carries the same seven fields in the same order the template ships them,
with the four new ones blank. Existing lines are untouched, whatever quoting they used,
and nothing was reordered that was already there.

Purely mechanical, and checked that way rather than asserted: 120 lines added, 0 removed,
and every added line is one of the four blanks. The built site is byte-identical — 76
files compared by SHA-256 before and after, 0 differences — because a blank field renders
nothing at all.
</content></entry>
<entry><id>urn:git:3dc63b8daffe31d8b85da7d85d48669289707a9d</id><title>feat: a post template every field can be left blank in</title><published>2026-09-23T17:23:43+09:00</published><updated>2026-09-23T17:23:43+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/3dc63b8daffe31d8b85da7d85d48669289707a9d.html"/><content type="text">feat: a post template every field can be left blank in

Writing a post meant remembering which keys exist: the 30 posts in the corpus carry
title, date and tags, and nothing else, because the other four fields were easy to forget
and the template offered no help. The template now ships every field the schema has,
blank, so writing a post is filling in what you have something to say about.

Blank had to be made safe first. Three fields rejected it: a bare `type:`, `draft:` or
`tags:` is YAML null, and the enum, the boolean and the array default all refuse null, so
copying the old template and clearing its values broke the build. One wrapper now absorbs
the three spellings of nothing — absent, null and empty string — for every optional field,
replacing four hand-rolled variations of the same idea.

Blank also means invisible: a blank deck draws no deck paragraph, and the description
falls back to the post&#39;s opening sentence. Verified on a post copied from the template
with only title and date filled in.

Two pre-chosen values are gone from the template. `draft: true` was the dangerous one —
copy it, write a post, build clean, and the post is nowhere on the site. `date:` ships
blank because an inherited date is a wrong date, and the year segment of the URL comes
from it.

The field guide moved into the template&#39;s body rather than sitting as YAML comments: a
comment in front matter is copied into every post made from the template and stays there,
while the body is overwritten by the first sentence typed.

The gate learned the failures an author actually hits. It parses the front matter as YAML
now instead of only pattern-matching it, so a quote typed inside the template&#39;s `deck: &quot;&quot;`
fails at the file with a line number rather than somewhere inside Astro with a message
about a collection entry. An unquoted `#` silently truncating a value is warned about by
comparing what was typed against what parsed. Whole-line and trailing comments are allowed
in the template, which is the one file whose job is to explain itself, and a value left in
it is a warning rather than a failed deploy.

Also fixed on the way: JSON.stringify does not escape `&lt;`, so a deck containing
`&lt;/script&gt;` closed the JSON-LD element early and the rest of the object was parsed as
HTML. Now that deck is a field authors fill in freely, that is a string they can type.
</content></entry>
<entry><id>urn:git:de512a179fa3a71df2129ca36072693316f0795c</id><title>feat: give the site an icon</title><published>2026-09-23T14:59:35+09:00</published><updated>2026-09-23T14:59:35+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/de512a179fa3a71df2129ca36072693316f0795c.html"/><content type="text">feat: give the site an icon

/favicon.ico and /favicon.svg both answered 404, so every tab showed the browser&#39;s
placeholder.

It is the author&#39;s portrait, cropped square on the head rather than scaled down from the
4:5 original — a whole portrait at 16px is a smudge, where a head fills the frame and
still reads as a face. The crop is derived from the picture rather than eyeballed: the
subject&#39;s top edge and the head&#39;s width were found by sampling against the solid
background, and the square is centred on what that gives.

No SVG icon, because this is a photograph. The .ico carries 16, 32 and 48 in one file
for the sizes that actually get asked for, with a 180px apple-touch-icon beside it.
</content></entry>
<entry><id>urn:git:f35d87195d510923b25b863ada4aaae76022daf8</id><title>feat: ship the feed the pages have been advertising, plus robots and a sitemap</title><published>2026-09-23T14:54:03+09:00</published><updated>2026-09-23T14:54:03+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/f35d87195d510923b25b863ada4aaae76022daf8.html"/><content type="text">feat: ship the feed the pages have been advertising, plus robots and a sitemap

Every page&#39;s &lt;head&gt; has carried a link to /index.xml since the migration and nothing
answered it, so a reader who subscribed got a 404 from an address the site printed
itself. The feed now exists at that address.

- One feed, not one per section. The Hugo site&#39;s section feeds were dropped on purpose
  and a blog publishing into a single stream has nothing to split.
- Ordering and post addresses come from src/lib/order.ts rather than being restated, so
  the feed cannot drift from the Writing index. Descriptions reuse the same excerpt() the
  pages use for &lt;meta name=&quot;description&quot;&gt;, which means filling in a post&#39;s deck improves
  both at once.
- Escaping is the library&#39;s. The corpus is full of what breaks hand-rolled XML:
  ampersands in shell pipelines, angle brackets in log output, quotes in Korean prose.
- Dates are parsed as UTC, like every other date on this site. Local-time parsing moves
  half the corpus back a day.

robots.txt is an endpoint rather than a file in public/ so its Sitemap line is built from
`site` — a hard-coded host is one rename away from naming someone else&#39;s sitemap, and
this domain has already moved once. It allows everything; the file exists for that line.

The sitemap is why: the Hugo site shipped one and the migration lost it, and a robots
line pointing at a 404 would be the same defect as the head link that started this.

Verified: 30 items, all with title, absolute link and pubDate; no unescaped ampersands;
sitemap 35 entries, matching what the previous site published.
</content></entry>
<entry><id>urn:git:cef552971b8c087ee5a990f88ccd9896093dd0eb</id><title>feat: links take the accent, and the footer names where else to find me</title><published>2026-09-23T14:23:49+09:00</published><updated>2026-09-23T14:23:49+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/cef552971b8c087ee5a990f88ccd9896093dd0eb.html"/><content type="text">feat: links take the accent, and the footer names where else to find me

Brings link styling in line with the reference and finishes the About and Code pages&#39;
identity details.

Links
- Body links are crimson at the weight of the text around them, with no rule at rest.
  What stood there was weight 700 plus an underline, which made a link a different piece
  of type from the sentence holding it.
- Hover is a colour step and nothing else, everywhere in the body of a page: the index
  entries, the project list, the contents rail, the kicker, the adjacent-post titles.
  The animated rule stays where the reference keeps it, on the two bars that bracket the
  page.
- The hover colour is derived, not picked. The reference moves its body links
  rgb(160,32,32) to rgb(192,48,48), a luminance increase of 1.572x; the same factor on
  this site&#39;s crimson lands on #cb233b, which holds 5.30:1 against the page.

Two corrections worth recording, because both were measured wrong the first time and the
wrong readings had already been written into comments:
- The reference does change colour on hover. The first probe said otherwise because the
  page sets scroll-behavior: smooth, so the element&#39;s position was read before the scroll
  settled and the pointer landed on nothing — a null result that looked like a null state.
- Project names on /code/ do wrap, at 360px. Two reports had said they never do.

Footer
- GitHub, LinkedIn and Instagram, at the copyright&#39;s size, carrying rel=&quot;me&quot; because
  these are identity claims rather than references.

Content
- The About page is running prose with a portrait, replacing the stub, and its LinkedIn
  handle is the author&#39;s own correction.
- VTZero&#39;s post pointed at github.com/devastator-x, an account that 404s. Both the link
  and the clone command now name the repository that exists.

Verified: 36 pages, 0 errors; the content gate passes; hover driven over CDP at every
link on the site; no layout shift at any of them; the forced-colors fallbacks were
re-pointed when the class they named went away.
</content></entry>
<entry><id>urn:git:87002dd449870fa572ac26b81d668b115589dfb9</id><title>feat: rebuild the design on Lora, and fill the Code and About pages</title><published>2026-09-22T16:23:05+09:00</published><updated>2026-09-22T16:23:05+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/87002dd449870fa572ac26b81d668b115589dfb9.html"/><content type="text">feat: rebuild the design on Lora, and fill the Code and About pages

Retunes the whole visual system against two references — the Harvard Gazette for form,
nicholas.carlini.com for density — and replaces the placeholder Code and About pages with
real content. Every number below was measured in a browser against the built output.

Type
- One Latin face. Lora replaces Newsreader and Inter, so the site ships two subset files
  instead of three: 22KB Latin plus a 118KB Korean face gated by unicode-range, which a
  reader who never meets a Hangul character never downloads.
- Every Korean metric was re-derived against Lora rather than carried over: KoBody&#39;s
  size-adjust 102.9% to 94.29%, the local-fallback overrides with it. The arithmetic is
  in fonts.css next to each value.
- Body is a flat 16px, not a clamp. It had reached 18.81px at 1440 against the
  reference&#39;s 16, which is what read as too large. Headings came down with it: h1 64 to
  38px, h2 48 to 28.5px. Paragraph spacing 24px to 11.2px, the reference&#39;s own .7em.
- Header type is measured from the reference&#39;s header rather than from body, so body
  could fall without dragging the bar down with it.

Geometry
- The measure widens 728 to 928px, spending 100px of each side gutter at 1488. The cost
  is a longer line in Korean and it was taken deliberately.
- The header is full-bleed and identical on every route: symmetric padding, same wordmark
  and nav positions, measured across seven routes at ten widths. Two separate causes were
  fixed — it used to travel with the article page box, and it used to shift 7.5px between
  pages that scroll and pages that do not.
- A contents rail stands in the left gutter above 1440px, with a rule that runs from under
  the header to the foot of the viewport, and falls back to an inline disclosure below.

Article pages
- The date is one thing in one place: &quot;December 7, 2025&quot; on the byline, not 09.08 in the
  rail with the year three lines above it. Reading time is gone.
- Mermaid fences render as diagrams, baked to inline SVG at build time. A reader who opens
  a page without one downloads 110 bytes gzipped for the privilege; the post with three
  gets 18KB and no extra request, and they render with JavaScript off.
- Code blocks carry a copy control. Images carry no frame.

Pages
- /code/ reads content/code/*.md, seven projects, so an eighth is one file and a push.
  Descriptions are traceable to each repository&#39;s own description or README.
- /about/ is rewritten as running prose with a portrait, replacing the stub. Its GitHub
  link pointed at an account that 404s; it now points at the live one.
- Section pages print no page title — the bar already names the page — and keep an
  sr-only h1 so the document still has one.
- A footer carries the copyright on every page, its closing year stamped at build time.

Verified: 36 pages, 0 errors; the content gate passes and now validates the code
collection too; no horizontal overflow from 320 to 2560px; the Korean face is fetched
only by the 32 pages that paint Hangul.
</content></entry>
<entry><id>urn:git:9f810dcd230587d0ddc97731e95854c522ed386d</id><title>docs: reduce the README to what the site is</title><published>2026-09-17T21:06:33+09:00</published><updated>2026-09-17T21:06:33+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/9f810dcd230587d0ddc97731e95854c522ed386d.html"/><content type="text">docs: reduce the README to what the site is

This repository exists to keep one blog running, not to be adopted by anyone, so the
README no longer explains how to operate it. Nothing was lost in the cut: the front
matter schema is annotated field by field in src/content.config.ts, and the staging of
CONTENT_STRICT is documented at the top of scripts/check-content.mjs next to the code
that reads it.
</content></entry>
<entry><id>urn:git:bfc31eaf7bb7ad3decdd916e6af3b92b2c01e150</id><title>docs: drop the design-notes section from the README</title><published>2026-09-17T21:05:09+09:00</published><updated>2026-09-17T21:05:09+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/bfc31eaf7bb7ad3decdd916e6af3b92b2c01e150.html"/><content type="text">docs: drop the design-notes section from the README

Each of the five points was already a comment on the code it constrains: the trailing
slash in astro.config.mjs, ASCII-only slug lowercasing in content.config.ts, the sort
contract in order.ts, and the token set in tokens.css. Two copies of a rule is one copy
that goes stale.

What is left is the part written nowhere else: how to start a post, what each front
matter key means, and what the gate refuses to deploy.
</content></entry>
<entry><id>urn:git:c1db776dd7640aaf30cb7bab0c968d40158d2bc5</id><title>feat: design system, self-hosted fonts, and the writing index</title><published>2026-09-17T20:42:05+09:00</published><updated>2026-09-17T20:42:05+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/c1db776dd7640aaf30cb7bab0c968d40158d2bc5.html"/><content type="text">feat: design system, self-hosted fonts, and the writing index

Puts a real typographic system under the site, replacing the styling that came across
with the migration. The reference points are the Harvard Gazette for form and
nicholas.carlini.com/writing for density.

Type and color
- Four subsetted variable faces served from the repository: Newsreader for prose, Inter
  for UI, Pretendard for Korean, plus an Inter italic. No third-party font requests, so
  nothing external can stall first paint.
- Korean faces are gated behind unicode-range, so a reader who never meets a Hangul
  character never downloads the 210 KB that serves it.
- Per-face size-adjust aligns x-heights across the three families, which keeps the
  fallback swap from shifting the page.
- Five color families of four steps each on an OKLCH ladder with fixed lightness per
  step, so every step holds its contrast when the hue changes. Harvard Crimson is the
  brand color.
- Small-caps is deliberately absent: none of the three families ships an smcp table, so
  asking for it yields a synthesized fake. Uppercase with tracking stands in for it.

Writing index
- Grouped by year, with a date rail and hairline rules instead of thumbnails. Every
  entry is the title and its date, which is what a reader scanning 30 posts needs.

Article pages
- Typesetting is derived from the body text: measure, leading and excerpt length follow
  the document&#39;s own shape rather than a single fixed template.
- Code blocks become a figure with the language named in a caption, keyed on
  data-language rather than the class, which Astro rewrites before transformers run.
- Shiki now aliases vi to ini rather than falling back silently, which is what the
  build was warning about.

Code and documentation are English throughout, matching the site&#39;s default language.
</content></entry>
<entry><id>urn:git:5174da3bc4a3ad6a8cbf1940fe24616141a3055b</id><title>chore: pin line endings to LF</title><published>2026-09-17T19:05:01+09:00</published><updated>2026-09-17T19:05:01+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/5174da3bc4a3ad6a8cbf1940fe24616141a3055b.html"/><content type="text">chore: pin line endings to LF

Keeps a Windows working tree (core.autocrlf=true) from handing CI different bytes than
the author sees. The previous system hit this for real: the same file measured 3,568,689
bytes locally and 3,565,102 bytes when served, which breaks any pipeline that hashes
file contents.
</content></entry>
<entry><id>urn:git:b6ec07249ecd6bf47533f488977d1bfeb7730e8a</id><title>init: Astro static site, migrated off Next.js and Supabase</title><published>2026-09-17T19:04:40+09:00</published><updated>2026-09-17T19:04:40+09:00</updated><author><name>sqzer</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/blog/commit/b6ec07249ecd6bf47533f488977d1bfeb7730e8a.html"/><content type="text">init: Astro static site, migrated off Next.js and Supabase

Merges blog-app (Next.js 15 + Supabase + Upstash on Vercel) and blog-content (a markdown
mirror of the database) into one public repository, and returns the site to static
generation. The runtime database, session store and serverless functions all disappear;
pushing to main is what publishes.

- 30 posts plus About, migrated losslessly with Korean prose left as it is
- 26 body images moved into the repository, ending Supabase Storage as their only copy
- URLs move to /writing/&lt;year&gt;/&lt;slug&gt;/. Old URLs are not redirected
- A content gate runs before the build, standing in for a test suite that did not exist

Verified: all 30 posts render; ordering matches the previous site exactly; 26 image
references resolve; the gate catches URL collisions, malformed dates, reserved slugs,
missing images and absent front matter, and stops the build before Astro runs.
</content></entry>
</feed>
