Created
August 26, 2025 16:45
-
-
Save pyrho/3d794fd96d358b2e5c8b58fc1d703181 to your computer and use it in GitHub Desktop.
A tampermonkey script to automatically populate fields when saving a bookmark to linkhut
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 Linkhut Auto-Fill Title and Summary | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0 | |
| // @description Automatically fetch page title and summary when pasting URLs in linkhut | |
| // @author You | |
| // @match *://*/ | |
| // @match *://*/* | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM_addStyle | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| console.log('Running'); | |
| // Only run on linkhut pages that contain the add link form | |
| if (!document.querySelector('#link_url')) { | |
| return; | |
| } | |
| console.log("in here"); | |
| // Add some styling for loading indicator | |
| GM_addStyle(` | |
| .linkhut-loading { | |
| opacity: 0.6; | |
| pointer-events: none; | |
| } | |
| .linkhut-status { | |
| font-size: 12px; | |
| color: #666; | |
| margin-top: 5px; | |
| } | |
| .linkhut-error { | |
| color: #d32f2f; | |
| } | |
| .linkhut-success { | |
| color: #388e3c; | |
| } | |
| `); | |
| const urlInput = document.querySelector('#link_url'); | |
| const titleInput = document.querySelector('#link_title'); | |
| const notesInput = document.querySelector('#link_notes'); | |
| // Create status element | |
| const statusElement = document.createElement('div'); | |
| statusElement.className = 'linkhut-status'; | |
| urlInput.parentNode.insertBefore(statusElement, urlInput.nextSibling); | |
| function isValidUrl(string) { | |
| try { | |
| const url = new URL(string); | |
| return url.protocol === 'http:' || url.protocol === 'https:'; | |
| } catch (_) { | |
| return false; | |
| } | |
| } | |
| function setStatus(message, type = '') { | |
| statusElement.textContent = message; | |
| statusElement.className = `linkhut-status ${type}`; | |
| } | |
| function setLoading(isLoading) { | |
| if (isLoading) { | |
| titleInput.classList.add('linkhut-loading'); | |
| notesInput.classList.add('linkhut-loading'); | |
| } else { | |
| titleInput.classList.remove('linkhut-loading'); | |
| notesInput.classList.remove('linkhut-loading'); | |
| } | |
| } | |
| function extractMetaContent(html, property) { | |
| const patterns = [ | |
| new RegExp(`<meta\\s+property=["']${property}["']\\s+content=["']([^"']+)["']`, 'i'), | |
| new RegExp(`<meta\\s+content=["']([^"']+)["']\\s+property=["']${property}["']`, 'i'), | |
| new RegExp(`<meta\\s+name=["']${property}["']\\s+content=["']([^"']+)["']`, 'i'), | |
| new RegExp(`<meta\\s+content=["']([^"']+)["']\\s+name=["']${property}["']`, 'i') | |
| ]; | |
| for (const pattern of patterns) { | |
| const match = html.match(pattern); | |
| if (match) { | |
| return match[1]; | |
| } | |
| } | |
| return null; | |
| } | |
| function extractTitle(html) { | |
| const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i); | |
| return titleMatch ? titleMatch[1].trim() : null; | |
| } | |
| function extractSummary(html) { | |
| // Try different meta tags for description/summary | |
| const descriptionSources = [ | |
| 'og:description', | |
| 'twitter:description', | |
| 'description', | |
| 'twitter:title', | |
| 'og:title' | |
| ]; | |
| for (const source of descriptionSources) { | |
| const content = extractMetaContent(html, source); | |
| if (content && content.length > 20) { // Only use if substantial content | |
| return content; | |
| } | |
| } | |
| return null; | |
| } | |
| function fetchPageInfo(url) { | |
| setLoading(true); | |
| setStatus('Fetching page info...', ''); | |
| GM_xmlhttpRequest({ | |
| method: 'GET', | |
| url: url, | |
| timeout: 10000, | |
| onload: function(response) { | |
| setLoading(false); | |
| if (response.status >= 200 && response.status < 300) { | |
| const html = response.responseText; | |
| // Extract title | |
| const title = extractTitle(html); | |
| if (title && !titleInput.value.trim()) { | |
| titleInput.value = title; | |
| } | |
| // Extract summary/description | |
| const summary = extractSummary(html); | |
| if (summary && !notesInput.value.trim()) { | |
| notesInput.value = summary; | |
| } | |
| if (title || summary) { | |
| setStatus('✓ Page info fetched successfully', 'linkhut-success'); | |
| } else { | |
| setStatus('⚠ No title or description found', 'linkhut-error'); | |
| } | |
| } else { | |
| setStatus(`✗ Failed to fetch page (${response.status})`, 'linkhut-error'); | |
| } | |
| // Clear status after 3 seconds | |
| setTimeout(() => setStatus(''), 3000); | |
| }, | |
| onerror: function() { | |
| setLoading(false); | |
| setStatus('✗ Error fetching page info', 'linkhut-error'); | |
| setTimeout(() => setStatus(''), 3000); | |
| }, | |
| ontimeout: function() { | |
| setLoading(false); | |
| setStatus('✗ Request timed out', 'linkhut-error'); | |
| setTimeout(() => setStatus(''), 3000); | |
| } | |
| }); | |
| } | |
| // Handle paste events | |
| urlInput.addEventListener('paste', function(e) { | |
| setTimeout(() => { | |
| const url = urlInput.value.trim(); | |
| if (isValidUrl(url)) { | |
| fetchPageInfo(url); | |
| } | |
| }, 100); // Small delay to ensure paste is complete | |
| }); | |
| // Handle input events (for manual typing) | |
| let inputTimeout; | |
| urlInput.addEventListener('input', function(e) { | |
| clearTimeout(inputTimeout); | |
| inputTimeout = setTimeout(() => { | |
| const url = urlInput.value.trim(); | |
| if (isValidUrl(url)) { | |
| fetchPageInfo(url); | |
| } | |
| }, 1000); // Wait 1 second after user stops typing | |
| }); | |
| console.log('Linkhut Auto-Fill script loaded'); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment