sqzass.git

sqzass

~ A static site generator written in Rust
git clone https://github.com/debuman/sqzass.git
commit 03a1dc5a15e671a0939db725d9165d2770313f38
parent f2732eb8767e3b339687bf50ea1f549ace44b4c7
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Fri, 31 Jul 2026 22:16:48 +0900

문서가 코드와 어긋난 자리를 코드에 맞춘다

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

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

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

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

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

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

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

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

나머지: dev-server의 감시 대상(사이트 루트 전체가 아니라 네 디렉터리와
sqzass.toml), 검색 색인 크기(몇 KB가 아니라 ~80KB), search.js 줄 수,
하이라이트 출력의 pre/data-lang, 피드의 "같은 날짜"→"같은 순간",
page.section·prev·next가 섹션 인덱스에서 비는 것, 색인 생성 조건.

Diffstat:
Mdocs/content/benchmark/_index.ko.md | 4++--
Mdocs/content/benchmark/_index.md | 4++--
Mdocs/content/content/_index.ko.md | 8+++++---
Mdocs/content/content/_index.md | 8+++++---
Mdocs/content/content/languages.ko.md | 2+-
Mdocs/content/content/languages.md | 2+-
Mdocs/content/content/links.ko.md | 7+++++--
Mdocs/content/content/links.md | 8++++++--
Mdocs/content/deploy/_index.ko.md | 2+-
Mdocs/content/deploy/_index.md | 2+-
Mdocs/content/deploy/github-pages.ko.md | 19++++++++++++++++++-
Mdocs/content/deploy/github-pages.md | 20+++++++++++++++++++-
Mdocs/content/features/dev-server.ko.md | 4+++-
Mdocs/content/features/dev-server.md | 5++++-
Mdocs/content/features/feeds.ko.md | 4++--
Mdocs/content/features/feeds.md | 5+++--
Mdocs/content/features/highlighting.ko.md | 2+-
Mdocs/content/features/highlighting.md | 2+-
Mdocs/content/features/search.ko.md | 6+++---
Mdocs/content/features/search.md | 6+++---
Mdocs/content/start/cli.ko.md | 4++--
Mdocs/content/start/cli.md | 2+-
Mdocs/content/start/configuration.ko.md | 2+-
Mdocs/content/start/configuration.md | 2+-
Mdocs/content/templates/data.ko.md | 10++++++----
Mdocs/content/templates/data.md | 6++++--
Mdocs/content/templates/functions.ko.md | 9++++++---
Mdocs/content/templates/functions.md | 11+++++++----
28 files changed, 114 insertion(+), 52 deletion(-)

diff --git a/docs/content/benchmark/_index.ko.md b/docs/content/benchmark/_index.ko.md
@@ -8,7 +8,7 @@ toc = true
 ## minimal — 제목 하나와 문단 하나
 
 <div class="bench">
-<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.6%" height="100%" rx="3"></rect></svg><span class="b-val">18 ms</span></span>
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.385%" height="100%" rx="3"></rect></svg><span class="b-val">18 ms</span></span>
 <span class="b-name">Hugo</span><span class="b-row"><svg><rect width="2%" height="100%" rx="3"></rect></svg><span class="b-val">93 ms</span></span>
 <span class="b-name">Zola</span><span class="b-row"><svg><rect width="3%" height="100%" rx="3"></rect></svg><span class="b-val">141 ms</span></span>
 <span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="9.5%" height="100%" rx="3"></rect></svg><span class="b-val">442 ms</span></span>
@@ -18,7 +18,7 @@ toc = true
 ## blog — 문단 여섯, 목록, 인용, 링크
 
 <div class="bench">
-<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.6%" height="100%" rx="3"></rect></svg><span class="b-val">25 ms</span></span>
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.373%" height="100%" rx="3"></rect></svg><span class="b-val">25 ms</span></span>
 <span class="b-name">Hugo</span><span class="b-row"><svg><rect width="1.7%" height="100%" rx="3"></rect></svg><span class="b-val">112 ms</span></span>
 <span class="b-name">Zola</span><span class="b-row"><svg><rect width="2.6%" height="100%" rx="3"></rect></svg><span class="b-val">172 ms</span></span>
 <span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="11.2%" height="100%" rx="3"></rect></svg><span class="b-val">752 ms</span></span>
