블로그를 만들기로 했을 때 첫 번째 질문은 “어떤 플랫폼을 쓸까”였다. Velog, Tistory, Medium, Ghost. 다 좋은 선택지였는데 하나씩 따져보니 공통점이 있었다. 내 글이 내 것이 아닌 느낌. 플랫폼에 종속되거나, DB를 따로 운영하거나.
그래서 직접 만들기로 했다. 조건은 하나였다. AI가 CLI에서 직접 글을 쓰고 발행할 수 있을 것.
그러려면 데이터베이스가 없어야 했다.
AI가 파일을 읽고 쓰는 건 자연스럽다. 하지만 DB 쿼리를 날리고, API를 통해 CMS에 등록하고, 미디어를 업로드하는 건 복잡해진다. 마크다운 파일 하나를 만들어 폴더에 넣으면 블로그 글이 되는 구조라면 훨씬 단순했다.
스택은 Astro 5. 콘텐츠가 마크다운이라 자연스러운 선택이었다. 정적 사이트 생성기라 DB가 필요 없고, Content Layer로 파일 시스템을 그대로 콘텐츠 소스로 쓸 수 있었다.
콘텐츠 소스는 second 위키로 잡았다.
export/slowloop/blog/ 아래에 시리즈별로 폴더를 만들고, 각 폴더 안에 마크다운 파일을 넣는 구조다. Astro가 CONTENT_DIR 환경변수로 이 경로를 glob으로 읽어 발행본으로 처리한다.
이렇게 하면 second 위키가 진짜 단일 원천이 된다. 글을 쓰는 것도 위키에서, 수정하는 것도 위키에서, 발행 상태 관리도 frontmatter 필드 하나로. 파일이 곧 DB다.
second 위키/
export/slowloop/
blog/ ← Astro가 읽는 KO 발행본
slowloop/
세컨드브레인/
...
blog_en/ ← EN 번역 발행본 (동일 구조)
content-drafts/ ← 초안 (빌드 제외)
영문 동시 발행은 같은 구조를 두 벌 운영하는 방식으로 풀었다.
blog/(한국어)와 blog_en/(영어)를 동일한 slug로 관리한다. 글을 발행할 때 한국어 발행본과 영어 번역본을 동시에 만든다. 직역 금지 원칙 — 영어 독자가 읽기 자연스럽게 문장 구조를 재구성하되 저자 톤은 유지한다.
사이트에서는 언어 토글 하나로 같은 slug의 다른 언어 버전으로 전환된다. 번역본이 없는 글은 토글이 비활성화된다. 언어 선호는 localStorage에 저장돼서 다음 방문 때도 유지된다.
문서 간 연결은 두 층으로 설계했다.
하나는 본문 내 인라인 링크. [[slug|표시텍스트]] 형식으로 쓰면 빌드 시 실제 포스트 URL로 변환된다. 아직 발행 안 된 slug는 링크가 비활성화되고 텍스트만 표시된다.
다른 하나는 포스트 하단 관련 글 섹션. frontmatter의 references: [slug1, slug2] 필드를 기반으로 자동 생성된다. 글을 발행할 때 전체 발행 코퍼스를 스캔해서 주제·태그·개념이 겹치는 슬러그를 골라 자동으로 채워준다.
결국 블로그 글 하나의 생애는 이렇다.
second 위키 안에서 마크다운 파일로 태어나고, export/slowloop/blog/ 에 발행되고, Astro가 빌드하면 slowloop.app/blog 에 뜬다. 수정도 파일 편집 한 번이고, 숨기고 싶으면 frontmatter에 status: archived 한 줄이다.
DB가 없으니까 AI가 직접 관리할 수 있다. 편집기 UI가 없어도 된다. CLI에서 전부 된다. 그게 처음부터 원했던 거였다.