birchholt 직접 해보고 남기는 기록

글을 고쳤는데 sitemap 의 lastmod 가 발행일에 멈춰 있었습니다

9월 하순에 글 여러 편을 손봤습니다. 틀린 문장을 고치는 작업이었습니다. 그런데 sitemap(사이트의 주소 목록을 검색엔진에 알려주는 파일)을 열어보니 lastmod(마지막 수정 시각) 22건 중 20건이 고친 날이 아니라 발행일에 멈춰 있었습니다. 빌드는 성공했고 경고도 없었습니다. 그날(2026-09-22) 설정 파일 주석에 남겨둔 숫자입니다.

새로 손본 책들이 꽂혀 있는데 서가 앞 안내 카드에는 처음 들여온 날짜만 적혀 있는 작은 도서관

고치는 데는 설정 두 줄이면 됐습니다. 다만 둘이 짝이라 한쪽만 고치면 효과가 없고 고친 뒤에도 목록 페이지는 따로 손을 봐야 했습니다. 아래 숫자는 전부 2026-10-04 에 Hugo v0.165.0 extended 로 저장소 사본을 빌드해 잰 값입니다.

lastmod 가 발행일로 떨어지는 이유

날짜 도장이 하나뿐인 접수대에서 고쳐 온 서류에도 처음 접수일 도장을 그대로 찍는 직원

Hugo 가 .Lastmod 를 어디서 읽는지는 설정에 순서대로 적혀 있습니다. hugo config 로 실제 적용값을 뽑아봤습니다.

$ hugo config | grep -A5 '^\[frontmatter\]'
[frontmatter]
  date = ['publishdate', 'pubdate', 'published', 'date']
  expirydate = ['expirydate', 'unpublishdate']
  lastmod = [':git', 'lastmod', 'modified', 'date', 'publishdate', 'pubdate', 'published']
  publishdate = ['publishdate', 'pubdate', 'published', 'date']

맨 앞의 :git 은 그 파일을 마지막으로 바꾼 커밋의 시각입니다. 그런데 이 값은 enableGitInfo = true 일 때만 채워집니다 (Hugo 문서). 꺼져 있으면 :git 을 건너뛰고 원고에 lastmod: 를 적지 않았다면 결국 date, 즉 발행일까지 내려갑니다.

저장소 사본에서 enableGitInfo 만 끄고 빌드해 켠 쪽과 비교했습니다.

주소켰을 때껐을 때
/posts/pagination-pitfalls/2026-09-22T15:02:53+09:002026-09-03T00:00:00+00:00
/posts/soft-404-on-pages/2026-09-22T15:02:53+09:002026-09-02T00:00:00+00:00
/posts/domain-name-checks/2026-09-22T15:02:53+09:002026-09-01T00:00:00+00:00
구조화 데이터 dateModified 가 나간 글12편 중 12편12편 중 0편

껐을 때 값은 원고의 date 에 시각이 없어 자정 UTC 로 찍힌 것입니다. dateModified 가 0편인 건 제 템플릿이 「수정일이 공개일보다 뒤일 때만」 그 값을 넣기 때문입니다. 수정일이 발행일로 떨어지면 그 조건이 어디서도 참이 안 됩니다.

켜는 것은 한 줄, 확인할 것은 두 가지

같은 서류 두 장에 찍힌 날짜 도장 중 하나는 접수인의 것, 하나는 결재인의 것임을 돋보기로 비교하는 사람

설정은 이 한 줄입니다.

enableGitInfo = true

켜고 나서 두 가지를 더 확인했습니다. 둘 다 문서에 있지만 실제로 해보기 전에는 몰랐습니다.

첫째, :git 은 커밋한 시각이 아니라 작성자 시각(author date)입니다. 작성자 시각을 1월로, 커밋 시각을 10월로 따로 넣어 커밋하고 빌드했습니다.

author=2026-01-15T10:00:00+09:00 committer=2026-10-04T01:30:00+09:00
<lastmod>2026-01-15T10:00:00+09:00</lastmod>

리베이스(rebase, 커밋을 다른 지점 위로 옮겨 다시 쌓는 것)나 체리픽은 작성자 시각을 그대로 둡니다. 오래 묵힌 브랜치를 옮겨 올리면 sitemap 에는 처음 쓴 날이 나갑니다.

둘째, 원고에 lastmod: 를 손으로 적어도 기본 순서에서는 :git 에 집니다. 글 하나에 lastmod: 2026-09-30 을 넣고 커밋했더니 sitemap 에는 그 커밋 시각이 찍혔습니다. 손으로 적은 값을 우선하고 싶으면 순서를 바꿉니다.