diff --git a/docs/content/benchmark/_index.md b/docs/content/benchmark/_index.md
@@ -8,7 +8,7 @@ toc = true
 ## minimal — a heading and a paragraph
 
 <div class="bench">
-<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.6%" height="100%" rx="3"></rect></svg><span class="b-val">18 ms</span></span>
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.385%" height="100%" rx="3"></rect></svg><span class="b-val">18 ms</span></span>
 <span class="b-name">Hugo</span><span class="b-row"><svg><rect width="2%" height="100%" rx="3"></rect></svg><span class="b-val">93 ms</span></span>
 <span class="b-name">Zola</span><span class="b-row"><svg><rect width="3%" height="100%" rx="3"></rect></svg><span class="b-val">141 ms</span></span>
 <span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="9.5%" height="100%" rx="3"></rect></svg><span class="b-val">442 ms</span></span>
@@ -18,7 +18,7 @@ toc = true
 ## blog — six paragraphs, a list, a quote, a link
 
 <div class="bench">
-<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.6%" height="100%" rx="3"></rect></svg><span class="b-val">25 ms</span></span>
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.373%" height="100%" rx="3"></rect></svg><span class="b-val">25 ms</span></span>
 <span class="b-name">Hugo</span><span class="b-row"><svg><rect width="1.7%" height="100%" rx="3"></rect></svg><span class="b-val">112 ms</span></span>
 <span class="b-name">Zola</span><span class="b-row"><svg><rect width="2.6%" height="100%" rx="3"></rect></svg><span class="b-val">172 ms</span></span>
 <span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="11.2%" height="100%" rx="3"></rect></svg><span class="b-val">752 ms</span></span>
diff --git a/docs/content/content/_index.ko.md b/docs/content/content/_index.ko.md
@@ -22,9 +22,11 @@ content/
         └── dive.md      →  /guide/deep/dive/
 ```
 
-`_index.md`가 **없는** 디렉터리도 그 안의 페이지 URL은 그대로 만들어집니다.
-다만 섹션은 아닙니다. 그 페이지들을 묶어 주는 것이 없고, 디렉터리 자체를 가리키는
-내비게이션 항목도 생기지 않습니다.
+`_index.md`가 **없는** 디렉터리도 섹션이 됩니다. 제목은 디렉터리 이름이 되고,
+그 안의 페이지들이 묶이고, 내비게이션에도 나타납니다 — 파일 하나 깜빡했다고
+페이지가 사이드바에서 사라지면 안 되니까요. 다만 인덱스 페이지는 생기지 않아서
+`/guide/` 자체는 하나 만들기 전까지 404입니다. 섹션에 고유한 제목이나 설명, 본문을
+주고 싶을 때 `_index.md`를 둡니다.
 
 ## 모든 URL이 디렉터리인 이유
 
diff --git a/docs/content/content/_index.md b/docs/content/content/_index.md
@@ -22,9 +22,11 @@ content/
         └── dive.md      →  /guide/deep/dive/
 ```
 
-A directory *without* an `_index.md` still produces URLs for the pages inside
-it, but it is not a section: nothing collects those pages, and no navigation
-entry exists for the directory itself.
+A directory *without* an `_index.md` still becomes a section, titled after the
+directory, so its pages are collected and it appears in navigation — forgetting
+the file must not make pages vanish from the sidebar. What it does not get is an
+index page: `/guide/` itself 404s until you add one. Add `_index.md` when the
+section needs a title of its own, a description, or a body.
 
 ## Why every URL is a directory
 
diff --git a/docs/content/content/languages.ko.md b/docs/content/content/languages.ko.md
@@ -95,7 +95,7 @@ on_this_page = "이 페이지"
 ```
 
 ```html
