A small, safe Jekyll plugin can automatically add target="_blank" and rel="noopener noreferrer" to external links at build time. This keeps runtime JavaScript out of the page and enforces security best practices for links that open in a new tab — while avoiding common breakage (image wrappers, hash links, mailto:/tel: links, internal links).
🔎 What the plugin does
- Runs as a
post_render hook for pages, posts and documents.
- Parses full HTML with Nokogiri (preserves scripts, TOC and wrapper elements).
- Only modifies absolute
http(s) and protocol-relative links.
- Skips internal links (same hostname), anchors,
mailto: and tel: links.
- Skips anchors that wrap
<img> (prevents lightbox/gallery breakage).
- Preserves existing
target/rel attributes.
⚙️ Install the plugin
- Create a plugin file at
_plugins/modify-links.rb and paste the code below. This variant is careful to avoid breaking image links and TOC anchors.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
require 'nokogiri'
require 'uri'
Jekyll::Hooks.register [:pages, :documents, :posts], :post_render do |doc|
next unless doc.output_ext == '.html'
site_url = (doc.site.config['url'] || '').strip
site_host = begin
URI.parse(site_url).host if site_url != ''
rescue
nil
end
# Use full document parse to avoid stripping important wrapper elements
html = Nokogiri::HTML::Document.parse(doc.output)
html.css('a[href]').each do |a|
href = a['href'].to_s.strip
next if href.empty?
# skip internal anchors and non-http links
next if href.start_with?('#') || href.start_with?('mailto:') || href.start_with?('tel:')
begin
uri = URI.parse(href)
rescue
next
end
# Only handle absolute http/https URLs and protocol-relative URLs
is_http = uri.scheme == 'http' || uri.scheme == 'https' || href.start_with?('//')
next unless is_http
# determine host for protocol-relative URLs
href_host = uri.host
if href_host.nil? && href.start_with?('//')
href_host = href.sub(%r{^//}, '').split('/').first
end
# don't touch links pointing to the same host (internal)
if site_host && href_host && href_host == site_host
next
end
# skip anchors that wrap images (prevents breaking image lightboxes / thumbnails)
next if a.at_css('img')
# set attributes only if not already present (preserve custom attributes)
a.set_attribute('target', '_blank') unless a['target']
rel_vals = (a['rel'] || '').split(/\s+/)
%w[noopener noreferrer].each { |v| rel_vals << v unless rel_vals.include?(v) }
a.set_attribute('rel', rel_vals.join(' ').strip)
end
doc.output = html.to_html
end
|
- Ensure
nokogiri is available in your build environment. If you use Bundler, add it to your Gemfile:
Then run:
- Build your site locally to test:
1
|
bundle exec jekyll build
|
✅ Quick verification
- Find external links that received the attributes:
1
|
grep -R --include="*.html" -n 'target="_blank"' _site | head -n 50
|
- Ensure no images are wrapped in anchors with
target:
1
|
grep -R --include="*.html" -nP '<a[^>]+target="_blank"[^>]*>\s*<img' _site || echo "No image-wrapping anchors with target found"
|
- Spot-check a page with TOC or a gallery and inspect anchors in DevTools.
🔁 Client-side fallback (GitHub Pages / no plugin support)
If you cannot use plugins (for example on GitHub Pages without a custom builder), add a tiny JS snippet that sets the attributes at runtime.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
document.addEventListener('DOMContentLoaded', function () {
try {
var host = location.hostname;
document.querySelectorAll('a[href^="http"]').forEach(function (a) {
try {
var u = new URL(a.href);
if (u.hostname !== host) {
if (!a.hasAttribute('target')) a.setAttribute('target', '_blank');
var rel = (a.getAttribute('rel') || '').split(/\s+/);
if (rel.indexOf('noopener') === -1) rel.push('noopener');
if (rel.indexOf('noreferrer') === -1) rel.push('noreferrer');
a.setAttribute('rel', rel.join(' ').trim());
}
} catch (e) { /* ignore invalid URLs */ }
});
} catch (e) {}
});
|
Include it in your base layout near </body>:
1
|
<script src="/assets/js/external-links.js" defer></script>
|
⚠️ Caveats & recommendations
- GitHub Pages (default) does not allow arbitrary plugins — use the client-side JS there or build with GitHub Actions and push the generated
_site.
- The plugin relies on
site.url in _config.yml to detect internal links. If site.url is missing, some internal links may be treated as external. Set site.url to your canonical URL to be safe.
- The plugin preserves existing attributes; if you intentionally want a link to open same-tab, set
target on that anchor explicitly in your Markdown/HTML.
🧠 Final Thoughts
This build-time approach gives you a clean, fast site with security best practices applied to outbound links while avoiding the most common regressions (broken image links, TOC issues). Use the client-side JS only where plugins are not possible.