Publishing Markdown to Substack from an Agent Skill
에이전트 스킬로 Markdown을 Substack에 게시하기
publishing-kit은 Markdown 문서를 Substack 웹 편집기에 붙여 넣고, 저장된 초안을 다시 읽어 코드 블록·이미지·링크가 보존됐는지 확인합니다. 글은 ProseMirror 편집기의 조용한 콘텐츠 손실을 피하는 방법과 구독자 이메일 발송을 포함한 게시 절차를 설명합니다.
- 주제
AI 요약
publishing-kit은 Claude Code, Codex, Antigravity에서 쓰는 에이전트 스킬입니다. 이 글은 dev.to, Medium, AWS Builder Center, LinkedIn에 이어 다섯 번째 게시 대상으로 Substack을 추가하고, Markdown 파일 하나를 Substack 게시물로 만드는 과정을 설명합니다. Substack에는 게시 API가 없어서 웹 편집기에 HTML을 붙여 넣어야 합니다. 편집기가 받아들인 내용이 그대로 저장된다고 가정하지 않고, 저장된 초안을 다시 읽어 결과를 검증합니다.
붙여 넣기만으로는 알 수 없는 손실
Substack 편집기는 ProseMirror 기반이며, 게시물에 들어갈 수 있는 요소와 서식을 스키마로 제한합니다. 저장 결과를 확인해 보니 여러 줄 코드 블록은 줄과 문자 수를 유지했고, 공개 URL 이미지도 Substack 서버로 옮겨지면서 대체 텍스트(alt text)가 남았습니다. 일반 링크 역시 보존됐습니다. 반면 표는 편집기에 대응하는 요소가 없어 사라졌고, 인라인 코드에 링크를 건 경우 코드 서식만 남았습니다. 언어가 text인 코드 블록은 수학(LaTeX) 블록으로 저장됐습니다. 제목은 붙여 넣은 수준을 따르지만, Substack이 지원하는 여섯 단계 크기로 표시됩니다.
특히 코드 서식(mark)의 스키마에는 excludes: "_"가 지정돼 있습니다. 인라인 코드는 굵게·기울임·링크 같은 다른 서식과 함께 쓸 수 없다는 뜻입니다. 따라서 <a><code>...</code></a>를 붙여 넣으면 오류 없이 링크가 사라지고 코드만 저장됩니다. 글에 들어간 링크 네 개 가운데 두 개가 이런 형태였고, 원래 HTML을 그대로 붙여 넣었을 때 초안에 남은 링크 대상은 네 개 중 두 개뿐이었습니다. 해결 방법은 붙여 넣기 전에 앵커에서 <code>를 풀어 링크를 일반 텍스트에 연결하는 것입니다.
Medium용 HTML을 재사용하는 방법
표와 도형이 이미지로 바뀌는 점은 Medium과 같습니다. publishing-kit은 Medium용으로 이미지를 만든 HTML을 재사용합니다. make-medium.py로 생성한 hosted HTML은 표와 상자 문자 도형을 PNG로 저장하고, 공개 GitHub URL을 가리킵니다. Substack은 이미지를 가져와 자체 이미지 서버에 보관합니다. 반면 data URI를 포함한 embed HTML은 Medium에서 이미지가 모두 사라지므로 쓰지 않습니다. 이미지 파일은 저장소에 커밋하고 푸시해야 합니다.
두 플랫폼은 붙여 넣은 콘텐츠를 다르게 처리합니다. Medium은 여러 줄 코드 블록을 한 줄로 합치고 이미지 대체 텍스트를 지우지만, Substack은 둘 다 보존합니다. Medium의 제목 단계는 두 가지뿐이라 섹션 제목을 <h4>로 낮추지만, Substack에서는 <h4>가 본문 글자 크기 19px보다 약간 큰 21.375px로 표시됩니다. Substack에 붙여 넣기 전에는 이 제목을 <h3>로 올립니다.
브라우저에서 초안을 만들고 검증하기
필요한 환경은 publishing-kit 0.32.0 이상, 페이지 안에서 JavaScript를 실행하는 브라우저 자동화, 로그인한 Substack publication입니다. Substack의 콘텐츠 보안 정책(CSP)은 편집기 페이지에서 로컬 컴퓨터로 직접 요청하는 방식을 막습니다. 이 절차는 같은 탭에서 페이지를 이동해도 유지되는 window.name에 HTML과 헬퍼 스크립트를 담아 전달합니다. 저장소를 127.0.0.1에서 제공하고, 같은 탭을 새 초안 주소로 이동하면 스크립트가 페이로드를 읽습니다. 예시에서는 HTML 20,066자를 전달했습니다.
ss.prepare()는 HTML의 제목 블록을 제거하고, 인라인 코드에 걸린 링크 두 개를 풀며, 섹션 제목을 <h4>에서 <h3>로 바꿉니다. 모든 <pre>에서 언어 클래스를 제거하는 처리도 합니다. 언어가 text인 코드 블록을 그대로 붙이면 LaTeX 블록으로 저장되기 때문입니다. 이 Substack 코드 블록은 언어 정보를 저장하지 않으므로, 클래스를 지워도 저장 결과에서 잃는 정보는 없습니다. 이후 ss.paste()가 합성 붙여넣기를 실행합니다. 편집기에 이미 텍스트가 있으면 붙여 넣기를 거부해 같은 글이 두 번 들어가는 상황을 막습니다. 키 입력이 안정적이지 않은 백그라운드 탭에서도 이 방식을 사용할 수 있습니다.
화면에 보이는 내용만 검사해서는 충분하지 않습니다. 초안의 실제 문서는 /api/v1/drafts/<id>에서 JSON으로 읽을 수 있습니다. ss.audit()는 자동 저장을 기다린 뒤 저장된 문서를 세어, 이 사례에서 코드 블록 네 개의 줄 수가 각각 2·2·1·4로 유지됐는지, 이미지 두 개와 대체 텍스트가 남았는지, 링크 대상 네 개가 모두 존재하는지 확인합니다. 제목과 부제목도 저장된 초안에서 검사합니다. SEO 설정에 있는 제목·설명 필드와 게시물 자체의 제목·부제목 필드가 따로 있으므로, 올바른 필드를 채워야 합니다.
부제목은 255자 제한이 있습니다. 이를 넘기면 편집 화면에는 본문이 남아 있어도 초안 전체가 저장되지 않을 수 있습니다. 오류 배너가 나중에 저장에 성공한 뒤에도 남아 있으므로 화면이나 배너만으로 상태를 판단하지 말고 저장된 초안을 확인해야 합니다. 이 사례에서는 설명이 제한을 넘어서 Substack 부제목을 두 번째 문장 끝에서 잘랐습니다.
게시 전 확인할 설정
게시 화면의 기본 설정은 독자와 댓글 공개 범위가 모두 전체이며, ‘Send via email and the Substack app’이 선택돼 있습니다. 그대로 게시하면 모든 구독자에게 이메일을 보내고 Substack 앱 알림도 전송합니다. 이메일은 회수할 수 없으므로 게시 전에 발송 여부를 결정해야 합니다. 이 글에서는 발송하기로 했고, ss.publish({email: true, audience: "everyone"})처럼 선택한 설정을 지정했습니다. 헬퍼는 이메일 발송 여부를 명시하지 않거나 현재 설정이 지정값과 다르면 게시를 거부합니다.
게시 흐름에는 ‘Add subscribe buttons to your post’라는 두 번째 대화상자도 있습니다. 이 사례에서는 버튼 없이 게시를 선택했습니다. 게시가 끝나면 publication 목록에서 게시물 URL과 발행 시각을 읽습니다. URL은 제목으로 추측하지 않고 Substack이 돌려준 slug를 사용합니다. 이 URL을 links.txt에 기록하면 LinkedIn, Slack, Google Chat 게시 스크립트에서 재사용할 수 있습니다. 링크 검사 결과 Substack은 HTTP 200을 반환했고, 미등록 slug에는 404를 반환했습니다.
이 절차의 범위는 Linux의 Claude Code와 publishing-kit 0.32.0으로 한 publication에 한 편을 게시한 사례입니다. 게시 시각은 2026년 10월 5일이며, 표·코드·제목 크기와 편집기 동작에 관한 결과도 해당 편집기의 스키마와 스타일을 기준으로 합니다. 글은 Medium용 빌드를 재사용하되 Substack에 맞게 HTML을 조정하고, 게시 전에 저장된 초안을 검증하는 과정을 보여줍니다.
dev.to 반응
- @reidmarlow —
window.name에 페이로드를 넣어 탐색 중 Substack CSP를 우회한 방식이 깔끔합니다. 파일 입력 처리와 콘솔 길이 제한을 피할 수 있습니다. ProseMirror에서 코드 mark가 링크 전체를 조용히 버리는 동작은 웹 편집기에서 겪은 가장 까다로운 손실 중 하나입니다. 비슷한 에디터 파이프라인에서 셸 코드의 이스케이프되지 않은 달러 기호가 LaTeX 파서를 작동시켜 환경 변수 할당을 깨진 수식 블록으로 바꾸는 일을 겪었습니다. 오류도 나지 않았습니다. 직렬화된 초안 JSON을 입력 내용의 개수와 대조하는 방법이 가장 믿을 만합니다. - @swarmery — “화면이 아니라 저장된 초안을 읽으라”는 원칙을 여러 곳에서 다시 배우게 됩니다. 저는 Claude Code 세션용 로컬 대시보드를 만들었는데, 화면에는 맞아 보이는 상태와 실제 데이터가 달라서 가장 큰 버그를 겪었습니다. 그 뒤로는 저장된 데이터와 늘 비교합니다. 같은 원칙을 게시에도 적용한 점이 좋습니다. Markdown 파일 하나로 다섯 곳에 게시한다면 대표 URL을 한 곳으로 지정하나요, 아니면 플랫폼마다 별도로 두나요?
- @anh_nguynvn_0478e614ba — ProseMirror가 코드 mark와 함께 링크 전체를 버리는 동작은 웹 편집기에서 본 조용한 실패 가운데 가장 까다로운 축에 듭니다. 지원하지 않는 노드를 대체 처리나 경고 없이 적극적으로 지우는 서식 있는 텍스트 파이프라인에서도 같은 종류의 문제를 계속 겪습니다. 편집기의 붙여넣기 처리 전에 Markdown을 미리 검증하나요, 아니면 삽입한 뒤에만 손실을 처리하나요? 스키마를 아는 정화기(sanitizer)로 먼저 처리하면 표나 중첩 서식 문제를 많이 줄일 수 있었습니다.
window.name방식도 흥미롭습니다. 전체 페이지를 새로고침한 뒤에도 페이로드가 남나요, 아니면 페이지 안에서 이동할 때만 유지되나요? 헤드리스 게시 흐름에서도 비슷한 제약을 겪었고, 파일 입력 방식은 번거롭게 느껴졌습니다.- @xbill — 이 부분들은 모두 계속 바뀌고 있습니다. 게시를 시도할 때마다 새로운 문제를 만납니다. Medium 쪽 문제와 몇 주째 씨름하고 있습니다.
- @jkming — 붙여넣기 후 살아남는 항목을 정리한 표가 가장 유용합니다. “실패해도 오류가 나지 않는다”는 점이 까다롭습니다. ProseMirror 편집기 자동화에서 같은 일을 겪었습니다. 저장 요청은 어느 쪽이든 성공으로 돌아오고, 실제로 저장된 초안을 읽어 입력과 비교하는 방법만 믿을 수 있습니다.
excludes: "_"가 링크를 없애는 버그를 설명해 주네요. 저장 후 비교에서 빠진 항목을 찾으면 도구가 게시를 중단하나요, 아니면 경고만 하고 계속 게시하나요?
원문: dev.to / 번역·요약: Trawling