-<a href="{{ home }}">{{ t("home") }}</a>
+<a href="{{ site.base_path }}/">{{ t("home") }}</a>
 ```
 
 `t`는 지금 렌더 중인 페이지에서 언어를 읽습니다. 그래서 템플릿은 자기가 어느
diff --git a/docs/content/content/languages.md b/docs/content/content/languages.md
@@ -98,7 +98,7 @@ on_this_page = "이 페이지"
 ```
 
 ```html
-<a href="{{ home }}">{{ t("home") }}</a>
+<a href="{{ site.base_path }}/">{{ t("home") }}</a>
 ```
 
 `t` reads the language from the page being rendered, so a template never asks
diff --git a/docs/content/content/links.ko.md b/docs/content/content/links.ko.md
@@ -19,7 +19,7 @@ toc = true
 아무것도 가리키지 못하는 `@/` 링크는 에러입니다.
 
 ```
-docs/content/start/first-site.md: 해석할 수 없는 내부 링크가 있습니다:
+docs/content/start/first-site.md: 어디도 가리키지 않는 링크가 있습니다:
   @/start/setup.md
 ```
 
@@ -46,7 +46,10 @@ docs/content/start/first-site.md: 해석할 수 없는 내부 링크가 있습
 따옴표를 빼면 조용히 처리에서 빠집니다. 빌드가 아니라 프로덕션에서 발견되는 종류의
 버그입니다.
 
-이미지 경로도 같은 길을 지나므로 `![](@/images/x.png)`도 동작합니다.
+이미지도 같은 *검사기*를 지나지만 문법은 다릅니다. `@/`는 마크다운 페이지 표를
+뒤지는데 `static/`의 이미지는 그 표에 없으므로 `![](@/images/x.png)`는 빌드를
+멈춥니다. 루트 절대 경로로 씁니다 — `![](/images/x.png)`. 그러면 빌드가 파일이
+있는지 확인하므로, 이미지 경로의 오타도 링크의 오타와 똑같이 빌드를 멈춥니다.
 
 ## 생성된 파일도 링크 대상입니다
 
diff --git a/docs/content/content/links.md b/docs/content/content/links.md
@@ -20,7 +20,7 @@ follows.
 An `@/` link that resolves to nothing is an error:
 
 ```
-docs/content/start/first-site.md: 해석할 수 없는 내부 링크가 있습니다:
+docs/content/start/first-site.md: 어디도 가리키지 않는 링크가 있습니다:
   @/start/setup.md
 ```
 
@@ -49,7 +49,11 @@ exists. The shortcut — running a regex over the finished HTML — silently ski
 any element whose attributes are single-quoted or unquoted, which produces a
 class of bug you find in production rather than in the build.
 
-Image sources go through the same path, so `![](@/images/x.png)` works too.
+Images go through the same *checker*, but not the same syntax. `@/` resolves
+against the table of markdown pages, and an image in `static/` is not in it, so
+`![](@/images/x.png)` stops the build. Write the root-absolute path instead —
+`![](/images/x.png)` — and the build verifies the file is there, so a typo in an
+image path fails the same way a typo in a link does.
 
 ## Generated files are link targets too
 
diff --git a/docs/content/deploy/_index.ko.md b/docs/content/deploy/_index.ko.md
@@ -113,7 +113,7 @@ GitHub이 영영 찾지 않을 파일입니다.
 | `assets/highlight.<해시>.css` | `[highlight] enabled = false`가 아니면 |
 | `asset-manifest.json` | 논리 이름 → 실제 URL, 항상 |
 | `<경로>/index.html` | 페이지마다 하나 |
-| `search-<언어>.json` | 언어마다 하나 |
+| `search-<언어>.json` | `[search] enabled = false`가 아니면 언어마다 하나 |
 | `feed-<언어>.xml` | 날짜 있는 페이지가 있는 언어마다 하나 |
 | `sitemap.xml`, `robots.txt` | 같은 이름을 `static/`에 두지 않았다면 |
 | `llms.txt` | 같은 조건 |
diff --git a/docs/content/deploy/_index.md b/docs/content/deploy/_index.md
@@ -119,7 +119,7 @@ rather than take on trust:
 | `assets/highlight.<hash>.css` | unless `[highlight] enabled = false` |
 | `asset-manifest.json` | logical name → written URL, always |
 | `<path>/index.html` | one per page |
