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)
|