Notion에서 글을 쓰고 Astro 블로그에 자동 게시하는 워크플로
Notion에서 글을 쓰는 데 익숙하지만 블로그는 Astro 같은 file-based static site라면 언젠가 작은 단절을 느끼게 됩니다.
Notion에서는 문단을 정리하고 이미지를 넣고 구조를 바꾸는 일이 편합니다. 그런데 게시하려는 순간 repository로 본문을 옮기고, frontmatter를 채우고, 여러 언어 버전을 만들고, build하고, push하고, deployment를 기다리는 별도 작업이 시작됩니다.
글이 몇 개 없을 때는 괜찮지만 계속 쓰기 시작하면 이 “기계적인 운반 작업”이 점점 귀찮아집니다.
그래서 저는 역할을 분리했습니다. Notion은 작성 화면이고, Git repository 안의 Markdown은 실제 공개 산출물입니다. GitHub Actions가 Notion page를 가져와 Markdown으로 만들고, metadata와 다국어 버전을 준비한 뒤 Astro content directory에 저장합니다. 그다음 build, commit, push, production deploy까지 연결합니다.
목표는 모든 것을 자동으로 만드는 것이 아닙니다. 반복해서 하던 이동 작업을 줄이고 글쓰기와 운영 사이의 전환 비용을 낮추는 것입니다.
파일 기반 블로그를 선택한 이유는 Astro 블로그 vs WordPress: 왜 Markdown + GitHub Actions 구조를 선택했나에서 더 자세히 다뤘습니다. repository에서 직접 게시하는 흐름은 로컬에서 Markdown을 쓰고 git push로 자동 배포하는 다국어 블로그 게시 흐름과 연결됩니다.
이 구성은 그 두 단계 사이에 Notion을 writing UI로 넣는 방식입니다.
처음부터 복잡한 event-driven system으로 만들지 않았다
처음에는 Notion에서 공개 버튼을 누르는 순간 webhook이나 Worker를 실행하고, repository를 업데이트하고, deployment까지 이어지는 구조도 생각할 수 있습니다. 기술적으로는 더 즉각적이고 멋져 보입니다.
하지만 개인 블로그의 첫 단계에서 더 중요한 것은 속도가 아니라 문제가 생겼을 때 어느 단계에서 멈췄는지 바로 알 수 있는 것이라고 봤습니다.
그래서 현재 규칙은 일부러 단순합니다.
- 본문은 Notion page body에 작성
- page property에는 title, description, category, tags, slug, status 같은 metadata만 저장
- 한 workflow 실행에서 한 글만 처리
发布 = true이고状态 = Ready인 page를 기본 대상으로 사용- 기존 글 업데이트 시 같은 slug 유지
- overwrite는 명시적인 update permission이 있을 때만 허용
이렇게 해 두면 failure를 분리해서 볼 수 있습니다.
queue에 들어가지 않으면 property를 확인합니다. 번역이 실패하면 localization layer를 봅니다. build가 실패하면 content schema를 확인합니다. deploy가 시작되지 않으면 production workflow를 봅니다.
팀 CMS에서는 realtime trigger, 여러 사람의 review, 복잡한 state machine이 필요할 수 있습니다. 개인 블로그에서는 “오늘 쓴 글이 기존 글을 깨뜨리지 않고 안정적으로 게시되는가”가 더 중요했습니다. 그 목적에서는 단순한 manual trigger와 보수적인 상태 관리가 오히려 운영하기 쉽습니다.
현재 전체 흐름은 다음과 같습니다.
Notion page body
-> Ready + 게시
-> GitHub Actions가 Markdown 수집
-> slug / frontmatter 구성
-> 영어 버전 생성
-> zh-TW / ja-JP / ko-KR localization
-> src/content/posts/<locale>/ 에 저장
-> Astro build
-> main으로 push
-> production deployment
-> Notion에 status / URL / commit SHA 기록
현재 이 사이트는 다섯 언어 디렉터리를 사용합니다.
src/content/posts/
├── zh-cn/
├── en-us/
├── zh-tw/
├── ja-jp/
└── ko-kr/
같은 글은 모든 언어에서 동일한 path를 사용합니다. 그래서 Astro routing, language switch, hreflang, topic/category aggregation을 slug 기준으로 맞추기 쉽습니다.
Notion property는 예를 들어 다음 정도로 제한합니다.
- 名称
- 摘要
- 分类
- 标签
- Slug
- 发布
- 状态
- 自动翻译英文
- 允许更新已发布文章
본문 자체는 property에 넣지 않고 page body에 그대로 작성합니다.
Notion을 “자유로운 CMS”보다 편한 Markdown 원고 편집기로 쓴다
자동 동기화를 안정적으로 유지하려면 Notion의 모든 visual block을 블로그에서 똑같이 재현하려고 하지 않는 편이 좋습니다.
저는 Notion을 “쓰기 편한 Markdown editor”에 가깝게 사용합니다.
- 제목은 올바른 heading level
- 본문은 일반 paragraph
- command / config는 fenced code block
- image는 관련 문단 가까이에 배치
- 중요한 external link나 video는 독립된 한 줄
- 복잡한 column layout이나 장식형 card에는 덜 의존
Notion에서 예쁘게 보이는 block이 Markdown export 뒤에도 같은 구조를 갖는 것은 아닙니다. 처음부터 export하기 쉬운 문서로 쓰는 편이 자동화 전체를 더 안정적으로 만듭니다.
제가 실제로 쓰는 보수적인 원고 형태는 이 정도입니다.
# 제목
처음 두세 문단에서 문제와 선택 이유를 설명한다.
## 중요한 지점
본문을 이어 쓴다.
```bash
pnpm build
node scripts/notion-auto-publish.mjs process
```
그 뒤 troubleshooting, 제약, recovery path를 정리한다.
https://example.com/some-reference
핵심은 “Notion visual page를 Astro로 완전히 변환”하는 것이 아니라, Notion에서 이미 깨끗한 source Markdown에 가까운 원고를 작성하는 것입니다.
이미지나 video, 외부 link도 같은 원칙을 적용합니다. 중요한 video는 설명 문장 뒤에 URL을 단독 줄로 두고, 외부 reference도 왜 중요한지 한 문장 설명한 뒤 URL 또는 Markdown link를 한 줄로 둡니다. 이렇게 하면 export 이후의 normalization layer가 더 안정적으로 처리할 수 있습니다.
script 책임을 나눠 둔다
현재 주요 script entry는 다음과 같습니다.
node scripts/notion-auto-publish.mjs process
node scripts/notion-auto-publish.mjs finalize-success
node scripts/notion-auto-publish.mjs finalize-failure
process는 다음까지 담당합니다.
- 대상 Notion page 선택
- 본문 Markdown 가져오기
- metadata 검증
- slug 결정
- zh-CN / en-US source file 생성
- 이후 localization 단계가 사용할 output path 제공
그다음 workflow가 Traditional Chinese, Japanese, Korean localization을 만들고 Astro build를 실행합니다. build가 통과하면 commit / push / deploy trigger가 이어집니다.
success / failure의 Notion write-back을 별도 command로 둔 이유는 큰 함수 하나에 모든 side effect를 섞지 않기 위해서입니다. 어느 단계까지 성공했는지 분리하기 쉽습니다.
자동 게시에서 번역보다 더 위험한 것은 기존 글 overwrite 판단입니다.
새 글이라고 생각했는데 자동으로 만든 slug가 기존 글과 충돌하면 오래된 글을 덮어쓸 수 있습니다. 그래서 규칙은 일부러 보수적입니다.
- 새 글은 가능하면 unique slug를 명시
- repository에 같은 slug가 존재하면 기본적으로 중단
- 기존 글 업데이트는 원래 slug 유지
- 명시적인 update permission이 있을 때만 overwrite
자동화는 “똑똑하게 추측해서 계속한다”보다 애매할 때 멈추는 편이 블로그에서는 안전합니다.
AI / localization layer를 production truth로 두지 않는다
이 publishing pipeline에서 AI는 편리한 가공 계층이지만 최종 일관성의 책임을 맡기지 않습니다.
OpenAI-compatible gateway는 구현에 따라 동작이 다를 수 있습니다. 실제로 운영하면서 다음과 같은 유형의 문제가 생길 수 있습니다.
- HTTP 200이지만 본문이 비어 있음
- non-streaming response가 비어 있고 streaming에서만 정상 반환
- schema를 줬지만 기대한 field name과 다른 형식
- GitHub Runner에서 특정 gateway까지 network connection 자체가 실패
그래서 localization script에는 fallback과 retry가 필요합니다.
하지만 더 중요한 것은 모델 output을 그대로 production truth로 만들지 않는 것입니다. 최종적인 hard boundary는 다음에 남겨 둡니다.
- content schema
- slug collision protection
- code block / config protection
- Astro build
- Git history
- deployment verification
모델은 title, description, localization을 도울 수 있지만 repository 규칙을 넘어 임의로 게시하는 권한까지 주지 않습니다.
특히 일본어와 한국어 생성에서는 fenced code block과 inline code를 먼저 placeholder로 보호한 뒤 자연어 본문만 localization하도록 구성했습니다. CLI flag, file path, API name, config key가 번역 과정에서 바뀌는 것을 줄이기 위한 장치입니다.
먼저 dry run으로 원고와 schema를 검증한다
특정 Notion page를 시험할 때 바로 production publish부터 하지 않는 편이 좋습니다.
workflow에서 page_id를 지정하고 dry_run을 사용할 수 있습니다. 이 상태에서는 다음 단계까지 실행합니다.
- Notion page 가져오기
- article file 생성
- localization
- Astro build
하지만 main에는 push하지 않고 production state도 바꾸지 않습니다.
이 한 단계만 있어도 Notion property, Markdown export, localization, content schema 문제를 deployment 전에 분리할 수 있습니다.
실제로 문제가 생겼을 때는 마지막 exit 1만 보지 말고 처음 실패한 step을 보는 것이 중요합니다. process, zh_tw, ja, ko, build, commit_push, trigger_deploy가 각각 독립적인 failure point입니다.
지금 유지하는 최소 규칙
전체 구현을 외우지 않아도 다음 원칙만 고정하면 꽤 안전합니다.
- 본문은 Notion page body에 작성
- metadata는 property에 저장
- 새 글은 unique slug를 명시
- 기존 글 업데이트에서는 slug를 바꾸지 않음
- overwrite는 명시적으로 허용
- source language를 게시 가능한 상태까지 먼저 다듬은 뒤 localization
- command / config는 fenced code block에 넣기
- failure는 마지막 step이 아니라 최초 실패 지점을 확인
- Astro build를 deployment gate로 사용
이 workflow는 “가장 고급 CMS”를 만들기 위한 것이 아닙니다.
Notion의 쓰기 편함, Astro의 file-based content, GitHub Actions의 반복 작업 자동화를 이어서 일상적으로 계속 쓸 수 있을 정도로 마찰을 낮추는 것이 목적입니다.
저에게는 관리자 화면에 버튼을 더 추가하는 것보다 이 방식이 오래 유지하기 쉽습니다.