From 60f3b259df4b1474d31f5f8abe4380f014045ba9 Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 15 Jun 2026 15:07:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(viewer):=20md=20=EB=B3=B8=EB=AC=B8=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=83=88=20=ED=83=AD?= =?UTF-8?q?=20+=20rel=20=EB=B3=B4=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문서 본문 markdown 의 외부 http(s) 링크(코퍼스 실측 521문서/9,496개)가 target/rel 없이 같은 탭으로 열려 SPA 를 이탈하던 문제 수정. MarkdownDoc 에 heading-anchor 와 동일한 DOM 후처리 $effect 추가 — sanitize 후 라이브 DOM 의 a[href^=http(s)] 에 target="_blank" + rel="noopener noreferrer" 부여. marked 렌더러/DOMPurify(전역 hook)· ADD_ATTR 무수정. 앵커(#)·상대경로·mailto 는 미변경(SPA 내부 항법 보존). 내부 위키링크([[...]])·백링크 그래프는 코퍼스 실측상 실신호 ~8개로 데이터 미지원이라 본 PR 범위에서 제외(보류, 내부 링크 증가가 트리거). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/lib/components/MarkdownDoc.svelte | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/frontend/src/lib/components/MarkdownDoc.svelte b/frontend/src/lib/components/MarkdownDoc.svelte index be83685..47d44a2 100644 --- a/frontend/src/lib/components/MarkdownDoc.svelte +++ b/frontend/src/lib/components/MarkdownDoc.svelte @@ -160,6 +160,24 @@ ph.dataset.mdImageSwapped = '1'; } }); + + // 외부 http(s) 링크 → 새 탭(target=_blank) + 보안 rel(noopener noreferrer). + // marked 렌더러/DOMPurify 를 건드리지 않고 sanitize 후 라이브 DOM 에 속성을 부여한다 — + // heading anchor 와 동일한 DOM 후처리 패턴(전역 DOMPurify hook 오염·렌더러 API 의존 없이 안전). + // 앵커(#)·상대경로·mailto 등 비-http(s) 링크는 손대지 않는다(SPA 내부 항법 보존). + $effect(() => { + void renderedHtml; + if (!containerRef) return; + const links = containerRef.querySelectorAll('a[href]'); + for (const a of links) { + if (a.dataset.extlinkProcessed === '1') continue; + const href = a.getAttribute('href') ?? ''; + if (!/^https?:\/\//i.test(href)) continue; // 외부 http(s) 만 + a.setAttribute('target', '_blank'); + a.setAttribute('rel', 'noopener noreferrer'); + a.dataset.extlinkProcessed = '1'; + } + });