| name | garden-to-wikidocs |
| description | 정한의 디지털 가든(Quartz/Hugo MD, ~/repos/gh/notes/content)을 위키독스 깃허브 연동 책(book_id 20676)으로 내보낼 때 사용한다. 폴더 미러 3단계 파이프라인(build 씨뿌리기 → recover 회수 → relink 링크 실화). 가든 원본은 절대 수정하지 않고, denote-id 파일명·날짜접두어 제목·callout·relref·citeproc·이미지·회사신원 난독화를 처리한다. push 하면 웹훅이 위키독스를 동기화한다. |
가든 → 위키독스 깃허브 연동 책 내보내기 (v2)
이 리포(junghan0611/garden2wikidocs)는 위키독스 책 20676 "Junghanacs's Digital
Garden" 과 깃허브 연동돼 있다. git push → 웹훅 → 위키독스 동기화.
정본·발견면 계약
cross-repo 정책 SSOT는 garden의
docs/WIKIDOCS_MIRROR.md
(로컬: /home/junghan/repos/gh/notes/docs/WIKIDOCS_MIRROR.md)다.
- garden은 canonical/latest/authored source다.
- WikiDocs는 한국어 검색과 읽기를 위한 discovery mirror다.
- garden2wikidocs는 garden 원본을 바꾸지 않는 read-only translation harness다.
이 리포는 미러 생성물·page_id·WikiDocs URL의 기준 경로이며, 위키독스 웹 UI 편집은
기준 경로가 아니다. source authority와 publication ordering은 위 정책 SSOT가 소유한다.
구현 세부는 이 skill이 소유하지만 그 정책을 독자적으로 재정의하지 않는다. 가든 전체를
폴더 단위로 올려 위키독스 안에서 자기완결적으로 순회하게 하는 것이 목표다.
3단계 파이프라인
[1] build.py 씨뿌리기 — 가든 폴더 → pages/<folder>/<denote-id>.md + TOC.md + mapping.json
+ BUILD-MANIFEST.json, content/index.md → README.md(위키독스 책 '대문'),
폴더·autholog 표지 → `## 제목` + 날짜·태그 + description + 읽기 링크의
AEO recent-first 집합면. garden frontmatter의 title/description/date/lastmod를
읽고, 각 본문 페이지에 abstract-first `원본·최신본` block + <!-- gid:ID --> 앵커를 둔다.
내부 relref 는 가든 절대URL, provenance source URL은 relink에서 보호.
기존 mapping의 page_id/url은 동일 Denote ID에 승계하되, 직전 판 TOC 밖이던
항목이 발행면에 새로 들어오면 죽은 id라 승계하지 않는다. 회수 이력이 있는데
직전 판 TOC를 못 읽으면 판정 불가라 생성물을 쓰기 전에 멈춘다.
[2] recover.py 회수 — 최초 push 또는 새 페이지 동기화 후 book get 으로 gid<->page_id 및
collection marker<->standalone 표지 page_id 회수
[3] relink.py 링크 실화 — 현재 TOC 발행면 페이지 + README 안에서, 가든 URL 중
발행면에 있는 page_id 만 wikidocs.net/<page_id> 로 재작성(나머지는
가든 URL 유지, 하이브리드). 발행면 밖 파일은 아예 열지 않는다.
`/tags/autholog/` 링크도 회수된 어쏠로그 집합 표지로 실화한다.
[검증] audit.py 품질 게이트 — TOC·mapping·gid·page_id·source metadata/provenance·
AEO 표지 exact match + `##`/읽기 링크 수·순서·uniqueness/completeness·
abstract ordering·미처리 relref·원본/미러 헤딩 보존
[상태] status.py push 후 웹훅 반영 진척 — book get 라이브 본문 vs 로컬 pages/ 대조로
gid 페이지와 collection marker의 synced/pending/missing 카운트.
대량 push 는 한 번에 안 도는 일이 잦다.
pages/x.md 같은 상대 링크는 위키독스에서 작동하지 않는다(메인으로 튕김). 페이지 간
링크는 반드시 https://wikidocs.net/<page_id> 절대 URL이어야 하고, page_id 는 페이지 생성
후에만 생기므로 3단계가 구조적으로 불가피하다.
실행 — audit 이전 push 금지
push 한 번이 약 20분짜리 전체 WikiDocs webhook을 촉발한다. build 직후 push하지 말고,
반드시 relink와 audit을 먼저 통과한다.
정상 갱신(코어 발행면 — 500 상한 아래)
python3 .claude/skills/garden-to-wikidocs/scripts/build.py \
--folders journal,meta,bib,notes,botlog --core
python3 .claude/skills/garden-to-wikidocs/scripts/relink.py
python3 .claude/skills/garden-to-wikidocs/scripts/audit.py --core
python3 -m unittest discover -s tests -q
WIKIDOCS_TOKEN="$(pass personal/token/wikidocs/junghanacs)" \
python3 .claude/skills/garden-to-wikidocs/scripts/status.py --book-id 20676 --list
build 는 pages/ 를 통째로 다시 쓰므로 relink 결과가 매번 지워진다. build 를 돌렸으면
relink 도 반드시 다시 돌린다. audit 은 relink 뒤에 돌려야 발행면 밖 page_id 참조까지
검사한다.
신규 page_id가 있는 갱신
새 노트만이 아니다. 가든에서 기존 노트에 autholog 태그를 붙이면 그 노트가 발행면에
새로 들어오면서 같은 2단계가 필요해진다. 어쏠로그 회수는 계속 도는 작업이라 이 경우가
정상 갱신보다 흔하다. build 가 [warn] 발행면 page_id 미회수 N개 를 찍으면 이 절차다.
python3 .claude/skills/garden-to-wikidocs/scripts/build.py \
--folders journal,meta,bib,notes,botlog --core
python3 .claude/skills/garden-to-wikidocs/scripts/relink.py
python3 .claude/skills/garden-to-wikidocs/scripts/audit.py \
--core --allow-missing-page-ids
WIKIDOCS_TOKEN="$(pass personal/token/wikidocs/junghanacs)" \
python3 .claude/skills/garden-to-wikidocs/scripts/recover.py --book-id 20676
python3 .claude/skills/garden-to-wikidocs/scripts/build.py \
--folders journal,meta,bib,notes,botlog --core
python3 .claude/skills/garden-to-wikidocs/scripts/relink.py
python3 .claude/skills/garden-to-wikidocs/scripts/audit.py --core
python3 -m unittest discover -s tests -q
--core 발행면에서는 새 표지 하나를 올리는 데도 이 2단계가 필요하다. 회수 뒤 build 를
다시 돌려야 회수한 page_id 가 표지 목록에 반영된다.
진척이 멈춰 pending이 남으면 위키독스 책 수정 > 깃허브 > 지금 동기화를 수동 재트리거한다.
build.py --folders journal,meta,bib,notes,botlog 처럼 쉼표로 여러 폴더 동시 처리.
알려진 챕터는 입력 순서와 무관하게 1 저널·2 메타·3 참고문헌·4 노트·5 봇로그로 정렬.
--core 는 TOC 등록을 발행 코어(autholog 태그 ∪ botlog 폴더)로 제한한다. 500 상한
대응이며 pages/·mapping.json 은 가든 전량을 그대로 만든다. 상한을 넘기면 build 가
생성물을 쓰기 전에 실패한다. 코어에서 저널을 뺀 이유는 저널만이 시간축으로 계속 늘어나는
유일한 발행 증가원이기 때문이다(나머지는 새 노트 생성이 아니라 수선으로 자란다).
--garden-links 는 표지·집합면의 링크를 전부 가든 원본으로 낸다. 기본값이 아니다.
코어에 담은 노트끼리는 위키독스 안에서 순회해야 읽기가 끊기지 않으므로, 평소에는 이
옵션 없이 build 하고 relink.py 를 돌린다(2026-07 실측: 발행면 249개 안에서 내부
순회율 33%, 나머지는 코어 밖 노트라 가든으로 나가는 게 맞다). 발행면을 크게 갈아엎는
전환기에 링크를 한 곳으로 몰고 싶을 때만 켜는 안전판이다. audit 에는 build 와 같은
옵션을 그대로 넘긴다.
--garden 기본 ~/repos/gh/notes, --out 기본은 README.md 있는 리포 루트.
- build는 garden
content/·change-text.sh가 dirty/untracked면 생성물 쓰기 전에 실패한다.
canonical garden commit 뒤에만 미러를 생성한다.
- 의존성 0(Python 표준 라이브러리). 토큰은
pass personal/token/wikidocs/junghanacs.
실측으로 확정된 불변식 (깨지 말 것 — 실험으로 검증됨)
- 책 한 권의 상한은 TOC.md 등록 페이지 500개다. 넘으면 웹훅이 동기화를 통째로
거부한다(
GitHub 동기화 실패: TOC.md에 2244개의 페이지가 등록되어 있습니다. 책의 페이지는 최대 500개까지만 가능합니다.). 커밋 델타 크기와 무관하게 매번 걸리므로
TOC 를 줄이는 것 외에 우회로가 없다. 2026-07-18 까지는 없던 규칙이 사후 도입됐다.
- TOC.md 는 목차가 아니라 발행 목록이다. TOC 에서 뺀 페이지는 라이브에서 삭제된다.
2026-07-26 실측: 2,243 → 249 노드, 뺀 페이지 URL 은 404, 복구 경로 없음(TOC 를
되돌리는 push 도 500 에 다시 막힌다). 리포의
pages/·mapping.json 은 전량 보존되므로
상한이 풀리면 TOC 복원으로 본문은 되돌아간다. 다만 URL 은 아니다(아래).
- 살아남은 페이지의 page_id 는 흔들리지 않는다. 같은 실측에서 생존 244개 전부
page_id 유지(변경 0). 대량 삭제와 page_id 안정성은 별개다.
- 삭제된 페이지의 page_id 는 부활하지 않는다. 2026-07-27 실측: 컷 때 지워진 두 노트가
autholog 태그로 발행면에 다시 들어오자 위키독스는 옛 381403/381716 이 아니라 새
387071/387072 를 발급했다. 그래서 mapping 의 page_id 는 그 페이지가 직전 판 발행면에
있었을 때만 살아 있다. 이 조건은 조용히 깨진다 — 죽은 id 도 숫자가 멀쩡히 있고 TOC
안이라 link_target·relink·audit 이 전부 통과한다. 라이브를 재는 status.py 의
미생성 만이 신호다. 그래서 build 가 덮어쓰기 전 TOC.md 를 읽어 발행면에 새로
들어오는 항목의 page_id 를 승계하지 않고, relink 가 가든 원본으로 내보내게 한 뒤
push→recover 로 새 id 를 받는다. 발행면 밖에 머무는 항목의 죽은 id 는 그대로 둔다 —
노출 경로가 없고, 2천여 항목을 흔들면 실제 위험 구간이 diff 에 묻힌다. 가든에서 기존
노트에 autholog 를 붙이는 것만으로 이 경로를 타므로 드문 일이 아니다.
- 회수 이력이 있는데 직전 판 발행면을 못 읽으면 build 는 멈춘다.
TOC.md 가 없거나
등록 0개인데 mapping 에 page_id 가 있으면 어느 id 가 살아 있는지 판정할 수 없다. 여기서
fail-open 하면 위 안전장치가 막으려던 승계가 그대로 열리므로, 500 상한 사전검사와 같이
생성물을 건드리기 전에 실패한다(relink 의 TOC 부재 exit 1 과 같은 방향). 통과하는 것은
회수 이력 자체가 없는 순수 bootstrap 뿐이다 — 비울 id 가 없으니 위험도 없다. 정상 clone
에는 tracked TOC.md 가 있으므로 이 경로를 밟지 않는다.
- build 의 두 warning 은 층이 다르다.
발행면 page_id 미회수 N개 는 2단계 push 가
필요하다는 운영 신호로, 죽은 id 를 비운 항목과 처음 올라가는 항목을 함께 센다. 그중 M개는 발행면 신규 진입 은 그 부분집합이자 안전장치가 실제로 발동했다는 신호다. 앞의 것만
절차 트리거로 읽는다 — 뒤의 것은 0 이어도 회수는 필요할 수 있다.
- 위키독스 URL 은 발행된 페이지에만 유효하다. mapping 에 page_id 가 남아 있어도 TOC
밖이면 404 다.
build --garden-links(표지·집합면)와 relink 의 TOC 게이트가 이 규칙을
강제하고, audit 이 발행면 밖 page_id 참조를 오류로 잡는다. 저자가 본문에 직접 쓴 외부
위키독스 링크는 mapping 소유가 아니므로 대상이 아니다.
- relink 는 발행면 안팎 양쪽에 게이트를 건다. 링크 대상이 TOC 안이어야 실화하고,
링크를 담은 파일도 TOC 안이어야 연다. 발행면 밖 페이지는 라이브에 없어서 그 안의
위키독스 URL 을 아무도 볼 수 없는데, 고치면 발행면이 바뀔 때마다 리포 전체가 흔들린다
(2026-07 실측: 코어 249개 발행면인데 file 게이트 없이는 826개 파일이 바뀌었고 그 중
639개가 발행면 밖이었다. 게이트 후 185개). 책 대문
README.md 만 TOC 밖 예외다.
TOC.md 가 없으면 relink 는 게이트를 풀지 않고 exit 1 로 멈춘다. 발행면을 모르는
채 전량을 고치는 fail-open 이 바로 이 게이트가 막으려는 사고이기 때문이다.
- URL = page_id 기반, 매우 안정적. 파일명·제목·순서를 다 바꿔도 page_id 유지됨.
단 우리가 URL을 직접 지정할 수 없으므로
mapping.json 으로 회수·관리한다.
- 파일명 = denote-id (
pages/journal/20220310T000000.md). URL 안정·회수 앵커·편집관리.
- 제목(TOC 링크텍스트) 앞에 source 날짜 8자리 접두어. journal은 garden
date
(주간/일간 저널이 나타내는 created date), meta/bib/notes/botlog는 garden lastmod를 쓰고
없을 때만 date로 fallback한다. build/git/file mtime/WikiDocs sync 시각은 금지한다.
제목이 선택된 source 날짜(ISO 또는 8자리)로 이미 시작하면 중복 접두어를 생략한다.
- recent-first AEO 표지. TOC.md와 각 챕터 cover는 위 source 날짜 기준 내림차순이다.
WikiDocs sidebar는 제목 오름차순을 강제하므로 stable title/page_id를 깨는 순번 재부여를
하지 않는다.
_chapter.md의 항목은 ## 깨끗한 원제목 → 작성·수정일·태그 → frontmatter
description → 읽기 링크 순서다. TOC의 날짜접두어 subject와 표지 heading은 역할이
다르다. 같은 표지 안의 중복 제목만 — 작성일(그래도 겹치면 Denote ID)로 구분한다.
발행면 안은 WikiDocs, 밖은 garden 원본으로 보내며 링크 문구도 목적지를 밝힌다. 표지에는
[TOC]를 넣지 않는다(수백 heading 폭발 방지). audit은 exact regeneration과 별개로
item ## 수·heading 유일성·읽기 링크 수/순서를 독립 검증한다.
- 페이지 provenance 순서. 저자의
이 노트에 대하여 abstract가 있으면
abstract → 원본·최신본 → [TOC]/본문, 없으면 provenance가 첫 본문 블록이다. 블록의
exact garden source URL은 relink하지 않는다. source date/lastmod만 페이지에 표시하고
mirror sync date는 README 책 수준에만 둔다.
- mapping의 source metadata는 cache. Denote ID가 join key다. 기존
page_id, url,
path, folder, subject를 호환 보존하고 source_url, source_date,
source_lastmod, description, tags를 garden frontmatter에서 매 build마다 다시 파생한다.
표지에 공개되는 description/tags cache에는 본문과 같은 신원 난독화를 적용한다. description이
비면(현재 1건) 요약 문단만 생략하고 abstract를 대신 복사하지 않는다. tags/lastmod가 없으면
해당 메타 segment만 생략한다.
- 재현 입력 manifest. build는 루트
BUILD-MANIFEST.json에 garden full commit SHA,
relevant source clean 여부, canonical folders/page count/book id, 선택 입력의 deterministic
content SHA256을 기록한다. manifest는 commit 대상 운영 provenance이며 WikiDocs ingest 대상이
아니다. audit은 현재 garden 입력과 exact match를 검증한다. 생성 시각은 넣지 않는다.
- pages/ 서브디렉토리 지원됨.
pages/<folder>/... 로 가든 폴더 구조를 미러한다.
- 폴더 = 챕터.
pages/<folder>/_chapter.md에 전 항목 AEO recent-first index를 생성한다.
표지 자신도 별도 원본·최신본 provenance 블록으로 대응 가든 폴더 URL로 돌아간다.
단순 링크 목록이 아니라 제목·source 날짜·태그·description·목적지 명시 링크를 싣는다.
description은 plain text를 기본으로 다루되 authored *강조*/_강조_와 중간 #태그,
기존 HTML entity는 보존한다. angle bracket의 HTML 소실과 미래의 줄 시작 Markdown block
문법만 방어하며, &를 바꿔 기존 entity를 이중 인코딩하지 않는다.
- autholog = 0순위 가상 챕터. WikiDocs에 태그 기능이 없으므로 전 canonical folder에서
frontmatter
tags에 autholog가 있는 문서만 pages/autholog/_chapter.md에 모은다.
원본/본문을 복제하지 않고 폴더 표지와 같은 AEO 항목 renderer로 기존 미러 URL을 잇는다.
정렬은 폴더와 무관하게 lastmod 내림차순(date fallback, 같은 시각은 Denote ID
내림차순)이다. standalone 표지는
<!-- collection:autholog -->로 회수하며 _chapters.autholog에 page_id를 보관한다.
TOC와 사용자 스크립트 탐색면에서는 authored folder보다 앞선 0 어쏠로그로 둔다.
첫 생성 때는 audit --allow-missing-page-ids → push/status → recover/relink → audit의 신규 ID
2단계가 필요하다. 그 전까지 README의 태그 링크는 가든 URL fallback이 정상이다.
--core 에서도 발행한다. autholog 는 코어 정의(autholog 태그 ∪ botlog 폴더)의
절반인데 botlog 만 5 봇로그 표지로 커버되고 있었다. 이 집합면은 항목이 전부 발행면
안이라 링크 이탈이 0인 유일한 탐색면이다(2026-07 실측: 어쏠로그 170/170·봇로그 80/80 이
위키독스 내부, 노트 163/837, 메타 1/538, 저널·참고문헌 0). 발행 비용은 TOC 1칸이다.
- 사이드바 사용자 스크립트는 웹훅 밖에 있다.
wikidocs-user-script.js 는 위키독스 책
설정(책 수정 > 사용자 스타일/스크립트)에 손으로 붙여넣는다. GitHub 동기화가 건드리지
않으므로 리포와 조용히 어긋날 수 있어, audit 이 CH 배열을 TOC 챕터 목록·순서와
mapping._chapters 의 page_id 에 대조한다(목록/순서 불일치·page_id 불일치·미회수인데
숫자 선언="출처 없는 ID"=오류, 회수했는데 null=경고, 파일 자체가 없으면 경고). 챕터를
더하거나 표지를 재생성하면 CH 를 고치고 위키독스에 다시 붙여넣어야 반영된다.
- 본문 맨 위 H1 없음, frontmatter 없음. 제목은 TOC 가 관리.
- 위키독스는 인제스트 때 이미지를 자기 CDN 으로 재업로드·URL 재작성한다. 로컬
 → 라이브 .
