birchholt 직접 해보고 남기는 기록

Hugo 블로그 링크를 카톡에 보내면 썸네일이 없을 때 — og:image와 카카오 캐시

블로그 글 주소를 카카오톡으로 보내면 말풍선 아래에 미리보기 카드가 붙습니다. 제목과 설명, 그리고 그림 한 장입니다. 이 카드는 카카오가 제 페이지를 한 번 읽어 가서 만듭니다. 그래서 카드에 무엇이 실릴지는 제 HTML의 <head>에 무엇이 적혀 있느냐로 정해집니다.

이름표만 붙은 빈 액자 여러 개가 걸린 복도에서 사다리를 들고 선 사람

글마다 대표 그림을 하나씩 만들어 두고 있으니 당연히 그게 나갈 거라고 생각했습니다. 확인해 보니 카드에 쓸 그림을 알려주는 태그가 한 페이지에도 없었습니다. 본문에는 그림이 있는데 미리보기용으로는 아무것도 지정하지 않은 상태였습니다.

크롤러가 받아 가는 HTML부터 봤습니다

창구 너머로 건네받은 봉투를 열어 내용물을 하나씩 책상 위에 꺼내 놓는 우체국 직원

미리보기 카드는 오픈 그래프(Open Graph, 페이지 제목·설명·대표 이미지를 <meta> 태그로 적어 두는 약속)를 읽어서 만듭니다. 카카오 문서도 그렇게 적고 있습니다. 공유된 웹페이지의 <head>에서 오픈 그래프 메타 태그를 꺼내 og:image는 대표 이미지로, og:title과 og:description은 제목과 설명으로 씁니다.

카카오 개발자 문서 · 메시지 템플릿 · 스크랩 메시지 — 「해당 웹페이지의 HTML 코드의 <head> 영역에서 오픈 그래프 프로토콜의 메타 태그 정보를 추출합니다」. 2026-10-04에 확인했습니다.

그래서 라이브 글 세 편에서 og:와 twitter:로 시작하는 태그만 뽑아 봤습니다.

$ curl -s https://birchholt.com/posts/deploy-automation/ \
    | grep -oE '<meta (property|name)="(og|twitter):[^"]*"'
<meta property="og:type"
<meta property="og:url"
<meta property="og:title"
<meta property="og:description"
<meta property="og:site_name"
<meta property="og:locale"

세 편 모두 똑같이 여섯 줄이었습니다. og:image가 없고 og:image:width·height도 없고 twitter:card도 없습니다. 제목과 설명은 제대로 들어가 있었습니다.

혹시 봇에게는 다른 HTML을 주는 건 아닌지도 봤습니다. 브라우저 흉내, 페이스북 수집기 이름(facebookexternalhit/1.1), 트위터 수집기 이름(Twitterbot/1.0)으로 같은 주소를 받아 비교하니 세 응답의 해시가 같았습니다. 크기도 25,667바이트로 같았습니다. 누가 와서 읽든 같은 HTML을 받고, 그 HTML에 그림 태그가 없는 겁니다.

카카오 스크랩 서버가 어떤 이름(User-Agent)으로 오는지는 카카오 문서에서 찾지 못했습니다. 문서에 있는 것은 스크랩 서버의 IP 대역뿐이라 이 단계에서는 카카오 이름으로 요청해 보지 않았습니다.

원인은 head 템플릿이었습니다

요리책 한 장을 펼쳐 들고 재료 목록에서 빠진 한 줄을 손가락으로 짚는 사람

Hugo에서 <head>를 찍어 내는 건 layouts/_partials/head.html 하나입니다. 열어 보니 오픈 그래프 줄이 정확히 위 여섯 개였습니다.

<meta property="og:type" content="...">
<meta property="og:url" content="{{ $canon }}">
<meta property="og:title" content="{{ $title }}">
<meta property="og:description" content="{{ $desc }}">
<meta property="og:site_name" content="{{ site.Title }}">
<meta property="og:locale" content="ko_KR">

그림은 전혀 다른 곳에서 처리되고 있었습니다. 본문 이미지는 마크다운 이미지 렌더 훅(layouts/_markup/render-image.html, 마크다운의 ![]()를 HTML로 바꿀 때 끼어드는 템플릿)이 assets/ 아래 파일을 읽어 가로세로를 붙여 줍니다. 본문용 처리만 있고, 그 결과를 <head>로 올려 주는 연결은 없었습니다.

제가 착각한 부분입니다. 대표 그림 00.webp가 글 맨 위에 보이니 그게 곧 대표 이미지라고 여겼습니다. 그런데 “맨 위에 있는 그림"은 사람이 보는 순서일 뿐이고 기계에게 대표 이미지라고 알려 주는 건 og:image 한 줄뿐입니다. 오픈 그래프 규격도 필수 속성 네 개 중 하나로 og:image를 꼽습니다.

The Open Graph protocol — 「The four required properties for every page are」 다음에 og:title, og:type, og:image, og:url 네 개를 설명과 함께 듭니다. 2026-10-04에 확인했습니다.

대표 이미지를 og:image로 내보내기

작은 사진 여러 장 가운데 하나를 골라 가게 쇼윈도 맨 앞 받침대에 세우는 손

이 블로그는 글마다 그림을 assets/images/posts/<slug>/00.webp에 둡니다. 경로 규칙이 이미 있으니 head.html에서 그 파일을 찾아 태그를 만들면 됩니다. og:locale 바로 아래에 이렇게 넣었습니다.

{{- with resources.Get (printf "images/posts/%s/00.webp" .Slug) }}
<meta property="og:image" content="{{ .Permalink }}">
<meta property="og:image:type" content="{{ .MediaType.Type }}">
<meta property="og:image:width" content="{{ .Width }}">
<meta property="og:image:height" content="{{ .Height }}">
<meta property="og:image:alt" content="{{ $.Title }}">
{{- end }}

세 가지를 신경 썼습니다.

.RelPermalink가 아니라 .Permalink입니다. 앞의 것은 /images/...처럼 도메인 없는 주소를 내고 뒤의 것은 https://부터 시작하는 전체 주소를 냅니다. 카카오 문서의 예시도 전체 주소입니다. Hugo 문서에 따르면 이 메서드를 부르는 순간 파일이 결과물 폴더에 실제로 쓰입니다. 그래서 본문에서 같은 그림을 쓰지 않는 페이지여도 그림 파일이 빠질 일이 없습니다.

Hugo · Permalink 메서드 — 「writes the resource to the public directory and returns its permalink」. 2026-10-04에 확인했습니다.

with로 감쌌습니다. 그림이 없는 페이지(홈, 개인정보처리방침, 404)에서는 resources.Get이 빈 값을 돌려주니 블록 전체가 건너뛰어집니다. 빈 content=""가 찍히지 않습니다.

og:image:alt도 넣었습니다. 오픈 그래프 문서는 og:image를 쓰면 og:image:alt도 쓰라고 합니다(should specify).

먼저 복사본 저장소에서 빌드해 보고 같은 블록을 운영 사이트에 반영해 배포했습니다. 배포가 끝난 뒤 라이브 글에서 다시 뽑아 봤습니다.

$ curl -s https://birchholt.com/posts/deploy-automation/ \
    | grep -oE '<meta property="og:image[^>]*>'
<meta property="og:image" content="https://birchholt.com/images/posts/deploy-automation/00.webp">
<meta property="og:image:type" content="image/webp">
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="715">
<meta property="og:image:alt" content="push 한 번으로 배포되게 만들면서 걸린 것들">

사이트맵에 있는 글 14편 모두에 og:image가 한 줄씩 들어갔고 홈·개인정보처리방침·없는 주소(404)에는 0줄이었습니다. 고치기 전에는 어느 글에도 없던 줄입니다.

WebP를 그대로 둬도 되는지는 문서로 확인되지 않았습니다

같은 그림을 두 가지 액자에 넣어 놓고 어느 쪽이 문틀에 맞을지 재보는 표구사

여기서 걸리는 게 하나 있습니다. 제 그림은 전부 WebP(구글이 만든 이미지 형식, 같은 화질에서 JPG보다 작게 나오는 편)입니다. 카톡 미리보기가 WebP를 받아 주는지 확인하려고 카카오 문서를 찾아봤습니다.

문서에 있는 이미지 조건은 이렇습니다.

메시지 템플릿 · 컴포넌트 제약 사항, 메시지 템플릿 FAQ. 2026-10-04에 확인했습니다.

지원하는 파일 형식은 어디에도 적혀 있지 않았습니다. 카카오톡 공유 문서와 공유 FAQ, JavaScript SDK 공유 문서, 메시지 템플릿 문서와 FAQ, 도구 문서, 방화벽 문서까지 일곱 개를 내려받아 webp를 찾아보니 0건이었습니다. jpg·png도 예시 이미지 주소(intro.jpg 등)에만 나왔습니다. 되는지 안 되는지 문서로는 말할 수 없습니다.

예로 든 글의 대표 그림은 1,280×715, 23,098바이트라 적힌 크기·용량 조건은 모두 맞습니다. Cloudflare Pages가 이 파일을 content-type: image/webp, 200으로 내주는 것도 확인했습니다. 페이스북 수집기 이름으로 요청해도 같았습니다. 배포 뒤에는 kakaotalk-scrap/1.0이라는 이름으로도 글과 그림을 요청해 봤고 둘 다 200이었습니다. 다만 이 이름은 카카오 문서에서 찾은 게 아니라서 실제 스크랩 서버가 이 이름으로 오는지는 확인하지 못했습니다.

혹시 JPG가 필요할 때를 대비해 Hugo 안에서 JPG를 만드는 방법도 빌드해 봤습니다. 원본은 그대로 두고 <head>에 넣을 때만 형식을 바꿉니다.

{{- with resources.Get (printf "images/posts/%s/00.webp" .Slug) }}
{{- with .Process "jpg q85" }}
<meta property="og:image" content="{{ .Permalink }}">
<meta property="og:image:type" content="{{ .MediaType.Type }}">
<meta property="og:image:width" content="{{ .Width }}">
<meta property="og:image:height" content="{{ .Height }}">
<meta property="og:image:alt" content="{{ $.Title }}">
{{- end }}
{{- end }}

결과물은 이렇게 나왔습니다.

<meta property="og:image" content="https://birchholt.com/images/posts/deploy-automation/00_hu_ee33a80456dcc392.jpg">
<meta property="og:image:type" content="image/jpeg">
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="715">

.MediaType.Type을 쓴 덕분에 og:image:type이 image/jpeg로 같이 바뀝니다. 형식만 바꾸고 손으로 image/webp라고 적어 두면 태그와 파일이 서로 다른 말을 하게 됩니다.

크기를 비교했습니다. 같은 1,280×715 그림 한 장입니다.

00.webp (원본)                  23,098 바이트
Hugo .Process "jpg q85"        58,651 바이트
macOS sips 품질 85              90,610 바이트

복사본으로 실험할 때 있던 글 12편 전체로 보면 대표 그림 WebP 합계가 316,848바이트이고 JPG를 따로 만들면 892,945바이트가 더 올라갑니다. 본문은 계속 WebP를 쓰니 JPG는 미리보기에만 쓰이는 파일입니다.

운영에는 JPG 변환 없이 WebP 그대로 내보냈습니다. 그리고 카카오 쪽에서 직접 봤습니다.

먼저 카카오 개발자 사이트의 [카카오톡 URL 메타정보 관리]에 글 주소를 넣고 [메타 정보 조회]를 눌렀습니다. 돌아온 건 「오류가 발생했습니다. 다시 시도해 주세요.」 한 줄이었습니다. 그 주소를 카톡에 한 번도 보내기 전이었고 왜 오류가 났는지는 확인하지 못했습니다.

그래서 카카오톡 「나와의 채팅」에 같은 주소를 보냈습니다. 미리보기 카드에 WebP 대표 그림이 썸네일로 제대로 붙었습니다. 제목은 「push 한 번으로 배포되게 만들면서 걸린 것들 — birchhol…」에서 잘렸고 그 아래에 birchholt.com이 나왔습니다. og:description에 적은 설명은 카드에 보이지 않았습니다. 왜 빠졌는지는 확인하지 못했습니다.

이번에는 WebP로 떴습니다. 그래서 지금은 WebP를 그대로 둡니다. 다만 글 한 편을 「나와의 채팅」에 한 번 보내 본 결과입니다. 다른 채팅방이나 다른 글에서도 같은지는 보지 않았고 JPG로 바꿔 카드를 비교해 보지도 않았습니다. 문서가 WebP를 보장해 주는 것도 아니니, 위의 JPG 변환은 WebP 썸네일이 안 뜨는 경우가 생기면 꺼낼 방법으로 남겨 둡니다.

고쳐도 예전 미리보기가 남는 이유

오래된 게시판 앞에서 새 공지를 붙이고도 이전 종이를 떼어 내려 손을 뻗는 사람