-| `search-<lang>.json` | one per language |
+| `search-<lang>.json` | one per language, unless `[search] enabled = false` |
 | `feed-<lang>.xml` | one per language that has a dated page |
 | `sitemap.xml`, `robots.txt` | unless `static/` supplied one with that name |
 | `llms.txt` | same terms |
diff --git a/docs/content/deploy/github-pages.ko.md b/docs/content/deploy/github-pages.ko.md
@@ -16,6 +16,7 @@ name: Deploy docs
 on:
   push:
     branches: [main]
+  workflow_dispatch:
 
 permissions:
   contents: read
@@ -44,15 +45,31 @@ jobs:
     runs-on: ubuntu-latest
     environment:
       name: github-pages
-      url: ${{ steps.deployment.outputs.page_url }}
+      url: ${{ steps.deployment.outputs.page_url || steps.retry.outputs.page_url }}
     steps:
       - id: deployment
         uses: actions/deploy-pages@v5
+        continue-on-error: true
+
+      - name: Wait for the previous deployment to settle
+        if: steps.deployment.outcome == 'failure'
+        run: sleep 90
+
+      - id: retry
+        if: steps.deployment.outcome == 'failure'
+        uses: actions/deploy-pages@v5
 ```
 
 `cancel-in-progress: false`는 그대로 두는 게 좋습니다. 진행 중인 배포를 취소하면
 사이트가 반쯤 갱신된 상태로 남는데, 그건 기다리는 것보다 나쁩니다.
 
+재시도는 혹시 몰라 넣은 게 아니라 이 사이트가 실제로 겪은 실패의 해결책입니다.
+`concurrency`는 워크플로 **실행**을 직렬화하는데, Pages 배포는 그보다 오래 삽니다.
+앞선 실행이 끝난 뒤에도 GitHub 쪽에서 처리 중일 수 있고, 커밋을 연달아 밀면 두
+번째가 그 창에 들어가 400 "due to in progress deployment"로 죽습니다. 기다렸다
+한 번 더 시도하면 됩니다. 안전하기도 합니다 — 배포가 무언가를 바꾸기 전에
+실패하는 것이라, 재시도가 사이트를 반쯤 갱신된 상태로 만들지 않습니다.
+
 ## 커스텀 도메인
 
 도메인을 `static/CNAME`에 넣습니다.
diff --git a/docs/content/deploy/github-pages.md b/docs/content/deploy/github-pages.md
@@ -17,6 +17,7 @@ name: Deploy docs
 on:
   push:
     branches: [main]
+  workflow_dispatch:
 
 permissions:
   contents: read
@@ -45,15 +46,32 @@ jobs:
     runs-on: ubuntu-latest
     environment:
       name: github-pages
-      url: ${{ steps.deployment.outputs.page_url }}
+      url: ${{ steps.deployment.outputs.page_url || steps.retry.outputs.page_url }}
     steps:
       - id: deployment
         uses: actions/deploy-pages@v5
+        continue-on-error: true
+
+      - name: Wait for the previous deployment to settle
+        if: steps.deployment.outcome == 'failure'
+        run: sleep 90
+
+      - id: retry
+        if: steps.deployment.outcome == 'failure'
+        uses: actions/deploy-pages@v5
 ```
 
 `cancel-in-progress: false` is worth keeping. Cancelling a deploy that is
 halfway through leaves the site partly updated, which is worse than waiting.
 
+The retry is not defensive padding — it is the fix for a failure this site hit.
+`concurrency` serialises workflow *runs*, but a Pages deployment outlives its
+run: GitHub can still be processing the previous one after the workflow that
+started it has finished. Push two commits close together and the second lands
+in that window and dies with a 400, "due to in progress deployment". Waiting and
+trying once more is enough, and it is safe: the deploy fails before it changes
+anything, so a retry cannot leave the site half updated.
+
 ## A custom domain
 
 Put the domain in `static/CNAME`:
diff --git a/docs/content/features/dev-server.ko.md b/docs/content/features/dev-server.ko.md
@@ -9,7 +9,9 @@ toc = true
 sqzass serve -i docs
 ```
 
-<http://127.0.0.1:3000>에 뜨고, 사이트 루트 아래 파일이 바뀔 때마다 다시 빌드합니다.
+<http://127.0.0.1:3000>에 뜨고, `content/`·`templates/`·`static/`·`i18n/` 안의
+무엇이든, 또는 `sqzass.toml` 자체가 바뀔 때마다 다시 빌드합니다. 출력 디렉터리는
+일부러 보지 않습니다. 자기가 쓴 것을 보고 다시 빌드하면 끝나지 않습니다.
 
 | 플래그 | | |
 |---|---|---|
diff --git a/docs/content/features/dev-server.md b/docs/content/features/dev-server.md
@@ -9,7 +9,10 @@ toc = true
 sqzass serve -i docs
 ```
 
-<http://127.0.0.1:3000>, rebuilding whenever a file under the site root changes.
+<http://127.0.0.1:3000>, rebuilding whenever anything in `content/`, `templates/`,
+`static/` or `i18n/` changes, or `sqzass.toml` itself. The output directory is
+deliberately not watched: a build that triggered on its own output would never
+stop rebuilding.
 
 | Flag | | |
 |---|---|---|
diff --git a/docs/content/features/feeds.ko.md b/docs/content/features/feeds.ko.md
@@ -56,8 +56,8 @@ Atom은 RFC 3339를 쓰고, 그건 TOML 날짜가 이미 갖고 있는 모양입
 `description`이 요약으로 들어갑니다. 20은 우연이 아니라 상한입니다 — 끝없이 자라는
 피드는 언젠가 다운로드가 됩니다.
 
-같은 날짜의 페이지는 제목순입니다. 그래야 같은 날 쓴 글 둘이 빌드할 때마다 같은
-순서로 나옵니다.
+같은 시각에 놓인 페이지는 제목순입니다. 그래야 빌드할 때마다 같은 순서로 나옵니다.
+날짜만 적으면 자정 UTC가 되므로, 실제로는 같은 날 쓴 글끼리가 모두 여기 해당합니다.
 
 시각이 없는 날짜는 자정 UTC가 됩니다. Atom은 날짜만으로는 받지 않고, 시각을 파싱
 못 한 리더는 그 항목을 아무 말 없이 버립니다.
diff --git a/docs/content/features/feeds.md b/docs/content/features/feeds.md
@@ -57,8 +57,9 @@ The 20 most recent dated pages, newest first, each with a title, a permalink, an
 `updated` timestamp, and the `description` as its summary. Twenty is a cap, not
 a coincidence — a feed that grows without limit eventually becomes a download.
 
-Pages within a day are ordered by title, so two posts dated the same day come
-out in the same order on every build.
+Pages that land on the same instant are ordered by title, so they come out in
+the same order on every build. Since a bare date becomes midnight UTC, in
+practice that is every pair of posts written on the same day.
 
 A date with no time becomes midnight UTC. Atom will not accept a bare date, and
 a reader that cannot parse a timestamp drops the entry without telling anyone.
diff --git a/docs/content/features/highlighting.ko.md b/docs/content/features/highlighting.ko.md
@@ -20,7 +20,7 @@ theme_dark  = "base16-ocean.dark"
 강조된 블록은 이렇게 생겼습니다.
 
 ```html
-<code class="language-rust"><span class="hl-source hl-rust">…</span></code>
+<pre class="highlight"><code class="language-rust" data-lang="rust"><span class="hl-source hl-rust">…</span></code></pre>
 ```
 
 `style="color:#268bd2"`가 아닙니다. 이 차이가 세 가지를 결정합니다.
diff --git a/docs/content/features/highlighting.md b/docs/content/features/highlighting.md
@@ -20,7 +20,7 @@ theme_dark  = "base16-ocean.dark"
 A highlighted block looks like this:
 
 ```html
-<code class="language-rust"><span class="hl-source hl-rust">…</span></code>
+<pre class="highlight"><code class="language-rust" data-lang="rust"><span class="hl-source hl-rust">…</span></code></pre>
 ```
 
 Not `style="color:#268bd2"`. The distinction decides three things.
diff --git a/docs/content/features/search.ko.md b/docs/content/features/search.ko.md
@@ -44,12 +44,12 @@ JSON 파일 하나입니다.
 
 ## 대가
 
-색인은 모든 페이지의 본문입니다. 이 사이트 기준으로 언어당 몇 KB이고, 첫 검색 때
+색인은 모든 페이지의 본문입니다. 이 사이트 기준으로 언어당 80KB쯤이고, 첫 검색 때
 한 번 받습니다. 문서가 늘면 선형으로 커지고, 어느 크기를 넘으면 이 설계가 틀린
 선택이 되는 지점이 분명히 있습니다. 다만 그 지점은 문서 사이트보다 훨씬 뒤에 있고,
 거기 도달하는 건 자기 문서의 단어를 못 찾는 검색을 배포하는 것보다 나은 문제입니다.
 
-검색 UI가 없는 사이트라면 그 몇 KB도 낼 이유가 없습니다.
+검색 UI가 없는 사이트라면 그 80KB도 낼 이유가 없습니다.
 
 ```toml
 [search]
@@ -98,7 +98,7 @@ enabled = false
 
 ## 클라이언트
 
-이 사이트의 `search.js`는 150줄 정도이고 의존성이 없습니다. 대화상자는 `<dialog>`라서
+이 사이트의 `search.js`는 200줄 남짓이고 의존성이 없습니다. 대화상자는 `<dialog>`라서
 Esc, 백드롭, 포커스 트랩, 닫을 때 트리거로 포커스를 되돌리는 것까지 전부 코드가 아니라
 플랫폼의 동작입니다. `Ctrl`/`⌘` + `/`로 엽니다.
 
diff --git a/docs/content/features/search.md b/docs/content/features/search.md
@@ -46,8 +46,8 @@ and it costs a JSON file.
 
 ## What it costs
 
-The index is the body text of every page. For this site that is a couple of
-kilobytes per language, fetched once, on the first search. It grows linearly
+The index is the body text of every page. For this site that is around 80 KB
+per language, fetched once, on the first search. It grows linearly
 with your documentation, and there is a size past which this is the wrong
 design — but that size is far beyond a documentation site, and reaching it is a
 better problem than shipping search that cannot find your own words.
@@ -103,7 +103,7 @@ nobody reads by hand.
 
 ## The client
 
-`search.js` on this site is about 150 lines and has no dependencies. The dialog
+`search.js` on this site is a couple of hundred lines and has no dependencies. The dialog
 is a `<dialog>`, so Escape, the backdrop, the focus trap and returning focus to
 the trigger are the platform's behaviour rather than code. Open it with
 `Ctrl`/`⌘` + `/`.
diff --git a/docs/content/start/cli.ko.md b/docs/content/start/cli.ko.md
@@ -12,7 +12,7 @@ sqzass serve [-i DIR] [-b ADDR] [-p PORT] [--drafts] [--base-url URL]
 sqzass doctor [-i DIR] [--fail-on note|warn] [--drafts]
 ```
 
-`--json`은 셋 다에 붙습니다.
+`--json`은 어느 명령에나 붙습니다.
 
 ## init
 
@@ -96,7 +96,7 @@ sqzass doctor -i mysite --json            # 모든 지적을 데이터로
 식별자는 메시지와 함께 찍히므로 그대로 검색할 수 있습니다.
 
 ```
-error: [SQZASS_E_CONTENT] content/_index.md: 해석할 수 없는 내부 링크가 있습니다:
+error: [SQZASS_E_CONTENT] content/_index.md: 어디도 가리키지 않는 링크가 있습니다:
   @/nope.md
 ```
 
diff --git a/docs/content/start/cli.md b/docs/content/start/cli.md
@@ -97,7 +97,7 @@ without parsing text.
 The identifier is printed with the message, so it can be searched for:
 
 ```
-error: [SQZASS_E_CONTENT] content/_index.md: 해석할 수 없는 내부 링크가 있습니다:
+error: [SQZASS_E_CONTENT] content/_index.md: 어디도 가리키지 않는 링크가 있습니다:
   @/nope.md
 ```
 
