Created
February 12, 2026 09:01
-
-
Save getdave/e8867855dade847aa6148f187948ce74 to your computer and use it in GitHub Desktop.
Tampermonkey: Copy GitHub PR/issue title as rich link for Slack
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 GitHub Copy Title Link | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.1 | |
| // @description Copies PR/issue title as a rich link (emulates CMD+C on selected title) for Slack etc. | |
| // @author You | |
| // @match https://github.com/*/pull/* | |
| // @match https://github.com/*/issues/* | |
| // @grant clipboardWrite | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| ( function () { | |
| 'use strict'; | |
| function getTitleElement() { | |
| return ( | |
| document.querySelector( '.js-issue-title.markdown-title' ) || | |
| document.querySelector( '.js-issue-title' ) || | |
| document.querySelector( 'h1[data-paste-element="issue_title"]' ) || | |
| document.querySelector( '.gh-header-title h1' ) | |
| ); | |
| } | |
| function getTitleText() { | |
| const el = getTitleElement(); | |
| if ( ! el ) return null; | |
| return el.textContent?.trim() || el.innerText?.trim(); | |
| } | |
| function escapeHtml( str ) { | |
| const div = document.createElement( 'div' ); | |
| div.textContent = str; | |
| return div.innerHTML; | |
| } | |
| function copyAsRichLink() { | |
| const url = window.location.href; | |
| const title = getTitleText(); | |
| if ( ! title ) { | |
| return navigator.clipboard.writeText( url ); | |
| } | |
| // Emulate CMD+C on selected link: write HTML so Slack pastes as "title as link" | |
| // Escape title/url to prevent XSS when pasted into HTML-rendering apps | |
| const html = `<a href="${ escapeHtml( url ) }">${ escapeHtml( title ) }</a>`; | |
| const blobHtml = new Blob( [ html ], { type: 'text/html' } ); | |
| const blobPlain = new Blob( [ url ], { type: 'text/plain' } ); | |
| return navigator.clipboard.write( [ | |
| new ClipboardItem( { | |
| 'text/html': blobHtml, | |
| 'text/plain': blobPlain, | |
| } ), | |
| ] ); | |
| } | |
| function createButton() { | |
| const btn = document.createElement( 'button' ); | |
| btn.type = 'button'; | |
| btn.textContent = 'Copy Title Link'; | |
| btn.className = 'btn btn-sm'; | |
| btn.style.cssText = 'margin-left: 12px;'; | |
| btn.setAttribute( 'aria-label', 'Copy title as link (for Slack)' ); | |
| btn.addEventListener( 'click', async () => { | |
| try { | |
| await copyAsRichLink(); | |
| const original = btn.textContent; | |
| btn.textContent = 'Copied!'; | |
| btn.disabled = true; | |
| setTimeout( () => { | |
| btn.textContent = original; | |
| btn.disabled = false; | |
| }, 1500 ); | |
| } catch ( err ) { | |
| btn.textContent = 'Failed'; | |
| setTimeout( () => { | |
| btn.textContent = 'Copy Title Link'; | |
| }, 1500 ); | |
| } | |
| } ); | |
| return btn; | |
| } | |
| function injectButton() { | |
| if ( document.getElementById( 'copy-title-link-btn' ) ) return; | |
| const actionsEl = document.querySelector( '.gh-header-actions' ); | |
| if ( ! actionsEl ) return; | |
| const btn = createButton(); | |
| btn.id = 'copy-title-link-btn'; | |
| actionsEl.appendChild( btn ); | |
| } | |
| if ( document.readyState === 'loading' ) { | |
| document.addEventListener( 'DOMContentLoaded', () => { | |
| setTimeout( injectButton, 500 ); | |
| } ); | |
| } else { | |
| setTimeout( injectButton, 500 ); | |
| } | |
| const observer = new MutationObserver( () => { | |
| if ( | |
| document.querySelector( '.gh-header-actions' ) && | |
| ! document.getElementById( 'copy-title-link-btn' ) | |
| ) { | |
| injectButton(); | |
| } | |
| } ); | |
| observer.observe( document.body, { childList: true, subtree: true } ); | |
| } )(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment