Created
September 27, 2025 20:25
-
-
Save danielrosehill/aad59bdfdbbb780e6ba56ed87b4d3897 to your computer and use it in GitHub Desktop.
Tampermonkey - open custom GPTs in a new tab
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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