diff --git a/docs/content/start/configuration.ko.md b/docs/content/start/configuration.ko.md
@@ -16,7 +16,7 @@ base_url = "https://example.com"
 ## 오타는 에러입니다
 
 ```
-error: sqzass.toml 파싱 실패: TOML parse error at line 8, column 1
+error: [SQZASS_E_CONFIG] sqzass.toml 파싱 실패: TOML parse error at line 8, column 1
   |
 8 | theme_ligth = "InspiredGitHub"
   | ^^^^^^^^^^^
diff --git a/docs/content/start/configuration.md b/docs/content/start/configuration.md
@@ -16,7 +16,7 @@ base_url = "https://example.com"
 ## A typo is an error
 
 ```
-error: sqzass.toml 파싱 실패: TOML parse error at line 8, column 1
+error: [SQZASS_E_CONFIG] sqzass.toml 파싱 실패: TOML parse error at line 8, column 1
   |
 8 | theme_ligth = "InspiredGitHub"
   | ^^^^^^^^^^^
diff --git a/docs/content/templates/data.ko.md b/docs/content/templates/data.ko.md
@@ -17,8 +17,9 @@ toc = true
 | `site.base_path` | 서브경로 아래 사이트일 때의 경로 접두사, 루트면 빈 값. 템플릿이 직접 쓰는 URL에만 필요합니다. |
 | `site.language` | 지금 렌더 중인 페이지의 언어. |
 | `site.sections` | **이 언어의** 최상위 섹션들. |
-| `site.highlight_css` | 생성된 하이라이트 스타일시트 URL. 강조가 꺼져 있으면 없습니다. |
+| `site.highlight_css` | 생성된 하이라이트 스타일시트 URL. 구문 강조가 꺼져 있으면 없습니다. |
 | `site.search` | 이 언어의 검색 색인 URL. `[search] enabled = false`면 없습니다. |
+| `site.feed` | 이 언어의 Atom 피드 URL. 날짜 있는 페이지가 하나도 없으면 없습니다. [피드](@/features/feeds.md)를 참고하세요. |
 
 `site.sections`에는 현재 언어의 트리만 담깁니다. 내비게이션이 안전한 이유가
 이것입니다. 번역되지 않은 페이지는 여기 없으므로 그 링크를 그릴 수가 없습니다.
@@ -38,11 +39,12 @@ toc = true
 | `page.toc` | 저자가 목차를 원했는지. |
 | `page.toc_entries` | 목차 자체 — `{level, id, title, children}`, 중첩된 형태. |
 | `page.translations` | 이 페이지가 존재하는 언어만. 비어 있으면 전환 UI를 그리지 않으면 됩니다. |
-| `page.section` | 이 페이지가 속한 섹션. 최상위 페이지에는 없습니다. |
-| `page.prev`, `page.next` | **같은 섹션 안에서의** 이웃 페이지. |
+| `page.section` | 이 페이지가 속한 섹션. 최상위 페이지에는 없고, 섹션 인덱스에도 없습니다 — 섹션은 자기 안에 있지 않으니까요. |
+| `page.prev`, `page.next` | **같은 섹션 안에서의** 이웃 페이지. 같은 이유로 섹션 인덱스에는 없습니다. |
 | `page.children` | 섹션의 자식 페이지들. 일반 페이지에서는 비어 있습니다. |
 | `page.is_section` | |
-| `page.extra` | 당신의 `[extra]` 테이블. |
+| `page.date` | 발행 날짜를 조각으로 — `year`, `month`, `day`, `date`, `iso`. 없으면 없습니다. [피드](@/features/feeds.md)를 참고하세요. |
+| `page.extra` | 직접 정의한 `[extra]` 테이블. |
 
 ## page.children은 두 가지입니다
 
diff --git a/docs/content/templates/data.md b/docs/content/templates/data.md
@@ -19,6 +19,7 @@ Two objects are in scope on every page: `site` and `page`.
 | `site.sections` | Top-level sections **in this language**. |
 | `site.highlight_css` | URL of the generated highlight stylesheet, or nothing if highlighting is off. |
 | `site.search` | URL of this language's search index, or nothing with `[search] enabled = false`. |