텍스트·줄수·이미지 개수/위치는 보존되고 URL 만 바뀐다. 라이브 vs 로컬 본문 대조(status.py)
는 이미지 URL 을  로 중립화해야 정확하다 — 안 하면 이미지 있는 페이지가 영영
pending 오탐으로 잡힌다.
- 회사/직장 신원 난독화 필수.
scrub_identity 가 가든 change-text.sh 의 치환 규칙을
런타임에 읽어 본문뿐 아니라 AEO 표지의 title/description/tags 공개값에도 적용한다.
민감어를 이 스크립트나 문서에 하드코딩하지 않는다(그 자체가
pre-commit 훅에 걸린다). change-text.sh 가 어떤 핸들의 특정 번호 변형만 다루는 경우, build.py
가 그 베이스를 전 변형(숫자 0개 이상)으로 일반화해 훅이 막는 모든 형태를 덮는다.
변환 매핑
| 가든 (Quartz/Hugo) | 위키독스 | 함수 |
|---|
frontmatter title/date/lastmod | journal=date, 그 외=lastmod→date의 <날짜8> <제목> + recent-first TOC | source_sort_timestamp/subject_for |
frontmatter title/description/date/lastmod/tags | 챕터 표지의 ## 제목 + 메타 + plain-text 요약 + 목적지 링크 | public_index_metadata/index_item_blocks |
| 본문 페이지의 frontmatter | 제거 | split_frontmatter |
## 제목 {#anchor} | ## 제목 | HEAD_ANCHOR |
<span class="timestamp-wrapper">…[날짜]…</span> | [날짜] | TIMESTAMP |
> [!type] 제목 callout 11종+ | [[TIP("라벨")]]…[[/TIP]] | convert_callouts |
<div class="csl-entry">·<a href> citeproc | - 참고문헌 마크다운 목록/링크 | convert_html |
[텍스트]({{< relref "/x/y.md" >}}) | 가든 절대URL(씨뿌리기) → page_id URL(relink) | relref_repl |
{{< figure src=… >}} |  → assets 복사 | figure_repl |
 |  + assets 복사 | make_images |
코드펜스 ```/ ```` | 원형 보존(3+ backtick 개수 매칭, 줄앵커) | protect_code |
| 회사/직장 신원 | change-text.sh 규칙으로 난독화 | scrub_identity |
위키독스 확장문법 착지점
[[TIP]]/[[TIP("라벨")]]…[[/TIP]], [TOC], [[MARK]]/[[SMARK]].
[[SubPages]]는 WikiDocs가 지원하지만 generator는 안정적인 explicit recent-first index를 위해
의도적으로 사용하지 않는다.
배포·동기화
push 하면 웹훅이 README.md(책 대문)·TOC.md·pages/·assets/ 를 읽어 동기화
(.claude/·AGENTS.md·NEXT.md·mapping.json·BUILD-MANIFEST.json 은 무시).
README.md 는 가든
content/index.md 를 변환한 책 대문이므로 index 가 바뀌면 build 가 재생성한다. 안 보이면:
push 반영 확인 → GitHub Settings > Webhooks → 위키독스 책 수정 > 깃허브 연결/웹훅 →
지금 동기화. 103페이지 동기화에 ~70초. 갱신 때는 기존 page_id를 승계하므로
build → relink → audit → push로 기존 페이지를 한 번에 복구할 수 있다. 새 페이지가 있으면
동기화 뒤 recover → relink → audit → push를 한 번 더 수행한다.
대량(2천여 페이지) push 는 웹훅이 한 번에 다 안 돈다. 서버측에서 나눠 처리되거나 중간에
멈춰, 반영이 부분적으로 끝나고 지금 동기화 수동 재트리거가 몇 번 필요할 수 있다. push 후
status.py --list 로 synced/pending 을 재서 pending 이 0 이 될 때까지 확인한다(멈춰 있으면
수동 재트리거). status.py 는 커밋 범위에 안 묶이고 라이브 vs 현재 리포를 비교하므로 언제