Write on the margins of the internet. Powered by the AT Protocol.

Add bookmarklet userscripts and HTML page with download links

- Create userscript files for compose, quick-highlight, and annotate-prompt
- Add bookmarklets.html with draggable bookmarklets and userscript download links
- Include bookmarklet.ts source code for regenerating bookmarklets

rektide 62f8cb1a a946ff7b

+322
+37
bookmarklet.annotate-prompt.user.js
··· 1 + // ==UserScript== 2 + // @name Margin.at Annotate with Prompt 3 + // @namespace https://margin.at/ 4 + // @version 1.0 5 + // @description Post annotation to margin.at with prompts for text and tags 6 + // @author margin.at 7 + // @match *://*/* 8 + // @grant none 9 + // ==/UserScript== 10 + 11 + (async function () { 12 + const u = window.location.href; 13 + const s = window.getSelection().toString().trim(); 14 + const q = new URLSearchParams(window.location.search).get("quote") || s; 15 + const t = prompt("Annotation text:"); 16 + if (t === null) return; 17 + const g = prompt("Tags (comma-separated):"); 18 + const b = { url: u }; 19 + if (q) b.selector = { exact: q }; 20 + if (t) b.text = t; 21 + if (g) b.tags = g.split(",").map((x) => x.trim()).filter((x) => x); 22 + try { 23 + const r = await fetch("https://margin.at/api/annotations", { 24 + method: "POST", 25 + headers: { "Content-Type": "application/json" }, 26 + credentials: "include", 27 + body: JSON.stringify(b), 28 + }); 29 + if (r.ok) alert("Posted!"); 30 + else if (r.status === 401) { 31 + alert("Log in first"); 32 + window.open("https://margin.at/login", "_blank"); 33 + } else alert("Error: " + (await r.text())); 34 + } catch (e) { 35 + alert("Failed: " + e.message); 36 + } 37 + })();
+22
bookmarklet.compose.user.js
··· 1 + // ==UserScript== 2 + // @name Margin.at Compose 3 + // @namespace https://margin.at/ 4 + // @version 1.0 5 + // @description Open margin.at compose form with current page URL and selected text 6 + // @author margin.at 7 + // @match *://*/* 8 + // @grant none 9 + // ==/UserScript== 10 + 11 + (function () { 12 + const s = window.getSelection().toString().trim(); 13 + const q = new URLSearchParams(window.location.search).get("quote") || s; 14 + const u = encodeURIComponent(window.location.href); 15 + const p = q 16 + ? "&selector=" + 17 + encodeURIComponent( 18 + JSON.stringify({ type: "TextQuoteSelector", exact: q }), 19 + ) 20 + : ""; 21 + window.open("https://margin.at/new?url=" + u + p, "_blank"); 22 + })();
+32
bookmarklet.quick-highlight.user.js
··· 1 + // ==UserScript== 2 + // @name Margin.at Quick Highlight 3 + // @namespace https://margin.at/ 4 + // @version 1.0 5 + // @description Post a highlight directly to margin.at without opening a form 6 + // @author margin.at 7 + // @match *://*/* 8 + // @grant none 9 + // ==/UserScript== 10 + 11 + (async function () { 12 + const u = window.location.href; 13 + const s = window.getSelection().toString().trim(); 14 + const q = new URLSearchParams(window.location.search).get("quote") || s; 15 + const b = { url: u }; 16 + if (q) b.selector = { exact: q }; 17 + try { 18 + const r = await fetch("https://margin.at/api/annotations", { 19 + method: "POST", 20 + headers: { "Content-Type": "application/json" }, 21 + credentials: "include", 22 + body: JSON.stringify(b), 23 + }); 24 + if (r.ok) alert("Posted!"); 25 + else if (r.status === 401) { 26 + alert("Log in first"); 27 + window.open("https://margin.at/login", "_blank"); 28 + } else alert("Error: " + (await r.text())); 29 + } catch (e) { 30 + alert("Failed: " + e.message); 31 + } 32 + })();
+103
bookmarklet.ts
··· 1 + interface BookmarkletConfig { 2 + mode: "compose" | "post"; 3 + tags?: string[]; 4 + text?: string; 5 + } 6 + 7 + function getSelectedText(): string { 8 + const selection = window.getSelection(); 9 + return selection ? selection.toString().trim() : ""; 10 + } 11 + 12 + function getQuoteParam(): string { 13 + const params = new URLSearchParams(window.location.search); 14 + return params.get("quote") || ""; 15 + } 16 + 17 + function getSelector(exact: string) { 18 + return { type: "TextQuoteSelector", exact }; 19 + } 20 + 21 + function composeBookmarklet() { 22 + const url = encodeURIComponent(window.location.href); 23 + const quote = getSelectedText() || getQuoteParam(); 24 + const selectorParam = quote 25 + ? `&selector=${encodeURIComponent(JSON.stringify(getSelector(quote)))}` 26 + : ""; 27 + window.open(`https://margin.at/new?url=${url}${selectorParam}`, "_blank"); 28 + } 29 + 30 + async function postBookmarklet(config?: BookmarkletConfig) { 31 + const pageUrl = window.location.href; 32 + const quote = getSelectedText() || getQuoteParam(); 33 + 34 + const body: Record<string, unknown> = { 35 + url: pageUrl, 36 + }; 37 + 38 + if (quote) { 39 + body.selector = { exact: quote }; 40 + } 41 + 42 + if (config?.text) { 43 + body.text = config.text; 44 + } 45 + 46 + if (config?.tags && config.tags.length > 0) { 47 + body.tags = config.tags; 48 + } 49 + 50 + try { 51 + const res = await fetch("https://margin.at/api/annotations", { 52 + method: "POST", 53 + headers: { "Content-Type": "application/json" }, 54 + credentials: "include", 55 + body: JSON.stringify(body), 56 + }); 57 + 58 + if (res.ok) { 59 + alert("Annotation posted!"); 60 + } else if (res.status === 401) { 61 + alert("Please log in to margin.at first"); 62 + window.open("https://margin.at/login", "_blank"); 63 + } else { 64 + alert(`Error: ${await res.text()}`); 65 + } 66 + } catch (e) { 67 + alert(`Failed: ${e instanceof Error ? e.message : "Unknown error"}`); 68 + } 69 + } 70 + 71 + async function main() { 72 + const { realpath } = await import("node:fs/promises"); 73 + const resolvedMain = process.argv[1] ? await realpath(process.argv[1]) : null; 74 + const thisFile = new URL(import.meta.url).pathname; 75 + if (resolvedMain && resolvedMain === thisFile) { 76 + console.log("Margin.at Bookmarklet Generator\n"); 77 + console.log("=== Compose Bookmarklet (opens form) ==="); 78 + console.log( 79 + "javascript:" + 80 + encodeURIComponent( 81 + `(function(){const s=window.getSelection().toString().trim();const q=new URLSearchParams(window.location.search).get("quote")||s;const u=encodeURIComponent(window.location.href);const p=q?"&selector="+encodeURIComponent(JSON.stringify({type:"TextQuoteSelector",exact:q})):"";window.open("https://margin.at/new?url="+u+p,"_blank")})()`, 82 + ), 83 + ); 84 + console.log("\n=== Quick Post Bookmarklet (posts directly) ==="); 85 + console.log( 86 + "javascript:" + 87 + encodeURIComponent( 88 + `(async function(){const u=window.location.href;const s=window.getSelection().toString().trim();const q=new URLSearchParams(window.location.search).get("quote")||s;const b={url:u};if(q)b.selector={exact:q};try{const r=await fetch("https://margin.at/api/annotations",{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify(b)});if(r.ok)alert("Posted!");else if(r.status===401){alert("Log in first");window.open("https://margin.at/login","_blank")}else alert("Error: "+await r.text())}catch(e){alert("Failed: "+e.message)}})()`, 89 + ), 90 + ); 91 + console.log("\n=== Annotate with Prompt (asks for text and tags) ==="); 92 + console.log( 93 + "javascript:" + 94 + encodeURIComponent( 95 + `(async function(){const u=window.location.href;const s=window.getSelection().toString().trim();const q=new URLSearchParams(window.location.search).get("quote")||s;const t=prompt("Annotation text:");if(t===null)return;const g=prompt("Tags (comma-separated):");const b={url:u};if(q)b.selector={exact:q};if(t)b.text=t;if(g)b.tags=g.split(",").map(x=>x.trim()).filter(x=>x);try{const r=await fetch("https://margin.at/api/annotations",{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify(b)});if(r.ok)alert("Posted!");else if(r.status===401){alert("Log in first");window.open("https://margin.at/login","_blank")}else alert("Error: "+await r.text())}catch(e){alert("Failed: "+e.message)}})()`, 96 + ), 97 + ); 98 + } 99 + } 100 + 101 + main(); 102 + 103 + export { composeBookmarklet, postBookmarklet };
+128
bookmarklets.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Margin.at Bookmarklets</title> 7 + <style> 8 + body { 9 + font-family: system-ui, sans-serif; 10 + max-width: 640px; 11 + margin: 2rem auto; 12 + padding: 1rem; 13 + line-height: 1.6; 14 + } 15 + h1 { margin-bottom: 0.5rem; } 16 + .bookmarklet { 17 + display: inline-block; 18 + background: #2563eb; 19 + color: white; 20 + padding: 0.5rem 1rem; 21 + border-radius: 0.5rem; 22 + text-decoration: none; 23 + margin: 0.5rem 0; 24 + cursor: grab; 25 + } 26 + .bookmarklet:hover { background: #1d4ed8; } 27 + .section { 28 + background: #f5f5f5; 29 + padding: 1rem; 30 + border-radius: 0.5rem; 31 + margin: 1rem 0; 32 + } 33 + code { 34 + background: #e5e5e5; 35 + padding: 0.125rem 0.25rem; 36 + border-radius: 0.25rem; 37 + font-size: 0.9em; 38 + } 39 + pre { 40 + background: #1e1e1e; 41 + color: #d4d4d4; 42 + padding: 1rem; 43 + border-radius: 0.5rem; 44 + overflow-x: auto; 45 + font-size: 0.85em; 46 + } 47 + .tip { 48 + background: #fef3c7; 49 + border-left: 3px solid #f59e0b; 50 + padding: 0.5rem 1rem; 51 + margin: 1rem 0; 52 + } 53 + .script-link { 54 + color: #2563eb; 55 + text-decoration: none; 56 + } 57 + .script-link:hover { 58 + text-decoration: underline; 59 + } 60 + </style> 61 + </head> 62 + <body> 63 + <h1>Margin.at Bookmarklets</h1> 64 + <p name="intro">Drag these links to your bookmarks bar to annotate any page.</p> 65 + 66 + <section name="compose" class="section"> 67 + <h2>Compose Bookmarklet</h2> 68 + <p name="compose-desc">Opens the margin.at compose form with the current page URL and any selected text pre-filled. <a class="script-link" href="bookmarklet.compose.user.js" download="margin-compose.user.js" title="Download userscript">🔗</a></p> 69 + <a name="compose-bookmarklet" class="bookmarklet" href="javascript:(function(){const s=window.getSelection().toString().trim();const q=new URLSearchParams(window.location.search).get(&quot;quote&quot;)||s;const u=encodeURIComponent(window.location.href);const p=q?&quot;&amp;selector=&quot;+encodeURIComponent(JSON.stringify({type:&quot;TextQuoteSelector&quot;,exact:q})):&quot;&quot;;window.open(&quot;https://margin.at/new?url=&quot;+u+p,&quot;_blank&quot;)})()">📎 Annotate on margin.at</a> 70 + 71 + <h3>What it does:</h3> 72 + <ul> 73 + <li>Gets the current page URL</li> 74 + <li>Gets any selected text (or the <code>?quote=...</code> URL parameter)</li> 75 + <li>Opens margin.at/new with url and selector pre-filled</li> 76 + </ul> 77 + </section> 78 + 79 + <section name="quick-highlight" class="section"> 80 + <h2>Quick Highlight Bookmarklet</h2> 81 + <p name="quick-highlight-desc">Posts a highlight directly to margin.at without opening a form. Creates a selector-only annotation. <a class="script-link" href="bookmarklet.quick-highlight.user.js" download="margin-quick-highlight.user.js" title="Download userscript">🔗</a></p> 82 + <a name="quick-highlight-bookmarklet" class="bookmarklet" href="javascript:(async function(){const u=window.location.href;const s=window.getSelection().toString().trim();const q=new URLSearchParams(window.location.search).get(&quot;quote&quot;)||s;const b={url:u};if(q)b.selector={exact:q};try{const r=await fetch(&quot;https://margin.at/api/annotations&quot;,{method:&quot;POST&quot;,headers:{&quot;Content-Type&quot;:&quot;application/json&quot;},credentials:&quot;include&quot;,body:JSON.stringify(b)});if(r.ok)alert(&quot;Posted!&quot;);else if(r.status===401){alert(&quot;Log in first&quot;);window.open(&quot;https://margin.at/login&quot;,&quot;_blank&quot;)}else alert(&quot;Error: &quot;+await r.text())}catch(e){alert(&quot;Failed: &quot;+e.message)}})()">⚡ Quick Highlight</a> 83 + 84 + <p class="tip">You must be logged into margin.at in your browser for this to work.</p> 85 + </section> 86 + 87 + <section name="annotate-prompt" class="section"> 88 + <h2>Annotate with Prompt</h2> 89 + <p name="annotate-prompt-desc">Posts directly with prompts for your annotation text and tags. <a class="script-link" href="bookmarklet.annotate-prompt.user.js" download="margin-annotate-prompt.user.js" title="Download userscript">🔗</a></p> 90 + <a name="annotate-prompt-bookmarklet" class="bookmarklet" href="javascript:(async function(){const u=window.location.href;const s=window.getSelection().toString().trim();const q=new URLSearchParams(window.location.search).get(&quot;quote&quot;)||s;const t=prompt(&quot;Annotation text:&quot;);if(t===null)return;const g=prompt(&quot;Tags (comma-separated):&quot;);const b={url:u};if(q)b.selector={exact:q};if(t)b.text=t;if(g)b.tags=g.split(&quot;,&quot;).map(x=&gt;x.trim()).filter(x=&gt;x);try{const r=await fetch(&quot;https://margin.at/api/annotations&quot;,{method:&quot;POST&quot;,headers:{&quot;Content-Type&quot;:&quot;application/json&quot;},credentials:&quot;include&quot;,body:JSON.stringify(b)});if(r.ok)alert(&quot;Posted!&quot;);else if(r.status===401){alert(&quot;Log in first&quot;);window.open(&quot;https://margin.at/login&quot;,&quot;_blank&quot;)}else alert(&quot;Error: &quot;+await r.text())}catch(e){alert(&quot;Failed: &quot;+e.message)}})()">✏️ Annotate with Text &amp; Tags</a> 91 + 92 + <h3>What it does:</h3> 93 + <ul> 94 + <li>Prompts for annotation text</li> 95 + <li>Prompts for comma-separated tags</li> 96 + <li>Posts to margin.at with URL, selector, text, and tags</li> 97 + </ul> 98 + <p class="tip">You must be logged into margin.at in your browser for this to work.</p> 99 + </section> 100 + 101 + <section name="api-reference"> 102 + <h2>API Reference</h2> 103 + 104 + <h3>GET /new (Compose Form)</h3> 105 + <p>Query parameters:</p> 106 + <pre>?url=https://example.com/page 107 + &amp;selector={"type":"TextQuoteSelector","exact":"selected text"}</pre> 108 + 109 + <h3>POST /api/annotations (Create Annotation)</h3> 110 + <pre>{ 111 + "url": "https://example.com/page", 112 + "text": "Your comment text", 113 + "selector": { "exact": "quoted text" }, 114 + "tags": ["tag1", "tag2"], 115 + "labels": ["sexual", "violence"] 116 + }</pre> 117 + 118 + <p>Fields:</p> 119 + <ul> 120 + <li><code>url</code> - Required: target URL</li> 121 + <li><code>text</code> - Your annotation/comment</li> 122 + <li><code>selector</code> - Object with <code>exact</code> (and optional <code>prefix</code>/<code>suffix</code>)</li> 123 + <li><code>tags</code> - Array of tag strings (max 10, each max 64 chars)</li> 124 + <li><code>labels</code> - Content warnings: <code>sexual</code>, <code>nudity</code>, <code>violence</code>, <code>gore</code>, <code>spam</code>, <code>misleading</code></li> 125 + </ul> 126 + </section> 127 + </body> 128 + </html>