www.git

www

~ Personal site — projects, posts, about. Built with sqzass.
git clone https://github.com/debuman/www.git
commit 73b2893016098bd7d33aa116f97afbbedd2244b7
parent 70c24d5ad1232c2dd0713775d6c9fd4282292df1
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Wed, 5 Aug 2026 01:39:42 +0900

There is no Korean home page

Removing the language switch from the front door was the decision that the
front door is English. I then wrote Korean copy for a page that should not
exist. content/_index.ko.md is gone and /ko/ with it.

What stays Korean is what a reader came to read: posts, project pages, About,
and the two section listings those pages need as parents. The switch still sits
on each of them.

The brand mark now points at the English root rather than at the current
language's home, because for Korean that address is no longer a page. This is
the one link on the site where "go home" and "the prefix that identifies a
section" had been the same string; they are two variables now, and the comment
says which is which.

Nothing caught the resulting dead link. sqzass validates markdown `@/` links and
template variables, but not an address a template assembled, so the logo pointed
at a missing page on eleven pages and both the build and CI stayed quiet.
tools/check.py scans built HTML for internal links now, and CI runs it a second
time after the build, where public/ exists for it to read. It asks is_file()
rather than exists() — public/ko/ is a real directory because of the sections
underneath it, and the first version of this check passed it for that reason,
which is how the dead link got out in the first place.

Diffstat:
M.github/workflows/pages.yml | 9+++++++++
Dcontent/_index.ko.md | 10----------
Mstatic/fonts/display-kr.woff2 | 0binary
Mtemplates/base.html | 10++++++++--
Mtools/check.py | 36+++++++++++++++++++++++++++++++++++-
5 files changed, 52 insertion(+), 13 deletion(-)

diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
@@ -24,6 +24,9 @@ jobs:
 
       # 빌드보다 먼저 돈다. 셋 다 sqzass 가 확인해 주지 않는 것들이라,
       # 여기서 안 잡으면 조용히 배포된다.
+      #
+      # check.py 의 죽은 링크 검사만은 public/ 이 있어야 돌기 때문에 빌드 뒤에
+      # 한 번 더 부른다. 여기서는 조용히 건너뛴다.
       - name: Check content integrity
         run: python3 tools/check.py
 
@@ -56,6 +59,12 @@ jobs:
       - name: Build
         run: sqzass build -i .
 
+      # 이번에는 죽은 링크 검사까지 돈다. 템플릿이 조립한 주소는 sqzass 가
+      # 확인해 주지 않는다 — 한국어 홈을 없앤 날 로고 링크가 열한 장에서
+      # 없는 페이지를 가리킨 채로 배포됐다.
+      - name: Check built links
+        run: python3 tools/check.py
+
       # 재현성은 sqzass 가 내세우는 것이므로 이 사이트에서도 확인한다.
       # 같은 입력을 두 번 빌드해서 바이트가 다르면 배포하지 않는다.
       - name: Build again and compare
diff --git a/content/_index.ko.md b/content/_index.ko.md
@@ -1,10 +0,0 @@
-+++
-title = "홈"
-description = "터미널 도구, 정적 사이트 생성기, 그리고 보안 유틸리티."
-template = "home.html"
-
-[extra]
-hero_title = "소프트웨어 엔지니어, 보안 엔지니어, 그리고 창업자"
-hero_sub   = "안녕하세요. 안혁진입니다. 온라인에서는 여러 이름을 씁니다(OPSEC 은 지켜야죠). 한국에 있는 소프트웨어·보안 엔지니어입니다."
-hero_door  = "소개"
-+++
diff --git a/static/fonts/display-kr.woff2 b/static/fonts/display-kr.woff2
Binary files differ.
diff --git a/templates/base.html b/templates/base.html
@@ -7,8 +7,14 @@
     동작하는 일이 없다. #}
 {%- set ko = page.language == "ko" %}
 {#- 서브경로에 올라갈 수도 있으므로 손으로 쓰는 링크에는 base_path를 붙인다.
-    페이지 URL들은 이미 이 접두사를 품고 있어서 손댈 필요가 없다. #}
+    페이지 URL들은 이미 이 접두사를 품고 있어서 손댈 필요가 없다.
+
+    `home` 은 링크가 아니라 **갈래 URL 을 알아보는 접두사**다(nav_label 이 이걸로
+    /posts/ 인지 /ko/posts/ 인지 가른다). 실제로 가는 첫 화면은 `root` 이고 둘은
+    한국어에서 갈라진다 — 한국어 홈은 없다. 첫 화면은 영어 한 장뿐이고, 번역되는
+    것은 읽으러 온 문서다. 브랜드가 /ko/ 로 가면 그건 없는 페이지다. #}
 {%- set home = site.base_path ~ ("/ko/" if ko else "/") %}
+{%- set root = site.base_path ~ "/" %}
 {% from "partials/nav.html" import nav_label %}
 <!doctype html>
 {#- 기본값을 여기 적는다. prefers-color-scheme 에 맡기지 않는 이유는 생성된
@@ -76,7 +82,7 @@
         타서 테마와 호버를 글자와 똑같이 따라간다.
         aria-hidden 인 이유는 바로 옆 텍스트가 이미 같은 이름을 말하기 때문이다 —
         여기 이름을 또 넣으면 스크린리더가 두 번 읽는다. #}
-    <a class="brand" href="{{ home }}">
+    <a class="brand" href="{{ root }}">
       <span class="brand-mark" aria-hidden="true"></span>
       <span class="brand-name">{{ site.title }}</span>
     </a>
diff --git a/tools/check.py b/tools/check.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-"""빌드가 잡아 주지 않는 두 가지를 검사한다.
+"""빌드가 잡아 주지 않는 세 가지를 검사한다.
 
 sqzass 는 마크다운의 `@/` 링크와 템플릿 변수와 에셋 이름을 전부 확인하고 틀리면
 빌드를 세운다. 확인하지 않는 것은 `[extra]` 안이다 — 거기는 생성기가 읽지 않는
@@ -9,11 +9,18 @@ i18n 도 마찬가지로 반쪽이다. 템플릿이 실제로 부르는 키가 
 어느 템플릿도 아직 안 부르는 키가 한쪽 언어에만 있는 것은 그냥 지나간다. 그
 상태는 그 키를 처음 쓰는 날 빌드를 세운다.
 
+세 번째는 템플릿이 손으로 쓴 링크다. sqzass 는 마크다운의 `@/` 는 확인해 주지만
+`href="{{ home }}"` 처럼 템플릿이 조립한 주소는 확인하지 않는다. 한국어 홈을
+없앤 날 로고 링크가 열한 장에서 `/ko/` 를 가리킨 채로 남았고, 빌드도 CI 도
+아무 말을 하지 않았다. public/ 이 있을 때만 돈다 — 없으면 조용히 건너뛴다.
+
     python3 tools/check.py
 """
 
+import re
 import sys
 import tomllib
+import urllib.parse
 from pathlib import Path
 
 ROOT = Path(__file__).resolve().parent.parent
@@ -84,8 +91,35 @@ def check_i18n():
         fails.append(f"i18n: '{key}' 가 {b} 에만 있습니다")
 
 
+def check_links():
+    """빌드된 HTML 의 내부 링크가 전부 실제 파일에 닿는지.
+
+    디렉터리가 있는 것과 페이지가 있는 것은 다르다 — public/ko/ 는 하위 갈래
+    때문에 존재하지만 그 안에 index.html 이 없으면 그 주소는 404 다. 처음 짠
+    검사는 exists() 를 써서 이걸 통과시켰고, 그래서 죽은 링크가 하나 배포됐다.
+    is_file() 로 묻는다.
+    """
+    pub = ROOT / "public"
+    if not pub.is_dir():
+        return
+
+    dead = {}
+    for f in pub.rglob("*.html"):
+        for href in re.findall(r'href="([^"#?]+)', f.read_text(encoding="utf-8")):
+            if href.startswith(("http", "mailto:", "#", "//")):
+                continue
+            t = pub / urllib.parse.unquote(href).lstrip("/")
+            if not (t.is_file() or (t / "index.html").is_file()):
+                dead.setdefault(href, set()).add(str(f.relative_to(pub)))
+
+    for href, pages in sorted(dead.items()):
+        where = ", ".join(sorted(pages)[:3]) + ("…" if len(pages) > 3 else "")
+        fails.append(f"죽은 내부 링크: {href}  ({len(pages)}장: {where})")
+
+
 check_projects()
 check_i18n()
+check_links()
 
 if fails:
     print("\n".join(fails), file=sys.stderr)