2026-07-08
Making Every Off-Site Link Open in a New Tab
Someone clicking a link to my LinkedIn profile or my GitHub repo shouldn't lose the page they were reading on my site. That should be a one-line fix in React, with target="_blank" rel="noopener noreferrer", except this site renders links two different ways, and each needed its own fix.
Two link sources, two problems
Some links on this site are plain JSX, written by hand in a component. Those are easy. I just add the attributes directly. But the blog posts are markdown files, compiled to HTML at build time by remark-html, then injected with dangerouslySetInnerHTML. There's no per-link JSX to edit. By the time I see it, it's already a string of HTML.
The Projects page has a third wrinkle: each case study can link to either an internal page, like the blog post about the comments feature, or an external one, like the GitHub repo. Those need opposite treatment. Internal links should navigate normally with Next.js's <Link>. External ones should open in a new tab as a plain <a>.
Fix 1: post-processing the markdown output
In lib/posts.ts, after remark converts markdown to HTML, I run one more pass over the string that rewrites any absolute http(s) link to add the new-tab attributes:
function openExternalLinksInNewTab(html: string): string {
return html.replace(
/<a\s+href="(https?:\/\/[^"]+)"/g,
'<a target="_blank" rel="noopener noreferrer" href="$1"'
);
}
The regex only matches links starting with http:// or https://. Internal links in my posts are always written as relative paths (/blog/other-post), so they're untouched. They keep navigating normally within the site.
Fix 2: branching on the href in JSX
On the Projects page, each case study can carry a list of links, and I don't want to hand-flag each one as internal or external in the data file. Instead, the component checks the URL shape at render time:
{study.links.map((link) =>
link.href.startsWith("http") ? (
<a
key={link.href}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-accent hover:underline"
>
{link.label} →
</a>
) : (
<Link key={link.href} href={link.href} className="text-sm font-medium text-accent hover:underline">
{link.label} →
</Link>
)
)}
If it starts with http, it's off-site. That's a plain anchor tag with the new-tab attributes. Otherwise it's one of my own routes, so it stays a Next.js <Link> and behaves like normal client-side navigation.
Why rel="noopener noreferrer", not just target="_blank"
target="_blank" alone lets the page you just opened access window.opener and reach back into the tab you came from. That's a real, if narrow, security and performance issue. rel="noopener" closes that. rel="noreferrer" additionally withholds the referrer header. Both are cheap to add and there's no real reason not to.
The PM angle
This is a good example of how a one-line request can hide a lot of scope. "Make external links open in a new tab" sounds like a single change, but the site has two entirely different link-rendering paths, plus a page that mixes internal and external links in the same list. The fix ended up being two small, independent changes rather than one. It's worth checking how many code paths a "simple" request actually touches before you scope it as one task.
Comments
Loading comments...