www.git

www

~ Personal site — projects, posts, about. Built with sqzass.
git clone https://github.com/debuman/www.git
commit 2a27475a2bdf2daaa2772043a74d51f4a0f23576
parent 52adcf9765d08d4a3edc3739d2f43bd6cf271bae
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Wed, 5 Aug 2026 16:46:04 +0900

A New Beginning, with a preview that plays on hover

The two posts that were here were mine, written to give the templates something
to render. They are gone. The first real post takes their place, verbatim.

The home page's top entry gets a 16:9 preview. It sits above the date and title
in the same language the project frames use — a border on the picture, the words
floating below it — and it plays when the pointer or the keyboard reaches the
link, stopping and rewinding to frame one when they leave. Frame one is the
poster, so the still you see before and after is the same still, and nothing
cuts.

The box does not move, resize or lift. What moves is inside the picture, which
leaves the rule about hover intact: the layout still never shifts under a
cursor.

Nothing is fetched until then. preload="none" plus a poster means the front page
costs one 62KB still, and the 714KB clip is only requested by the hand that
asks for it. If the script never arrives, the still is what remains.

The source was 2.5MB at 1280x720 with an audio track nobody will ever hear.
Stripped, scaled to 960x540 and re-encoded at crf 32, it is 714KB and the
difference is invisible at four times the size it renders. VP9 was tried and
lost to H.264 on this material by 240KB, so there is one file rather than two.

The clip's path cannot live in the post's own front matter: listings carry only
title, description, url, weight and date, so the home page keeps a one-entry
catalogue keyed by URL, the same arrangement and the same reason as the project
catalogue. A post that is not in it simply has no preview. tools/check.py now
verifies that the URL names a real post and that both files exist, because a
typo there fails by showing nothing.

Diffstat:
Mcontent/_index.md | 11+++++++++++
Acontent/posts/a-new-beginning.md | 40++++++++++++++++++++++++++++++++++++++++
Dcontent/posts/listings-that-cannot-show-their-own-dates.ko.md | 73-------------------------------------------------------------------------
Dcontent/posts/listings-that-cannot-show-their-own-dates.md | 76----------------------------------------------------------------------------
Dcontent/posts/what-this-site-is-built-on.ko.md | 41-----------------------------------------
Dcontent/posts/what-this-site-is-built-on.md | 43-------------------------------------------
Mstatic/css/main.css | 20++++++++++++++++++++
Mstatic/fonts/display-kr.woff2 | 0binary
Astatic/images/posts/a-new-beginning.webp | 0binary
Mstatic/js/ui.js | 32++++++++++++++++++++++++++++++++
Astatic/video/a-new-beginning.mp4 | 0binary
Mtemplates/home.html | 9+++++++++
Mtools/check.py | 22++++++++++++++++++++++
13 files changed, 134 insertion(+), 233 deletion(-)

diff --git a/content/_index.md b/content/_index.md
@@ -8,4 +8,15 @@ template = "home.html"
 hero_title = "Software Engineer, Security Engineer and Founder"
 hero_sub   = "Hello! I'm An Hyukjin, though I go by various aliases online (protect your OPSEC!). I'm a Software & Security Engineer based in South Korea."
 hero_door  = "About me"
+
+# 맨 위 글의 미리보기. 목록이 실어 오는 것은 title·description·url·weight·date 뿐이라
+# (PageRefCtx 에는 extra 가 없다) 영상 경로가 거기서 나올 수 없다. Projects 카탈로그가
+# [extra] 에 사는 것과 같은 이유이고, url 이 실제 페이지를 가리키는지는 CI 가 검사한다.
+#
+# 여기 없는 글은 그냥 미리보기가 없다. 새 글이 맨 위에 올라와도 페이지는 멀쩡하고,
+# 영상이 있는 글이면 이 줄을 고치면 된다.
+[[extra.preview]]
+url    = "/posts/a-new-beginning/"
+poster = "images/posts/a-new-beginning.webp"
+video  = "video/a-new-beginning.mp4"
 +++
