Giscus 댓글의 commentId 기반 매칭

변경 요약

  • Giscus 매핑을 mapping: 'specific'로 사용 시 각 문서의 Frontmatter commentId를 강제하여, 문서 이동/제목 변경과 무관하게 동일한 토론 스레드를 유지.
  • SPA 내비게이션 시 기존 giscus iframe/script를 정리하고, data-term이 없으면 위젯 삽입을 건너뛰도록 안전장치 추가.
  • 레이아웃에서 commentId가 없는 문서는 댓글 영역 자체를 렌더링하지 않도록 가드.

변경된 파일

  • quartz/components/Comments.tsx
  • quartz/components/scripts/comments.inline.ts
  • (레이아웃 사용) quartz.layout.ts

주요 구현 포인트 및 실제 변경 코드

1) quartz/components/Comments.tsx: commentId 강제 및 data-term 전달

const mapping = opts.options.mapping ?? "url"
const commentId = fileData.frontmatter?.commentId as string | undefined
 
if (mapping === "specific" && !commentId) {
  const identifier = fileData.filePath ?? fileData.slug ?? "unknown"
  console.warn(`[Quartz] Missing commentId for page ${identifier}, skipping giscus mounting.`)
  return <></>
}
 
return (
  <div
    class={classNames(displayClass, "giscus")}
    ...
    data-mapping={mapping}
    data-term={mapping === "specific" ? commentId : undefined}
    ...
  ></div>
)

핵심:

  • mapping === 'specific'인데 commentId가 없으면 경고를 남기고 컴포넌트를 렌더링하지 않음.
  • 유효한 경우에만 data-term으로 commentId를 giscus에 전달.

2) quartz/components/scripts/comments.inline.ts: SPA 재장착 + 불완전 매핑 차단

document.addEventListener("nav", () => {
  const giscusContainer = document.querySelector(".giscus") as GiscusElement
  if (!giscusContainer) return
 
  giscusContainer
    .querySelectorAll("iframe.giscus-frame, script[src*='giscus.app']")
    .forEach((node) => node.remove())
 
  if (giscusContainer.dataset.mapping === "specific" && !giscusContainer.dataset.term) {
    console.warn("[Giscus] mapping='specific' but data-term is missing; skipping widget injection.")
    return
  }
 
  const giscusScript = document.createElement("script")
  giscusScript.src = "https://giscus.app/client.js"
  ...
  giscusScript.setAttribute("data-mapping", giscusContainer.dataset.mapping)
  if (giscusContainer.dataset.term) {
    giscusScript.setAttribute("data-term", giscusContainer.dataset.term)
  }
  ...
  giscusContainer.appendChild(giscusScript)
})

핵심:

  • 내비게이션 시 기존 giscus 리소스를 정리 후 재삽입.
  • mapping='specific'인데 data-term이 비어 있으면 위젯 삽입을 건너뛰어 잘못된 스레드 연결을 방지.

3) quartz.layout.ts: 실제 적용 설정값 전체 + 렌더 가드

Component.ConditionalRender({
  component: Component.Comments({
    provider: "giscus",
    options: {
      repo: "<owner>/<repo>",
      repoId: "<repoId>",
      category: "<discussionCategoryName>",
      categoryId: "<discussionCategoryId>",
      mapping: "specific",
      strict: true,
      reactionsEnabled: true,
      inputPosition: "top",
      lang: "en",
      lightTheme: "light",
      darkTheme: "dark",
      // themeUrl은 미지정 → cfg.baseUrl 기준으로 자동 계산됨
    },
  }),
  condition: ({ fileData }) => Boolean(fileData.frontmatter?.commentId),
})

참고:

  • themeUrl은 생략했으며, 런타임에서 cfg.baseUrl을 기준으로 https://<baseUrl>/static/giscus로 계산됩니다. 예: baseUrl: "example.com"themeUrl = https://example.com/static/giscus.
  • 문서 Frontmatter에 commentId가 있을 때만 댓글 컴포넌트를 렌더링합니다.
  • giscus는 mapping: 'specific'로 고정하여 스레드 식별을 commentId로 일원화합니다.

왜 변경했는가

  • 문서 경로/제목 변경으로 기존 댓글이 분리되는 문제를 방지하고, 하나의 영구 식별자(commentId)로 스레드를 유지하려는 목적.
  • SPA 내비게이션 환경에서 위젯 중복 삽입/테마 불일치 문제를 해소.

운영 체크리스트

  • 공개 문서는 모두 고유 commentId를 가져야 하며, Vault 측 템플러(Templater)로 자동 발급 및 중복 검증을 수행.
  • 프리뷰에서 라이트/다크 테마 전환 시 giscus 테마가 동기화되는지 확인.

관련 커밋

  • 820413d feat(Giscus): persist discussions via commentId

commentId 자동 부여 템플러(Templater)

Obsidian Templater를 이용해 blog/ 이하의 공개 문서(frontmatter publish: true)에 commentId를 자동으로 채우고, 중복을 감지합니다. 기본적으로는 무소음으로 동작하며, 중복이 발견된 경우에만 경고를 출력합니다.

필수: .obsidian/plugins/templater/user_scripts/uuid.js (아래 참조)

동작 요약(댓글 ID 관련)

  • 공개 문서에서 commentId가 비어 있으면 UUID를 생성해 채움.
  • commentId가 숫자 또는 배열인 경우 문자열로 정규화.
  • 처리 이후 모든 파일의 최종 commentId를 수집하여 중복 그룹이 있으면 경고(본문 출력 + Obsidian Notice) 표시.

핵심 코드 발췌

// commentId 필요 여부와 신규 ID 생성
const needsId = !fmCache.commentId || String(fmCache.commentId).trim() === ""
const newId = needsId ? await tp.user.uuid() : null
 
await app.fileManager.processFrontMatter(file, (fm) => {
  // commentId 보강/정규화
  if (needsId) fm.commentId = newId
  else if (typeof fm.commentId !== "string") fm.commentId = String(fm.commentId)
})
 
// 최종 commentId 추적(중복 검출용)
const finalId = (needsId ? newId : String(fmCache.commentId ?? "")).trim()
if (finalId) {
  if (!idGroups.has(finalId)) idGroups.set(finalId, [])
  idGroups.get(finalId).push(file.path)
}
 
// 중복 commentId만 경고 출력 + Notice
const dups = [...idGroups.entries()].filter(([_, list]) => list.length > 1)
if (dups.length) {
  tR += `⚠️ Duplicate commentId detected (${dups.length} group${dups.length>1?'s':''}):\n` +
    dups.map(([id, list]) => `- ${id}\n  - ${list.join('\n  - ')}`).join('\n')
  try { new Notice(`giscus: duplicate commentId ${dups.length} group(s) found`) } catch(_) {}
}

UUID 유저 스크립트(.obsidian/plugins/templater/user_scripts/uuid.js)

const { randomUUID } = require('crypto')
 
module.exports = async function uuid() {
  return randomUUID()
}

이 템플러로 commentId가 항상 존재하도록 보장하면, 위의 Quartz 변경사항(mapping: 'specific' + 렌더 가드)과 결합되어 문서 이동/제목 변경에도 giscus 스레드가 안정적으로 영구 매칭됩니다.