feat(viewer): md 본문 외부 링크 새 탭 + rel 보안

문서 본문 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) <noreply@anthropic.com>
This commit is contained in:
hyungi
2026-06-15 15:07:39 +09:00
parent 4927c585c7
commit 60f3b259df
@@ -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<HTMLAnchorElement>('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';
}
});
</script>
<div class="mb-2">