diff --git a/content/posts/a-new-beginning.md b/content/posts/a-new-beginning.md
@@ -0,0 +1,40 @@
++++
+title = "A New Beginning"
+description = "The launch of my new profile site."
+date = 2026-08-05
+toc = false
++++
+
+Learning something new is always a thrilling experience. It is especially
+meaningful to take what I've learned, use it to create something entirely new,
+and continuously rebuild it.
+
+That is why I truly love the title of "Engineer," and I want to carry it with me
+for the rest of my life. To be honest, while I love engineering, I don't think I
+have a natural aptitude for it. Ever since I was young, my true talents have
+always been in writing and planning. It might be unfortunate that what I love
+and what I am good at are different, but I am doing my best to create a synergy
+between the two.
+
+Over the past 20 years of running blogs and portfolio websites, I have migrated
+through countless platforms. It all started with Naver Blog, followed by
+Tistory, Google Blogger, Medium, and Ghost, before finally leading me to build
+and operate my own custom platform.
+
+As I used those services, I kept finding myself disappointed by missing
+features. After wandering from one platform to another, I finally decided that
+it would be better to just build one myself, integrating the exact features I
+wanted. That is how I ended up here. (I absolutely love Markdown!)
+
+After operating under many different names across various platforms, I am now
+officially greeting you as SQZER.
+
+I built this profile site to consolidate my projects—which were previously
+scattered across CherryTree, old blogs, Outline, and GitHub—into a single,
+unified space. Moving forward, this site will be home to my refined projects and
+technical posts.
+
+I hope this space becomes helpful not only to me but to all of you as well. I
+kindly ask for your continued interest and support.
+
+Thank you.
diff --git a/content/posts/listings-that-cannot-show-their-own-dates.ko.md b/content/posts/listings-that-cannot-show-their-own-dates.ko.md
@@ -1,73 +0,0 @@
-+++
-title = "자기 날짜를 못 보여 주는 목록"
-description = "날짜순으로 세워 놓고 그 날짜를 숨기는 생성기. 고치는 데 든 값과, 일부러 빼놓은 것."
-date = 2026-08-03
-toc = true
-+++
-
-[sqzass](@/projects/sqzass.md) 는 처음부터 섹션을 날짜순으로 정렬할 수 있었다.
-섹션 인덱스에 `sort_by = "date"` 를 적으면 최신 글이 먼저 나오고 날짜 없는
-페이지가 뒤로 간다. 거기까지는 됐다.
-
-못 하던 것은 그 날짜를 보여 주는 일이었다.
-
-템플릿은 목록을 `page.children` 으로 보는데, 그 안의 항목은 정확히 네 개였다.
-`title`, `description`, `url`, `weight`. 그래서 블로그 인덱스가 완벽한 시간순으로
-정렬돼 있으면서도 그렇다고 말할 방법이 없었다. 순서는 진짜인데 보이지 않았다.
-둘 중 나쁜 쪽만 남은 셈이다. 읽는 사람은 이 목록이 정렬된 것인지 그냥 늘어놓은
-것인지 알 수가 없다.
-
-## 택하지 않은 우회로
-
-뻔한 탈출구는 `[extra]` 다. 템플릿까지 손대지 않고 전달되는 자유 형식 테이블이니,
-거기에 날짜를 적고 URL 로 짝을 지어 템플릿에서 맞추면 된다.
-
-```toml
-[[extra.year]]
-label = "2026"
-posts = [{ url = "/posts/some-post/", date = "08.03" }]
-```
-
-동작한다. 그리고 글 하나를 새로 쓸 때마다 파일 두 개를 고쳐야 하는데, 잊는 쪽은
-언제나 두 번째다. 설명하려는 대상 옆에 손으로 유지하는 대응표는 결국 어긋나고,
-파일 이름을 처음 바꾸는 순간 조용히 깨진다. 항목이 그냥 안 맞게 되고 날짜가
-사라진다.
-
-## 실제로 든 값
-
-구조체 하나에 필드 하나, 그리고 그걸 만드는 자리 여섯 곳.
-
-```rust
-pub struct PageRefCtx {
-    pub title: String,
-    pub description: String,
-    pub url: String,
-    pub weight: i64,
-    pub date: Option<PageDate>,
-}
-```
-
-값은 헬퍼 하나를 거치므로, 목록과 그 목록이 가리키는 페이지가 발행일에 대해
-서로 다른 말을 할 수 없다. 섹션의 날짜는 그 섹션 `_index.md` 에서 오고, 인덱스가
-없는 섹션에는 날짜도 없다.
-
-이게 전부다. 대신하려던 우회로보다 작다.
-
-## 일부러 빼놓은 것
-
-같은 구조체는 필드 하나만 더하면 프로젝트 목록에도 제대로 쓸모가 생긴다.
-`extra` 를 얹으면 모든 목록이 임의의 메타데이터를 실어 올 수 있다. 언어 배지,
-링크, 상태 같은 것들.
-
-넣지 않았고, 이유는 `PageRefCtx` 가 쓰이는 자리에 있다. `page.children` 만이
-아니다. 사이트 전체의 `site.sections` 내비게이션 트리도 이 구조체로 만들어지고,
-그 트리는 언어당 한 번 직렬화해서 모든 페이지가 나눠 쓴다. 여기에 페이지마다의
-`[extra]` 를 넣으면 모든 기능 목록과 모든 코드 예제가 내비게이션 스냅샷에
-실린다. 그걸 들여다보지도 않는 페이지들이 페이지마다 그 값을 치르게 된다.
-
-날짜는 작은 필드 다섯 개다. 열린 테이블은 오늘 얼마나 썼느냐만큼이다.
-
-그래서 이 사이트의 프로젝트 인덱스는 여전히 카탈로그를 한곳에 손으로 두고, 모든
-항목이 실재하는 페이지를 가리키는지 CI 가 검사한다. 방금 날짜에 대해 거부한 것보다
-나쁜 답인데, 여기서는 그게 맞다. 날짜 쪽 수정을 싸게 만든 바로 그 성질이 `extra`
-쪽을 비싸게 만들기 때문이다.
diff --git a/content/posts/listings-that-cannot-show-their-own-dates.md b/content/posts/listings-that-cannot-show-their-own-dates.md
@@ -1,76 +0,0 @@
-+++
-title = "Listings that cannot show their own dates"
-description = "A generator that sorts by date and then hides it. What the fix cost, and what I left out."
-date = 2026-08-03
-toc = true
-+++
-
-[sqzass](@/projects/sqzass.md) has always been able to sort a section by date.
-Put `sort_by = "date"` on a section index and its pages come out newest first,
-undated ones last. That part worked.
-
-What it could not do was show the date.
-
-Templates see a listing as `page.children`, and every entry in it was exactly
-four fields — `title`, `description`, `url`, `weight`. So a blog index could be
-in perfect chronological order and have no way to say so. The order was real and
-invisible, which is the worst of both: the reader cannot tell whether the list is
-sorted or just arranged.
-
-## The workaround I did not take
-
-The obvious escape hatch is `[extra]`, a free-form table that reaches templates
-untouched. Write the dates there, key them by URL, match them back up in the
-template:
-
-```toml
-[[extra.year]]
-label = "2026"
-posts = [{ url = "/posts/some-post/", date = "08.03" }]
-```
-
-It works. It also means every new post is two edits in two files, and the second
-one is the one you forget. A mapping maintained by hand next to the thing it
-describes is a mapping that drifts, and the first rename breaks it silently —
-the entry just stops matching and the date quietly disappears.
-
-## What it actually cost
-
-One field on one struct, and six places that build it:
-
-```rust
-pub struct PageRefCtx {
-    pub title: String,
-    pub description: String,
-    pub url: String,
-    pub weight: i64,
-    pub date: Option<PageDate>,
-}
-```
-
-The value goes through a single helper, so a listing and the page it links to
-cannot disagree about what day something was published. A section's date comes
-from its own `_index.md`; a section with no index has none.
-
-That is the whole change. It is smaller than the workaround it replaces.
-
-## What I deliberately left out
-
-The same struct is one field away from being genuinely useful for a projects
-index — put `extra` on it and every listing could carry arbitrary metadata:
-language badges, links, status.
-
-I did not add it, and the reason is where `PageRefCtx` is used. It is not only
-`page.children`. It is also what the whole `site.sections` navigation tree is
-made of, and that tree is serialised once per language and shared by every page
-on the site. Putting each page's `[extra]` in it would drag every feature list
-and every code sample into the navigation snapshot — a per-page cost paid by
-pages that never look at it.
-
-A date is five small fields. An open table is however much you wrote today.
-
-So the projects index on this site still keeps its catalogue in one place by
-hand, and a CI step checks that every entry points at a page that exists. That
-is a worse answer than the one I just rejected for dates, and it is the right
-one here, because the thing that made the date fix cheap is exactly what makes
-the `extra` version expensive.
diff --git a/content/posts/what-this-site-is-built-on.ko.md b/content/posts/what-this-site-is-built-on.ko.md
@@ -1,41 +0,0 @@
-+++
-title = "이 사이트는 무엇으로 만들어졌나"
-description = "바이너리 하나, 스타일시트 하나, 스크립트 둘, 그리고 생성기 말고는 빌드 단계가 없다."
-date = 2026-08-02
-toc = true
-+++
-
-이 사이트는 직접 만든 [sqzass](@/projects/sqzass.md) 가 빌드하고, 바이너리를
-설치해서 실행하는 워크플로가 GitHub Pages 로 배포한다. 번들러도, 패키지
-매니저도, `node_modules` 도 없다.
-
-금욕이 아니다. 저 하나하나가 문장을 쓰는 시점과 그 문장이 화면에 뜨는 시점 사이에서
-깨질 수 있는 물건이라서다.
-
-## 전부
-
-**스타일시트 하나.** 스무 개가 아니라. 이 사이트를 설계하면서 참고한 레퍼런스는
-렌더를 막는 스타일시트를 스무 개 내보내는데, 캐시가 비어 있으면 첫 페인트 전에
-스무 번 왕복한다. 파일 하나면 생성기가 내용 해시를 붙여 주고, 바뀐 그때만 캐시가
-무효가 된다.
-
-**스크립트 둘, 둘 다 있어도 되고 없어도 된다.** 하나는 검색 팔레트를, 하나는 테마
-토글과 스크롤 리빌을 맡는다. JavaScript 를 끄면 팔레트가 없어지고 모든 섹션이
-처음부터 드러나 있다. 읽으러 온 것 중에 이 둘에 기대는 건 없다.
-
-**한글 웹폰트는 안 받는다.** 커버리지가 제대로 있는 한글 웹폰트는 2 MB 쯤 된다.
-시스템 스택 — Apple SD Gothic Neo, Noto Sans KR, 맑은 고딕 — 은 이미 그 기계에
-있고 이미 맞다. 이 사이트가 내려받는 폰트는 JetBrains Mono 뿐인데 라틴 전용
-21 KB 이고, 두 번째 목소리 노릇을 해서 그 값을 한다. 날짜, 배지, 명령, 워드마크가
-그 목소리다.
-
-## 제일 오래 걸린 부분
-
-레이아웃이 아니었다. 날짜였다.
-[다른 글](@/posts/listings-that-cannot-show-their-own-dates.md)에 적어 뒀다.
-
-## 재현되고, 확인한다
-
-같은 입력을 두 번 빌드하면 같은 바이트가 나오고, CI 가 두 번 빌드해서 비교한다.
-결벽처럼 들리는데, 설명이 안 되는 차이를 배포에서 처음 마주치는 날 생각이 달라진다.
-생성기가 결정적인지를 가장 알아야 할 순간에 알게 된다.
diff --git a/content/posts/what-this-site-is-built-on.md b/content/posts/what-this-site-is-built-on.md
@@ -1,43 +0,0 @@
-+++
-title = "What this site is built on"
-description = "One binary, one stylesheet, two scripts, and no build step that is not the generator."
-date = 2026-08-02
-toc = true
-+++
-
-This site is built by [sqzass](@/projects/sqzass.md), which I wrote, and
-deployed to GitHub Pages from a workflow that installs the binary and runs it.
-There is no bundler, no package manager, and no `node_modules`.
-
-That is not asceticism. It is that every one of those would be a thing that can
-break between me writing a sentence and the sentence appearing.
-
-## The whole stack
-
-**One stylesheet.** Not twenty. The reference I looked at while designing this
-ships twenty separate render-blocking stylesheets, which is twenty requests
-before the first paint on a cold cache. One file, content-hashed by the
-generator, invalidates only when it changes.
-
-**Two scripts, both optional.** One runs the search palette, one handles the
-theme toggle and the scroll reveals. Turn JavaScript off and you lose the
-palette and get every section already revealed. Nothing you came here to read
-depends on either.
-
-**No web font for Korean.** A Hangul webfont with real coverage is around two
-megabytes. The system stack — Apple SD Gothic Neo, Noto Sans KR, Malgun Gothic —
-is already on the machine and already correct. The only font this site downloads
-is JetBrains Mono, which is Latin-only and 21 KB, and it earns that by being the
-second voice: dates, badges, commands, the wordmark.
-
-## The part that took the longest
-
-Not the layout. The dates — see
-[the other post](@/posts/listings-that-cannot-show-their-own-dates.md).
-
-## Reproducible, and checked
-
-Two builds of the same input produce identical bytes, and CI verifies it by
-building twice and diffing. It sounds like a purity exercise until the first
-time a deploy produces a diff you cannot explain, and you find out whether your
-generator is deterministic at the moment you most need it to be.
diff --git a/static/css/main.css b/static/css/main.css
@@ -734,6 +734,26 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
 
 /* 홈의 맨 위 글 — 액자와 같은 문법, 더 크게. */
 .feature { display: block; position: relative; text-align: center; }
+
+/* 미리보기. /projects/ 의 액자와 같은 어법이다 — 테두리는 그림에만 있고 글은
+   그 아래 뜬다. 손이 닿으면 안에서 돌아간다: 상자는 자리도 크기도 안 바꾸고
+   움직이는 것은 그림 안쪽뿐이라, 호버에 레이아웃이 움직이지 않는다는 이 집의
+   규칙은 그대로다.
+   poster 는 영상의 첫 프레임이다. 재생이 시작될 때도, 손을 떼고 되감을 때도
+   같은 그림이라 이음매가 없다. */
+.feature-media {
+  display: block;
+  /* height:auto 가 없으면 <video> 의 height 속성이 그대로 쓰여 540px 로 서고
+     aspect-ratio 가 지나간다. 속성은 남긴다 — 그게 있어야 파일이 도착하기 전에도
+     자리가 잡혀서 글이 밀리지 않는다. */
+  width: 100%; height: auto; max-width: 36rem;
+  margin: 0 auto clamp(1.25rem, 3vw, 1.75rem);
+  aspect-ratio: 16 / 9;
+  object-fit: cover;
+  border: 1px solid var(--rule);
+  border-radius: var(--radius);
+  background: var(--bg-2);
+}
 .feature-date {
   font-family: var(--mono); font-size: 0.7rem;
   letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-4);