+| `site.feed` | URL of this language's Atom feed, or nothing when no page in it carries a date. See [Feeds](@/features/feeds.md). |
 
 `site.sections` contains only the current language's tree, which is what makes
 navigation safe: an untranslated page is not in it, so a link to it cannot be
@@ -39,10 +40,11 @@ and `weight`.
 | `page.toc` | Whether the author asked for a contents list. |
 | `page.toc_entries` | The contents themselves — `{level, id, title, children}`, nested. |
 | `page.translations` | Only languages this page exists in. Empty means no switcher. |
-| `page.section` | The section this page belongs to, or nothing at the top level. |
-| `page.prev`, `page.next` | The neighbouring pages **within this section**. |
+| `page.section` | The section this page belongs to. Nothing at the top level, and nothing on a section index — a section is not inside itself. |
+| `page.prev`, `page.next` | The neighbouring pages **within this section**. Nothing on a section index, for the same reason. |
 | `page.children` | A section's own pages. Empty on ordinary pages. |
 | `page.is_section` | |
+| `page.date` | The publication date in parts — `year`, `month`, `day`, `date`, `iso` — or nothing. See [Feeds](@/features/feeds.md). |
 | `page.extra` | Your `[extra]` table. |
 
 ## page.children is two things
diff --git a/docs/content/templates/functions.ko.md b/docs/content/templates/functions.ko.md
@@ -46,14 +46,17 @@ toc = true
 `{% set %}`, 그리고 익숙한 필터들 — `safe`, `escape`, `length`, `join`,
 `default`, `upper`, `lower`, `replace`, `trim`, `first`, `last`, `reverse`,
 `sort`, `map`, `select`, `selectattr`, `batch`, `slice`, `int`, `float`, `abs`,
-`round`, `urlencode`, `striptags`, `indent`.
+`round`, `indent`.
 
 이 사이트의 템플릿이 `{% macro %}`와
 `{% from "partials/sidebar.html" import nav %}`를 쓰므로, 그 둘은 빌드마다 실제로
 돌고 있습니다.
 
-`tojson`은 **없습니다.** minijinja의 `json` 피처가 꺼져 있고, `{% for %}` 루프로
-이미 되는 일을 위해 의존성을 켜지는 않습니다.
+Jinja2 습관으로 손이 가는 필터 셋은 **없습니다.** `tojson`, `urlencode`,
+`striptags`입니다. 셋 다 켜지 않은 minijinja 피처 뒤에 있고, 셋 다 이미 되는
+일을 위한 의존성입니다 — `{% for %}` 루프, 바로 쓸 수 있는 형태로 오는 URL,
+직접 썼으니 벗겨 낼 일이 없는 마크업. 불러도 빈 문자열이 아니라 빌드 에러라서
+페이지가 아니라 빌드 시점에 알게 됩니다.
 
 ## 그 밖엔 없습니다
 
diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md
@@ -47,14 +47,17 @@ Standard Jinja2 syntax works: `{% if %}`, `{% for %}`, `{% extends %}`,
 and the usual filters — `safe`, `escape`, `length`, `join`, `default`, `upper`,
 `lower`, `replace`, `trim`, `first`, `last`, `reverse`, `sort`, `map`,
 `select`, `selectattr`, `batch`, `slice`, `int`, `float`, `abs`, `round`,
-`urlencode`, `striptags`, `indent`.
+`indent`.
 
 Our own templates use `{% macro %}` and `{% from "partials/sidebar.html" import nav %}`,
 so those two are exercised on every build.
 
-`tojson` is **not** available: minijinja's `json` feature is off, and turning it
-on to serialise a value into a template is a dependency for something a
-`{% for %}` loop already does.
+Three filters a Jinja2 habit reaches for are **not** here: `tojson`,
+`urlencode` and `striptags`. Each is behind a minijinja feature we leave off,
+and each is a dependency for something already covered — a `{% for %}` loop, a
+URL that arrives ready to use, or markup you did not have to strip because you
+wrote it. Calling one is a build error, not an empty string, so you find out at
+build time rather than in the page.
 
 ## Nothing else exists