What is happening is that the HTML in the link is expanding (spaces being replaced by %20, for instance). This happens when you copy and paste the button in a handout. I assume that's what's going on--buttons that aren't copied and pasted generally preserve their link just fine. Here is a short script that the Aaron wrote for me that keeps this from happening. If you open the handout with the bad button, and make a change anywhere, this script should fix it when you save, and keep future buttons from misbehaving. on('ready',()=>{ const JournalCommandDecoder = (t) => { const fixStart = t => t.replace(/^"https:\/\/app.roll20.net\/editor\/%60/,'"`'); const percentOut = t => t.replace(/%{/g,'::PERCENT_OPEN_CURLY::'); const percentIn = t => t.replace(/::PERCENT_OPEN_CURLY::/g,'%{'); return percentIn(decodeURIComponent(percentOut(fixStart(t)))); }; const fixHandoutURLs = (data) => { const re = new RegExp(`"<a href="https://app.roll20.net/editor/%60[^"]*"`,'g" rel="nofollow">https://app.roll20.net/editor/%60[^"]*"`,'g</a>'); return data.replace(re,JournalCommandDecoder); }; const alterHandout = (obj)=>obj.get('notes',(notesPrime)=>obj.get('gmnotes',(gmnotesPrime)=>{ let notes = fixHandoutURLs(notesPrime); let gmnotes = fixHandoutURLs(gmnotesPrime); if(notes !== notesPrime){ setTimeout(()=>obj.set('notes',notes), 1000); } if(gmnotes !== gmnotesPrime){ setTimeout(()=>obj.set('gmnotes',gmnotes), 2000); } })); on('chat:message',msg=>{ if('api'===msg.type && /^!fix-all-handout-urls(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){ const who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); let handouts=findObjs({type:'handout'}); let c = handouts.length; sendChat('',`/w "${who}" processing ${c} handout(s)"`); const burndown = () => { let h = handouts.shift(); if(h) { alterHandout(h); setTimeout(burndown,0); } else { sendChat('',`/w "${who}" examined ${c} handout(s)"`); } }; burndown(); } }); on('change:handout',alterHandout); });