diff --git a/static/fonts/display-kr.woff2 b/static/fonts/display-kr.woff2
Binary files differ.
diff --git a/static/images/posts/a-new-beginning.webp b/static/images/posts/a-new-beginning.webp
Binary files differ.
diff --git a/static/js/ui.js b/static/js/ui.js
@@ -79,6 +79,38 @@
     });
   }
 
+  /* ---------- feature preview ---------- */
+
+  /* 홈 맨 위 글의 미리보기. 손이 닿거나 포커스가 오면 돌고, 떠나면 첫 프레임으로
+     되감는다 — 그 프레임이 곧 poster 라 되돌아간 자리가 원래 자리다.
+
+     영상은 preload="none" 이라 여기서 play() 를 부르기 전에는 한 바이트도 받지
+     않는다. 첫 화면에 700KB 를 얹지 않으려는 것이고, 그래서 이 파일이 못 와도
+     남는 것은 정지 그림 한 장이다.
+
+     동작을 줄이라는 설정이면 아무것도 걸지 않는다. 이 사이트에서 저절로 움직이는
+     것은 이것 하나뿐이라, 끄라고 한 사람에게는 없는 것과 같아야 한다. */
+  var feature = document.querySelector(".feature");
+  var clip = feature && feature.querySelector(".feature-media");
+  if (clip && !reduced) {
+    var play = function () {
+      var p = clip.play();
+      /* 자동재생이 막히면 거절된 프로미스가 콘솔에 빨간 줄을 남긴다. 미리보기가
+         안 도는 것은 고장이 아니라 브라우저의 정책이므로 조용히 삼킨다. */
+      if (p && p.catch) p.catch(function () {});
+    };
+    var stop = function () {
+      clip.pause();
+      clip.currentTime = 0;
+    };
+    feature.addEventListener("mouseenter", play);
+    feature.addEventListener("mouseleave", stop);
+    /* 키보드로 온 사람도 같은 것을 본다. 미리보기가 링크 하나에 붙어 있으므로
+       포커스가 곧 "지금 이걸 보고 있다"는 뜻이다. */
+    feature.addEventListener("focus", play);
+    feature.addEventListener("blur", stop);
+  }
+
   /* ---------- reading rail ---------- */
 
   /* 넓은 화면에서 오른쪽 가장자리에 서는 목차. 항목마다 세로 헤어라인 한 칸이고,
diff --git a/static/video/a-new-beginning.mp4 b/static/video/a-new-beginning.mp4
Binary files differ.
diff --git a/templates/home.html b/templates/home.html
@@ -33,6 +33,15 @@
   {%- for p in s.pages %}
   {%- if loop.first %}
   <a class="feature" href="{{ p.url }}">
+    {#- 미리보기가 있으면 액자가 먼저 선다. poster 만 먼저 오고 영상은 preload="none"
+        이라, 손이 닿기 전에는 한 바이트도 안 받는다. 재생은 ui.js 가 건다. #}
+    {%- for m in page.extra.preview %}
+    {%- if m.url == p.url %}
+    <video class="feature-media" poster="{{ asset(m.poster) }}" src="{{ asset(m.video) }}"
+           width="960" height="540" muted loop playsinline preload="none"
+           aria-hidden="true" tabindex="-1"></video>
+    {%- endif %}
+    {%- endfor %}
     {%- if p.date %}
     <time class="feature-date" datetime="{{ p.date.date }}">{{ p.date.year }}</time>
     {%- endif %}
diff --git a/tools/check.py b/tools/check.py
@@ -74,6 +74,27 @@ def check_projects():
             fails.append(f"[{lang}] 페이지가 카탈로그에 없습니다: {url}")
 
 
+def check_preview():
+    """홈의 미리보기 카탈로그가 실제 글을 가리키는지.
+
+    이것도 [extra] 라 생성기가 안 읽는다. url 이 어긋나면 에러가 아니라 그냥
+    미리보기가 안 나오고, 그건 배포된 뒤에야 눈으로만 보인다.
+    """
+    for lang, prefix in LANGS.items():
+        index = ROOT / "content" / ("_index.ko.md" if lang == "ko" else "_index.md")
+        if not index.exists():
+            continue  # 그 언어에 첫 화면이 없을 수 있다. 한국어가 그렇다.
+        for item in front_matter(index).get("extra", {}).get("preview", []):
+            url = item["url"]
+            slug = url.rstrip("/").rsplit("/", 1)[-1]
+            suffix = ".ko.md" if lang == "ko" else ".md"
+            if not (ROOT / "content" / "posts" / f"{slug}{suffix}").exists():
+                fails.append(f"[{lang}] 미리보기가 가리키는 글이 없습니다: {url}")
+            for key in ("poster", "video"):
+                if not (ROOT / "static" / item[key]).exists():
+                    fails.append(f"[{lang}] 미리보기 {key} 파일이 없습니다: {item[key]}")
+
+
 def check_i18n():
     """두 언어의 키 집합이 같은지. 다르면 그 키를 처음 쓰는 날 빌드가 선다."""
     keys = {}
@@ -118,6 +139,7 @@ def check_links():
 
 
 check_projects()
+check_preview()
 check_i18n()
 check_links()