[frontmatter]
  lastmod = ['lastmod', ':git', 'date']

이렇게 하니 그 글은 2026-09-30T00:00:00+00:00 으로 바뀌었고 lastmod: 가 없는 나머지 글은 계속 커밋 시각을 썼습니다. 오타 하나 고친 커밋까지 「수정」으로 내보내고 싶지 않을 때 쓸 만합니다.

로컬에서 맞아도 CI 에서는 틀립니다

두꺼운 일지에서 마지막 한 장만 찢어 들고 온 사람이 모든 항목에 같은 날짜를 적어 넣는 책상

제 사이트는 GitHub Actions 에서 빌드합니다. 여기서 actions/checkout 은 기본값으로 커밋 하나만 받아옵니다. 공식 README 에 “Only a single commit is fetched by default” 라고 적혀 있고 fetch-depth 의 기본값이 1 입니다(actions/checkout). 커밋 이력이 없으니 파일마다 마지막 커밋을 찾을 수가 없습니다.

저는 저장소 주석에 이 경우 「조용히 date 로 떨어진다」 고 적어뒀습니다. 이번에 checkout 과 같은 방식으로 받아 빌드해보니 그게 아니었습니다.

$ git init && git fetch --no-tags --depth=1 <저장소> +<커밋>:refs/remotes/origin/main
$ git checkout --force -B main refs/remotes/origin/main
$ git rev-parse --is-shallow-repository
true
$ hugo && grep -o '<lastmod>[^<]*' public/sitemap.xml | sort | uniq -c
  15 <lastmod>2026-10-04T00:29:41+09:00

sitemap 의 15개 주소가 전부 마지막 커밋 시각 하나로 찍혔습니다. 발행일로 떨어지는 게 아니라 모든 글이 「방금 고쳐짐」이 됩니다. dateModified 도 12편 전부 나갑니다. 날짜가 전부 같을 뿐입니다. 숫자만 세면 고쳐진 것처럼 보여서 더 알아채기 어렵습니다. Hugo 문서에도 얕은 클론(shallow clone, 최근 커밋만 받아온 사본)이면 실제로 파일을 바꾼 커밋이 아니라 가장 최근 커밋을 반영할 수 있다고 적혀 있습니다 (GitInfo — Hosting considerations). 제가 적어둔 주석이 틀렸던 것입니다.

해결은 checkout 에 한 줄을 넣는 것입니다.

- uses: actions/checkout@v4
  with:
    fetch-depth: 0

enableGitInfo 와 fetch-depth: 0 은 반드시 같이 넣습니다. 로컬 저장소는 이력이 다 있으니 enableGitInfo 만 켜도 로컬 빌드는 맞게 나오고 배포본만 틀립니다.

Cloudflare Pages 의 Git 연동으로 빌드하면 Actions 를 거치지 않습니다. 같은 Hugo 문서 표에 Cloudflare 는 기본이 얕은 클론이고 저장소를 받은 뒤 빌드 스크립트에서 얕은 클론이면 git fetch --unshallow 를 실행하라고 안내합니다. 저는 Actions 로 빌드해서 이 경로는 직접 재보지 못했습니다.

목록 페이지는 자기 _index.md 의 날짜를 씁니다

새 책이 계속 꽂히는 서가 위에 페인트칠한 날짜가 그대로인 서점 간판

두 줄을 고치고 나서 글 페이지는 커밋 시각과 초 단위로 맞았습니다. 그런데 홈과 글 목록 /posts/ 는 안 움직였습니다. 지금 라이브 sitemap 에서도 /posts/ 는 2026-09-09T14:38:44+09:00 입니다. 그 뒤로 글이 여러 번 바뀌었는데도 그렇습니다.

원인은 목록 페이지에도 파일이 있다는 데 있었습니다. /posts/ 는 content/posts/_index.md 에서, 홈은 content/_index.md 에서 만들어지고, :git 은 그 파일 자신의 마지막 커밋 시각을 읽습니다.

$ git log -1 --format=%cI -- content/posts/_index.md
2026-09-09T14:38:44+09:00

사본에서 글 하나만 고쳐 커밋하고 다시 빌드했습니다.

주소lastmod
/posts/hugo-over-astro/(고친 글)2026-10-04T01:40:00+09:00
/posts/2026-09-09T14:38:44+09:00 그대로
/2026-10-04T00:29:41+09:00 그대로

목록은 새 글이 붙어도, 카드의 요약이 바뀌어도 「안 바뀜」으로 나갑니다. _index.md 를 지우면 어떻게 되는지도 봤습니다. 그러면 /posts/ 가 하위 글 중 가장 최근 값 (01:40)을 가져갑니다. 그 대신 목록 페이지의 제목과 소개문이 같이 사라지니 저는 이 방법을 쓰지 않았습니다.

sitemap 템플릿에서 하위 글의 최신값으로 올렸습니다

여러 화분의 새순 중 가장 최근 것에 달린 꼬리표를 온실 입구 팻말에 옮겨 거는 정원사

목록 페이지의 .Lastmod 를 건드리는 대신 sitemap 을 만드는 쪽을 바꿨습니다. Hugo 내장 sitemap 템플릿을 layouts/sitemap.xml 로 복사하고 목록 페이지(.IsNode)면 하위 글 전체의 Lastmod 중 가장 늦은 값으로 올리는 부분만 넣었습니다.

{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
  xmlns:xhtml="http://www.w3.org/1999/xhtml">
  {{ range where .Pages "Sitemap.Disable" "ne" true }}
    {{- if .Permalink -}}
    {{- $lm := .Lastmod -}}
    {{- if .IsNode -}}
      {{- range .RegularPagesRecursive -}}
        {{- if .Lastmod.After $lm }}{{ $lm = .Lastmod }}{{ end -}}
      {{- end -}}
    {{- end }}
  <url>
    <loc>{{ .Permalink }}</loc>{{ if not $lm.IsZero }}
    <lastmod>{{ safeHTML ( $lm.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
    <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
    <priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .AllTranslations }}
    <xhtml:link
                rel="alternate"
                hreflang="{{ .Language.Locale }}"
                href="{{ .Permalink }}"
                />{{ end }}{{ end }}
  </url>
    {{- end -}}
  {{ end }}
</urlset>

내장 템플릿으로 만든 sitemap 과 diff 했습니다. 공백과 줄바꿈을 빼면 두 줄만 달랐습니다.

/posts/  2026-09-09T14:38:44+09:00  →  2026-10-04T01:40:00+09:00
/        2026-10-04T00:29:41+09:00  →  2026-10-04T01:40:00+09:00

글 페이지 13개의 값은 그대로였습니다. _index.md 를 고친 날이 더 늦으면 $lm 이 처음부터 그 값이라 그쪽이 남습니다(아래에서 확인했습니다).

이 방법으로도 못 잡는 것. 그 상태에서 글을 하나 지우고 커밋했더니 sitemap 주소는 15개에서 14개로 줄었는데 /posts/ 와 홈의 lastmod 는 01:40 그대로였습니다. 남은 글 중 최신값을 고르는 방식이라 사라진 글은 계산에 들어오지 않습니다. 목록에서 글을 뺀 날을 알리려면 _index.md 를 같이 커밋해야 합니다. 실제로 그 뒤 content/posts/_index.md 를 고쳐 커밋하니 /posts/ 는 그 시각(02:00)으로 올라갔습니다. 홈은 01:40 그대로였습니다. .RegularPagesRecursive 는 일반 글만 세고 하위 목록의 _index.md 는 보지 않기 때문입니다. 홈까지 올리려면 content/_index.md 도 함께 고쳐야 합니다.

확인 순서만 남기면

등불을 든 사람이 일지의 마지막 장과 서가의 날짜 카드를 한 칸씩 대조하며 체크하는 저녁 서재

같은 문제를 의심한다면 저는 이 순서로 보겠습니다.

  1. hugo config | grep -A5 '^\[frontmatter\]' 로 lastmod 순서의 맨 앞이 :git 인지, enableGitInfo 가 켜졌는지 봅니다.
  2. 배포된 sitemap.xml 에서 lastmod 값을 sort | uniq -c 로 셉니다. 전부 같은 값이면 얕은 클론, 원고의 date 와 같으면(시각을 안 적었다면 자정으로 나옵니다) git 정보가 꺼진 것입니다.
  3. 글 하나를 골라 git log -1 --format=%aI -- <파일> 과 sitemap 값을 맞춰봅니다. 작성자 시각과 같아야 합니다.
  4. 홈과 섹션 목록의 값을 따로 봅니다. 자기 _index.md 의 커밋 시각이면 위 템플릿 같은 처리가 필요합니다.

제 경우 라이브 sitemap 15건이 이력 전체를 받은 사본으로 빌드한 값과 전부 일치하는 것까지 확인했습니다. 검색엔진이 이 값을 받아 언제 다시 오는지는 아직 모릅니다. 그건 몇 주 더 봐야 압니다.