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