Hugo는 buildFuture = false면 날짜가 미래인 글을 빌드에서 뺀다. 그래서 예약 발행은 공짜처럼 보인다. 날짜만 미래로 적어 두면 되니까. 하지만 정적 사이트에서 진짜 문제는 그 다음이다. 날짜가 지났다고 해서 이미 만들어진 HTML이 저절로 바뀌지 않는다. 누군가 그 시각 뒤에 사이트를 다시 빌드해야 글이 나타난다. 폰 시절에는 매일 07:30 크론이 그 역할을 했다. 클라우드로 오면서 이 역할을 다시 만들어야 했다.

첫 번째 안전망: 매일 07:30 재배포

가장 먼저 GitHub Actions 워크플로를 하나 만들었다. 매일 22:30 UTC(07:30 KST)에 wrangler deploy를 실행한다. Workers Builds의 빌드 명령이 그대로 돌기 때문에 Hugo가 다시 빌드되고, 밤사이 날짜가 지난 글이 공개된다. 폰 크론과 같은 동작을 GitHub가 대신하는 셈이다. 다만 이것만으로는 “저녁 8시 예약 글이 다음 날 아침에야 보이는” 문제가 남는다.

두 번째: 공개 시각에 맞춰 배포 시작하기

그래서 Worker의 cron 트리거를 썼다. wrangler.jsonc에 "crons": ["*/10 * * * *"]를 넣으면 10분마다 scheduled()가 실행된다. 여기서 판단할 것은 하나다. “마지막 빌드 이후에 공개 시각이 지난 글이 있는가.”

이 판단에는 빌드 결과만으로는 부족하다. 미래 글은 빌드에서 빠지기 때문이다. 그래서 빌드 스크립트가 Hugo를 돌리기 전에 시각을 하나 잡고, 빌드 뒤에 content/ 폴더의 front matter를 직접 읽어 그 시각보다 늦은 공개 시각 목록을 JSON으로 남기게 했다.

{ "builtAt": "2026-10-11T12:00:00Z", "times": ["2026-10-12T13:00:00.000Z", "..."] }

제목이나 주소는 넣지 않는다. 시각만 있으면 되고, 이 파일은 외부 요청에는 404로 답해 내부에서만 읽는다. scheduled()는 이 파일을 읽어 builtAt < 시각 <= 지금인 항목이 하나라도 있으면 GitHub Actions의 그 워크플로를 workflow_dispatch로 실행시킨다. 배포가 끝나면 builtAt이 갱신되므로 다음 cron에서는 “밀린 글 없음"이 되어 아무것도 하지 않는다. 공개 시각 정확도는 cron 간격과 빌드 시간을 합쳐 10분 안팎이다.

토큰은 두 개 중 하나만 있으면

워크플로를 실행시키려면 GitHub 토큰이 필요하다. 배포 시작용 토큰과 글 저장용 토큰을 따로 두되, 배포용이 거부되면(401, 403, 404) 글 저장용으로 한 번 더 시도하게 했다. 둘 다 같은 저장소용 fine-grained 토큰이라 Actions 권한을 같이 줄 수 있어서다. 실제로 처음 켠 날 배포용 토큰 값이 거부되고 글 저장용으로 성공했다. 어느 토큰이 통했는지 이름만 기록에 남긴다.

보이지 않는 글로 시험하기

진짜 예약 글로 시험하면 실패했을 때 글이 늦게 나간다. 그래서 expiryDate를 과거로 둔 점검용 글을 하나 만들었다. Hugo는 buildExpired = false라 이 글을 화면에 절대 내지 않지만, 일정 생성기는 date만 보므로 cron은 “공개 시각이 지났다"고 판단해 배포를 시작시킨다. 방문자에게는 아무것도 안 보이면서 전체 경로를 끝까지 돌려 볼 수 있었다. 확인이 끝난 뒤 그 글은 지웠다.

상태를 밖에서 볼 수 있게

cron이 돌았는지, 뭘 판단했는지, 실패했으면 왜인지를 알 수 없으면 운영이 안 된다. 그래서 매 실행 결과를 D1에 기록하고 공개 상태 API로 내놓았다. 마지막 점검 시각과 판정(not-due, dispatched, error), 마지막 배포 시작과 통한 토큰 이름, 현재 빌드 시각, 밀린 공개 시각, 다음 공개 예정 시각이 나온다. 비밀값은 없다. 운영 안내서의 “예약 글이 안 보일 때” 항목은 이 주소를 먼저 열어 보라고 적혀 있다.

두 겹으로 두는 이유

10분 cron이 있으면 07:30 안전망은 필요 없어 보인다. 그래도 남겼다. 토큰이 만료되거나 cron이 어떤 이유로 안 돌면, 적어도 하루 한 번은 글이 공개되게 하는 바닥이기 때문이다. 그리고 바로 다음 날 밤, 이 두 겹 설계가 생각보다 빨리 시험대에 올랐다. Cloudflare 쪽 장애로 첫 예약 글이 21분 늦게 나간 이야기가 다음 글이다.