<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><id>https://oni.sqzer.com/sqzass/atom.xml</id><title>sqzass</title><subtitle>A static site generator written in Rust</subtitle><link rel="self" href="https://oni.sqzer.com/sqzass/atom.xml"/><updated>2026-11-12T12:58:14+09:00</updated><entry><id>urn:git:c2adc6746ea436500c459c6ad3d1f326e786bd04</id><title>Remove needless borrow in page navigation</title><published>2026-11-12T12:58:14+09:00</published><updated>2026-11-12T12:58:14+09:00</updated><author><name>debuman</name><email>110329244+debuman@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/sqzass/commit/c2adc6746ea436500c459c6ad3d1f326e786bd04.html"/><content type="text">Remove needless borrow in page navigation
</content></entry>
<entry><id>urn:git:a52b8272deec46fbe4dc86e1a388c6d67b999013</id><title>Update documentation links and installer for debuman</title><published>2026-11-12T12:55:42+09:00</published><updated>2026-11-12T12:55:42+09:00</updated><author><name>debuman</name><email>110329244+debuman@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/sqzass/commit/a52b8272deec46fbe4dc86e1a388c6d67b999013.html"/><content type="text">Update documentation links and installer for debuman
</content></entry>
<entry><id>urn:git:8ce103a9335ea6dddb6ee39bac84462d2a6998f0</id><title>Keep the channel marker moving under prefers-reduced-motion</title><published>2026-08-01T01:01:03+09:00</published><updated>2026-08-01T01:01: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/sqzass/commit/8ce103a9335ea6dddb6ee39bac84462d2a6998f0.html"/><content type="text">Keep the channel marker moving under prefers-reduced-motion

The marker is not decoration — it is what tells you which channel is selected.
It was living below the early return that skips the hero sequence, so a reader
who asks for reduced motion got no marker at all and fell back to the tinted
label. That works, but it means the one piece of state on this control was being
dropped for an accessibility preference rather than the animation around it.

Positioning now happens before that return and takes one of two paths, chosen
once at load: Motion when animation is allowed, plain inline styles when it is
not. The two are never mixed on the same element, because Motion composes
transforms its own way and interleaving a hand-written `transform` with it
leaves the marker out of step. What reduced motion loses is the slide and the
squeeze; the marker still lands on the selected tab, immediately.

Verified by driving a real click in headless: with reduced motion the marker
ends at translateX(130px)/49px, the AUR tab, in both the DOM and a screenshot.
The animated path cannot be read the same way — Motion drives these through its
own rAF loop, which headless virtual time does not advance, so the computed
style stays put and `getAnimations()` is empty. Wrapping `Motion.animate` shows
the calls that matter do fire with the right targets: `{width:&quot;49px&quot;,x:130}`,
then `{scaleX:[0.72,1]}`, then the incoming panel.
</content></entry>
<entry><id>urn:git:edd9d7488f51f1d7d95589c2f1b8356933fd7f22</id><title>Publish a setup prompt for coding agents</title><published>2026-08-01T00:43:20+09:00</published><updated>2026-08-01T00:43: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/sqzass/commit/edd9d7488f51f1d7d95589c2f1b8356933fd7f22.html"/><content type="text">Publish a setup prompt for coding agents

An agent told to &quot;add a static site generator&quot; reaches for `cargo install
sqzass` and gets &quot;crate does not exist&quot; — sqzass is installed from a release
tarball or from git. That is the first of several things it cannot infer:
front matter is TOML fenced with `+++` rather than YAML, `@/` links are source
paths and not URLs, images take root-absolute paths instead, and broken
references are errors with no flag to downgrade them. It also cannot know that
runtime messages are Korean while the error identifiers are ASCII and stable,
which is the difference between parsing output and guessing at it.

`static/agent.md` is that briefing, served at /agent.md so an agent can be
pointed at one URL. It is the only copy — the docs link to it rather than
repeating it, so the two cannot drift.

On the landing it is a quiet button under the install card, not a fourth tab.
What it copies goes into a different tool than the commands above it, and
putting both in one tab strip would blur which is which. The button carries a
short standalone version of the prompt: enough to act on without a fetch, plus
the URL for the rest.

Two fixes fell out of building it. The copy handler bound to `.inst-copy`, so
reusing it meant inheriting a 2rem square icon-button box — the label spilled
out of it and off the left edge of the viewport. It now binds to
`[data-copy-text]`, which is what actually marks a copy button, and the handler
swaps a visible label when there is one instead of only the aria-label.

Verified both languages at 1100px in the settled state, and confirmed in the
rendered DOM that the marker and the new button coexist.
</content></entry>
<entry><id>urn:git:2b92dabed8ba5bf5501184232733e68458ef2ff9</id><title>Rebuild the install picker as one card with a sliding, squeezing marker</title><published>2026-08-01T00:29:19+09:00</published><updated>2026-08-01T00:29: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/sqzass/commit/2b92dabed8ba5bf5501184232733e68458ef2ff9.html"/><content type="text">Rebuild the install picker as one card with a sliding, squeezing marker

The previous pass put three pills above a floating command box. Two problems:
the pills and the box read as separate objects with only proximity to say one
drives the other, and the box resized to whatever command was showing, so the
copy button jumped roughly 300px between the AUR and cargo tabs. A control that
moves that far when you switch is a control you have to re-find.

Now it is one card: tab row, hairline divider, command. The panels are stacked
in a single grid cell rather than toggled with `display`, so the card is always
as wide as the longest command and nothing reflows on switch, and the copy
button is pinned to the right edge so it stays where you left it.

The motion is the site&#39;s own verb. This project is named for squeezing and the
hero already springs its emphasis line in on scaleX; the marker now does the
same thing — it compresses toward the direction of travel and releases into
place, and the incoming command expands from slightly over-wide. Reusing that
spring rather than inventing an easing keeps the page speaking one language.

Marker movement is progressive enhancement layered on top of the radio + CSS
selection that already worked: without JavaScript the checked label keeps a
tinted background, and `is-enhanced` hands that job to the marker only once the
script has measured the tabs. Position is recalculated after webfonts settle
and on resize, because tab widths change under both.

`x` and `scaleX` are animated as separate keys — animating the `transform`
string twice would have let the spring overwrite the translation and the marker
would never move.

Verified all three tabs in both themes at 1100px and 500px, and confirmed in the
rendered DOM that the marker lands on the checked tab.
</content></entry>
<entry><id>urn:git:455c1a70dadbd8c083c5c1a6c9b6577ca76328ab</id><title>Make the landing install a channel picker, and use yay for the AUR</title><published>2026-08-01T00:17:04+09:00</published><updated>2026-08-01T00:17:04+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/sqzass/commit/455c1a70dadbd8c083c5c1a6c9b6577ca76328ab.html"/><content type="text">Make the landing install a channel picker, and use yay for the AUR

The three channels were a big curl block plus a line of prose reading
&quot;With Rust instead: cargo install … · On Arch: paru -S sqzass&quot;. That is a list
of commands, not something a visitor can act on: the two alternatives are
small, unclickable, and share one copy button that only ever copies the curl
line.

Now the block has a row of pills — curl / cargo / AUR — and picking one swaps
the command and its copy button. The commands come from `[[extra.install]]` in
front matter, so adding a channel is a data change, not a template change.

Switching is radio inputs and CSS, no JavaScript. The install command is the
most important thing on this page and it should not wait for a script, so all
three are in the DOM and the first panel shows by default; `:has()` only moves
the selection. A browser without `:has()` still shows the curl command and
simply has inert tabs. Radios are also the honest semantics here — a screen
reader announces &quot;curl, radio button, checked, 1 of 3&quot;, which is what this
widget is.

The AUR command is `yay`, not `paru`. yay is what most Arch users have and what
comparable projects print; paru was an arbitrary pick on my part. The docs still
mention paru and makepkg as alternatives, where there is room to.

Caught while screenshotting: the panels were selected with `:nth-of-type`, which
counts every sibling `div` — including the tab row — so the AUR tab showed the
cargo command. Panels now live in their own container and are selected with
`:nth-child`. Verified all three tabs, both themes, at 1100px and 500px.
</content></entry>
<entry><id>urn:git:f9363cb454f1be97f6c89ad6426515c926b1137d</id><title>Show all three install channels on the landing page and in the docs</title><published>2026-08-01T00:04:23+09:00</published><updated>2026-08-01T00:04: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/sqzass/commit/f9363cb454f1be97f6c89ad6426515c926b1137d.html"/><content type="text">Show all three install channels on the landing page and in the docs

The landing offered the curl one-liner in a big block and `cargo install` on a
quiet line below it, and never mentioned the AUR package at all — which had
just been published. The docs page had the same gap in a different shape:
`cargo install` was a paragraph buried inside &quot;One line&quot;, so the table of
contents listed only two of the three channels.

The landing keeps one big block. Putting three commands at equal weight would
make &quot;which one do I pick&quot; the first question a visitor has, and the curl line
is the answer for someone who has nothing installed yet. The other two sit on
the alternatives line, separated by a middot that becomes a line break under
34rem so each gets its own row on a phone. Verified at 1440px and 500px with
headless screenshots.

The docs page promotes cargo to its own section, so the contents list now reads
&quot;One line / With Cargo / Arch Linux&quot; — the three channels, in the order of how
little you need to already have.

The README had no install instructions at all: it opened with `sqzass init`,
which assumes the binary is already there. It also still said &quot;There are no
prebuilt binaries yet&quot;, which stopped being true with v0.1.0 — three targets
ship with checksums.
</content></entry>
<entry><id>urn:git:fa3eac57026abcb2c908dd633dd86ca72233936a</id><title>배포 채널을 curl·cargo·AUR 셋으로 못박는다</title><published>2026-07-31T23:57:31+09:00</published><updated>2026-07-31T23:57:31+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/sqzass/commit/fa3eac57026abcb2c908dd633dd86ca72233936a.html"/><content type="text">배포 채널을 curl·cargo·AUR 셋으로 못박는다

Homebrew·Snap·Nix를 조사했고 셋 다 하지 않기로 했다. 코드로 만든 건 없어서
되돌릴 것도 없지만, 근거를 안 적어 두면 같은 조사를 다시 하게 된다.

셋을 고른 기준은 승인받을 사람이 없다는 것이다. 우리가 고치면 그날 반영되고,
대상도 겹치지 않는다 — curl은 CI와 서버, cargo는 Rust를 이미 가진 사람,
AUR은 Arch 사용자다.

안 하는 이유는 셋 다 &quot;안 된다&quot;가 아니다.

Homebrew는 core의 notability 기준에 걸린다(스타·포크·워처 전부 0). 자기 탭은
오늘도 되지만 릴리스의 macOS 타깃이 aarch64 하나뿐이라 소스 빌드 formula여야
하고, 그건 `cargo install`과 거의 같은 것을 준다.

Snap은 strict + home + network-bind로 기술적으로 된다. 이 도구는 홈 디렉터리
조회도 Command::new도 런타임 HTTP 클라이언트도 없다. 안 하는 건 관객 문제다 —
snap의 관객은 Ubuntu 데스크톱인데 이 도구의 사용자는 이미 셋을 가진 개발자이고,
배포 문서 1순위가 ubuntu-latest에서 cargo run으로 도는 GitHub Pages라 snapd가
낄 자리가 없다. 결정적으로 개발 머신이 Arch라 snapd도 LXD도 없어 검증할 수
없다. 눈으로 확인하기 전에 완료라고 하지 않는 레포에서 그건 그냥 못 하는 것이다.

Nix는 오히려 잘 맞는다. Cargo.lock이 커밋돼 있고 락에 git 소스가 0건이라
cargoLock.lockFile 한 줄로 벤더링이 끝나며, 네트워크 없는 네임스페이스에서
`cargo test --release --offline --locked`가 147개 전부 통과한다 — Nix 샌드박스가
요구하는 조건을 이미 만족한다는 뜻이다. 그런데도 미루는 건 이 머신에 nix가 없어
nix build를 돌려 볼 수 없고, Nix 사용자가 있다는 신호가 아직 없어서다.

다시 꺼낼 조건도 적어 뒀다. &quot;있으면 좋겠다&quot;가 아니라 그 생태계 사용자의 요청,
또는 검증할 환경이 생기는 것이다.

곁가지로 AUR README가 아직 &quot;올리기 전&quot;처럼 읽혀서 등록 완료 사실을 첫 줄에
적었다. 실제로 어제 올렸는데 문서만 그 이전 상태였다.
</content></entry>
<entry><id>urn:git:385fba8592cb5a31aaf8a71bc1bbd573cc695b6b</id><title>설치 문서에 AUR을 안내한다</title><published>2026-07-31T23:28:04+09:00</published><updated>2026-07-31T23:28:04+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/sqzass/commit/385fba8592cb5a31aaf8a71bc1bbd573cc695b6b.html"/><content type="text">설치 문서에 AUR을 안내한다

패키지를 올려 놓고 문서가 그 사실을 모르는 상태였다. Arch 사용자가 실제로 찾는
자리는 `paru -S`이지, curl 한 줄이 아니다.

`sqzass-bin`을 만들지 않은 이유도 같이 적는다. 패키지가 둘이면 어느 쪽이
최신인지 묻는 이슈가 생기고, Arch에서 Rust 도구를 소스에서 빌드하는 건 정상이다.
같은 근거가 packaging/aur/README.md에도 있다.
</content></entry>
<entry><id>urn:git:1bb6ed1a6668c271644e0ee5732ff89426beb457</id><title>AUR 의존을 ldd가 말하는 것에 맞추고 .SRCINFO를 넣는다</title><published>2026-07-31T23:20:19+09:00</published><updated>2026-07-31T23:20: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/sqzass/commit/1bb6ed1a6668c271644e0ee5732ff89426beb457.html"/><content type="text">AUR 의존을 ldd가 말하는 것에 맞추고 .SRCINFO를 넣는다

`depends=(&#39;gcc-libs&#39;)`가 두 가지로 틀렸다. Arch가 gcc-libs를 잘게 쪼개면서
그건 이제 libasan·libatomic·libgfortran·libgomp·libhwasan·liblsan·libobjc·
libquadmath·libstdc++·libtsan·libubsan까지 끌어오는 상위 패키지가 됐는데, 이
바이너리는 그중 하나도 열지 않는다. 반대로 정말 필요한 glibc는 빠져 있었다.
gcc-libs가 glibc를 전이 의존으로 물고 있어서 설치는 됐지만, 그건 맞아서가
아니라 우연이다.

빌드한 패키지에 ldd를 걸면 libc·libm(glibc)과 libgcc_s(libgcc)뿐이다.
공식 Rust 패키지들(ripgrep, fd, bat)도 전부 glibc + libgcc를 적는다. 그대로
맞췄다. oniguruma는 onig_sys가 정적으로 넣으므로 의존이 아니다 — bat이
oniguruma를 적는 것은 그쪽이 시스템 라이브러리를 링크하기 때문이다.

.SRCINFO는 AUR이 요구하는 파일인데 없었다. PKGBUILD 옆에 두면 AUR 저장소로
올릴 내용이 이 디렉터리의 사본이 되고, 버전을 올릴 때 둘이 어긋나면 diff에
보인다. `makepkg --printsrcinfo`로 생성했다.

makepkg로 실제로 빌드해 확인했다: 패키지에 들어가는 것은 /usr/bin/sqzass와
README, LICENSE 셋뿐이고, 꺼내 실행하면 `sqzass 0.1.0`이 나온다.
</content></entry>
<entry><id>urn:git:b769b53bb780166f4733c48f03d441e7a8cee435</id><title>ARM 리눅스용 바이너리를 릴리스에 추가한다</title><published>2026-07-31T23:10:34+09:00</published><updated>2026-07-31T23:10:34+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/sqzass/commit/b769b53bb780166f4733c48f03d441e7a8cee435.html"/><content type="text">ARM 리눅스용 바이너리를 릴리스에 추가한다

릴리스가 내는 타깃은 x86_64 리눅스와 Apple Silicon macOS 둘뿐이었다. 그래서
라즈베리파이·Graviton·Ampere 같은 ARM 리눅스에서는 install.sh가 &quot;지원하지 않는
플랫폼&quot;으로 끝나고 소스 빌드밖에 길이 없었다. AUR PKGBUILD는 이미
arch=(&#39;x86_64&#39; &#39;aarch64&#39;)로 선언해 두고 있어서, 소스 빌드는 되는데 바이너리
릴리스만 못 따라가던 셈이다.

크로스 컴파일이 아니라 네이티브 ARM 러너에서 짓는다. 크로스로도 바이너리는
나오지만 그러면 &quot;바이너리가 실제로 사이트를 만드는지&quot; 보는 스모크 스텝이 못
돈다. 링크만 되고 런타임에 죽는 조합을 잡으라고 둔 검사인데, 실행할 수 없는
아티팩트를 내보내는 건 그 검사를 포기하는 것이다.

install.sh는 `Linux-aarch64`와 `Linux-arm64`를 함께 받는다. 커널은 보통
aarch64로 답하지만 일부 이미지가 arm64로 답한다.

**타깃이 없는 릴리스를 만났을 때의 안내를 넣었다.** 타깃은 나중에 추가되므로
그 이전 릴리스에는 없고, v0.1.0이 바로 그 경우다. 이걸 잡지 않으면 ARM 사용자가
curl의 404 한 줄만 보고 무엇이 없는 건지 알 수 없다. 지금은 &quot;v0.1.0 릴리스에는
aarch64-unknown-linux-musl 빌드가 없습니다&quot;라고 말하고 소스 빌드로 안내한다.
가짜 uname으로 두 경로를 다 돌려 확인했다.

문서의 &quot;순수 Rust 엔진을 쓰는 유일한 빌드&quot;는 이제 둘이라 복수로 고치고, 타르볼
하나가 Debian·Fedora·Arch에서 그대로 도는 이유(정적 링크라 배포판을 구분하지
않는다)를 한 줄 적었다.
</content></entry>
<entry><id>urn:git:f7c2d41657b22bc97a3bbb9b7c80d3752ba307e1</id><title>같은 말을 두 번 하던 자리를 정리한다</title><published>2026-07-31T22:31:51+09:00</published><updated>2026-07-31T22:31:51+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/sqzass/commit/f7c2d41657b22bc97a3bbb9b7c80d3752ba307e1.html"/><content type="text">같은 말을 두 번 하던 자리를 정리한다

markdown 문서에 원시 HTML 문단이 두 번 있었다. &quot;## 원시 HTML&quot; 절과 &quot;키가 아닌
설정 둘&quot;의 첫 불릿이 거의 자구까지 같아서, 열 줄 간격으로 같은 말을 두 번 읽고
목차에도 사실상 같은 항목이 두 개 떴다. 영어 원문부터 그랬으니 편집 잔재다.
뒤쪽이 &quot;전역 스위치가 아니라 소스별 신뢰 등급&quot;을 더 담고 절 제목의 목적과도
맞으므로 앞 절을 지운다.

deploy/_index의 손으로 쓴 안내서 표도 같은 문제였다. section.html이 자식 목록을
이미 렌더하므로 같은 여섯 링크가 한 페이지에 두 번 나왔고, 설명문이 서로 달라
어느 쪽이 정본인지 알 수 없었다. 여섯 섹션 인덱스 중 손 목록을 둔 곳은 여기뿐이라
패턴도 어긋났다. 표에만 있던 좋은 문장은 각 페이지 description으로 옮기고 표를
지운다 — github-pages의 &quot;이 사이트가 배포되는 워크플로&quot;보다 표의 &quot;이 사이트가
있는 곳이자, 설계를 강제한 호스트&quot;가 낫다.

알림 상자 예제가 사실과 어긋났다. &quot;미리 빌드된 바이너리는 계획에 있지만 아직
배포하지 않았습니다&quot;가 예제로 실려 있는데 installation은 릴리스마다 붙는다고
하고 실제로 v0.1.0에 붙어 있다. 예제여도 렌더된 페이지에서는 진짜 NOTE로 보인다.
시효 없는 문구로 바꾼다.

toc 기준이 &quot;긴 글이냐&quot;가 아니라 &quot;섹션이냐&quot;로 갈려 있었다. H2 여덟에 129줄인
deploy/_index에는 목차가 없고 H2 넷에 58줄인 selection에는 있었다. front-matter가
스스로 &quot;toc는 저자의 의사&quot;라고 해 놓고 가장 긴 축인 _index들이 그 의사를 한 번도
행사하지 않은 셈이다. deploy와 content의 _index에 넣었다.

start/_index의 &quot;왜 sqzass인가&quot;는 랜딩 벤토 라벨과 제목이 겹치면서 내용은 달랐다.
랜딩에서 넘어온 독자가 방금 본 제목 아래 다른 목록을 다시 만난다. &quot;설계가 다른
지점&quot;으로 바꾼다.

configuration은 여섯 절 중 넷만 정본 링크를 달고 [nav]와 [build]만 아무 데도
가리키지 않았다. `date`가 왜 혼자 내림차순인지 거기서 알 길이 없었다. 둘 다
링크를 달아 파일이 예외 없이 &quot;키 표 + 정본 링크&quot;가 되게 한다.

description 문체가 명사형 24곳 대 완결문 6곳으로 갈려서, 섹션 인덱스의 자동 자식
목록에서 두 문체가 나란히 보였다. 다수인 명사형으로 맞춘다. 영어판도 같은 혼재라
양쪽을 함께 고쳤다.

installation의 &quot;다음은 첫 사이트 만들기입니다&quot;는 한국어판에만 있었다. 다음 단계
안내는 있는 편이 나으므로 영어판에 넣어 맞춘다.

벤치마크 페이지, deploy 인덱스, start 인덱스를 헤드리스로 띄워 목차·자식 목록·
막대가 의도대로 나오는지 눈으로 확인했다. 두 번 빌드해 바이트가 같은 것도 확인했다.
</content></entry>
<entry><id>urn:git:35fd132612a643c278cdb8738cba6079e17528cc</id><title>같은 개념을 파일마다 다르게 부르던 것을 정리한다</title><published>2026-07-31T22:26:42+09:00</published><updated>2026-07-31T22:26: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/sqzass/commit/35fd132612a643c278cdb8738cba6079e17528cc.html"/><content type="text">같은 개념을 파일마다 다르게 부르던 것을 정리한다

제목 앵커가 세 이름을 갖고 있었다 — start/_index는 &quot;heading anchor&quot;,
markdown은 &quot;anchor&quot;, links와 languages는 &quot;앵커&quot;. 한국어만 읽는 독자에게 이 셋이
같은 것이라는 보장이 없다. 본문 표기는 &quot;앵커&quot;로 모으고 설정 키
`heading_anchors`만 원문으로 둔다.

`강조`는 단순 통일이 아니라 오역 수정이었다. 이 한 단어가 마크다운 emphasis,
구문 강조, `hl_lines` 줄 표시 셋을 가리키고 있었다. highlighting의 &quot;강조는 줄
경계를 건너 이어집니다&quot;는 바로 위가 `&lt;mark class=&quot;hl-line&quot;&gt;` 설명이라 &quot;표시한
줄이 다음 줄까지 이어진다&quot;로 읽히는데, 영어 원문의 Highlighting은 구문 강조
쪽이다. 문맥이 흐려지는 자리에서는 &quot;구문 강조&quot;로 적는다.

콜아웃은 절 제목이 &quot;알림 상자&quot;, 같은 절 첫 문장이 &quot;콜아웃 문법&quot;, 설정 표가
&quot;콜아웃&quot;이었다. 설정 표를 보고 온 독자가 제목과 연결하려면 한 번 더 생각해야
한다. 설정 키가 `alerts`이므로 제목을 &quot;콜아웃&quot;으로 맞추고 키 이름도 본문에
적어 둔다.

벤치마크의 &quot;하이라이팅&quot;과 &quot;빌드 타임&quot;도 나머지 문서가 쓰는 &quot;구문 강조&quot;와
&quot;빌드 시점&quot;으로 맞춘다.

front matter 페이지 제목만 한국어 29개 중 유일하게 소문자로 시작했다. 사이드바와
브레드크럼에 그대로 드러나므로 영문판처럼 대문자로 쓴다 — 본문의 소문자
`front matter`는 개념어이고 이미 전 파일이 일치하니 그대로 둔다.

i18n의 `sections = &quot;문서 갈래&quot;`는 건드리지 않는다. 영문이 &quot;Sections&quot;라
불일치처럼 보이지만 ko.toml은 랜드마크 라벨을 서술형 우리말로 붙이는 규칙을
이미 따르고 있다 — breadcrumb=&quot;현재 위치&quot;, pagination=&quot;이어 보기&quot;. &quot;섹션&quot;으로
바꾸면 그 규칙이 이 키에서만 깨진다.
</content></entry>
<entry><id>urn:git:b9cb002cd6dced47feaac190891e74df1aa55d64</id><title>한국어에서 직역 자국을 지운다</title><published>2026-07-31T22:25:29+09:00</published><updated>2026-07-31T22:25: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/sqzass/commit/b9cb002cd6dced47feaac190891e74df1aa55d64.html"/><content type="text">한국어에서 직역 자국을 지운다

&quot;AI가 쓴 것 같다&quot;는 지적을 계량해 보니 전면적인 문제가 아니라 반복되는 패턴
넷이었다. 목소리는 그대로 두고 자국만 지운다.

`당신` 24회. 영어판의 you/your 119회가 그대로 넘어온 자리다. 한국어 기술 문서는
2인칭을 생략하고, 이 문서의 다른 문장 대부분이 이미 그렇게 쓰여 있었다. 한
문장에 두 번 나오는 자리도 있었다 — &quot;당신의 저장소에, 당신이 쓰고&quot;.

줄표 삽입구 뒤 조사 6곳. `빌드 락 — 를 지웠습니다`, `없음 — 는 플래그 하나
거리입니다`. 영어 em-dash 구문을 위치까지 그대로 옮겨서 닫는 줄표 뒤에 조사가
홀로 남았다. 한국어 문장이 아니다. 괄호로 바꾸거나 조사를 삽입구 앞으로 뺐다.

직역된 은유 10곳. caveat을 &quot;단서&quot;로 옮기면 실마리로 읽히고, gap과 absence의
대비는 한국어에서 두 말이 동의어라 성립하지 않고, 경쟁 조건의 window는 &quot;창&quot;이
아니라 &quot;틈&quot;이다. 그림이 안 넘어오면 뜻을 쓴다.

한국어에 없는 조어 셋. &quot;생성 코퍼스&quot;, &quot;바이트 동일&quot;, &quot;마크업 세분도&quot; — 영어
명사구를 한자어로 압축한 것들이라 절로 풀었다.

덤으로 피동 직역(&quot;신뢰된 것입니다&quot;), `~를 통해`, 코드 스팬 뒤에 띄어 쓴 조사
셋, `theme_light`과/와 조사 오류를 함께 고쳤다.

기준을 CLAUDE.md에 남긴다. 규칙이 없으면 다음 페이지에서 같은 자국이 다시
생긴다. 판정 기준은 &quot;번역이 맞느냐&quot;가 아니라 &quot;한국어 개발자가 이 문장을 처음부터
이렇게 썼겠느냐&quot;다.
</content></entry>
<entry><id>urn:git:ae201dbe3c856c305a32850cf6584eb90ec7d9fa</id><title>&quot;전부&quot;라고 약속한 레퍼런스의 구멍을 메운다</title><published>2026-07-31T22:18:49+09:00</published><updated>2026-07-31T22:18: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/sqzass/commit/ae201dbe3c856c305a32850cf6584eb90ec7d9fa.html"/><content type="text">&quot;전부&quot;라고 약속한 레퍼런스의 구멍을 메운다

front matter 문서가 &quot;페이지가 가질 수 있는 모든 필드&quot;라고 하면서 `aliases`를
빼먹었다. content.rs에 실재하는 필드이고, deploy 문서 둘이 이미 독자를 이쪽으로
보내고 있다 — cloudflare가 &quot;front matter의 aliases를 먼저 보세요&quot;라고 하고
deploy/_index는 출력물 표에 alias 스텁을 싣는다. 안내받고 온 독자가 도착지에서
아무것도 못 찾는 상태였다. 행을 넣고 안내 쪽에 링크도 걸었다.

`translation_key`의 기본값은 &quot;아래 참고&quot;였는데 그 아래에 설명이 없다. 실제
기본값(언어 접미사를 뗀 경로)을 적고 언어 문서로 넘긴다.

data 문서는 &quot;템플릿이 읽을 수 있는 모든 것&quot;이라면서 `site.feed`와 `page.date`가
없었다. 둘 다 render.rs에 있고, features/feeds가 이미 문서화하고 있고, 무엇보다
이 사이트 base.html이 `site.feed`를 쓴다 — 자기 템플릿이 쓰는 값이 자기 문서에
없었다. functions의 &quot;그 밖엔 없습니다&quot;가 이 표의 완결성을 한 번 더 보증하는
자리라 구멍이 두 배로 눈에 띈다.

features 섹션 소개는 자식 넷 중 셋만 셌다. `weight = 25`가 피드를 20과 30
사이에 끼워 넣은 흔적이고 그때 소개를 안 고쳤다. 이 문자열은 사이드바·검색
결과·llms.txt로 그대로 나간다. 피드를 넣고 weight도 10/20/30/40으로 폈다.
</content></entry>
<entry><id>urn:git:cf22d8f72b9e3848a3eb24a220bd52813ee018b5</id><title>없는 page_template도 빌드를 멈춘다</title><published>2026-07-31T22:17:23+09:00</published><updated>2026-07-31T22:17: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/sqzass/commit/cf22d8f72b9e3848a3eb24a220bd52813ee018b5.html"/><content type="text">없는 page_template도 빌드를 멈춘다

front matter `template`이 없는 이름을 가리키면 에러인데, 섹션의
`page_template`은 조용히 `page.html`로 떨어졌다. `templates.has(pt)`가
조건절에 섞여 있어서 참이 아니면 그냥 다음 단계로 흘렀다.

두 자리는 같은 규칙이어야 한다. CLAUDE.md가 불변식으로 적어 둔 것도 &quot;없는
템플릿은 에러&quot;다. 조용히 떨어지면 섹션 하나가 통째로 잘못된 템플릿으로
렌더되고, 빌드는 성공했다고 말한다. 알아채는 자리가 빌드가 아니라 브라우저다.

이걸 문서에 예외로 적는 선택지도 있었지만, 예외를 적는 건 불변식을 하나
무르는 것이라 코드를 불변식에 맞췄다. 에러 문구는 `template` 쪽과 같은 모양으로
없는 이름과 쓸 수 있는 템플릿 목록을 짚는다.

doctor는 `if let Ok(name)`으로 받으므로 영향이 없다 — 이제 그 페이지를 미사용
템플릿 집계에서 빼는데, 애초에 렌더되지 않을 페이지다.
</content></entry>
<entry><id>urn:git:03a1dc5a15e671a0939db725d9165d2770313f38</id><title>문서가 코드와 어긋난 자리를 코드에 맞춘다</title><published>2026-07-31T22:16:48+09:00</published><updated>2026-07-31T22:16:48+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/sqzass/commit/03a1dc5a15e671a0939db725d9165d2770313f38.html"/><content type="text">문서가 코드와 어긋난 자리를 코드에 맞춘다

적대적 리뷰가 잡은 것들이다. 각각 재현하거나 코드에서 근거를 확인했다.

`_index.md` 없는 디렉터리 설명이 정반대였다. site.rs의 build_tree는 그런
디렉터리도 섹션으로 승격시키고 테스트 directory_without_index_still_appears가
그걸 고정하는데, 문서는 &quot;섹션이 아니고 내비게이션에도 안 나온다&quot;고 했다. 실제로
빌드해 보면 title=디렉터리 이름, url=/guides/ 인 섹션이 나오고 다만
/guides/index.html은 생기지 않는다 — 그대로 적었다.

`![](@/images/x.png)`는 동작하지 않는다. 이미지에도 같은 리졸버가 붙는 건
맞지만 그 리졸버는 페이지 링크 표만 뒤지므로 static/ 이미지는 해석될 수 없고,
넣어서 빌드하면 실제로 멈춘다. 검증되는 쪽은 루트 절대 경로다 —
`![](/images/typo.png)`가 빌드를 멈추는 것까지 확인하고 문장을 뒤집었다.

`{{ home }}` 예제는 i18n 키 바로 밑에 놓여 &quot;i18n 키가 템플릿 변수가 된다&quot;로
읽힌다. 실제로는 이 사이트 base.html이 만든 지역 변수라, 맨 템플릿에 복붙하면
SemiStrict에서 렌더 에러다. site.base_path로 바꿨다.

`urlencode`와 `striptags`를 쓸 수 있는 필터로 나열하고 있었다. 문서가 나열한
필터를 하나씩 돌려 보니 둘 다 unknown filter다. tojson과 함께 &quot;없음&quot; 문단으로
옮겼다.

에러 문구 인용 넷이 옛 문구였고(&quot;해석할 수 없는 내부 링크&quot;), 설정 에러 인용은
식별자가 빠져 있었다. start/_index가 &quot;실제 문자열을 그대로 인용한다&quot;고 약속하는
자리라 손으로 옮기지 않고 바이너리를 돌려 출력을 복사했다.

벤치마크 막대 스무 개 중 둘만 실측과 어긋났다 — 우리 것, 그것도 가장 얇아질
minimal과 blog에서만 0.385%·0.373% 대신 0.6%였다. 나머지 열여덟은 소수점
셋째 자리까지 맞는다. 랜딩이 &quot;막대 길이는 실제 비율&quot;이라고 못박은 이상 실측을
넣는다.

github-pages 안내서에 배포 400 재시도가 빠져 있었다. 우리가 값을 치른 함정을
독자가 그대로 밟게 두는 셈이라 재시도와 그 이유를 넣고 workflow_dispatch도
맞췄다.

나머지: dev-server의 감시 대상(사이트 루트 전체가 아니라 네 디렉터리와
sqzass.toml), 검색 색인 크기(몇 KB가 아니라 ~80KB), search.js 줄 수,
하이라이트 출력의 pre/data-lang, 피드의 &quot;같은 날짜&quot;→&quot;같은 순간&quot;,
page.section·prev·next가 섹션 인덱스에서 비는 것, 색인 생성 조건.
</content></entry>
<entry><id>urn:git:f2732eb8767e3b339687bf50ea1f549ace44b4c7</id><title>릴리스에 태그 없는 이름의 아티팩트 사본을 붙인다</title><published>2026-07-31T22:07:07+09:00</published><updated>2026-07-31T22:07:07+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/sqzass/commit/f2732eb8767e3b339687bf50ea1f549ace44b4c7.html"/><content type="text">릴리스에 태그 없는 이름의 아티팩트 사본을 붙인다

배포 안내서 여섯이 전부
`releases/latest/download/sqzass-x86_64-unknown-linux-musl.tar.gz`를 쓰는데,
Package 스텝이 만드는 이름은 `sqzass-&lt;태그&gt;-&lt;타깃&gt;.tar.gz`다. 태그가 파일명에
박혀 있으니 그 URL은 존재한 적이 없는 파일을 가리켰고, netlify·vercel·
cloudflare·gitlab·codeberg 안내서를 그대로 따라 한 사람은 curl 단계에서 404를
받았다.

`latest/download/&lt;파일명&gt;`은 파일명이 릴리스마다 같아야 쓸 수 있다. 고칠 곳을
문서 열두 군데로 잡으면 릴리스마다 열두 군데를 다시 고쳐야 하므로, 대신 태그
없는 이름의 사본을 하나 더 낸다. 태그 붙은 이름은 그대로 둔다 — install.sh가
그 이름을 조립하고, 사본은 순수 추가다.

v0.1.0은 이미 나갔으므로 같은 사본을 손으로 올려 두었다. tar 안의 디렉터리는
여전히 태그를 갖지만 문서가 쓰는 --strip-components=1이 이름과 무관하게
벗겨내므로 영향 없다. 문서의 curl 명령을 그대로 실행해 200과 정상 압축 해제,
`sqzass 0.1.0`까지 확인했다.

installation은 `latest/download/sqzass-v0.1.0-...`이라 지금은 되지만 다음
릴리스에서 같은 이유로 깨진다. 같이 고정 이름으로 옮긴다.
</content></entry>
<entry><id>urn:git:388107b49d085ebd8870f85e11c8665a30132802</id><title>Drop the colophon note from the footer</title><published>2026-07-31T16:59:03+09:00</published><updated>2026-07-31T16:59: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/sqzass/commit/388107b49d085ebd8870f85e11c8665a30132802.html"/><content type="text">Drop the colophon note from the footer

The &quot;built with sqzass&quot; line restated what the THIS BUILD column
already demonstrates -- the linked files are the evidence, the sentence
was narration. Remove the line, its built_with strings, and the
.colophon-note rules from both stylesheets.
</content></entry>
<entry><id>urn:git:39df997018af487c27ff10927747d42fef90ada8</id><title>Restore the logo&#39;s original line weight</title><published>2026-07-31T16:50:45+09:00</published><updated>2026-07-31T16:50:45+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/sqzass/commit/39df997018af487c27ff10927747d42fef90ada8.html"/><content type="text">Restore the logo&#39;s original line weight

Thickening the mark cannot be reconciled with looking like the artwork:
at any scale, dilation closes the narrow gaps between the fingers and
the crease marks, and those hairlines are what the mark reads by. Tried
dilating at the high-res original (proportional 13px kernel, then
downscale) and the hand still fuses into a claw. The favicon keeps its
medium weight -- at 16px legibility wins -- but the logo renders at
26px and up, where fidelity wins.
</content></entry>
<entry><id>urn:git:00ad58f9cf1f5ce16df0f08bef9e073c7f626864</id><title>Leave dilation headroom so the logo edges are not clipped</title><published>2026-07-31T16:44:38+09:00</published><updated>2026-07-31T16:44: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/sqzass/commit/00ad58f9cf1f5ce16df0f08bef9e073c7f626864.html"/><content type="text">Leave dilation headroom so the logo edges are not clipped

The previous logo was extracted at the tight bbox and then dilated, so
strokes that already touched the canvas edge grew outward into nothing
and rendered flat-cut. Keep the same 121x120 canvas but fit the mark at
117x116 with a 2px margin before dilating; the margin absorbs the
growth and all four edges now carry zero ink.
</content></entry>
<entry><id>urn:git:496ce51e6c04b1d380fc17c166c5831212abdbf1</id><title>Give the logo the favicon&#39;s medium line weight</title><published>2026-07-31T16:38:49+09:00</published><updated>2026-07-31T16:38: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/sqzass/commit/496ce51e6c04b1d380fc17c166c5831212abdbf1.html"/><content type="text">Give the logo the favicon&#39;s medium line weight

The favicon already thickens the mark by one dilation pass because the
original hairlines dissolve at small sizes; the logo in the masthead and
footer renders at ~26 CSS px and has the same problem. Re-extract the
mark from the high-res original (same tight bbox at threshold 128, which
lands on the same 121x120 box) and apply the same single dilation at the
final size, so every placement of the mark carries the same weight.

The 180px apple-touch icon keeps the original line weight on purpose --
at that size the thin lines are the detail, not a legibility problem.
</content></entry>
<entry><id>urn:git:5b57171dc97d571b6c02a510c366e8189f35f4b0</id><title>랜딩 푸터를 docs와 같은 colophon으로 통일한다</title><published>2026-07-31T16:31:18+09:00</published><updated>2026-07-31T16:31:18+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/sqzass/commit/5b57171dc97d571b6c02a510c366e8189f35f4b0.html"/><content type="text">랜딩 푸터를 docs와 같은 colophon으로 통일한다

푸터는 사이트 어디서나 같은 물건으로 보여야 한다. 두 벌을 두면 반드시
어긋나므로 마크업을 partials/colophon.html 하나로 빼서 base.html과
landing.html이 같은 파일을 include한다. docs 쪽 렌더 출력은 변경 전과
byte-identical함을 확인했다.

스타일은 main.css의 .colophon 블록을 landing.css의 토큰으로 옮겨 적었다.
docs의 회색 두 단(ink-2/ink-3)은 랜딩 팔레트에 --muted 하나뿐이라 한 단으로
눕는 것이 유일한 차이다. 한국어 예외(h2에 모노·대문자 미적용)와 모바일 1열
붕괴도 docs와 같은 계약을 따른다.

landing 전용이던 foot_line 키와 죽은 셀렉터(.foot [data-reveal])는 지운다.
</content></entry>
<entry><id>urn:git:510888c280b2ac030376d6cc8d7c49c4fb2e64a2</id><title>파비콘은 꽉 채우고, apple-touch는 불투명 정사각형으로</title><published>2026-07-31T00:13:04+09:00</published><updated>2026-07-31T00:13:04+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/sqzass/commit/510888c280b2ac030376d6cc8d7c49c4fb2e64a2.html"/><content type="text">파비콘은 꽉 채우고, apple-touch는 불투명 정사각형으로

탭에서 마크가 작아 보이던 건 여백(로고 82%)과 깊은 라운드 탓 —
로고 94%, 라운드 18%로 채운다. iPad의 흰 테두리는 apple-touch에
투명 모서리를 준 탓이다: iOS는 투명을 흰색으로 메우고 모서리를
직접 깎으므로, 라운드 없는 불투명 검정 정사각형에 로고를 80% 안전
영역으로 앉혀야 한다.
</content></entry>
<entry><id>urn:git:73c28a19f049c83f9738a76c888fb8c83f7f1150</id><title>아이콘을 고해상 원본에서 다시 뽑는다 — 진짜 180px apple-touch 추가</title><published>2026-07-31T00:07:19+09:00</published><updated>2026-07-31T00:07: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/sqzass/commit/73c28a19f049c83f9738a76c888fb8c83f7f1150.html"/><content type="text">아이콘을 고해상 원본에서 다시 뽑는다 — 진짜 180px apple-touch 추가

원본(2816×1536, ~/Downloads)에서 마크를 타이트 bbox로 잘라(흐린
워터마크는 임계값 128로 배제, 크롭은 흰 패딩 후라 경계 침범 없음)
파비콘 128을 다시 만들고, favicon.png을 업스케일해 쓰던
apple-touch-icon을 원래 선 굵기의 진짜 180px로 교체한다. 파비콘은
중간 굵기 유지 — 16px 가독은 굵힘이, 180px 디테일은 원본 선이 맡는다.
</content></entry>
<entry><id>urn:git:424562a1026cc7957df529e461c0466ce5e409ff</id><title>파비콘 선을 중간 굵기(1회 팽창)로 잡는다</title><published>2026-07-30T23:59:28+09:00</published><updated>2026-07-30T23:59: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/sqzass/commit/424562a1026cc7957df529e461c0466ce5e409ff.html"/><content type="text">파비콘 선을 중간 굵기(1회 팽창)로 잡는다
</content></entry>
<entry><id>urn:git:aea29c3dfa4fd8d04e125acef1c33f80b404289b</id><title>파비콘 선을 원래 굵기로 되돌린다</title><published>2026-07-30T23:56:28+09:00</published><updated>2026-07-30T23:56: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/sqzass/commit/aea29c3dfa4fd8d04e125acef1c33f80b404289b.html"/><content type="text">파비콘 선을 원래 굵기로 되돌린다
</content></entry>
<entry><id>urn:git:5e45e6dc8da003ac51ab630e980c131e672cede0</id><title>파비콘을 검정 배경 + 흰 로고로 바꾼다</title><published>2026-07-30T23:52:24+09:00</published><updated>2026-07-30T23:52:24+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/sqzass/commit/5e45e6dc8da003ac51ab630e980c131e672cede0.html"/><content type="text">파비콘을 검정 배경 + 흰 로고로 바꾼다
</content></entry>
<entry><id>urn:git:1aa04e0737d240e96e961b72b5ec4b5760cca618</id><title>푸터에 특색 있는 태그라인, 파비콘을 배경 있는 브랜드 마크로</title><published>2026-07-30T23:48:24+09:00</published><updated>2026-07-30T23:48:24+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/sqzass/commit/1aa04e0737d240e96e961b72b5ec4b5760cca618.html"/><content type="text">푸터에 특색 있는 태그라인, 파비콘을 배경 있는 브랜드 마크로

푸터 한 줄이 무색무취라는 지적 — &#39;쥐어짜는&#39; 정체성을 넣어 다시
쓴다(A static site generator in Rust that squeezes a thousand pages
into 25 ms). 파비콘은 얇은 검정 선화라 탭에서 안 보이던 것을 코랄
그라디언트 라운드 사각형 + 두 번 굵힌 흰 선으로 교체 — 불투명 유색
배경이라 밝은 탭·어두운 탭 어디서든 구분된다. 두 변형(라이트·다크)이
같은 이유이기도 하다.
</content></entry>
<entry><id>urn:git:3c650817ed2b88f8d25d2a53d11828bdd60a1b75</id><title>푸터에 사이트 소개 한 줄을 되살린다 — t(tagline) 재사용</title><published>2026-07-30T23:42:21+09:00</published><updated>2026-07-30T23:42:21+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/sqzass/commit/3c650817ed2b88f8d25d2a53d11828bdd60a1b75.html"/><content type="text">푸터에 사이트 소개 한 줄을 되살린다 — t(tagline) 재사용
</content></entry>
<entry><id>urn:git:c4f8c801759ddfa68358ce8deed24139dc7b43dc</id><title>설치 한 줄을 히어로로 올리고, 군더더기 타일과 번역투를 걷어낸다</title><published>2026-07-30T23:35:21+09:00</published><updated>2026-07-30T23:35:21+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/sqzass/commit/c4f8c801759ddfa68358ce8deed24139dc7b43dc.html"/><content type="text">설치 한 줄을 히어로로 올리고, 군더더기 타일과 번역투를 걷어낸다

사용자 피드백 셋. curl 한 줄이 제일 중요하므로 Get started 바로
아래로 올려 악센트 테두리로 세운다(cargo 대안은 그 밑 한 줄).
&#39;한국어가 1급&#39;과 &#39;문서가 곧 데모&#39; 타일은 뺀다 — 벤토는 벤치 2×2,
0바이트, 바이너리, 깨진 참조(와이드) 넷으로 정리. 한국어 카피는
번역투(&#39;실패는 크게 말합니다&#39;)를 버리고 사람이 쓰는 문장으로 전부
다시 썼다.
</content></entry>
<entry><id>urn:git:a223a19c857b853301b388d2d8b8a0d07a6816af</id><title>랜딩을 브랜드대로 다시 세운다 — squeeze, 양테마, 글래스 내비, 벤토</title><published>2026-07-30T23:03:31+09:00</published><updated>2026-07-30T23:03:31+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/sqzass/commit/a223a19c857b853301b388d2d8b8a0d07a6816af.html"/><content type="text">랜딩을 브랜드대로 다시 세운다 — squeeze, 양테마, 글래스 내비, 벤토

sqzass는 squeeze+ass고 로고는 엉덩이를 쥔 손이다 — 심해·오징어로
읽은 2차안은 전제부터 틀렸다. 3차안은 이름을 그대로 디자인으로:
카피가 &#39;쥐어짜다&#39;를 말하고(A thousand pages, squeezed into 25 ms /
천 페이지를 25밀리초로 쥐어짭니다), 강조 줄은 스프링으로 옆에서
눌려 들어오고, 버튼과 타일은 누르면 움츠러들고, 히어로 뒤에는
말랑하게 뭉개지는 블롭이 있다. 팔레트는 피치/코랄 — docs 악센트와
같은 warm 계열.

구조도 사용자 요구대로 갈아엎었다: 고정 반투명(blur) 내비 + 테마
토글(라이트/다크, docs와 같은 data-theme·localStorage 키를 공유해
사이트 전체에서 선택이 따라다닌다), 섹션 나열 대신 벤토 그리드
(벤치 2×2·설치·0바이트·기능 3·데모), 터미널 세션 블록은 반려로
삭제. 레퍼런스(hwaro·gori)는 품질 기준으로만 쓰고 룩은 빌리지
않았다.
</content></entry>
<entry><id>urn:git:fa79d021fec6ababd54fd1eb4e820fdca1857748</id><title>랜딩을 심해 발광으로 다시 그린다</title><published>2026-07-30T22:48:11+09:00</published><updated>2026-07-30T22:48:11+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/sqzass/commit/fa79d021fec6ababd54fd1eb4e820fdca1857748.html"/><content type="text">랜딩을 심해 발광으로 다시 그린다

첫 판은 밝은 기록지 콘셉트였는데 사용자가 상용 수준이 아니라고
반려했다. hwaro·gori를 실렌더로 뜯어 공통 언어를 확인했다: 칠흑
캔버스, 초대형 헤드라인, 그라디언트 한 포인트, 동양화풍 아트워크,
설치 칩, 앰비언트 장식. 우리 축은 로고(오징어)에서: 먹물·심해·
생물발광. 손으로 쓴 인라인 SVG 먹물결 원판(외부 이미지 없음),
발광 시안 그라디언트, box-shadow 점묘 플랑크톤, JetBrains Mono
셀프호스트(woff2 21KB×2), 히어로 설치 칩에 복사 버튼(t(copy)/
t(copied) 재사용), 퀵스타트는 실행 원문 그대로의 터미널 세션.

검증에서 잡은 함정: 헤드리스가 스크립트 시점엔 780×493으로
레이아웃한 뒤 리사이즈해서, inView 리빌이 &#39;화면 밖&#39;으로 판정돼
캡처에만 안 나온다 — 실브라우저 동작은 정상. 레이아웃 검증은
--force-prefers-reduced-motion(연출 전량 스킵 경로)으로 찍는 걸로
레시피를 바꿨다. 좁은 window-size도 무시된다(500px부터 유효).
</content></entry>
<entry><id>urn:git:e9079c27d6bd627aa70360db81ff3822c89888f1</id><title>퀵스타트를 진짜 한 줄로 만든다</title><published>2026-07-30T22:28:04+09:00</published><updated>2026-07-30T22:28:04+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/sqzass/commit/e9079c27d6bd627aa70360db81ff3822c89888f1.html"/><content type="text">퀵스타트를 진짜 한 줄로 만든다

사용자 지적 둘. 푸터의 상태 문구(&#39;early… no theme system yet&#39;)는 뺀다
— 정직은 문서 몫이지 랜딩 푸터의 몫이 아니다. 설치는 curl/tar/install
석 줄 대신 한 줄 두 개로: 사이트가 서빙하는 install.sh(| sh)와 cargo
install --git. 스크립트는 플랫폼 판별→최신 태그(릴리스 리다이렉트에서,
API는 공유 IP에서 403)→체크섬 검증→install 네 가지만 하고, 검증 도구가
없으면 중단한다. 로컬 실행으로 끝까지 검증했다. 설치 문서에도 &#39;한 줄&#39;
절을 앞세운다.
</content></entry>
<entry><id>urn:git:d81d8ac33984449a05ece527b892d3dbf36d765b</id><title>홈을 랜딩 페이지로 바꾼다 — 측정 기록지</title><published>2026-07-30T22:14:15+09:00</published><updated>2026-07-30T22:14:15+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/sqzass/commit/d81d8ac33984449a05ece527b892d3dbf36d765b.html"/><content type="text">홈을 랜딩 페이지로 바꾼다 — 측정 기록지

루트를 base.html 밖의 독립 템플릿(landing.html)으로 교체한다. 문서
크롬도 테마 토글도 없는 단일 테마 포스터다 — 하이라이트된 코드가 없어
[data-theme]와 결합할 이유가 없다. 카피는 전부 양어 _index의 extra에
있고(SemiStrict가 키 비대칭을 빌드 에러로 잡는다), 본문 두 문단만
검색 색인에 들어간다.

비주얼은 문서(다크+주황)와 반대 축이다: 밝은 시트, 쪽빛 인디고, 왼쪽
눈금자 레일, 실척 벤치마크 막대(Hugo=100%, SVG 표현 속성 — 인라인
style 금지 유지). 연출은 벤더링한 motion 바닐라 번들(self-hosted,
CDN 없음)로 등장/스크롤 리빌만 주고, reduced-motion이면 전부 끈다.
JS가 없어도 페이지는 CSS만으로 완성 상태다.

밀려난 홈 소개문(왜 sqzass인가·상태)은 start/_index로 옮겼다.
</content></entry>
<entry><id>urn:git:44826ec464e259b1b8ceefe15823f279c9a17150</id><title>첫 릴리스가 나왔으니 &#39;아직 릴리스 없음&#39; 단서를 지운다</title><published>2026-07-30T20:57:24+09:00</published><updated>2026-07-30T20:57:24+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/sqzass/commit/44826ec464e259b1b8ceefe15823f279c9a17150.html"/><content type="text">첫 릴리스가 나왔으니 &#39;아직 릴리스 없음&#39; 단서를 지운다
</content></entry>
<entry><id>urn:git:4a0fa2111beb4209c448545d99f20d4f23942b70</id><title>AUR PKGBUILD를 v0.1.0으로 확정한다 — options=(!lto)가 필수다</title><published>2026-07-30T20:56:13+09:00</published><updated>2026-07-30T20:56:13+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/sqzass/commit/4a0fa2111beb4209c448545d99f20d4f23942b70.html"/><content type="text">AUR PKGBUILD를 v0.1.0으로 확정한다 — options=(!lto)가 필수다

makepkg 기본 OPTIONS의 lto가 CFLAGS에 -flto를 넣어 onig_sys의 C
오브젝트를 GCC LTO 비트코드로 만들고, rustc의 링커(lld)는 그걸 못 읽어
onig_* 심볼이 전부 undefined로 떨어진다. 실제 makepkg로 재현하고
!lto로 빌드·테스트·스모크까지 확인했다. 체크섬은 v0.1.0 태그
타르볼로 채웠다.
</content></entry>
<entry><id>urn:git:81bfdfbf05edd0afb86c4c8fa852a7c710e40252</id><title>벤치마크 페이지에서 머신 정보와 군말을 뺀다</title><published>2026-07-30T20:23:59+09:00</published><updated>2026-07-30T20:23: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/sqzass/commit/81bfdfbf05edd0afb86c4c8fa852a7c710e40252.html"/><content type="text">벤치마크 페이지에서 머신 정보와 군말을 뺀다

도입부 두 문단과 &#39;주장하지 않는 것&#39; 섹션은 방법 섹션·bench/와 겹치는
말이라 지운다. 머신 불릿과 results JSON의 machine 필드는 개인 시스템
정보라 걷어낸다 — 재현 방법은 bench/의 스크립트가 그대로 말한다.
</content></entry>
<entry><id>urn:git:c3887dedb76f84b5a7356e05654c5eaee22fdffc</id><title>같은 머신, 같은 코퍼스로 잰 벤치마크 페이지를 단다</title><published>2026-07-29T12:06:10+09:00</published><updated>2026-07-29T12:06: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/sqzass/commit/c3887dedb76f84b5a7356e05654c5eaee22fdffc.html"/><content type="text">같은 머신, 같은 코퍼스로 잰 벤치마크 페이지를 단다

Hugo, Zola, Jekyll, Astro와 시나리오 4종(minimal / blog / heavy·반복 /
heavy·고유)에서 대조했다. 1,000페이지, 마크다운 본문은 도구 간 바이트
동일, 콜드 3회 중앙값, taskset 4코어. heavy를 반복/고유로 가른 이유를
페이지가 직접 말한다 — 코드 반복도가 순위를 가르는 변수인데 대부분의
벤치마크는 자기 코퍼스의 반복도를 공개하지 않는다.

중앙값: minimal 18ms(2위 Hugo 93ms), blog 25ms(112ms), heavy·반복
187ms(3,559ms), heavy·고유 1,298ms(5,912ms). 다섯 도구 모두 빌드 타임
하이라이팅을 켰고, sqzass는 다섯 중 가장 세밀한 마크업(span 3,205 vs
Hugo 600)과 검색 색인·sitemap·llms.txt까지 만들면서 낸 수치다.

재현 스크립트(gen.py, bench.py)와 원본 데이터는 bench/에 있다. 밀리초는
머신을 타지만, 정의와 하네스가 공개면 비율은 재현된다. 페이지의 모든
수치·막대 폭은 results JSON과 기계 대조로 확인했다.

막대는 SVG rect의 표현 속성으로 그린다 — 인라인 style은 이 사이트가
지켜 온 strict style-src CSP 가능성을 깨기 때문이다. 도구 식별은 행
라벨이, 수치는 옆의 텍스트가 운반하므로 색이 정보를 단독으로 나르지
않는다.
</content></entry>
<entry><id>urn:git:68d97a07d0c94cbd76f6994e881f6a77c6a0f9aa</id><title>본문 있는 단일 페이지 섹션은 빈 섹션이 아니다</title><published>2026-07-29T12:06:10+09:00</published><updated>2026-07-29T12:06: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/sqzass/commit/68d97a07d0c94cbd76f6994e881f6a77c6a0f9aa.html"/><content type="text">본문 있는 단일 페이지 섹션은 빈 섹션이 아니다

empty-section은 만들다 만 디렉터리를 잡는 규칙인데, _index.md가 본문을
가진 단일 페이지 섹션까지 잡았다 — 눌렀을 때 보이는 건 빈 목록이 아니라
그 본문이므로 경고할 결함이 없다. 벤치마크 섹션이 정확히 그 구조라서
드러났다. 페이지도 본문도 없을 때만 경고하고, doctor 검사 표의 서술도
그렇게 고친다.
</content></entry>
<entry><id>urn:git:bf1a9dcb0855c5d03db06ab3bcbb80ed1f63fb52</id><title>구문 강조 엔진을 이중화한다 — 기본 Oniguruma, musl 아티팩트만 pure-rust</title><published>2026-07-29T10:59:37+09:00</published><updated>2026-07-29T10:59:37+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/sqzass/commit/bf1a9dcb0855c5d03db06ab3bcbb80ed1f63fb52.html"/><content type="text">구문 강조 엔진을 이중화한다 — 기본 Oniguruma, musl 아티팩트만 pure-rust

fancy-regex를 고른 원래 근거는 &quot;빌드 타임 작업이라 속도차 무의미&quot;였다.
1000페이지·전 블록 고유 코드 실측이 그 전제를 반증했다: 렌더 시간의
사실상 전부가 syntect 정규식이고, onig로 3.9s → 1.26s(3.1배)다.
minimal도 41ms → 20ms, 바이너리는 1.1MB 작아진다. musl 정적 링크를
깨는 건 여전히 C 바인딩이므로, 결정을 통째로 뒤집는 대신 깨지는
지점에만 원래 결정을 남긴다: 릴리스의 musl 잡만 --no-default-features
--features pure-rust로 빌드한다. 그 플래그가 유일한 가드다 — musl-gcc는
onig_sys 링크에 성공할 수도 있어서, 링크 실패는 순수 Rust를 지켜 주지
않는다.

엔진 피처는 [features] 경유로만 켠다. syntect의 엔진 피처를 직접 켜면
엔진=fancy + two-face 덤프=onig 어긋남이 컴파일은 되면서 일부 정규식을
조용히 평문으로 강등시킨다. 둘 다 켜지면 syntect·two-face 모두 onig로
수렴하므로(--all-features를 쓰는 AUR 경로 포함) 상호 배타 가드는 필요
없고, 피처 없음만 우리 이름으로 말하는 compile_error로 막는다.

두 덤프는 같지 않다. fancy 덤프는 문법 7개(PowerShell, JavaScript
(Babel), Salt State, ARM Assembly 등)를 통째로 빼므로 그 펜스는 정적
아티팩트에서 에러 없이 평문이 된다. ```js조차 두 빌드가 다른 문법
(Babel vs plain)으로 해석해 마크업 구조가 다르다. 이 차이는 버그가
아니라 installation.md가 지는 공개 계약이고, CI는 새 pure-rust 잡으로
엔진별 재현성만 지킨다 — 크로스 엔진 바이트 비교는 애초에 성립하지
않는다.

세 조합(기본/pure-rust/--all-features) 테스트 145개 전부 통과, 재작성 0.
onig 이중 빌드 결정성 확인. pure-rust여도 C 컴파일러는 필요하다 —
rustc가 링커 드라이버로 쓴다. 사라지는 것은 C 소스 컴파일이지 툴체인이
아니고, 문서도 정확히 그렇게 말한다.
</content></entry>
<entry><id>urn:git:06799ca4f8d7fe6fe3cb7b5580aa96cf0a41dad5</id><title>같은 코드 블록은 빌드에서 한 번만 하이라이트한다</title><published>2026-07-29T00:20:41+09:00</published><updated>2026-07-29T00:20: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/sqzass/commit/06799ca4f8d7fe6fe3cb7b5580aa96cf0a41dad5.html"/><content type="text">같은 코드 블록은 빌드에서 한 번만 하이라이트한다

문서 사이트는 설치 명령과 보일러플레이트가 페이지마다 반복되고, syntect
파싱이 렌더 비용의 사실상 전부다. (문법명, hl_lines, 코드) 키의 빌드
수명 블록 캐시로 반복 블록을 한 번만 칠한다. 인스턴스는 페이지마다지만
캐시는 Renderer가 소유해 빌드 전체가 공유한다 — 페이지별 상태가 아니라
순수 결과라서다.

키가 해시가 아니라 튜플 그 자체인 이유: 64비트 해시 충돌은 다른 코드
블록을 조용히 바꿔치기한다. 이 도구가 막겠다고 한 부류의 실패라, 코드
사본 하나를 더 드는 쪽을 택한다. name=은 키에 없어도 안전하다 — 라벨은
하이라이트 결과(body)가 아니라 code 태그의 속성으로 나간다.

에러가 난 블록은 캐시하지 않는다. hl_lines 범위 초과가 캐시되면 같은
블록을 쓰는 두 번째 페이지부터 빌드가 조용히 통과한다.

측정(1000페이지, 4코어): 코드가 반복되는 heavy 4.0s → 224ms. 전 블록이
고유한 최악 코퍼스는 캐시 무득으로 3.9s다. 산출물은 캐시 유무와 무관하게
diff -r 바이트 동일.
</content></entry>
<entry><id>urn:git:b13943df93b47729b60db081e55ce674b50e18e8</id><title>페이지 렌더를 병렬로 돌린다 — 결정성은 병합이 지킨다</title><published>2026-07-29T00:20:41+09:00</published><updated>2026-07-29T00:20: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/sqzass/commit/b13943df93b47729b60db081e55ce674b50e18e8.html"/><content type="text">페이지 렌더를 병렬로 돌린다 — 결정성은 병합이 지킨다

코드가 많은 코퍼스의 빌드 시간은 사실상 전부 syntect 하이라이팅이다
(1000페이지 heavy 실측: --profile render 랩 10.0s / 전체 11.6s).
페이지 렌더는 서로 완전히 독립이라 이 비용은 코어 수로 나눌 수 있다.

병렬을 막던 논거는 둘 다 소멸했다. &quot;출력 URL 충돌&quot;은 렌더와 병합을
분리하면 생기지 않는다 — 각 결과를 페이지 인덱스 자리에 꽂고 입력
순서대로 합치므로, 출력 바이트도 실패 시 어느 에러가 보이는지도 스레드
수와 무관하다. &quot;이 크기에서 이득 없음&quot;은 위 실측이 반증한다.

분배는 원자 카운터 훔치기다 — 의존성 없이 스무 줄이고, 코드 블록이
많은 페이지와 없는 페이지의 비용 차가 커서 미리 나누는 청크 분배는
가장 느린 청크를 기다리게 된다. 하이라이터 인스턴스는 페이지마다 새로
만든다: pending(펜스 옵션 핸드오프)과 errors가 페이지별 상태라,
공유하면 병렬에서 서로의 옵션과 에러가 섞인다. 8스레드×50회 동시 렌더
테스트가 이를 고정한다.

측정(1000페이지, 4코어, 3회 중앙값): heavy 11.6s → 4.0s, blog 64ms →
44ms. 변경 전 직렬 바이너리와 docs 58p·heavy 1002p 산출물 diff -r
바이트 동일, taskset 1워커 == 4워커 == 8워커 동일과 4워커 반복 빌드
동일까지 확인했다.
</content></entry>
<entry><id>urn:git:cc7fc4ea0123bd404740c01a1dcfa2596e213c40</id><title>복사 버튼 라벨을 선택에서 뺀다</title><published>2026-07-28T21:33:17+09:00</published><updated>2026-07-28T21:33:17+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/sqzass/commit/cc7fc4ea0123bd404740c01a1dcfa2596e213c40.html"/><content type="text">복사 버튼 라벨을 선택에서 뺀다

버튼이 pre 안에 있어서 블록을 통째로 긁어 복사하면 코드 끝에 &quot;Copy&quot;가
따라왔다. 헤드리스 실측: pre 전체 선택의 직렬화가 `…let c = 3;\nCopy`.
user-select: none 한 줄로 코드만 남는다 — 줄 번호 레시피가 gutter에 쓰는
것과 같은 이유다.

같은 실측에서 확인한 것: 복사 버튼(code.textContent)은 mark·빈 줄·라벨이
있어도 원문과 바이트 동일하고, 드래그 선택은 display:block mark 경계에서
개행을 이중으로 넣지 않는다. 드래그 복사에서 마지막 개행 하나가 빠지는
것은 mark 없는 블록에서도 같은, 브라우저 선택 직렬화의 표준 동작이다.
</content></entry>
<entry><id>urn:git:be2d3a70e22008fab2ba635124f2c5087cb4f28f</id><title>템플릿 문서에서 사라진 site.base_url 잔재를 지운다</title><published>2026-07-28T21:26:34+09:00</published><updated>2026-07-28T21:26:34+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/sqzass/commit/be2d3a70e22008fab2ba635124f2c5087cb4f28f.html"/><content type="text">템플릿 문서에서 사라진 site.base_url 잔재를 지운다

4d4a1d8이 site.base_url을 origin + base_path로 갈랐을 때 data.md의 표는
빠졌다. 표의 복붙 가능한 예시 {{ site.base_url }}{{ page.url }}는
SemiStrict 아래서 undefined value로 빌드를 깨뜨린다 — 문서가 약속하고
코드가 안 하는, page.extra 때와 정확히 같은 부류다. page.permalink 행의
base_url 표기도 origin으로 고친다.
</content></entry>
<entry><id>urn:git:65ca7b048230b63b3986fe4223ff27e274069b1b</id><title>serve 리빌드 사유에서 읽기 이벤트를 거른다</title><published>2026-07-28T21:26:34+09:00</published><updated>2026-07-28T21:26:34+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/sqzass/commit/65ca7b048230b63b3986fe4223ff27e274069b1b.html"/><content type="text">serve 리빌드 사유에서 읽기 이벤트를 거른다

첫 저장 이후 리빌드가 영원히 돌았다. inotify의 감시 마스크에 IN_OPEN이
들어 있어 읽기도 이벤트를 만드는데, 감시 스레드는 경로 노이즈만 거르고
이벤트 종류를 보지 않았다. 리빌드가 content/와 templates/를 읽는 것
자체가 다음 리빌드를 예약한다 — 1페이지 사이트 실측에서 touch 한 번에
10초간 리빌드 66회, 이후 무한 증가.

서빙과 라이브 리로드는 루프 중에도 멀쩡해서 증상이 가려져 있었다.
Access(_)를 리빌드 사유에서 빼면 touch 한 번은 리빌드 한 번이다.
</content></entry>
<entry><id>urn:git:32e7cae7ca2812ab0261996fd03cce8383b54f80</id><title>템플릿에 site.search 를 준다 — 색인 URL이거나, 껐으면 없음</title><published>2026-07-28T21:26:34+09:00</published><updated>2026-07-28T21:26:34+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/sqzass/commit/32e7cae7ca2812ab0261996fd03cce8383b54f80.html"/><content type="text">템플릿에 site.search 를 준다 — 색인 URL이거나, 껐으면 없음

[search] enabled를 끈 사이트에서 검색 UI가 든 테마는 존재하지 않는 색인을
가리켰다. 템플릿이 설정을 알 길이 없어서다. site.feed와 같은 꼴로 노출한다:
켜져 있으면 이 언어의 색인 URL, 꺼져 있으면 없음 — if는 방어가 아니라
테마가 두 경우 모두에서 동작하는 방법 그 자체다.

bool이 아니라 URL인 이유: 색인 파일명(search-&lt;언어&gt;.json)은 엔진 지식이다.
지금까지 base.html이 두 곳에서 `search-`에 언어 코드를 손으로 이어
붙였는데, 그 조립을 템플릿마다 반복시키는 것이 바로 이 필드가 없애는
것이다.

base.html의 버튼·팔레트·푸터 링크·search.js 로드가 전부 이 뒤로 들어간다.
search.js의 기존 null 가드 덕에 끈 사이트에서도 JS는 조용히 물러난다.
404 페이지에도 기본 언어 색인으로 준다 — 길 잃은 독자가 빠져나가는 길이다.
</content></entry>
<entry><id>urn:git:510b12d340b08740c22331de19a9d8e3df84fb27</id><title>[search] enabled 로 색인 생성을 끌 수 있게 한다</title><published>2026-07-28T15:41:36+09:00</published><updated>2026-07-28T15:41:36+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/sqzass/commit/510b12d340b08740c22331de19a9d8e3df84fb27.html"/><content type="text">[search] enabled 로 색인 생성을 끌 수 있게 한다

색인은 전 페이지의 본문 평문을 담아 코퍼스 크기에 비례한다 — 이 문서
사이트에서 언어당 ~80KB, 5000페이지 실측에서 16MB. 검색 UI가 없는
사이트가 그 바이트를 지불할 이유가 없다.

끈 사이트에서는 알려진 URL 목록에도 색인 경로를 넣지 않는다. 존재하지
않는 파일을 알려진 URL로 등록하면 죽은 링크를 살아 있다고 판정하게 된다.
색인 행 자체를 만들지 않으므로 페이지당 본문 사본 하나가 통째로 사라진다.

적대적 검증이 반대쪽 구멍을 찾았다: enabled=true일 때 static/에 직접 둔
search-&lt;언어&gt;.json을 생성 색인이 조용히 덮어썼다. 피드·sitemap·robots은
전부 static에 양보하는데 색인만 예외였다 — 같은 규칙으로 맞춘다.
static/feed-en.xml이 조용히 덮이던 것을 피드 작업 검증에서 고친 것과
같은 부류다.
</content></entry>
<entry><id>urn:git:a9bae04df9fddcb340455735eff71223c92e98a7</id><title>build --profile 로 페이즈별 시간을 stderr 에 낸다</title><published>2026-07-28T15:41:36+09:00</published><updated>2026-07-28T15:41:36+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/sqzass/commit/a9bae04df9fddcb340455735eff71223c92e98a7.html"/><content type="text">build --profile 로 페이즈별 시간을 stderr 에 낸다

느린 빌드가 콘텐츠 탓인지 이 도구 탓인지 구분할 수단이 없었다. discover /
assets / feeds / templates / render / search / generate / write 여덟
구간의 벽시계를 한 줄씩 찍는다.

stderr 전용이다 — stdout의 &quot;N pages → …&quot; 계약과 --json 출력, 빌드
산출물은 건드리지 않으므로 결정성과 무관하다. 실패하는 빌드에서도 찍힌
구간까지는 그대로 나오고, 에러 태그와 종료 코드는 변하지 않는다.
</content></entry>
<entry><id>urn:git:0f378562efa1880f57c9960971f3ec6a6af21421</id><title>코드 펜스에 hl_lines= 와 name= 옵션을 단다</title><published>2026-07-28T15:40:29+09:00</published><updated>2026-07-28T15:40: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/sqzass/commit/0f378562efa1880f57c9960971f3ec6a6af21421.html"/><content type="text">코드 펜스에 hl_lines= 와 name= 옵션을 단다

문법은 언어 뒤 공백 구분 key=value다: ```rust hl_lines=2-4,7 name=main.rs.
comrak이 info 문자열을 첫 공백에서 언어/메타로 가르므로 거기가 자연스러운
자리고, Zola식 `rust,hl_lines=…`는 콤마까지 언어 토큰에 붙어 문법 조회와
language- 클래스를 오염시킨다.

hl_lines 줄은 &lt;mark class=&quot;hl-line&quot;&gt;로 감싼다 — CSS가 하나도 없어도
브라우저 기본 스타일로 표시가 보이는 요소라서다. ClassedHTMLGenerator는
span을 줄 경계 너머로 열어 두므로 그대로는 줄을 감쌀 수 없다. 파스 상태는
줄을 넘겨 잇되(여러 줄 주석이 올바르게 칠해진다) 각 줄 시작에 열려 있는
스코프를 다시 열고 끝에서 전부 닫아 자립시킨다. 스코프→클래스 변환은
syntect가 비공개라 Scope::build_string()의 atom으로 재구성한다. name은
data-name 속성으로만 나가고 보여줄지는 사이트 CSS가 정한다.

잘못된 옵션은 빌드를 멈춘다. 모르는 키를 조용히 무시하면 hl_line= 오타가
강조 없는 채로 배포된다 — 깨진 링크와 같은 계약이다. 적대적 검증이 이
계약의 구멍 셋을 찾아 같이 막았다:

- 언어 없이 옵션부터 쓴 펜스(```hl_lines=2)는 comrak이 첫 토큰을 언어로
  삼아 옵션 파서에 도달조차 안 했고, name과 같이 쓰면 반쪽만 적용됐다.
  언어 이름에 =는 없으므로 언어 자리의 =를 에러로 잡는다 — 오탐이 있을
  수 없는 구분이다.
- 같은 키가 두 번 오면 앞의 것이 소리 없이 버려졌다. 에러로 바꾼다.
- 표시 줄의 배경과 라벨 바가 auto 폭이라, 스크롤되는 긴 줄에서 보이는 폭
  너머로 넘긴 순간 표시가 사라진 채 줄의 나머지가 보였다 — 어느 줄이
  강조인지 반대로 읽게 만든다. code를 max-content로 넓혀 가장 긴 줄까지
  칠하고, 문서의 CSS 레시피에도 같은 규칙을 넣었다.
</content></entry>
<entry><id>urn:git:70d7c2c01c68b9ba072a44443d083672f6f8ac3b</id><title>serve 감시 목록에서 존재하지 않는 data/ 를 뺀다</title><published>2026-07-27T21:53:19+09:00</published><updated>2026-07-27T21:53: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/sqzass/commit/70d7c2c01c68b9ba072a44443d083672f6f8ac3b.html"/><content type="text">serve 감시 목록에서 존재하지 않는 data/ 를 뺀다

WATCH_DIRS의 &quot;data&quot;는 hwaro의 data/ 디렉터리 개념을 참조하던 잔재다.
빌드 파이프라인 어디에도 data/ 를 읽는 코드가 없다는 걸 grep으로 재확인했다.
is_dir() 가드 덕에 무해했지만, 감시 목록은 곧 기능 목록으로 읽힌다 —
존재하지 않는 기능을 감시하지 않는다.
</content></entry>
<entry><id>urn:git:4ca60e81ae117864951a3b30d927b63e90a6ed48</id><title>렌더 루프의 O(n²)를 제거하고 serve 고정비를 캐시한다</title><published>2026-07-27T21:52:02+09:00</published><updated>2026-07-27T21:52:02+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/sqzass/commit/4ca60e81ae117864951a3b30d927b63e90a6ed48.html"/><content type="text">렌더 루프의 O(n²)를 제거하고 serve 고정비를 캐시한다

5000페이지 실측(ssg-benchmark 방법론 재현, 콜드 3회 중앙값, taskset 4코어)에서
빌드가 hwaro보다 7.7배 느렸다. 원인은 렌더 자체가 아니라 페이지마다 반복되던
O(n) 작업들이다 — 100→1000→5000 차분비가 28로 순수 이차식 예상치(≈24)에 붙어
있었다.

- 링크 표(LinkIndex)와 URL 집합(KnownUrls)을 Arc로 공유한다. Resolver는
  페이지마다 새로 만들어야 하지만(unresolved가 페이지별 상태다) 표는 사이트
  전체에서 같은 하나다. 값으로 들고 있던 시절에는 페이지마다 전 URL 집합이
  통째로 복사됐다.
- 사이드바 컨텍스트(section_ctx)를 언어당 한 번만 만들어 minijinja Value로
  사전 직렬화한다. minijinja는 Value를 직렬화 경계 너머로 그대로 통과시키므로
  페이지마다 드는 비용이 참조 카운트 복사로 준다. 404 렌더도 같은 캐시를 쓴다.
- neighbours_of의 섹션 내 위치 탐색을 URL 문자열 선형 탐색에서 Site::build 때
  한 번 만드는 표 조회로 바꾼다. 평평한 대형 섹션에서 O(k²)였다.
- SyntaxSet(two-face 덤프 역직렬화, 수십 ms급)과 ThemeSet을 OnceLock으로
  프로세스당 1회만 로드한다. serve가 저장마다 빌드를 새로 돌므로 이 둘이
  저장→반영 지연에 매번 얹혔다. 콘텐츠와 무관해 결정성에는 영향이 없다.

측정:
  minimal 5000p: 7.58s → 0.193s (39배; hwaro 0.985s 대비 5.1배 빠름)
  code    5000p: 21.7s → 9.63s  (남은 격차는 syntect 하이라이팅 자체)
  차분비 28 → 4.4 = 선형 예상치. 실제 docs 사이트(58페이지)는 전후 동일 ~0.2s.

수정 후 빌드는 수정 전 커밋 산출물(docs/public)과 바이트 동일 — 동작 보존이
산출물로 증명된다. 같은 입력 2회 빌드 동일성도 확인했다.
</content></entry>
<entry><id>urn:git:4733be73c5d01220f0ba0e99786975ea74493904</id><title>Add date front matter and Atom feeds, and fix what verifying them found</title><published>2026-07-26T10:36:26+09:00</published><updated>2026-07-26T10:36: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/sqzass/commit/4733be73c5d01220f0ba0e99786975ea74493904.html"/><content type="text">Add date front matter and Atom feeds, and fix what verifying them found

A page with a `date` enters its language&#39;s feed. `sort_by = &quot;date&quot;` orders a
section newest first — the opposite of weight and title, because that is the
order anyone reading a dated list expects — and undated pages go last rather
than first.

Atom rather than RSS 2.0. RSS dates are RFC 2822, which needs a day-of-week we
would compute ourselves and English month names we would embed, including in a
Korean feed where `Jul` is simply wrong. Atom uses RFC 3339, which is the shape
a TOML date already has.

A language with no dated pages gets no feed and no autodiscovery link. An empty
feed is worse than an absent one; a subscriber who sees nothing arriving reads
it as broken. This documentation has no dates, so it publishes no feed.

`page.date` is parts — year, month, day, the date, the timestamp — not a format
string. A format filter means shipping a date-formatting mini-language and then
owning locale rules for every language someone writes in.

Then I had the implementation attacked, and it came back with nine defects.
Every one of them was real.

The escaper passed C0 control characters through. XML 1.0 cannot carry those
even as character references, so a single one in a title made the whole feed
unparseable while the build reported success. They are dropped now, in the
sitemap escaper too.

Offsets were rewritten as `Z`, which publishes 09:00 in Seoul as 09:00 UTC — a
morning announced as an evening, off by up to a day. RFC 3339 accepts any
offset, so there was never any arithmetic to avoid: the offset is carried
through, and ordering compares instants rather than text.

The feed&#39;s `updated` came off the head of a sort that ties on title, so a feed
holding an 18:00 entry could announce 09:00 — and aggregators that poll on it
would never fetch the newer entries. It is the maximum now, taken after the
entry cap so it cannot name a page the feed does not contain.

The feed&#39;s `id` was the home page URL, which is character-for-character the
`id` of its own first entry. It is the feed&#39;s own address now.

A `date` TOML reads as a time with no date was dropped in silence. It is an
error.

Three were integration rather than feed. Linking to `/feed-en.xml` — or
`/sitemap.xml`, or `/llms.txt` — failed the build, because the link checker
added last week knows about pages and assets and not about the files the build
generates. A `static/feed-en.xml` was silently overwritten, where sitemap and
robots step aside. And a page whose URL matched a generated filename died
halfway through writing with `File exists (os error 17)`, which says nothing
about what is wrong; it is a named collision error now, beside the URL
collision check it belongs with.

`base_url` without a scheme now fails validation. Every absolute URL this tool
emits — canonical, sitemap, feed id, OpenGraph — comes from it, and RFC 4287
forbids a relative `id` outright.
</content></entry>
<entry><id>urn:git:0ee4dec4e3e689cb7120262034eb551cf2095ca7</id><title>Build the footer out of what this build produced</title><published>2026-07-26T09:45:16+09:00</published><updated>2026-07-26T09:45:16+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/sqzass/commit/0ee4dec4e3e689cb7120262034eb551cf2095ca7.html"/><content type="text">Build the footer out of what this build produced

It was one line — built with sqzass, source, MIT — which is a colophon, not a
footer. Three columns now: the mark with a tagline, the project, and the files
this build emitted.

The last column is the part worth having. sitemap.xml, robots.txt, llms.txt and
the search index are things the documentation claims the tool produces, and
linking them turns the claim into something a reader can click. It is a column
that only makes sense for a generator that builds its own documentation, which
is why it is not the one every footer has.

The one every footer has — a list of the documentation sections — is
deliberately absent. The masthead is sticky and the sidebar is always there, so
a third copy would be filling space rather than adding anything.

Two things surfaced while building it. `site.description` is a single value for
the whole site, so the Korean pages were carrying an English tagline, in the
footer and in the home page&#39;s &lt;title&gt;; both read from the i18n catalogue now,
which is the mechanism that exists for exactly this. And the responsive rules
written before the masthead rebuild were still in the file, hiding the table of
contents from 68rem while the new block hides it at 60rem — two sets of
breakpoints disagreeing about the same element. The old set is gone.
</content></entry>
<entry><id>urn:git:57e1b0295c3dfe18fbca627ec5e5290a1566cdbf</id><title>Treat scrollbars, selection and motion as part of the site</title><published>2026-07-26T00:56:04+09:00</published><updated>2026-07-26T00:56:04+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/sqzass/commit/57e1b0295c3dfe18fbca627ec5e5290a1566cdbf.html"/><content type="text">Treat scrollbars, selection and motion as part of the site

The scrollbar work last time covered exactly one element. Everywhere else that
scrolls — the page, the sidebar, the table of contents, the search results —
kept the browser default, so scrolling produced a different object depending on
where you were. It is one global rule now: thin, transparent track, rounded
thumb that brightens on hover, standard property for Firefox and the -webkit-
pseudo-elements for the rest. Code blocks keep one override, a brighter thumb,
because their surface is lighter than the page and the shared colour disappears
against it.

Text selection was the browser&#39;s blue, which is not a colour this site uses
anywhere. It is the accent at 30% now.

Anchor jumps got a `--tr` token and `scroll-behavior: smooth` so that following
a link from the table of contents shows you where you went, rather than
teleporting and leaving you to work out what moved. Reduced motion turns it
back off along with the transitions.

Those transitions now come from one token rather than three hand-written
durations. Interactive elements that respond at different speeds do not read as
parts of one thing.

And a regression I introduced when the masthead became sticky: `scroll-margin-top`
was still 2rem against a 3.5rem header, so every anchor landed 24px underneath
it — click a heading in the table of contents and the heading itself was hidden.
Measured, fixed to the header height plus a gap, and measured again: three
headings, all landing at 80px with 24px of clearance.
</content></entry>
<entry><id>urn:git:b40eb914c5a541524a8126b740dfdb565fa22b2f</id><title>Icons for GitHub and theme, and fix the table and code-block treatments</title><published>2026-07-26T00:45:32+09:00</published><updated>2026-07-26T00:45:32+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/sqzass/commit/b40eb914c5a541524a8126b740dfdb565fa22b2f.html"/><content type="text">Icons for GitHub and theme, and fix the table and code-block treatments

GitHub and the theme toggle are inline SVG now. There is a recorded decision in
this repository against glyphs — ◐ and ☾ are missing from enough fallback
stacks to render as a box — but the reason behind it is font coverage, and an
inline SVG does not consult a font. Icon-only controls have no accessible name,
so both carry aria-label and title, and both are square: a control whose width
follows its label breaks the rhythm of the row it sits in.

The theme button shows where you are going, not where you are — a sun on a dark
page, a moon on a light one. A button names its action.

Tables were `display: block` so they could scroll sideways, which costs them
their table layout: every one shrank to its content and used about 40% of the
column, reading as an afterthought rather than a reference. Ours are two or
three columns of short text, so they are ordinary full-width tables again, with
the header rule darkened so the labels separate from the data. A genuinely wide
table can have a wrapper the day one exists.

Code blocks were cut off at the right edge with nothing to say more was there.
I first drew the shadow with the background-attachment trick, and it left a grey
band on blocks that did not overflow at all — the mask was narrower than the
shadow it was meant to cover. The browser already ships something that appears
exactly when content overflows and never otherwise, so the scrollbar is styled
thin and made visible instead. Fewer moving parts, no artefact to tune.

Copy buttons, from the recipe features/highlighting.md tells readers to use.
Documenting a thing our own site does not do is a claim without evidence. They
read the `&lt;code&gt;` element rather than the `&lt;pre&gt;`, because the latter now
contains the button and would copy its own label.

The sidebar keeps its single dividing rule. I tried a surface under it as well
and it read as a card floating in the page rather than a zone beside it — the
layering that sidebar needs comes from the hover and current states on its
items, and a tint across the whole panel buries them.
</content></entry>
<entry><id>urn:git:c24222ce6413ae1d9cbcbad2c80b867d700ac7ec</id><title>Give the chrome a component system, and make the sidebar collapse</title><published>2026-07-26T00:34:21+09:00</published><updated>2026-07-26T00:34:21+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/sqzass/commit/c24222ce6413ae1d9cbcbad2c80b867d700ac7ec.html"/><content type="text">Give the chrome a component system, and make the sidebar collapse

The masthead grew with its contents, so its height changed from page to page,
and `align-items: baseline` sat a brand containing an image on a different line
from the nav beside it. The text looked scattered because it was. It is now a
fixed-height row aligned on centre, and sticky, which a documentation site
wants anyway.

Four controls sat side by side in three different treatments: search was a
bordered box, theme was a button, and the language and GitHub links were bare
text. To a reader they are all things you click, and the two that looked like
nothing read as unfinished. They share one shape now — `--control-h`, one
border, one radius, one hover — defined once and applied to all four. The shape
is keycap-like rather than pill-like because this is a tool you drive from a
terminal.

The sidebar dumped every section and every page, twenty-odd items, straight
down the page with no way to fold any of it. Sections are `&lt;details&gt;` now. That
is the whole mechanism: no JavaScript, keyboard and screen readers work
already, and — the part that matters — the section containing the current page
is opened by the server, so the state is right in the first paint rather than
after a script has run. Pages that belong to no section open the first one,
because five closed accordions tell a new reader nothing.

Narrow screens were not broken so much as absent: there was no breakpoint at
all, and at 420px &quot;sqzass&quot; wrapped one letter per line, as did &quot;GitHub&quot;. Below
60rem the section nav and the table of contents step aside — the drawer says
the same thing — GitHub goes (it is in the colophon) and the sidebar becomes a
drawer using the same `&lt;details&gt;`. It ships open so a page without JavaScript
is complete, and ui.js closes it on narrow screens before paint.

Also: the current page is a filled pill spanning the panel rather than a hairline
on its left edge, the sidebar fills the viewport height so its rule does not
stop mid-list and read as unfinished, and the table of contents marks where you
are as you scroll. That last one is an IntersectionObserver with a rootMargin
that pins the trigger near the top of the viewport; without it a heading counts
as current while still at the bottom of the screen and the marker runs ahead of
the reader.
</content></entry>
<entry><id>urn:git:c453f54128e6058a383d4696be7aa8a88ec4b9f8</id><title>Use the logo in the masthead and as the favicon</title><published>2026-07-26T00:13:10+09:00</published><updated>2026-07-26T00:13: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/sqzass/commit/c453f54128e6058a383d4696be7aa8a88ec4b9f8.html"/><content type="text">Use the logo in the masthead and as the favicon

The source was 2816×1536 and 2.9 MB, almost all of it white space, on an opaque
white background rather than a transparent one. Cropped to the ink, given an
alpha channel derived from luminance so the antialiasing survives, and shipped
at 120 px: 6 KB for the mark and 5 KB for the icon.

The line art is black, and this site is dark by default, so the mark would have
been invisible where most people see it. It is inverted in CSS rather than
shipped twice — for pure black-on-transparent artwork, inverting produces
exactly the white version, and one file cannot drift from the other.

The favicon cannot be inverted by CSS, so that one does ship twice, selected by
`prefers-color-scheme` on the link element. A browser that does not understand
the media attribute takes the last one, which is the black mark for a light tab
bar — the more common default.

Sized at 2.2em rather than the 1.6em I started with. Below that the hatching on
the hand aliases into a smudge and the drawing stops being a drawing.

The alt attribute is empty on purpose: the wordmark sits immediately beside it,
and naming the image too makes a screen reader say &quot;sqzass&quot; twice.

Not wired into og:image. That would put the mark in every Slack and Discord
unfurl of every page, which is a different decision from a favicon and is one
line in base.html whenever it is wanted.
</content></entry>
<entry><id>urn:git:6b2845b0db346b79379f17444877e1a05e216e12</id><title>Retry a Pages deployment that lost a race with the previous one</title><published>2026-07-26T00:02:42+09:00</published><updated>2026-07-26T00:02: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/sqzass/commit/6b2845b0db346b79379f17444877e1a05e216e12.html"/><content type="text">Retry a Pages deployment that lost a race with the previous one

`concurrency: group: pages` serialises workflow *runs*. It does not serialise
what GitHub does after one finishes — the Pages deployment outlives the run that
created it, and a run that starts inside that window gets a 400: &quot;Deployment
request failed … due to in progress deployment.&quot;

Pushing four commits inside ten minutes hit it. One deploy was cancelled out of
the queue by the next arrival and two more failed against a deployment the
Deployments API was already reporting as successful.

So the deploy step now tolerates that one failure, waits, and tries once more.
It is safe to retry precisely because the request fails before changing
anything: there is no half-deployed state to land in, only a deployment that was
never created.

This does not fix the case where GitHub&#39;s own state is stuck rather than merely
busy — the cancel endpoint answering &quot;already finished&quot; while the deploy
endpoint says &quot;still in progress&quot; is not something a workflow can resolve. For
that the answer is to re-run once it clears.
</content></entry>
<entry><id>urn:git:21014fcefe2efa3f3aa386e2aeca9db5d6538423</id><title>Write down what already works, and fix a link bug the writing exposed</title><published>2026-07-25T23:55:35+09:00</published><updated>2026-07-25T23:55: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/sqzass/commit/21014fcefe2efa3f3aa386e2aeca9db5d6538423.html"/><content type="text">Write down what already works, and fix a link bug the writing exposed

An audit against a much larger competitor&#39;s documentation found thirteen things
this tool does that nobody had written down. The absence of a page is
indistinguishable from the absence of the feature, so each of these was a
capability we had and did not have.

Two new pages. templates/functions.md is the complete callable surface —
`asset()`, `t()`, the minijinja builtins that are available, `tojson` which is
not, and the closing line that nothing else exists. That last part is the point:
without it a reader tries `url_for` from another generator and reads the build
error as a bug. templates/social.md is the OpenGraph and JSON-LD markup, which
needed no code at all — every input was already in the context and nobody had
written the eight lines down, so no sqzass site had them, including this one.
This one does now.

The rest went into the pages that were already almost right.
`asset-manifest.json` appears in every output and was named nowhere, so the
reasonable move for anyone auditing a deploy was to delete it. The search index
row schema was withheld on a page that tells you the client is yours to build.
`page.children` means two different things depending on where it is read and
neither was documented. `translation_key` was described as the filename stem
when it is the path with the language suffix removed — which matters, because
the stated version implies `a/notes.md` and `b/notes.md` are translations of
each other.

Writing that last one down is what exposed the bug. links.rs stripped any
trailing two-or-three-letter dotted segment as a language, while content.rs
strips one only when it is a declared code — so `content/notes.ab.md` keyed on
`notes.ab` and `@/notes.ab.md` looked up `notes` and failed the build on a file
that was right there. Fixed by trying the unstripped name first rather than by
giving links.rs its own copy of the language list, because a second copy of that
list is how these two drifted apart in the first place.

The static/ passthrough now has a test. `_headers`, `_redirects`, `.domains`,
`CNAME` and `.well-known/*` are files whose name is the contract, and the
guarantee that they come through unhashed was true by construction and asserted
nowhere.

And the docs now say plainly that every runtime message is Korean with no locale
switch. The English pages quote the real strings, which was honest but left an
English-speaking reader to discover the policy by running the binary.
</content></entry>
<entry><id>urn:git:c9549468fe65caa16869ac4f0744a266cf66807c</id><title>Document the five hosts we already worked on</title><published>2026-07-25T23:49:16+09:00</published><updated>2026-07-25T23:49:16+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/sqzass/commit/c9549468fe65caa16869ac4f0744a266cf66807c.html"/><content type="text">Document the five hosts we already worked on

deploy/_index.md claimed the same directory is correct on Cloudflare Pages,
Netlify, S3 and nginx, and then documented one host. A reader evaluating sqzass
for Netlify had to take that on faith, and `ls docs/content/deploy/` said
GitHub Pages was the only supported target. The claim was true; the evidence
was missing, and a claim without evidence reads as marketing.

Netlify, Vercel, Cloudflare Pages, GitLab Pages and Codeberg Pages, in both
languages. No code changed, because none was needed — every one of them is
&quot;run sqzass build, publish public/&quot;, and the pages differ only in which file
that goes in and what each host calls its preview URL.

Two things they gave the docs that GitHub Pages could not.

The subpath is now written down where people meet it. GitLab and Codeberg
project sites are served under /&lt;project&gt;/, as are GitHub project pages, and
omitting it from base_url is the only mistake in this whole area that is
silent — the build succeeds, the pages exist, and every link resolves one level
too high. It is called out on each of those three pages and in the index.

And Cloudflare Pages is where content-hashed filenames finally pay off, because
it reads `_headers` and GitHub Pages pins everything to max-age=600 with no way
to change it. A year on /css/* is safe precisely because the name changes when
the bytes do — which is the argument for hashing the name instead of appending
a query string, stated somewhere a reader can act on it.

The static/ passthrough is documented for the same reason: CNAME, .domains,
_headers and .well-known all work today, and none of it was written down, so
the honest reading was that they did not.
</content></entry>
<entry><id>urn:git:715d0e3d69d101c6823871577a6f4793fb3cdbb8</id><title>Emit 404.html and redirect stubs, and check absolute links too</title><published>2026-07-25T23:45:53+09:00</published><updated>2026-07-25T23:45:53+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/sqzass/commit/715d0e3d69d101c6823871577a6f4793fb3cdbb8.html"/><content type="text">Emit 404.html and redirect stubs, and check absolute links too

Three things a static host will not do for you, which is the same reason pretty
URLs and cache invalidation are done here.

`templates/404.html` now renders to `/404.html`. GitHub Pages, Netlify,
Cloudflare and Codeberg all serve that path by name, and `content/404.md`
produces `/404/index.html`, which none of them look for. A hand-written
`static/404.html` was the only alternative and it gets no layout, no `t()` and
no way to name the hashed stylesheet. The tool renders it only when the
template exists — the same contract as the highlight stylesheet — and it stays
one named branch rather than a general render-any-template mechanism, because
the moment that is general it stops being obvious who owns an output URL.

`aliases` in front matter emits a meta-refresh stub per old URL, with a
canonical pointing at the real page and `noindex` so the stub never competes
with it in search results. Renaming a page is the most ordinary event on a live
site and it silently kills every inbound link and bookmark; on a host with no
redirect rules there is nothing else to reach for. An alias that collides with
a real page or another alias is an error naming both, and an alias that is an
external URL or not root-absolute is rejected rather than quietly written
somewhere surprising.

&quot;Broken references stop the build&quot; was, until now, true only of `@/`. The link
a writer most naturally types is `[install](/start/install/)`, and that was
never checked at all — no build error, nothing from doctor, a 404 in
production. Both sides were already in memory at render time: the page URL set
and the asset manifest. Anchors, queries, external URLs, protocol-relative URLs
and bare fragments all pass; only a path this site does not emit fails.

That check also catches `/css/main.css`, correctly — fingerprinting means no
file is at that path once the site is built. The message says so, and points at
`asset()`, because the reader can see the file in `static/` and would otherwise
conclude the checker is wrong.

`llms.txt` comes out of data we already had. The build also stopped counting
redirect stubs and 404.html as pages; reporting &quot;5 pages&quot; for a two-page site
was a small lie with an easy fix.
</content></entry>
<entry><id>urn:git:4d4a1d859738f0c549b172b25a3e90d1746dde6a</id><title>Deliver page.extra, and make the site work under a URL subpath</title><published>2026-07-25T23:40:32+09:00</published><updated>2026-07-25T23:40:32+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/sqzass/commit/4d4a1d859738f0c549b172b25a3e90d1746dde6a.html"/><content type="text">Deliver page.extra, and make the site work under a URL subpath

Two things our own documentation promised and the code did not do.

`page.extra` was documented in two places, one of them with a copy-pasteable
`{% if page.extra.badge %}` example, and was never put into the template
context. Following our own docs produced `[SQZASS_E_TEMPLATE] undefined value`.
It is also the escape hatch every per-page need routes through — og:image, a
badge, an API version — so nothing per-page could be built on top of it either.

The second is worse because it failed silently. `base_url` with a path, as in
`https://user.github.io/repo`, built a site that looked fine and was entirely
broken: links and stylesheet hrefs came out root-absolute at `/start/` and
`/css/main.hash.css` while the sitemap and canonical said `/repo/…`. Nothing
failed, doctor said nothing, and the reader got a 404 on every page and an
unstyled one on the rest. A project page is the default outcome for anyone who
has not bought a domain, so this was the one class of broken reference we
neither resolved nor rejected.

The subpath now runs through URL generation, so page URLs, section URLs,
translation links, prev/next, `@/` resolution and the asset manifest all carry
it, together or not at all. Output paths deliberately do not: the directory we
write is the root the host serves, and where that root sits on the domain is a
separate question.

Absolute URLs are built from the origin rather than from `base_url`, since the
page URL already contains the path and concatenating the configured value would
double it. `site.base_url` is gone from templates and replaced by `site.origin`
and `site.base_path` — one that never contains a path and one that is only a
path. Keeping a `site.base_url` that is sometimes safe to concatenate and
sometimes not is precisely the kind of trap this project exists to avoid.
</content></entry>
<entry><id>urn:git:506a2c5792ff841760b5b8c88fefdd8b370446e0</id><title>Add a release workflow and an AUR PKGBUILD</title><published>2026-07-25T22:55:25+09:00</published><updated>2026-07-25T22:55:25+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/sqzass/commit/506a2c5792ff841760b5b8c88fefdd8b370446e0.html"/><content type="text">Add a release workflow and an AUR PKGBUILD

Tagging `v*` builds two targets and attaches them to the release:
x86_64-unknown-linux-musl and aarch64-apple-darwin, each with a checksum beside
it because otherwise nobody downloading has a way to check anything.

The musl job is not only a build target. Whether this project can link
statically against musl is what dictated its dependency choices — syntect is
pinned to its pure-Rust regex engine specifically because the default is an
Oniguruma C binding, and that binding is what breaks here. That reasoning has
been written down since the beginning and never once executed. Now it runs on
every release.

Each job then runs the binary it produced: `init`, `build`, and a check that
the page exists. A static build that links and dies at startup is a real
failure mode, and &quot;the compiler was happy&quot; is not evidence that a binary works.

The workflow also runs on workflow_dispatch, where it uploads artifacts and
creates no release. Finding out whether a target builds should not require an
irreversible act; tags are the only thing that publishes.

fail-fast is off. If macOS breaks, the Linux archive is still worth having
while the cause is found.

The PKGBUILD builds from the tag tarball rather than shipping a binary. Building
a Rust tool from source is normal on Arch, and two packages invite the question
of which one is current. Its checksums are SKIP because they cannot be computed
before a tag exists — packaging/aur/README.md gives the order to do it in, and
says plainly that publishing needs an AUR account and an SSH key, which is not
something to automate from here.

Nothing is released yet. The docs say so rather than describing binaries that
do not exist.
</content></entry>
<entry><id>urn:git:4e8d25d449d5f22ec264209d9548a7d73d0933f2</id><title>Add sqzass doctor, scoped to what the build lets through</title><published>2026-07-25T22:52:59+09:00</published><updated>2026-07-25T22:52: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/sqzass/commit/4e8d25d449d5f22ec264209d9548a7d73d0933f2.html"/><content type="text">Add sqzass doctor, scoped to what the build lets through

The build already refuses everything it cannot resolve: broken `@/` links,
missing templates, duplicate URLs, misspelled configuration and front matter
keys, absent assets, absent translation keys. Repeating those in doctor would
give two commands the same job, and one of them would go stale.

So doctor only reports what a build accepts and an author might not have meant.
Six checks, and the list was arrived at by asking what is left rather than by
asking what could be checked:

  base-url         base_url is still init&#39;s placeholder
  untranslated     a page exists in some languages and not others
  empty-section    a section has no pages, so its nav entry leads nowhere
  description      a page has none, which search results and &lt;meta&gt; use
  draft            a page is excluded from the build
  unused-template  no page selects it and no template names it

`--fail-on` gates, `warn` by default, exiting 7 — a number deliberately outside
the error kinds, because &quot;your site has notes&quot; and &quot;the build broke&quot; are
different events and CI should be able to tell them apart. The default is not
`note`: notes are things to know, and a pipeline that stops for them gets
switched off rather than fixed.

unused-template resolves `include`/`extends` references by substring rather
than by parsing. The check is wrong in only one direction — it stays quiet
about a template it cannot prove is unused — and telling someone to delete a
partial that is in use would be much worse than saying nothing.

CI runs it against docs/, which is currently clean. That keeps it clean, and
keeps doctor itself exercised against a real site rather than only fixtures.
</content></entry>
<entry><id>urn:git:9f6a5d12a828e9f2c7b055295651154dbcf2dccd</id><title>Give failures a code and an identifier, and add --json</title><published>2026-07-25T22:48:53+09:00</published><updated>2026-07-25T22:48:53+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/sqzass/commit/9f6a5d12a828e9f2c7b055295651154dbcf2dccd.html"/><content type="text">Give failures a code and an identifier, and add --json

Every failure exited 1, so a pipeline could tell that the build broke and
nothing else. Now the exit code answers &quot;whose fault is it&quot;: 3 for the config,
4 for content, 5 for templates, 6 for I/O. CI can branch on that without
parsing a message that is not in its language anyway.

The identifier is printed alongside — `[SQZASS_E_CONTENT] …` — so the thing a
person searches for is a stable string rather than a translated sentence. Both
the numbers and the identifiers are a contract; reassigning one would quietly
change what somebody&#39;s existing condition tests for, so there is a test that
fails if either moves.

Classification happens at the pipeline stage boundaries, not at each error site.
Twenty tagged call sites is twenty chances to tag the wrong thing, and the only
distinction a reader wants is which part of their site was rejected.

The first version got that wrong in an instructive way. It wrapped the whole
render call in the template kind, which sat outside the content kind already
attached to an unresolved `@/` link — so a broken link reported itself as a
template error, and the inner identifier leaked into the message as if it were
part of the text. Tags are now applied at the precise operation, the message
filters every identifier rather than only the outermost, and a test covers the
nesting case that produced it.

`--json` prints one object on stdout and nothing else, on failure as well as
success, so a script reads a single pipe rather than reassembling two. It is
global, so it works before or after the subcommand.
</content></entry>
<entry><id>urn:git:cdb8b01b7803dbc13c30985f1ae632e644c9a0b2</id><title>Move UI strings out of the templates and into i18n/</title><published>2026-07-25T22:39:26+09:00</published><updated>2026-07-25T22:39: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/sqzass/commit/cdb8b01b7803dbc13c30985f1ae632e644c9a0b2.html"/><content type="text">Move UI strings out of the templates and into i18n/

The templates carried thirteen `{{ &quot;이 페이지&quot; if ko else &quot;On this page&quot; }}`
conditionals. Each one is a place where a third language means editing markup,
and a place where a translation lives somewhere nobody translating would think
to look.

They are now keys in `i18n/en.toml` and `i18n/ko.toml`, read by `t(&quot;…&quot;)`.

`t` takes the language from the page being rendered rather than from an
argument. A template that has to be told which language it is in is a template
someone will eventually forget to tell, and that page alone comes out in the
wrong language — with nothing failing.

A key missing from one language is an error, and the message names the file it
is missing from and the languages that do have it. Falling back to the default
would put English labels inside a Korean page: visible to every Korean reader
and invisible to whoever maintains the site. That is the same reasoning that
hides untranslated pages instead of duplicating them.

The `page.translations` loops were renamed from `t` to `lang`. They did not
call the function, so nothing was broken — but a loop variable shadowing `t`
means the first person to add a label inside that block gets a confusing error
for a reason that is nowhere near where they are looking.

Sites without an i18n/ directory are unaffected. The catalogue is only read by
templates that call `t`.
</content></entry>
<entry><id>urn:git:f321d666af9fb164718fac2303ac5b4c340e8d91</id><title>Stop the README claiming features are missing that shipped</title><published>2026-07-25T22:28:30+09:00</published><updated>2026-07-25T22:28:30+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/sqzass/commit/f321d666af9fb164718fac2303ac5b4c340e8d91.html"/><content type="text">Stop the README claiming features are missing that shipped

It said syntax highlighting, the table of contents, the asset pipeline, search
and the dev server were &quot;not built yet&quot; and the tool was &quot;not usable for a real
site&quot;. All five have been in for a while, and the documentation site is built
with it. A status line that is wrong in that direction is worse than no status
line: it turns away the exact reader who would have tried it.

Replaced with what is actually true, including what is still missing — no
prebuilt binaries, no theme system — because the value of a status section is
entirely in being believed.

Also leads with `sqzass init` now that there is one, and documents the two rules
someone hits first: broken references stop the build, and Korean is handled
rather than merely permitted.
</content></entry>
<entry><id>urn:git:ad38e2ef2022e57f1f43e5d52c5f16158fbcb056</id><title>Make an unknown config or front matter key an error</title><published>2026-07-25T22:27:47+09:00</published><updated>2026-07-25T22:27:47+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/sqzass/commit/ad38e2ef2022e57f1f43e5d52c5f16158fbcb056.html"/><content type="text">Make an unknown config or front matter key an error

A key sqzass does not read is a key that does nothing. Accepting it silently is
the same failure as accepting a broken link: you asked for something, the tool
agreed, and nothing happened. Someone who writes `theme_ligth` has changed their
theme as far as they know, and will spend the afternoon looking for the reason
it did not take.

There was already a check, and it had three holes. It warned rather than failed,
so the build succeeded with the setting ignored. It only looked at top-level
keys, so `[highlight] theme_ligth` sailed through. And it printed twice, because
the build loaded the config twice — which reads as two problems in your file
rather than one.

serde&#39;s `deny_unknown_fields` covers all of it and deletes the hand-maintained
list of valid names along the way. That list was a copy of the struct fields
kept in sync by hand; it would have drifted the first time someone added a key
and forgotten. The generated message is also better than what it replaced —
line, column, the offending key and the candidates, where the old one had no
position at all.

Front matter is held to the same rule, and its errors now point at the line in
your file rather than one above it. Counting from the top of the file is the
reason the fence is split by hand in the first place, and that had never been
wired through to the parse error.

The new check immediately found `[highlight] line_numbers` in this repository&#39;s
own docs/sqzass.toml — a key removed from the code some time ago and left
behind in the configuration, doing nothing, exactly as predicted.
</content></entry>
<entry><id>urn:git:7fc540119b8f115a365518eff731759fca56d741</id><title>Add sqzass init</title><published>2026-07-25T22:21:15+09:00</published><updated>2026-07-25T22:21:15+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/sqzass/commit/7fc540119b8f115a365518eff731759fca56d741.html"/><content type="text">Add sqzass init

Three files: sqzass.toml, content/_index.md, templates/page.html. The same
three the getting-started page describes, so the tutorial and the tool now say
the same thing rather than two versions of it.

It scaffolds no example blog post, no .gitignore you did not ask for and no
directory the tool expects to own. A scaffold you have to delete half of is not
a head start, and every file in it is a file someone has to read before they
can trust what they are looking at.

The template links `site.highlight_css`. Without that line the build still
generates the stylesheet and nothing references it, which leaves a new user
with an unexplained file in their output on their first build — and it hides
the fact that code blocks come out coloured for free.

Running it where a sqzass.toml already exists is an error rather than a merge.
There is no reading of &quot;initialise this&quot; that means &quot;write into the site I am
already working on&quot;.

The test builds what init writes. A scaffold that does not build is worse than
no scaffold, and that is exactly the thing that rots quietly as the rest of the
tool moves.
</content></entry>
<entry><id>urn:git:73afe06b646f7007b8caa61cb04174e9c30a05c7</id><title>Generate sitemap.xml and robots.txt, and link neighbouring pages</title><published>2026-07-25T22:18:38+09:00</published><updated>2026-07-25T22:18: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/sqzass/commit/73afe06b646f7007b8caa61cb04174e9c30a05c7.html"/><content type="text">Generate sitemap.xml and robots.txt, and link neighbouring pages

The sitemap carries `&lt;loc&gt;` and, for pages that exist in more than one
language, the `&lt;xhtml:link&gt;` alternate set — including the page&#39;s own language,
which is what makes the set valid rather than merely present. On a bilingual
site that is the part search engines actually use.

It carries no `priority` or `changefreq`: Google said in 2023 it ignores both,
and code that computes an ignored value is only an opportunity to be wrong.

It carries no `lastmod` either, and that took more thought. Every source for it
available here is untrustworthy. A file&#39;s mtime is its checkout time, so CI
would stamp the whole site as changed this morning, and it would break the
guarantee that two builds produce identical bytes. Git commit times are right
but need full history, and `actions/checkout` is shallow by default — so the
failure mode is not an error, it is every page quietly claiming the same date.
Google drops a site&#39;s `lastmod` entirely once it decides the values are wrong,
which makes a plausible-looking wrong one worse than an absent one.

Both files step aside for a `static/` file of the same name, and step aside by
not being generated rather than by being overwritten. Writing your own
robots.txt and having it silently replaced is the failure this avoids.

Neighbours are per-section on purpose. A reader who reaches the end of one part
has finished that part; walking them across the boundary into an unrelated one
implies the sections are chapters of a single ordered book, which they are not.
Section indexes have no neighbours at all — a section does not stand in line
beside its own children.
</content></entry>
<entry><id>urn:git:ae9cb15306bc1e22374536950d2add7b875d9017</id><title>Write the documentation, and stop writing Korean like notes</title><published>2026-07-25T22:08:35+09:00</published><updated>2026-07-25T22:08: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/sqzass/commit/ae9cb15306bc1e22374536950d2add7b875d9017.html"/><content type="text">Write the documentation, and stop writing Korean like notes

The site had three pages and a sidebar with one entry in it. Whatever else was
wrong with it, most of what made it feel unfinished was that it was
unfinished. There are now five sections — getting started, writing content,
templates, features, deploying — and thirty-eight pages across two languages.

Only what the code actually does is documented. Every configuration key, front
matter field and template value here was read out of the source rather than
recalled, because a documented key that does not exist is worse than an
undocumented one that does: it sends a reader looking for a bug in their own
file. The pages that explain a decision explain the reasoning too — why URLs
are directories, why slugs come from filenames rather than titles, why template
selection is four steps and not twenty — since a rule whose reason is invisible
reads as an arbitrary restriction.

Cross-references are all `@/` links, which means the build checks them. Thirty
new pages of prose is exactly where a stale link would otherwise slip in
unnoticed.

The Korean was rewritten. It was in the terse declarative register you use for
your own notes — 빌드한다, 필요 없다 — which is fine in a commit message and
wrong in documentation a stranger is reading; it lands somewhere between curt
and machine-translated. It now uses complete sentences throughout, matching how
Korean technical writing actually reads.

The search index grew to 19 rows and 38 KB per language, still one fetch on
first use. Checked against the finished site: 최적화 finds the three pages that
mention 검색엔진최적화, and 존성 finds the three that say 의존성.
</content></entry>
<entry><id>urn:git:1c0ca27f86f24c2327e4839ed2905fe31522e953</id><title>Open search with Ctrl/⌘+/ instead</title><published>2026-07-25T21:51:25+09:00</published><updated>2026-07-25T21:51:25+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/sqzass/commit/1c0ca27f86f24c2327e4839ed2905fe31522e953.html"/><content type="text">Open search with Ctrl/⌘+/ instead

Requested, and it gives Save Page back — Ctrl+/ collides with nothing the
browser does, so the palette no longer has to take a shortcut away to get one.

Matched on e.key, not e.code: on layouts where the slash sits behind Shift the
physical key is elsewhere, and what the reader typed is what should count.
Ctrl+Shift+/ therefore opens it too, which is what those keyboards produce.
</content></entry>
<entry><id>urn:git:b13b9938ace09f7bf9e73f4a767c8febb70f8d06</id><title>Open search with Ctrl/⌘+S</title><published>2026-07-25T21:48:06+09:00</published><updated>2026-07-25T21:48: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/sqzass/commit/b13b9938ace09f7bf9e73f4a767c8febb70f8d06.html"/><content type="text">Open search with Ctrl/⌘+S

Requested. It costs the browser&#39;s Save Page shortcut, which is the right
trade here: nobody saves the HTML of a documentation page, and preventDefault
means Save is only lost while this site is focused.

Ctrl+K is no longer bound at all rather than bound to both — a shortcut that
silently works from a previous version is worse than one that plainly does
not, because it teaches the wrong key.
</content></entry>
<entry><id>urn:git:abcd30f05d6b951363d00228ed1be42c983497f8</id><title>Search the documentation by substring, and build the shell to search from</title><published>2026-07-25T21:30:01+09:00</published><updated>2026-07-25T21:30: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/sqzass/commit/abcd30f05d6b951363d00228ed1be42c983497f8.html"/><content type="text">Search the documentation by substring, and build the shell to search from

Korean is the reason this is not Pagefind. Pagefind indexes words and matches
a prefix on the last one, which covers the ordinary Korean query — 생성기 is a
prefix of 생성기는 — and covers nothing else. It cannot find 최적화 inside
검색엔진최적화, because Korean writes compounds without spaces, and 존성 inside
의존성이 is invisible to it for the same reason. Segmenting the index with
ko-dic makes that worse rather than better: the dictionary does not know
loanwords, so 템플릿은 becomes 템플/릿/은 and a technical document stops being
searchable in its own vocabulary. Measured on a 2000-page corpus, recall for
템플릿 went from 1018 pages to 27.

Nor can the browser be taught to segment. Intl.Segmenter(&quot;ko&quot;) returns 어절
whole — ICU has dictionary breakers for zh/ja/th and none for Korean — so an
index of morphemes could only ever disagree with the query typed against it.

So the index is the page text and the client scans it for substrings. That is
the whole design, and it is what makes 존성, 이너리 and release (from inside a
fenced block) all return the page they are on. It costs the body text of every
page, which at documentation scale is 2.5 KB per language, fetched once when
the palette first opens, and no Rust dependency at all — the binary stays 7 MB
instead of the 15 to 84 MB the Pagefind variants measured.

The text comes off the AST, from the same parse that produces the HTML, so
nothing strips tags out of finished markup. Block starts emit a separator:
without it a heading ran into the code block under it and the index grew
`빌드git`, a word in no document, which in a substring index is a false hit
waiting to happen.

The palette is a &lt;dialog&gt;. Escape, the backdrop, the focus trap and restoring
focus to the trigger are the element&#39;s behaviour, not ours.

The page around it changed to match. The section list moved into the masthead
so the sidebar is no longer the only way across the documentation, pages carry
a breadcrumb, and the theme went dark-first with a single warm accent that
marks what you can go to and what you are on. That accent is warm on purpose:
it has to sit beside a syntax theme without arguing with it.

The theme now comes from [data-theme] written into &lt;html&gt; by the template
rather than from prefers-color-scheme. The generated highlight stylesheet
already keyed off that attribute, and a page that went dark by media query
while the stylesheet stayed light would have served light code on a dark page
to every reader whose OS is set to light — including with JavaScript off.

The index filename is not content-hashed, unlike every other generated asset.
Its contents come from the rendered pages and templates resolve asset URLs
before rendering, so hashing it would close a loop. It is data a script
fetches, and a stale copy costs a few search hits for ten minutes.
</content></entry>
<entry><id>urn:git:c7a5d7c0f6d39fb50cd0cb4bc59ab5b9b2a5baa3</id><title>Delete three partials nothing includes</title><published>2026-07-25T21:29:39+09:00</published><updated>2026-07-25T21:29:39+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/sqzass/commit/c7a5d7c0f6d39fb50cd0cb4bc59ab5b9b2a5baa3.html"/><content type="text">Delete three partials nothing includes

base.html carries its own masthead and colophon markup, so partials/header,
partials/footer and the lang-switch that only header pulled in have never been
rendered. A template that is never reached is worse than no template: the next
person to change the header edits one of them and wonders why the page does
not move.
</content></entry>
<entry><id>urn:git:40221266e98c6cdeb9f08ab2f75a4995519a2407</id><title>Reject the Pagefind Korean segmentation patch, with numbers</title><published>2026-07-25T20:57:56+09:00</published><updated>2026-07-25T20:57:56+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/sqzass/commit/40221266e98c6cdeb9f08ab2f75a4995519a2407.html"/><content type="text">Reject the Pagefind Korean segmentation patch, with numbers

The plan called for a two-line patch to Pagefind — add charabia&#39;s &quot;korean&quot;
feature, add `| &quot;ko&quot;` to `should_segment` — on the theory that Korean is
unsearchable without morphological segmentation. Measured it against a
2000-page Korean corpus with grep-derived ground truth. The theory is wrong
twice over.

Wrong once: stock Pagefind already has full recall for the ordinary Korean
query. Pagefind prefix-matches the last word of a query, and Korean attaches
particles as suffixes, so `생성기` is a prefix of 생성기는/를/가. Recall for
`템플릿`, `저작권`, `하이라이팅`, `캐시`, `병렬` came back at 1018, 1013,
1019, 1033, 1006 — the ground truth to the page.

Wrong twice, and this is the expensive one: the patch destroys that. ko-dic
is a general-purpose Korean dictionary and does not know loanwords, so it
shreds them — `템플릿은` becomes 템플/릿/은, `하이라이팅도` becomes
하이/라이팅/도. Technical documentation is made of loanwords. The same five
queries drop to 27, 28, 31, 917, 967. A 97% loss on the words this project&#39;s
own docs are written in.

The browser cannot rescue it either. Its `needsWordSegmentation` lists the
same three languages, and adding &quot;ko&quot; would be a no-op: `Intl.Segmenter(&quot;ko&quot;)`
returns 어절 whole, because ICU ships no Korean dictionary breaker. There is
no way to run the same analyzer on both sides, so an index of morphemes can
only disagree with a query that isn&#39;t one.

The `extended` feature goes too. It exists to segment zh/ja/th, and it costs
48.6 MB of jieba and lindera-unidic to do it. Korean search results from a
build with it and a build without it were identical, query for query.

What stock does not find is a morpheme inside a compound written without
spaces — 최적화 inside 검색엔진최적화. Our Korean docs space their terms, so
this is nearly empty in practice. If it ever isn&#39;t, the answer is not this
patch: keep the 어절 as the primary token and add morphemes as compound parts
at the same position, which gets the compounds without giving up the prefix
match. That was measured too, and it holds.

Also noted for the integration: Pagefind 1.5.2 does not declare the tokio
features it uses, so `default-features = false` fails to compile until the
consumer enables `fs` and `io-util`.
</content></entry>
<entry><id>urn:git:c179031662fc20771c7df0fae552aa337f764c33</id><title>Ignore the private notes directory</title><published>2026-07-25T19:29:21+09:00</published><updated>2026-07-25T19:29:21+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/sqzass/commit/c179031662fc20771c7df0fae552aa337f764c33.html"/><content type="text">Ignore the private notes directory

Project notes -- the plan, the research reports, the operational runbook and
verification screenshots -- live in .notes/ so they travel with the project
instead of sitting in a scratch directory outside it, but they are not written
for publication and stay out of the repository.
</content></entry>
<entry><id>urn:git:53ddf1967a34e8fa313ccd83911c6f0157e709f8</id><title>Resolve @/ links, and design the documentation site</title><published>2026-07-25T18:41:34+09:00</published><updated>2026-07-25T18:41:34+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/sqzass/commit/53ddf1967a34e8fa313ccd83911c6f0157e709f8.html"/><content type="text">Resolve @/ links, and design the documentation site

The seed content used @/start/_index.md, a source-path link that nothing
implemented, so href=&quot;@/start/_index.md&quot; shipped to production. Links now
resolve through comrak&#39;s URL rewriter at the AST stage, and an unresolved one
fails the build -- shipping a dead link is exactly the class of bug this tool
claims to prevent.

Resolution is language-aware: the same @/start/installation.md sends an English
page to /start/installation/ and a Korean page to /ko/start/installation/, so
translated markdown needs no per-language link edits. A page with no
translation falls back to the default language rather than breaking.

For the design: the page is monochrome and the only colour on it comes from the
code. Highlighting is the feature this tool does properly, and a brand colour
competing with it would be noise. One accent exists, for links and focus.

Mono carries structure -- wordmark, headings, labels, code -- and the sans face
is reserved for prose. The +++ rule appears once per page, between the title
block and the body: the same boundary it marks in the source file.

Korean overrides all three of those Latin devices. letter-spacing pulls syllable
blocks apart, text-transform does nothing, and mono is the worst of them: Hangul
falls back to a proportional font while the space still comes from the mono
face, so every gap between 어절 renders double width. Verified by screenshot,
which is how the double-space was caught.
</content></entry>
<entry><id>urn:git:553e0b1f0f6147216ef60e3811166c9376ef1be1</id><title>Add an asset pipeline with content-hashed filenames</title><published>2026-07-25T18:27:33+09:00</published><updated>2026-07-25T18:27: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/sqzass/commit/553e0b1f0f6147216ef60e3811166c9376ef1be1.html"/><content type="text">Add an asset pipeline with content-hashed filenames

static/ is copied into the output and CSS/JS get a blake3 content hash spliced
before the extension, with an asset-manifest.json mapping logical names to real
ones. Templates look assets up with {{ asset(&quot;css/main.css&quot;) }}, and an unknown
name is an error rather than a silent passthrough, so a typo surfaces with the
template name and line instead of shipping a 404 link.

Hashing goes in the filename, not a query string: some CDNs and proxies drop
the query from the cache key, and the common &quot;one build stamp on every file&quot;
variant invalidates all twenty stylesheets when one changes.

Only CSS and JS are hashed. Images and fonts are referenced by literal path
from markdown and CSS, and files like CNAME have a name that *is* the contract
-- hashing those breaks them.

The generated highlight stylesheet goes through the same pipeline instead of
having its own hardcoded path.

This also removes the workflow&#39;s temporary static-copy step, which existed only
because the pipeline did not.
</content></entry>
<entry><id>urn:git:b9df84fe33ce8d3d49a8e141cbd8c9d2ef1bc7fb</id><title>Add a dev server with live reload</title><published>2026-07-25T18:24:03+09:00</published><updated>2026-07-25T18:24: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/sqzass/commit/b9df84fe33ce8d3d49a8e141cbd8c9d2ef1bc7fb.html"/><content type="text">Add a dev server with live reload

Builds into memory and serves from there. Nothing touches disk, so the browser
can never fetch a half-written file and saving does not amplify into writes.

The reload script is injected at serve time rather than at build time, which
keeps the build output byte-identical to what ships. A CSS-only change swaps
the stylesheet href instead of reloading, so scroll position and open sidebar
state survive -- without that, iterating on design is miserable. Everything
else reloads, restoring scroll from sessionStorage.

A failed build keeps serving the last good output and shows the error as an
overlay; dropping the site would hide the very page you are trying to fix. A
tab that connects while the build is broken is sent the current error
immediately, and gets an explicit clear when it is fixed, so a reconnecting tab
never sits on a stale &quot;build failed&quot; over a healthy site.

The WebSocket handshake validates Origin and fails closed: a missing or
unparseable Origin is rejected rather than allowed. Bracketed IPv6 authorities
are handled, and a prefix match like localhost.evil.example does not pass.

Path::extension() returns None for dotfiles, so /.nojekyll was being treated as
a directory and 404ing. Caught by a test before it shipped.
</content></entry>
<entry><id>urn:git:82cb8bb0d7bb156f11a835eb01ff1318b3d4346d</id><title>Use struct update syntax in two tests</title><published>2026-07-25T17:57:14+09:00</published><updated>2026-07-25T17:57: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/sqzass/commit/82cb8bb0d7bb156f11a835eb01ff1318b3d4346d.html"/><content type="text">Use struct update syntax in two tests

clippy::field_reassign_with_default failed CI on the previous commit. My local
check reported clean because the exit status was masked by a shell pipeline.
</content></entry>
<entry><id>urn:git:086207ad4f9c3f1cfaeab3db3d46a64747d88d11</id><title>Highlight code at build time, as CSS classes</title><published>2026-07-25T17:56:16+09:00</published><updated>2026-07-25T17:56:16+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/sqzass/commit/086207ad4f9c3f1cfaeab3db3d46a64747d88d11.html"/><content type="text">Highlight code at build time, as CSS classes

syntect emits semantic class names rather than inline style attributes. The
convenience path (highlighted_html_for_string) writes colours straight into the
markup, which pins one theme into every document forever: dark mode becomes
impossible without rebuilding, every page carries duplicated colour strings,
and a strict style-src CSP is off the table. With classes, the same HTML works
under both themes and switching is a stylesheet swap.

The stylesheet is generated from two themes and carries the dark rules twice:
once inside prefers-color-scheme for the OS setting, once under
[data-theme=&quot;dark&quot;] for a manual toggle. Native CSS nesting wraps syntect&#39;s
output as-is, so no CSS parsing is involved.

Classes are prefixed with hl-. Without a prefix syntect emits bare words like
&quot;source&quot;, &quot;keyword&quot; and &quot;string&quot;, which collide with ordinary site CSS.

two-face supplies TOML, TypeScript and Dockerfile, none of which are in
syntect&#39;s bundled set and all of which a developer-tool&#39;s documentation needs.
Both crates are pinned to fancy-regex: the default is Oniguruma, a C binding
that breaks musl static builds and cross-compilation, and two-face silently
pulls it back in unless its feature is matched. There are now zero references
to onig in the dependency tree.
</content></entry>
<entry><id>urn:git:7c51577c5e909d55ab9cbacb960952d8cd5917b4</id><title>Add heading anchors and a table of contents</title><published>2026-07-25T17:50:14+09:00</published><updated>2026-07-25T17:50: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/sqzass/commit/7c51577c5e909d55ab9cbacb960952d8cd5917b4.html"/><content type="text">Add heading anchors and a table of contents

Headings get ids and anchor links through comrak&#39;s HeadingAdapter, and the same
pass collects the table of contents. One Anchorizer instance serves both: it
appends -1, -2 to repeated titles, so a separate instance for the TOC would
produce ids that no longer match the headings and every duplicate-title link
would land on the wrong section.

Ids are emitted even when anchors are turned off, because the TOC and deep
links need them regardless.

The nesting pass does not assume levels are contiguous. Documents that jump
from h2 to h4 are common enough that treating them as an error, or flattening
them, would be wrong.

Korean headings keep their Hangul ids -- comrak&#39;s Anchorizer passes them
through rather than transliterating or stripping, which was verified before
building on it.
</content></entry>
<entry><id>urn:git:871f1fa00b70ad54fd8fe48afa3cc48e6e641660</id><title>Build a section tree, and stop escaping slashes in URLs</title><published>2026-07-25T17:46:11+09:00</published><updated>2026-07-25T17:46:11+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/sqzass/commit/871f1fa00b70ad54fd8fe48afa3cc48e6e641660.html"/><content type="text">Build a section tree, and stop escaping slashes in URLs

Pages are assembled into a per-language section tree, so the sidebar for a
language contains only pages that exist in it -- an untranslated page cannot
produce a dead link because it is never in that tree at all. Directories
without an _index.md are still promoted to sections, otherwise forgetting one
would silently drop its pages from navigation.

Template selection gains the parent section&#39;s page_template, completing the
four-step order: front matter, section.html, page_template, page.html.

Separately: minijinja&#39;s HTML escaper rewrites &quot;/&quot; as &quot;&amp;#x2f;&quot;, which turned
every link on every page into href=&quot;https:&amp;#x2f;&amp;#x2f;...&quot;. That escape is an
OWASP belt-and-braces measure aimed at &lt;/script&gt; breakouts, which escaping &quot;&lt;&quot;
already prevents, and Jinja2 itself does not do it. A custom formatter restores
the original semantics; everything else is still escaped, with tests covering
the XSS-shaped inputs. The v_htmlescape feature was tried first and rejected
because it escapes &quot;/&quot; too.
</content></entry>
<entry><id>urn:git:3709f8529af4a510b8e403ad34974a548faf6d59</id><title>Update references after GitHub account rename to sqzer-x</title><published>2026-07-25T17:18:44+09:00</published><updated>2026-07-25T17:18: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/sqzass/commit/3709f8529af4a510b8e403ad34974a548faf6d59.html"/><content type="text">Update references after GitHub account rename to sqzer-x

Repository URLs in Cargo.toml and the installation docs, plus the LICENSE
copyright holder. The git remote and commit email are updated locally too.

GitHub redirects repository URLs from the old account name, but not
&lt;user&gt;.github.io -- the old Pages URL now returns 404. That matters because
the DNS record for the custom domain has to target sqzer-x.github.io, which
is why the rename was worth doing before creating it rather than after.

Existing commits stay attributed: the previous noreply address was the
ID-based form, and GitHub matches on the numeric ID rather than the name.
</content></entry>
<entry><id>urn:git:ca12501e85e8c608f234aad0f7ff78344c7725d6</id><title>Do not commit .cargo/config.toml</title><published>2026-07-25T16:52:33+09:00</published><updated>2026-07-25T16:52:33+09:00</updated><author><name>sqzer</name><email>110329244+devastator-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/sqzass/commit/ca12501e85e8c608f234aad0f7ff78344c7725d6.html"/><content type="text">Do not commit .cargo/config.toml

The committed mold linker flag broke CI: GitHub runners do not have mold, so
every build script failed with &quot;collect2: fatal error: cannot find &#39;ld&#39;&quot;.

A linker is a property of the machine, not of the project. Committing
.cargo/config.toml forces that choice on CI and on everyone who clones. It is
gitignored now, with the mold snippet documented in the README for anyone who
wants it locally.
</content></entry>
<entry><id>urn:git:79c742bd888c498e0047d2951d57f370e4492163</id><title>Add CI and GitHub Pages deployment workflows</title><published>2026-07-25T16:50:51+09:00</published><updated>2026-07-25T16:50:51+09:00</updated><author><name>sqzer</name><email>110329244+devastator-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/sqzass/commit/79c742bd888c498e0047d2951d57f370e4492163.html"/><content type="text">Add CI and GitHub Pages deployment workflows

CI runs fmt, clippy -D warnings and tests, then builds the docs site with the
binary it just compiled -- that build is the integration test the unit tests
cannot be. It also builds twice and diffs, so a regression in output
determinism fails the pipeline rather than surfacing as a mystery later.

Pages builds docs/ and deploys via upload-pages-artifact. The custom domain
lives in docs/static/CNAME rather than inline in the workflow, so it travels
with the site; the explicit copy step goes away once the asset pipeline lands
and static/ is handled by sqzass itself.
</content></entry>
<entry><id>urn:git:5cec51af7e9eaf20547d8d12dda92ecde366d370</id><title>Initial commit: markdown to HTML build pipeline</title><published>2026-07-25T16:46:48+09:00</published><updated>2026-07-25T16:46:48+09:00</updated><author><name>sqzer</name><email>110329244+devastator-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/sqzass/commit/5cec51af7e9eaf20547d8d12dda92ecde366d370.html"/><content type="text">Initial commit: markdown to HTML build pipeline

Builds a site from TOML front matter markdown through comrak and minijinja,
emitting path/index.html so pretty URLs work on hosts that have no rewrite
rules, plus a .nojekyll marker for GitHub Pages.

- config: sqzass.toml with defaults and unknown-key detection that suggests
  the intended key rather than failing the build
- content: discovery, TOML front matter with real line offsets for error
  reporting, filename-suffix i18n (page.ko.md), URL derivation, hard error
  on slug collisions instead of a filesystem-order-dependent overwrite
- markdown: comrak with cjk_friendly_emphasis enabled, which Korean needs --
  without it emphasis attached to a particle does not parse
- render: minijinja with a snapshot loader so a rebuild triggered mid-save
  cannot read a half-written partial; autoescape on, SemiStrict undefined

comrak is pinned with default-features = false. Its defaults are
[&quot;cli&quot;, &quot;syntect-onig&quot;, &quot;bon&quot;], which drags in comrak&#39;s own CLI dependencies
and pulls syntect via Oniguruma, a C binding that breaks musl static builds
and cross-compilation. The adapter traits we need are not feature-gated.

Two builds of the same input are byte-identical. 26 tests, clippy clean.
</content></entry>
</feed>
