Skip to content

Commit f768881

Browse files
committed
feat(mirror): link canonical notes to WikiDocs
1 parent aa36a53 commit f768881

10 files changed

Lines changed: 2646 additions & 5 deletions

File tree

AGENTS.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Junghan Kim = GLG (힣) = GLGMAN (힣맨) = the junghanacs gardener. Use this eq
5252
| Repo | Purpose |
5353
|------|---------|
5454
| [garden_v5](https://github.com/junghan0611/garden_v5) | Quartz **v5** rebuild workspace (branch `main`) — separate from this live garden; do not merge or port |
55+
| [garden2wikidocs](https://github.com/junghan0611/garden2wikidocs) | Read-only publication translator from this canonical garden to the Korean-language WikiDocs discovery mirror |
5556
| [notes.junghanacs.com](https://github.com/junghanacs/notes.junghanacs.com) | **Retired source** — read-only history on `v4`, keeps old `blob/v4/…` permalinks alive, rollback target |
5657
| [GLG-Mono](https://github.com/junghan0611/GLG-Mono) | The garden's single typeface (header/title/body/code). Web subsetting SSOT: `docs/WEBFONT_SUBSET.md` |
5758
| [doomemacs-config](https://github.com/junghan0611/doomemacs-config) | Doom Emacs config — denote-export.sh, agent-server.el |
@@ -205,11 +206,29 @@ IndexNow: `scripts/post-build.sh` appends a "Recent Updates" block to `public/ll
205206
`quartz/components/Head.tsx` emits a single `@graph` with stable `@id`s so crawlers and LLMs merge node properties across pages. Restricted to the homepage and Denote-ID pages (tags/folder-index/404 excluded).
206207

207208
- **Person** (`@id …/#person`) on every page — `name`, `alternateName: [GLG, GLGMAN]`, `jobTitle`, `description`, `knowsAbout`, `knowsLanguage`, `sameAs` (GitHub, LinkedIn, Bluesky, Mastodon, homepage), `image`.
208-
- **WebSite** (`#website`), **ProfilePage** (homepage only, `mainEntity → #person`, `primaryImageOfPage` as `ImageObject`), **BlogPosting** (notes, `author/publisher → #person`, `isBasedOn → GitHub source`).
209+
- **WebSite** (`#website`), **ProfilePage** (homepage only, `mainEntity → #person`, `primaryImageOfPage` as `ImageObject`), and per-section content nodes (`author/publisher → #person`, `isBasedOn → GitHub source`).
209210
- Profile photo: `quartz/static/profile.jpg` (640×640 grayscale) → served at `/static/profile.jpg`. Separate channel from `og:image` (`/static/og-image.png`); the face photo never overrides link-preview cards.
210211
- **Standard schema.org only.** No invented properties (e.g. `temporalStatus`) — crawlers drop non-standard terms. Synthesize at build time; never mutate the org source. No content `license` field until a content licence (CC-*) is actually chosen — `LICENSE.txt` is Quartz's MIT, not a content licence.
211212
- Next AEO lever (not yet shipped): `#+reference:` citekeys → frontmatter `refs[]` → schema.org `citation` (org-export side) so bib notes expose what primary sources they engage — the "footprints, not facade" signal.
212213

214+
## WikiDocs discovery mirror
215+
216+
The complete cross-repository contract is [`docs/WIKIDOCS_MIRROR.md`](docs/WIKIDOCS_MIRROR.md).
217+
This garden is the canonical, latest, and authored source; WikiDocs is a Korean-language discovery and
218+
reading mirror, and `garden2wikidocs` is the read-only translation harness.
219+
220+
- Never modify Org or exported content Markdown for mirror plumbing. Quartz synthesizes mapped `sameAs` data
221+
and the visible mirror link from `quartz/data/wikidocs-mirror.json`.
222+
- `garden2wikidocs/mapping.json` owns WikiDocs `page_id`/URL facts. The garden owns the explicit import gate
223+
`scripts/sync-wikidocs-map.mjs` and the committed minimal snapshot; Netlify never reads a sibling checkout
224+
or fetches the mapping over the network.
225+
- `garden2wikidocs` never writes into this repository. It reports a recovered stable mapping, then the garden
226+
owner decides when to import it.
227+
- An unmapped new garden note is valid and must remain deployable. Mirror lag or failure never blocks the
228+
canonical publication path.
229+
- Content `sameAs` points to the corresponding rendering; it is not an HTML canonical directive. Preserve the
230+
uppercase-`T`/no-per-page-canonical invariant below.
231+
213232
## Build & URL invariants
214233

215234
Denote IDs and all built URLs use **uppercase `T`** (`20250727T094722`). The lone lowercase outlier is `sitemap.xml <loc>` (explicit `.toLowerCase()` in `quartz/plugins/emitters/contentIndex.tsx`). Netlify 301s uppercase→lowercase and serves lowercase `200`; `remark42.inline.ts` restores uppercase at runtime for comments. There is **no per-page `<link rel="canonical">`** — a deliberate decision: adding one would conflict the uppercase-HTML / lowercase-response / lowercase-sitemap signals (Quartz upstream refuses canonical for the same reason). Don't "fix" this without a canary confirming Netlify can preserve uppercase.

content/llms.txt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@
1010
- Robots: https://notes.junghanacs.com/robots.txt
1111
- RSS: https://notes.junghanacs.com/index.xml
1212

13+
## Canonical garden and WikiDocs discovery mirror
14+
15+
- Canonical and latest garden: https://notes.junghanacs.com/
16+
- Canonical source repository: https://github.com/junghan0611/garden
17+
- Korean-language discovery mirror: https://wikidocs.net/book/20676
18+
- Mirror translation harness: https://github.com/junghan0611/garden2wikidocs
19+
20+
The garden is the authored, canonical, and latest source.
21+
WikiDocs is a generated mirror for Korean-language search discovery and reading; it may appear first in search results but does not replace the garden.
22+
For current content, temporal history, backlinks, tags, comments, and source provenance, prefer the corresponding `notes.junghanacs.com` page.
23+
Mapped garden notes expose their stable WikiDocs rendering through schema.org `sameAs`, while each mirror page links back to its exact original and latest garden note.
24+
1325
## About
1426

1527
This site is the digital garden of Junghan Kim (김정한) — also identified in schema.org JSON-LD as GLG, GLGMAN, 힣, 힣맨, and 정한.
@@ -57,8 +69,10 @@ Canonical definitions live in [VOCABULARY.md](https://github.com/junghan0611/jun
5769

5870
## Connected Repositories
5971

60-
- **garden**: This garden's source (Quartz 4 + Markdown content)
72+
- **garden**: This garden's canonical source (Quartz 4 + Markdown content)
6173
- https://github.com/junghan0611/garden
74+
- **garden2wikidocs**: Read-only translation harness for the Korean-language WikiDocs discovery mirror
75+
- https://github.com/junghan0611/garden2wikidocs
6276
- **junghan0611**: Profile, résumé, identity-document set and vocabulary
6377
- https://github.com/junghan0611/junghan0611
6478
- **entwurf**: Garden-citizen dispatch substrate — v2 delivery, meta-bridge, ACP plugin, and pi adapter

docs/WIKIDOCS_MIRROR.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# WikiDocs mirror contract
2+
3+
This document is the technical contract between
4+
[`junghan0611/garden`](https://github.com/junghan0611/garden) and
5+
[`junghan0611/garden2wikidocs`](https://github.com/junghan0611/garden2wikidocs).
6+
The public rationale and project timeline live in Denote note
7+
`20260220T201100` (`§garden2wikidocs — 원본 가든을 건드리지 않는 위키독스 미러`).
8+
9+
## Center
10+
11+
> The garden is the canonical, latest, and authored source.
12+
> WikiDocs is a Korean-language discovery and reading mirror.
13+
> `garden2wikidocs` is the translation harness between them.
14+
15+
WikiDocs may appear before the garden in Korean search results. That is an intended
16+
property of the discovery surface, not a transfer of authorship or source authority.
17+
Readers arriving there must have an exact route back to the garden, where backlinks,
18+
tags, history, comments, source links, and the latest revision live.
19+
20+
## Field ownership
21+
22+
| Field | Authority |
23+
|---|---|
24+
| Body, title, description, section | garden Markdown exported from Org |
25+
| Denote ID, source URL | garden |
26+
| `date`, `lastmod` | garden frontmatter |
27+
| WikiDocs `page_id`, mirror URL | `garden2wikidocs/mapping.json` |
28+
| Mirror synchronization status | `garden2wikidocs` / WikiDocs |
29+
| Canonical-versus-mirror policy | this garden |
30+
31+
The Denote ID is the join key. Source metadata copied into `mapping.json` remains a
32+
cache of garden facts; it does not become their authority.
33+
34+
## Data flow
35+
36+
```text
37+
Org → exported garden Markdown → independent garden deploy
38+
↓ read-only
39+
garden2wikidocs build
40+
41+
WikiDocs page_id recovery
42+
43+
garden2wikidocs/mapping.json
44+
↓ explicit owner-run import
45+
garden/quartz/data/wikidocs-mirror.json
46+
47+
Quartz JSON-LD + visible mirror link
48+
```
49+
50+
The garden's Netlify build never reads a sibling checkout and never fetches a remote
51+
mapping over the network. `scripts/sync-wikidocs-map.mjs` is an explicit import gate:
52+
it validates the mirror ledger and commits only the minimal Denote-ID-to-URL snapshot
53+
needed by Quartz.
54+
55+
`garden2wikidocs` must not write into the garden repository. It updates its own ledger
56+
and reports that a new stable `page_id` is ready; the garden owner decides when to
57+
import the new snapshot.
58+
59+
## Publication ordering
60+
61+
1. Export and deploy the garden independently.
62+
2. Let `garden2wikidocs` read the public garden files and update the WikiDocs mirror.
63+
3. Recover new WikiDocs `page_id` values and audit the mirror.
64+
4. Explicitly import the refreshed mapping snapshot into the garden.
65+
5. On the next garden deploy, emit the corresponding JSON-LD and visible mirror links.
66+
67+
A new garden note may temporarily have no WikiDocs mapping. That is normal and must
68+
not fail a garden build. A mirror outage or delayed `page_id` recovery must never block
69+
the canonical publication path.
70+
71+
## Garden output
72+
73+
For a mapped Denote page, Quartz adds the stable WikiDocs URL to the page's
74+
CreativeWork JSON-LD as `sameAs` and shows a compact `WikiDocs mirror` link in content
75+
metadata. Both are synthesized at build time; Org and exported content Markdown are not
76+
modified for mirror plumbing.
77+
78+
`sameAs` records a relationship between two renderings of the work. It is not an HTML
79+
canonical directive and does not grant WikiDocs authority over source metadata.
80+
81+
The garden's existing uppercase-`T` URL behavior deliberately has no per-page
82+
`<link rel="canonical">`. This mirror integration does not change that invariant.
83+
84+
## Mirror output
85+
86+
Each generated WikiDocs page must contain a separate `원본·최신본` provenance block.
87+
It is system metadata, not part of the author's `이 노트에 대하여` abstract.
88+
89+
- If an abstract exists: abstract → provenance block → body.
90+
- If no abstract exists: provenance block → body.
91+
- The block links to the exact garden URL and identifies the garden as original/latest.
92+
- It directs readers to the garden for backlinks, tags, history, comments, and updates.
93+
- Creation and modification dates come only from garden frontmatter.
94+
- Journal title dates and recent-first ordering use `source_date` (the week/date represented by the journal).
95+
- Meta, bibliography, notes, and botlog title dates and recent-first ordering use `source_lastmod`,
96+
falling back to `source_date` only when `lastmod` is absent.
97+
- Mirror build, Git commit, file mtime, and sync time must never masquerade as either source date.
98+
- WikiDocs may force its sidebar into ascending title order. Preserve stable titles and `page_id` values;
99+
provide an explicit recent-first chapter index rather than renumbering every page to fight the sidebar.
100+
- Mirror sync time belongs at book level, not repeated as a content date on every page.
101+
102+
## Validation contract
103+
104+
### Garden
105+
106+
- Denote IDs and WikiDocs URLs are well formed.
107+
- Every imported WikiDocs page ID and URL is unique.
108+
- Every snapshot entry points to an existing garden Denote page.
109+
- A mapped page emits the exact `sameAs` URL and visible mirror link.
110+
- An unmapped new garden page remains valid and deployable.
111+
- Netlify requires no sibling repository or network mapping fetch.
112+
113+
### garden2wikidocs
114+
115+
- Every mirror page has exactly one exact source URL.
116+
- `gid`, source URL Denote ID, and mapping entry agree.
117+
- Journal title dates equal garden `date`; all other title dates equal garden `lastmod` with `date` fallback.
118+
- The provenance block is separate from the author's abstract.
119+
- An existing WikiDocs `page_id` remains stable across rebuilds.
120+
- Mirror-internal links use recovered WikiDocs URLs where available and retain the
121+
garden URL as fallback.
122+
123+
## Change protocol
124+
125+
This file is the cross-repository policy SSOT. A change to source authority, field
126+
ownership, publication ordering, or the provenance contract is agreed here first.
127+
The `garden2wikidocs` skill references this contract and owns implementation details of
128+
the mirror pipeline; the garden owns its importer, local snapshot, structured data, and
129+
public canonical policy.

quartz/components/ContentMeta.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { classNames } from "../util/lang"
55
import { i18n } from "../i18n"
66
import { JSX } from "preact"
77
import style from "./styles/contentMeta.scss"
8+
import { wikidocsMirrorUrl } from "../util/wikidocsMirror"
89

910
interface ContentMetaOptions {
1011
showReadingTime: boolean
@@ -61,8 +62,9 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
6162
segments.push(<span>{displayedTime}</span>)
6263
}
6364

64-
// GitHub links
65+
// Source/provenance links
6566
const showGitHubLinks = options.repoLink && options.branch && !isIndex
67+
const mirrorUrl = isIndex ? undefined : wikidocsMirrorUrl(fileData.slug)
6668

6769
return (
6870
<div class="contentmeta-container">
@@ -86,6 +88,15 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
8688
>
8789
ᨒ History ↗
8890
</a>
91+
{mirrorUrl && (
92+
<a
93+
class="github-link external"
94+
href={mirrorUrl}
95+
title="한국어 검색과 읽기를 위한 위키독스 발견 미러"
96+
>
97+
ᨒ WikiDocs mirror ↗
98+
</a>
99+
)}
89100
</p>
90101
)}
91102
<p

quartz/components/Head.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { googleFontHref, googleFontSubsetHref } from "../util/theme"
55
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
66
import { unescapeHTML } from "../util/escape"
77
import { CustomOgImagesEmitterName } from "../plugins/emitters/ogImage"
8+
import { wikidocsMirrorUrl } from "../util/wikidocsMirror"
89
export default (() => {
910
const Head: QuartzComponent = ({
1011
cfg,
@@ -205,6 +206,7 @@ export default (() => {
205206
}
206207
const contentType = typeBySection[section] ?? "BlogPosting"
207208
const crumbName = section.charAt(0).toUpperCase() + section.slice(1)
209+
const mirrorUrl = wikidocsMirrorUrl(slug)
208210

209211
const article = {
210212
// 타입이 바뀌어도 @id suffix(#article)는 고정 — 크롤러 노드 병합 안정성(v5에서 #content로 재명명).
@@ -214,6 +216,8 @@ export default (() => {
214216
"name": fm?.title ?? title,
215217
"url": socialUrl,
216218
"mainEntityOfPage": socialUrl,
219+
// 같은 저작물의 한국어 발견 미러. 검색엔진 canonical 지시가 아니다.
220+
...(mirrorUrl ? { "sameAs": mirrorUrl } : {}),
217221
"author": { "@id": `${origin}/#person` },
218222
"publisher": { "@id": `${origin}/#person` },
219223
// 가든은 blog가 아니다. 폴더별 의미는 @type이 표현하고, 모든 공개 MD 산출물은

0 commit comments

Comments
 (0)