+ {/* allow-popups (+ escape-sandbox): the overlay opens the doc's links in a new
+ tab rather than navigating this frame, which X-Frame-Options sites refuse. */}
diff --git a/app/d/[slug]/raw/route.ts b/app/d/[slug]/raw/route.ts
index 048478c..ace92f2 100644
--- a/app/d/[slug]/raw/route.ts
+++ b/app/d/[slug]/raw/route.ts
@@ -15,10 +15,13 @@ type Ctx = { params: Promise<{ slug: string }> };
// can never execute same-origin with our auth/session surface (birthday.md "The
// one security decision that matters").
//
-// Content-Security-Policy: sandbox allow-scripts allow-downloads → the response
-// is treated as a unique opaque origin; scripts run and user-initiated file
-// downloads work, but the document cannot read our cookies / tokens or reach
-// our origin's storage.
+// Content-Security-Policy: sandbox allow-scripts allow-downloads allow-popups
+// allow-popups-to-escape-sandbox → the response is treated as a unique opaque
+// origin; scripts run, user-initiated file downloads work, and the doc's links
+// can open in a new tab (the overlay routes them there — navigating the frame
+// itself breaks on every X-Frame-Options site), but the document cannot read
+// our cookies / tokens or reach our origin's storage. The popup escapes the
+// sandbox so the linked site loads as a normal page, never as our origin.
// X-Content-Type-Options: nosniff → no MIME sniffing.
//
// Directly linkable for zero-chrome viewing; same token rules as /d/:slug.
@@ -98,7 +101,8 @@ export async function GET(req: Request, ctx: Ctx): Promise
{
status: 200,
headers: {
"Content-Type": "text/html; charset=utf-8",
- "Content-Security-Policy": "sandbox allow-scripts allow-downloads",
+ "Content-Security-Policy":
+ "sandbox allow-scripts allow-downloads allow-popups allow-popups-to-escape-sandbox",
"X-Content-Type-Options": "nosniff",
// Never cache private content at shared caches; tokens are capability URLs.
"Cache-Control": "private, no-store",
diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts
index 4615122..377fd2b 100644
--- a/lib/docs/overlay.ts
+++ b/lib/docs/overlay.ts
@@ -709,6 +709,37 @@ export const OVERLAY_SCRIPT = String.raw`
send({type:"jh:focus", key:null, id:null, keys:[]});
}
+ // ---- links leave the frame, never navigate it ----
+ // The doc renders in an opaque-origin sandbox inside the shell's iframe, so a
+ // plain click navigates the FRAME, and any site sending
+ // X-Frame-Options / frame-ancestors (github.com, most of the web) then paints
+ // "refused to connect" where the doc was. The sandbox also can't reach the top
+ // window, and target="_blank" alone is a blocked popup — so we open the tab
+ // ourselves from inside the user gesture (sandbox carries allow-popups +
+ // allow-popups-to-escape-sandbox so the target loads as a normal page).
+ // Same-document fragments (#section, the gutter section links) stay in-frame.
+ function externalHref(a){
+ var raw = a.getAttribute("href");
+ if (!raw || raw.charAt(0) === "#") return null;
+ var u;
+ try { u = new URL(a.href, document.baseURI); } catch(e){ return null; }
+ if (u.protocol !== "http:" && u.protocol !== "https:") return null;
+ if (u.href.split("#")[0] === location.href.split("#")[0]) return null;
+ return u.href;
+ }
+ function openLink(ev){
+ if (editing || ev.defaultPrevented) return;
+ if (ev.type === "auxclick" && ev.button !== 1) return; // middle-click only
+ var t = ev.target;
+ var a = t && t.closest && t.closest("a[href]");
+ var href = a && externalHref(a);
+ if (!href) return;
+ ev.preventDefault();
+ window.open(href, "_blank", "noopener");
+ }
+ document.addEventListener("click", openLink);
+ document.addEventListener("auxclick", openLink);
+
// click-elsewhere (on non-highlight) clears focus + selection popovers
document.addEventListener("click", function(ev){
var t = ev.target;
From 44cca0364afab7f05074d44b546a39e223b01d82 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Tue, 28 Jul 2026 19:25:53 +0000
Subject: [PATCH 2/8] Intercept doc link clicks in the capture phase so
highlighted links open too
---
lib/docs/overlay.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts
index 377fd2b..80908ca 100644
--- a/lib/docs/overlay.ts
+++ b/lib/docs/overlay.ts
@@ -727,8 +727,10 @@ export const OVERLAY_SCRIPT = String.raw`
if (u.href.split("#")[0] === location.href.split("#")[0]) return null;
return u.href;
}
+ // Capture phase: a link inside a highlighted span would otherwise be swallowed
+ // by the segment's own click handler (it stopPropagation()s) and navigate.
function openLink(ev){
- if (editing || ev.defaultPrevented) return;
+ if (editing) return;
if (ev.type === "auxclick" && ev.button !== 1) return; // middle-click only
var t = ev.target;
var a = t && t.closest && t.closest("a[href]");
@@ -737,8 +739,8 @@ export const OVERLAY_SCRIPT = String.raw`
ev.preventDefault();
window.open(href, "_blank", "noopener");
}
- document.addEventListener("click", openLink);
- document.addEventListener("auxclick", openLink);
+ document.addEventListener("click", openLink, true);
+ document.addEventListener("auxclick", openLink, true);
// click-elsewhere (on non-highlight) clears focus + selection popovers
document.addEventListener("click", function(ev){
From 4f00dcdbf22a1a215b66f16c8eb18c524a9d6717 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Tue, 28 Jul 2026 19:46:17 +0000
Subject: [PATCH 3/8] Keep overlay chrome clicks out of the link interceptor
---
lib/docs/overlay.ts | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts
index 80908ca..ea782e6 100644
--- a/lib/docs/overlay.ts
+++ b/lib/docs/overlay.ts
@@ -733,10 +733,24 @@ export const OVERLAY_SCRIPT = String.raw`
if (editing) return;
if (ev.type === "auxclick" && ev.button !== 1) return; // middle-click only
var t = ev.target;
- var a = t && t.closest && t.closest("a[href]");
+ if (!t || !t.closest) return;
+ // Our own chrome can sit INSIDE an author link: a reaction chip is appended
+ // after the segment that ends its span, so a span ending mid-link puts the
+ // chip in the anchor. Those clicks are ours — swallow the anchor's navigation
+ // (the chip handler stops propagation but not the default action), and open
+ // no tab.
+ if (t.closest("[data-jh-chip]") || t.closest(".jh-pop")){
+ if (t.closest("a[href]")) ev.preventDefault();
+ return;
+ }
+ var a = t.closest("a[href]");
var href = a && externalHref(a);
if (!href) return;
+ // Following a link is navigation, not an annotation click: keep the segment
+ // handler from also focusing the rail / opening the anchor picker.
ev.preventDefault();
+ ev.stopPropagation();
+ hidePop();
window.open(href, "_blank", "noopener");
}
document.addEventListener("click", openLink, true);
From 9437ed81058a607b1fa7e21068d38e51a179a770 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Tue, 28 Jul 2026 19:55:24 +0000
Subject: [PATCH 4/8] Leave a live text selection alone instead of following
the link
---
lib/docs/overlay.ts | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts
index ea782e6..2783608 100644
--- a/lib/docs/overlay.ts
+++ b/lib/docs/overlay.ts
@@ -739,13 +739,21 @@ export const OVERLAY_SCRIPT = String.raw`
// chip in the anchor. Those clicks are ours — swallow the anchor's navigation
// (the chip handler stops propagation but not the default action), and open
// no tab.
- if (t.closest("[data-jh-chip]") || t.closest(".jh-pop")){
+ if (fromOverlayChrome(ev)){
if (t.closest("a[href]")) ev.preventDefault();
return;
}
var a = t.closest("a[href]");
var href = a && externalHref(a);
if (!href) return;
+ // A selection that survives the click is a comment/react gesture (a drag that
+ // ended in linked text), not navigation: keep the selection and stay put.
+ // Same emptiness test as reportSelection.
+ var sel = window.getSelection();
+ if (sel && sel.rangeCount && !sel.isCollapsed && sel.toString().trim()){
+ ev.preventDefault();
+ return;
+ }
// Following a link is navigation, not an annotation click: keep the segment
// handler from also focusing the rail / opening the anchor picker.
ev.preventDefault();
From c4f7c59c29c506ec3ec54ce238cc1e712c6f52a3 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Tue, 28 Jul 2026 20:05:06 +0000
Subject: [PATCH 5/8] Treat links as text on middle-click in edit mode too
---
lib/docs/overlay.ts | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts
index 2783608..a3f1951 100644
--- a/lib/docs/overlay.ts
+++ b/lib/docs/overlay.ts
@@ -730,21 +730,21 @@ export const OVERLAY_SCRIPT = String.raw`
// Capture phase: a link inside a highlighted span would otherwise be swallowed
// by the segment's own click handler (it stopPropagation()s) and navigate.
function openLink(ev){
- if (editing) return;
if (ev.type === "auxclick" && ev.button !== 1) return; // middle-click only
var t = ev.target;
if (!t || !t.closest) return;
+ var a = t.closest("a[href]");
+ if (!a) return;
+ // In edit mode a link is text to retype. The edit-mode handler already eats
+ // the click; this also covers middle-click, which would open a tab.
+ if (editing){ ev.preventDefault(); return; }
// Our own chrome can sit INSIDE an author link: a reaction chip is appended
// after the segment that ends its span, so a span ending mid-link puts the
- // chip in the anchor. Those clicks are ours — swallow the anchor's navigation
+ // chip in the anchor. That click is ours — swallow the anchor's navigation
// (the chip handler stops propagation but not the default action), and open
// no tab.
- if (fromOverlayChrome(ev)){
- if (t.closest("a[href]")) ev.preventDefault();
- return;
- }
- var a = t.closest("a[href]");
- var href = a && externalHref(a);
+ if (fromOverlayChrome(ev)){ ev.preventDefault(); return; }
+ var href = externalHref(a);
if (!href) return;
// A selection that survives the click is a comment/react gesture (a drag that
// ended in linked text), not navigation: keep the selection and stay put.
From 3304cb0d742b55da8c1b608333f6c9978aacb885 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Tue, 28 Jul 2026 20:15:36 +0000
Subject: [PATCH 6/8] Stop link clicks from reaching the segment handler when a
selection survives
---
lib/docs/overlay.ts | 17 +++++++----------
1 file changed, 7 insertions(+), 10 deletions(-)
diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts
index a3f1951..704ba7f 100644
--- a/lib/docs/overlay.ts
+++ b/lib/docs/overlay.ts
@@ -746,19 +746,16 @@ export const OVERLAY_SCRIPT = String.raw`
if (fromOverlayChrome(ev)){ ev.preventDefault(); return; }
var href = externalHref(a);
if (!href) return;
- // A selection that survives the click is a comment/react gesture (a drag that
- // ended in linked text), not navigation: keep the selection and stay put.
- // Same emptiness test as reportSelection.
- var sel = window.getSelection();
- if (sel && sel.rangeCount && !sel.isCollapsed && sel.toString().trim()){
- ev.preventDefault();
- return;
- }
- // Following a link is navigation, not an annotation click: keep the segment
- // handler from also focusing the rail / opening the anchor picker.
+ // A click on a link is navigation, not an annotation click: the frame must not
+ // move, and the segment handler must not focus the rail / open the anchor picker.
ev.preventDefault();
ev.stopPropagation();
hidePop();
+ // …but a selection that survives the click is a comment/react gesture (a drag
+ // that ended in linked text), so keep the selection and open nothing. Same
+ // emptiness test as reportSelection.
+ var sel = window.getSelection();
+ if (sel && sel.rangeCount && !sel.isCollapsed && sel.toString().trim()) return;
window.open(href, "_blank", "noopener");
}
document.addEventListener("click", openLink, true);
From afc6ffeadee66d7ebf8dd08bec4ec3b111aaa86b Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Tue, 28 Jul 2026 20:48:12 +0000
Subject: [PATCH 7/8] Clear pinned focus on link clicks and resolve fragments
against the doc base
---
lib/docs/overlay.ts | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts
index 704ba7f..d44189e 100644
--- a/lib/docs/overlay.ts
+++ b/lib/docs/overlay.ts
@@ -719,8 +719,6 @@ export const OVERLAY_SCRIPT = String.raw`
// allow-popups-to-escape-sandbox so the target loads as a normal page).
// Same-document fragments (#section, the gutter section links) stay in-frame.
function externalHref(a){
- var raw = a.getAttribute("href");
- if (!raw || raw.charAt(0) === "#") return null;
var u;
try { u = new URL(a.href, document.baseURI); } catch(e){ return null; }
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
@@ -740,17 +738,20 @@ export const OVERLAY_SCRIPT = String.raw`
if (editing){ ev.preventDefault(); return; }
// Our own chrome can sit INSIDE an author link: a reaction chip is appended
// after the segment that ends its span, so a span ending mid-link puts the
- // chip in the anchor. That click is ours — swallow the anchor's navigation
- // (the chip handler stops propagation but not the default action), and open
- // no tab.
- if (fromOverlayChrome(ev)){ ev.preventDefault(); return; }
+ // chip in the anchor, and the section anchor is a link of ours. Those clicks
+ // are ours — swallow the anchor's navigation (the chip handler stops
+ // propagation but not the default action), and open no tab.
+ if (fromOverlayChrome(ev) || t.closest("[data-jh-sec-anchor]")){ ev.preventDefault(); return; }
var href = externalHref(a);
if (!href) return;
// A click on a link is navigation, not an annotation click: the frame must not
// move, and the segment handler must not focus the rail / open the anchor picker.
ev.preventDefault();
ev.stopPropagation();
+ // Click-elsewhere semantics, which stopPropagation keeps the handler below from
+ // applying: a click off the annotation UI drops the popover and pinned focus.
hidePop();
+ if (focusKey && !t.closest("[data-jh-seg]")) clearFocus();
// …but a selection that survives the click is a comment/react gesture (a drag
// that ended in linked text), so keep the selection and open nothing. Same
// emptiness test as reportSelection.
From 99c12f4eedd2e9b550de83fa383775f02a4531b3 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Tue, 28 Jul 2026 21:10:13 +0000
Subject: [PATCH 8/8] Stop swallowing author link handlers; read link hrefs
from the attribute
---
lib/docs/overlay.ts | 31 ++++++++++++++++++-------------
1 file changed, 18 insertions(+), 13 deletions(-)
diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts
index d44189e..4775eca 100644
--- a/lib/docs/overlay.ts
+++ b/lib/docs/overlay.ts
@@ -629,6 +629,8 @@ export const OVERLAY_SCRIPT = String.raw`
span.addEventListener("mouseleave", function(){ setHover(null); send({type:"jh:hlHoverOut"}); });
span.addEventListener("click", function(ev){
ev.stopPropagation();
+ // A click the link interceptor claimed is navigation, not an annotation click.
+ if (ev === linkClick) return;
onSegClick(seg, ev);
});
}
@@ -718,20 +720,29 @@ export const OVERLAY_SCRIPT = String.raw`
// ourselves from inside the user gesture (sandbox carries allow-popups +
// allow-popups-to-escape-sandbox so the target loads as a normal page).
// Same-document fragments (#section, the gutter section links) stay in-frame.
+ // The raw attribute, not a.href: on an SVG that property is an
+ // SVGAnimatedString and stringifies to garbage.
function externalHref(a){
+ var raw = a.getAttribute("href");
+ if (raw == null) raw = a.getAttribute("xlink:href");
+ if (raw == null) return null;
var u;
- try { u = new URL(a.href, document.baseURI); } catch(e){ return null; }
+ try { u = new URL(raw, document.baseURI); } catch(e){ return null; }
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
if (u.href.split("#")[0] === location.href.split("#")[0]) return null;
return u.href;
}
// Capture phase: a link inside a highlighted span would otherwise be swallowed
- // by the segment's own click handler (it stopPropagation()s) and navigate.
+ // by the segment's own click handler (it stopPropagation()s) and navigate. We
+ // never stop propagation ourselves — the doc may have its own click handlers on
+ // the link — so the claimed event is recorded instead, and the segment handler
+ // skips focusing for it.
+ var linkClick = null;
function openLink(ev){
if (ev.type === "auxclick" && ev.button !== 1) return; // middle-click only
var t = ev.target;
if (!t || !t.closest) return;
- var a = t.closest("a[href]");
+ var a = t.closest("a");
if (!a) return;
// In edit mode a link is text to retype. The edit-mode handler already eats
// the click; this also covers middle-click, which would open a tab.
@@ -744,17 +755,11 @@ export const OVERLAY_SCRIPT = String.raw`
if (fromOverlayChrome(ev) || t.closest("[data-jh-sec-anchor]")){ ev.preventDefault(); return; }
var href = externalHref(a);
if (!href) return;
- // A click on a link is navigation, not an annotation click: the frame must not
- // move, and the segment handler must not focus the rail / open the anchor picker.
ev.preventDefault();
- ev.stopPropagation();
- // Click-elsewhere semantics, which stopPropagation keeps the handler below from
- // applying: a click off the annotation UI drops the popover and pinned focus.
- hidePop();
- if (focusKey && !t.closest("[data-jh-seg]")) clearFocus();
- // …but a selection that survives the click is a comment/react gesture (a drag
- // that ended in linked text), so keep the selection and open nothing. Same
- // emptiness test as reportSelection.
+ linkClick = ev;
+ // A selection that survives the click is a comment/react gesture (a drag that
+ // ended in linked text), not navigation: keep the selection, open nothing.
+ // Same emptiness test as reportSelection.
var sel = window.getSelection();
if (sel && sel.rangeCount && !sel.isCollapsed && sel.toString().trim()) return;
window.open(href, "_blank", "noopener");