태그를 넣고 배포해도 이미 한 번 공유된 주소는 예전 카드가 그대로 나올 수 있습니다. 카카오가 한 번 읽어 간 오픈 그래프 정보를 저장해 두고(캐시) 다시 쓰기 때문입니다. 카카오 FAQ도 미리보기 이미지가 안 바뀌는 건 캐시 때문이라고 적고 있습니다.

지우는 곳은 카카오 개발자 사이트의 **[도구] > [카카오톡 URL 메타정보 관리]**입니다. 문서에 적힌 순서는 이렇습니다.

  1. [URL 입력]에 주소를 넣습니다
  2. [메타 정보 조회]로 지금 저장된 값을 봅니다
  3. [캐시 초기화]를 누릅니다
  4. 다시 [메타 정보 조회]로 바뀐 값을 확인합니다

도구 · 카카오톡 URL 메타정보 관리 — 「URL의 OG 태그를 변경한 경우, 카카오 서버에 캐시된 스크랩 미리보기 정보를 초기화해야 이후 공유 시 변경 내용이 반영됩니다」. 2026-10-04에 확인했습니다.

두 가지를 적어 둡니다.

로그인이 필요합니다. 문서의 「도구 바로가기」 주소(developers.kakao.com/tool/debugger/sharing)를 그냥 요청하면 accounts.kakao.com 로그인 페이지로 넘어갑니다.

문서가 적은 대상 범위가 좁습니다. 이 도구로 관리할 수 있는 URL로 문서는 “카카오톡 메시지 API 또는 카카오톡 공유 API로 보낸 스크랩 메시지에 포함된 URL"과 “카카오스토리로 공유한 URL"을 듭니다. 채팅방에 주소를 손으로 붙여 넣은 경우가 여기 들어가는지는 문서 문장만으로는 알 수 없었습니다. FAQ 쪽은 메뉴 이름을 [도구] > [초기화 도구] > [OG(Open Graph) 캐시]로 적고 대상도 “카카오 플랫폼에 저장된 웹 페이지나 파일의 URL"이라고 넓게 적어서 두 문서가 서로 다른 말을 하고 있습니다.

손으로 보낸 주소도 이 도구로 지워지는지는 저도 가려 보지 못했습니다. 제가 이 주소를 카톡에 보낸 건 태그를 넣은 뒤가 처음이라 지울 예전 카드가 없었고 캐시 초기화 전후로 카드를 비교해 보지도 않았습니다. [메타 정보 조회]는 앞에 적은 대로 오류만 냈습니다.

그리고 캐시는 카카오 쪽에만 있는 게 아닙니다. Cloudflare Pages가 이 그림에 붙여 보내는 헤더는 cache-control: public, max-age=14400, must-revalidate였습니다. 그림 파일 이름이 그대로인 채 내용만 바꾸면, 이 헤더를 따르는 브라우저나 중간 캐시는 최대 4시간(14,400초) 동안 옛 그림을 쓸 수 있습니다. 카카오 스크랩 서버가 이 헤더를 따르는지는 문서에서 찾지 못했습니다. 위에서 JPG를 만들 때 파일 이름에 해시(00_hu_ee33a80456dcc392.jpg)가 붙는 건 그 점에서는 오히려 편합니다. 그림이 바뀌면 주소도 바뀌니까요.

다음에 링크가 맨몸으로 나가면

현관을 나서기 전 거울 앞에서 외투 주머니를 하나씩 두드려 보는 사람

미리보기에 그림이 안 붙으면 카카오 캐시를 지우기 전에 먼저 내 HTML에 태그가 있는지부터 봅니다. 이번에는 캐시를 아무리 지워도 소용없었을 겁니다. 처음부터 그림을 알려 주지 않았으니까요.

curl -s https://내-블로그/글-주소/ | grep -oE '<meta property="og:image[^>]*>'

이 줄이 아무것도 안 내놓으면 템플릿 문제입니다. 뭔가 나오면 그 주소를 curl -sI로 찍어 200과 content-type: image/...이 오는지 봅니다. 둘 다 정상인데도 카드가 예전 그대로면 그때가 캐시를 지울 차례입니다.

저는 head.html의 여섯 줄짜리 오픈 그래프 블록을 템플릿이 하는 일의 전부로 믿고 있었습니다. 빌드가 성공하고 페이지가 멀쩡히 열리는 동안에는, 빠진 메타 태그가 아무 신호도 내지 않습니다.