Skip to content

Instantly share code, notes, and snippets.

@danielrosehill
Created September 27, 2025 20:25
Show Gist options
  • Save danielrosehill/aad59bdfdbbb780e6ba56ed87b4d3897 to your computer and use it in GitHub Desktop.
Save danielrosehill/aad59bdfdbbb780e6ba56ed87b4d3897 to your computer and use it in GitHub Desktop.
Tampermonkey - open custom GPTs in a new tab
// ==UserScript==
// @name Force new tabs on chatgpt.com/gpts/mine
// @namespace daniel.utils
// @version 1.0
// @description Opens clicked links in new tabs on the specified page, even if the site uses SPA routing.
// @match https://chatgpt.com/gpts/mine*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- Config: tweak if you want ---
const SKIP_SCHEMES = ['mailto:', 'tel:', 'javascript:', 'blob:', 'data:']; // leave these alone
const RESPECT_USER_MODIFIERS = true; // don’t interfere with Ctrl/Meta/Shift or middle-click
const RESPECT_DOWNLOAD_ATTR = true; // don’t override <a download>
const SKIP_HASH_ONLY = true; // don’t force #anchor links into new tabs
// Utility: decide if an <a> should be forced into a new tab
function shouldForceNewTab(a) {
const rawHref = (a.getAttribute('href') || '').trim();
if (!rawHref) return false;
if (SKIP_SCHEMES.some(s => rawHref.startsWith(s))) return false;
if (RESPECT_DOWNLOAD_ATTR && a.hasAttribute('download')) return false;
if (SKIP_HASH_ONLY && rawHref.startsWith('#')) return false;
return true;
}
// Keep anchors stamped with target/_blank + rel, including future ones
function stampAnchor(a) {
if (!shouldForceNewTab(a)) return;
// Hint to the browser: open in new tab
a.setAttribute('target', '_blank');
// Security best practice for new tabs
const rel = (a.getAttribute('rel') || '').split(/\s+/);
if (!rel.includes('noopener')) rel.push('noopener');
if (!rel.includes('noreferrer')) rel.push('noreferrer');
a.setAttribute('rel', rel.filter(Boolean).join(' '));
}
function stampAllAnchors(root = document) {
root.querySelectorAll('a[href]').forEach(stampAnchor);
}
// MutationObserver to catch dynamically-added links
const mo = new MutationObserver(muts => {
for (const m of muts) {
if (m.type === 'childList') {
m.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
if (node.matches?.('a[href]')) stampAnchor(node);
if (node.querySelectorAll) stampAllAnchors(node);
});
} else if (m.type === 'attributes' && m.target.matches?.('a[href]')) {
stampAnchor(m.target);
}
}
});
// Start observing ASAP (document-start)
mo.observe(document.documentElement, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['href', 'rel', 'target']
});
// Initial pass (in case DOM is already there)
if (document.readyState !== 'loading') {
stampAllAnchors();
} else {
document.addEventListener('DOMContentLoaded', () => stampAllAnchors(), { once: true });
}
// Click-capture fallback:
// Some SPAs (Next.js/React Router) call preventDefault() and do client-side navigation.
// This handler forces a new tab anyway for simple left-clicks without modifiers.
document.addEventListener('click', function (e) {
// Respect user modifiers / middle-click, if enabled
if (RESPECT_USER_MODIFIERS) {
const modified = e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
if (modified) return;
} else {
// even if disabled, always let middle/right clicks pass
if (e.button !== 0) return;
}
const a = e.target.closest?.('a[href]');
if (!a) return;
if (!shouldForceNewTab(a)) return;
const href = a.href; // absolute URL
if (!href) return;
// Block the page’s router from hijacking the click
e.preventDefault();
e.stopImmediatePropagation();
// Open tab; relying on the user gesture keeps it out of popup-blocker territory
window.open(href, '_blank', 'noopener,noreferrer');
}, true); // capture phase so we run before app handlers
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment