Last active
March 23, 2026 09:25
-
-
Save brandontan/48498ad449248d0e20ea2e86d9d01c94 to your computer and use it in GitHub Desktop.
Universal Web Clipper for Obsidian
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
| (async () => { | |
| const host = window.location.hostname.replace('www.', ''); | |
| const url = window.location.href; | |
| const now = new Date(); | |
| const dateStr = now.toISOString().split('T')[0]; | |
| const year = now.getFullYear(); | |
| const month = String(now.getMonth() + 1).padStart(2, '0'); | |
| function meta(sel) { | |
| const el = document.querySelector(sel); | |
| return el ? (el.content || el.innerText || '').trim() : ''; | |
| } | |
| function text(sel) { | |
| const el = document.querySelector(sel); | |
| return el ? el.innerText.trim() : ''; | |
| } | |
| function allImages(sel) { | |
| return [...document.querySelectorAll(sel)] | |
| .filter(i => i.src && !/emoji|avatar|profile/.test(i.src)) | |
| .map(i => '') | |
| .join('\n\n'); | |
| } | |
| function truncate(txt, words) { | |
| const w = (txt || '').split(/\s+/).filter(Boolean); | |
| return w.slice(0, words).join(' ') + (w.length > words ? '...' : ''); | |
| } | |
| function autoTags(txt, base) { | |
| const t = (txt || '').toLowerCase(); | |
| const topics = { | |
| ai: ['llm','chatgpt','prompt','gpt-','agent','transformer','claude','openai','anthropic','gemini','diffusion','hugging','model','fine-tun'], | |
| dev: ['api','github','open-source','rust','python','javascript','typescript','react','docker'], | |
| startup: ['funding','seed','startup','venture','launch',' yc ','founder','series-a'], | |
| data: ['dataset','benchmark','analytics','machine-learning'], | |
| security: ['vulnerability','exploit','cve','breach'], | |
| infra: ['kubernetes','terraform','aws','cloud','devops'], | |
| web3: ['blockchain','crypto','solana','ethereum','defi','dao','nft','web3','onchain','wallet'] | |
| }; | |
| const found = [...base]; | |
| for (const [tag, kws] of Object.entries(topics)) { | |
| if (kws.some(k => t.includes(k))) found.push(tag); | |
| } | |
| return [...new Set(found)]; | |
| } | |
| function inject(name, value) { | |
| let tag = document.head.querySelector('meta[name="' + name + '"]'); | |
| if (!tag) { | |
| tag = document.createElement('meta'); | |
| tag.name = name; | |
| document.head.appendChild(tag); | |
| } | |
| tag.content = (value || '').substring(0, 100000); | |
| } | |
| /* ── Platform fingerprinting (custom domains) ── */ | |
| function detectPlatform() { | |
| if (document.querySelector('link[href*="substackcdn"], script[src*="substackcdn"], script[src*="substack.com"]')) return 'substack.com'; | |
| if (document.querySelector('img[src*="beehiiv"], link[href*="beehiiv"], script[src*="beehiiv"]') || document.documentElement.innerHTML.includes('media.beehiiv.com')) return 'beehiiv'; | |
| return null; | |
| } | |
| /* ── Platform extractors ── */ | |
| const extractors = { | |
| /* ── X (Twitter) ── */ | |
| 'x.com': () => { | |
| const articleView = document.querySelector('[data-testid="twitterArticleReadView"]'); | |
| const isArticle = !!articleView; | |
| const userEl = document.querySelector('[data-testid="User-Name"]'); | |
| let author = '', handle = ''; | |
| if (userEl) { | |
| for (const s of userEl.querySelectorAll('span')) { | |
| const t = s.innerText.trim(); | |
| if (t.startsWith('@')) { handle = t; break; } | |
| } | |
| const n = userEl.querySelector('span span'); | |
| if (n) author = n.innerText.trim(); | |
| } | |
| if (!author) { | |
| const m = (document.title || '').split(' on X:')[0]; | |
| if (m !== document.title) author = m.trim(); | |
| } | |
| if (!handle) { | |
| const m = url.match(/x\.com\/([A-Za-z0-9_]+)\//); | |
| if (m) handle = '@' + m[1]; | |
| } | |
| if (isArticle) { | |
| const titleEl = document.querySelector('[data-testid="twitter-article-title"]'); | |
| const articleTitle = titleEl ? titleEl.innerText.trim() : ''; | |
| const paragraphs = [...articleView.querySelectorAll('.longform-unstyled')] | |
| .map(b => b.innerText.trim()).filter(Boolean); | |
| const articleText = paragraphs.join('\n\n'); | |
| const imgs = allImages('[data-testid="twitterArticleReadView"] [data-testid="tweetPhoto"] img'); | |
| const hasVideo = articleView.querySelectorAll('video').length > 0; | |
| let body = '# ' + articleTitle + '\n\n**By:** ' + author + ' ' + handle + '\n\n---\n\n' + articleText + '\n\n'; | |
| if (imgs) body += '---\n### Images\n\n' + imgs + '\n\n'; | |
| if (hasVideo) body += '---\n### Video\n[Watch on X](' + url + ')\n\n'; | |
| return { | |
| source: 'x-article', path: '0_inbox/x-article', | |
| author, handle, | |
| title: articleTitle || truncate(articleText, 10), | |
| filename: dateStr + ' ' + (articleTitle || 'X Article') + ' — ' + author, | |
| body, tags: ['x-article'] | |
| }; | |
| } | |
| const urlHandle = (url.match(/x\.com\/([A-Za-z0-9_]+)\//) || [])[1] || ''; | |
| const allArticles = document.querySelectorAll('article[data-testid="tweet"]'); | |
| const threadParts = []; | |
| const threadImgs = []; | |
| allArticles.forEach(article => { | |
| const nameEl = article.querySelector('[data-testid="User-Name"]'); | |
| if (!nameEl) return; | |
| let tweetHandle = ''; | |
| for (const s of nameEl.querySelectorAll('span')) { | |
| const t = s.innerText.trim(); | |
| if (t.startsWith('@')) { tweetHandle = t.replace('@', ''); break; } | |
| } | |
| if (tweetHandle.toLowerCase() !== urlHandle.toLowerCase()) return; | |
| const txtEl = article.querySelector('[data-testid="tweetText"]'); | |
| if (txtEl) threadParts.push(txtEl.innerText.trim()); | |
| article.querySelectorAll('[data-testid="tweetPhoto"] img').forEach(img => { | |
| if (img.src && !/emoji|avatar|profile/.test(img.src)) threadImgs.push(img.src); | |
| }); | |
| }); | |
| let tweetText = threadParts.filter(Boolean).join('\n\n'); | |
| if (!tweetText) tweetText = meta('meta[property="og:description"]') || ''; | |
| if (!tweetText) { | |
| const match = (document.title || '').match(/on X: [""\u201C](.+)[""\u201D]$/); | |
| if (match) tweetText = match[1]; | |
| } | |
| const isThread = threadParts.length > 1; | |
| const hasVideo = !!document.querySelector('video'); | |
| const imgMd = threadImgs.map(s => '').join('\n\n'); | |
| let body = '## ' + author + ' ' + handle + '\n\n' + tweetText + '\n\n'; | |
| if (imgMd) body += '---\n### Images\n\n' + imgMd + '\n\n'; | |
| if (hasVideo) body += '---\n### Video\n[Watch on X](' + url + ')\n\n'; | |
| return { | |
| source: isThread ? 'x-thread' : 'x-post', | |
| path: isThread ? '0_inbox/x-thread' : '0_inbox/x-post', | |
| author, handle, | |
| title: truncate(tweetText, 10), | |
| filename: dateStr + ' ' + truncate(tweetText, 8) + ' — ' + author, | |
| body, tags: [isThread ? 'x-thread' : 'x-post'] | |
| }; | |
| }, | |
| /* ── LinkedIn ── */ | |
| 'linkedin.com': () => { | |
| const authorEl = document.querySelector('.update-components-actor__title span[aria-hidden="true"]'); | |
| const author = authorEl ? authorEl.innerText.trim() : ''; | |
| const contentNode = document.querySelector('.tvm-parent-container span[dir="ltr"]'); | |
| let content = contentNode | |
| ? contentNode.outerHTML | |
| .replace(/<span><br><\/span>/g, '\n') | |
| .replace(/<\/?[^>]+(>|$)/g, '') | |
| .replace(/>/g, '>').replace(/</g, '<').replace(/&/g, '&') | |
| .replace(/\n{2,}/g, '\n\n').trim() | |
| : ''; | |
| const image = document.querySelector('.tvm-parent-container img, .feed-shared-image img, img.update-components-image__image'); | |
| const video = document.querySelector('video'); | |
| const ytIframe = document.querySelector('iframe[src*="youtube.com"], iframe[src*="youtu.be"]'); | |
| const ytLink = document.querySelector('a[href*="youtube.com/watch"], a[href*="youtu.be/"]'); | |
| let ytUrl = ''; | |
| if (ytIframe) { | |
| const m = (ytIframe.src || '').match(/embed\/([^?/]+)/); | |
| ytUrl = m ? 'https://www.youtube.com/watch?v=' + m[1] : ytIframe.src; | |
| } else if (ytLink) { | |
| ytUrl = ytLink.href; | |
| } | |
| const externalLink = document.querySelector('.feed-shared-article__link, a[data-tracking-control-name="feed-type-content"]'); | |
| const externalUrl = externalLink ? externalLink.href : ''; | |
| let body = '## ' + author + '\n\n' + content + '\n\n'; | |
| if (image) body += '---\n### Image\n\n\n'; | |
| if (ytUrl) { | |
| const videoId = (ytUrl.match(/[?&]v=([^&]+)/) || [])[1] || ''; | |
| body += '---\n### YouTube\n[](' + ytUrl + ')\n' + ytUrl + '\n\n'; | |
| } else if (video) { | |
| body += '---\n### Video\n[Watch on LinkedIn](' + url + ')\n\n'; | |
| } | |
| if (externalUrl && externalUrl !== ytUrl) { | |
| body += '---\n### Shared Link\n' + externalUrl + '\n\n'; | |
| } | |
| const tags = ['linkedin']; | |
| if (ytUrl) tags.push('youtube'); | |
| return { | |
| source: 'linkedin', path: '0_inbox/linkedin', | |
| author, handle: '', | |
| title: truncate(content, 10), | |
| filename: dateStr + ' LinkedIn — ' + author, | |
| body, tags | |
| }; | |
| }, | |
| /* ── GitHub ── */ | |
| 'github.com': () => { | |
| const repoName = meta('meta[property="og:title"]') || document.title.split('\u00b7')[0].trim(); | |
| const desc = meta('meta[property="og:description"]') || ''; | |
| const readmeEl = document.querySelector('article.markdown-body, #readme article'); | |
| const readme = readmeEl ? readmeEl.innerText.trim() : ''; | |
| const topics = [...document.querySelectorAll('.topic-tag')].map(el => el.innerText.trim()); | |
| const starEl = document.querySelector('#repo-stars-counter-star'); | |
| const stars = starEl ? starEl.innerText.trim() : ''; | |
| const parts = url.replace('https://github.com/', '').split('/'); | |
| let body = '## ' + repoName + '\n\n'; | |
| if (desc) body += '> ' + desc + '\n\n'; | |
| if (stars) body += '**Stars:** ' + stars + '\n'; | |
| if (topics.length) body += '**Topics:** ' + topics.join(', ') + '\n'; | |
| body += '\n---\n\n### README\n\n' + truncate(readme, 500); | |
| return { | |
| source: 'github', path: '0_inbox/github', | |
| author: parts[0] || '', handle: parts[1] || '', | |
| title: repoName, | |
| filename: dateStr + ' ' + repoName, | |
| body, tags: ['github-repo', ...topics.slice(0, 5)] | |
| }; | |
| }, | |
| /* ── Reddit ── */ | |
| 'reddit.com': () => { | |
| const postTitle = text('h1') || meta('meta[property="og:title"]'); | |
| const postBody = text('[data-testid="post-rtjson-content"], [slot="text-body"]') || meta('meta[property="og:description"]'); | |
| const subreddit = (url.match(/\/r\/([^/]+)/) || [])[1] || ''; | |
| const authorName = text('[data-testid="post_author_link"]') || ''; | |
| const imgs = allImages('[data-testid="post-media-container"] img, [slot="post-media-container"] img'); | |
| let body = '## ' + postTitle + '\n\n**r/' + subreddit + '** \u00b7 ' + authorName + '\n\n' + postBody + '\n'; | |
| if (imgs) body += '\n---\n### Media\n\n' + imgs; | |
| return { | |
| source: 'reddit', path: '0_inbox/reddit', | |
| author: authorName, handle: 'r/' + subreddit, | |
| title: postTitle, | |
| filename: dateStr + ' Reddit r/' + subreddit, | |
| body, tags: ['reddit', subreddit] | |
| }; | |
| }, | |
| /* ── YouTube ── */ | |
| 'youtube.com': () => { | |
| const title = meta('meta[name="title"]') || text('h1.ytd-watch-metadata') || document.title; | |
| const channel = text('#channel-name a, ytd-channel-name a') || meta('meta[name="author"]'); | |
| const desc = text('#description-inner ytd-text-inline-expander, #description ytd-text-inline-expander') || meta('meta[name="description"]'); | |
| const thumb = meta('meta[property="og:image"]'); | |
| let body = '\n\n'; | |
| if (thumb) body += '\n\n'; | |
| body += '**Channel:** ' + channel + '\n\n### Description\n\n' + desc + '\n'; | |
| return { | |
| source: 'youtube', path: '0_inbox/youtube', | |
| author: channel, handle: '', | |
| title, | |
| filename: channel + ' – ' + title, | |
| body, tags: ['youtube'] | |
| }; | |
| }, | |
| /* ── Medium ── */ | |
| 'medium.com': () => { | |
| const title = text('h1') || meta('meta[property="og:title"]'); | |
| const author = meta('meta[name="author"]') || text('a[data-testid="authorName"]'); | |
| const content = text('article') || meta('meta[property="og:description"]'); | |
| return { | |
| source: 'medium', path: '0_inbox/medium', | |
| author, handle: '', | |
| title, | |
| filename: dateStr + ' Medium — ' + author, | |
| body: '## ' + title + '\n\n**By:** ' + author + '\n\n' + content + '\n', | |
| tags: ['medium'] | |
| }; | |
| }, | |
| /* ── Substack ── */ | |
| 'substack.com': () => { | |
| const title = text('h1.post-title') || text('h1') || meta('meta[property="og:title"]'); | |
| const subtitle = text('h3.subtitle') || ''; | |
| const author = text('.byline-name, .pencraft.pc-display-flex a') || meta('meta[name="author"]'); | |
| const content = text('.body.markup, .available-content') || meta('meta[property="og:description"]'); | |
| let body = '## ' + title + '\n\n'; | |
| if (subtitle) body += '*' + subtitle + '*\n\n'; | |
| body += '**By:** ' + author + '\n\n---\n\n' + content + '\n'; | |
| return { | |
| source: 'substack', path: '0_inbox/substack', | |
| author, handle: '', | |
| title, | |
| filename: dateStr + ' ' + title, | |
| body, tags: ['substack'] | |
| }; | |
| }, | |
| /* ── Beehiiv ── */ | |
| 'beehiiv': () => { | |
| const title = text('h1') || meta('meta[property="og:title"]'); | |
| const author = meta('meta[name="author"]') || ''; | |
| const postBody = document.querySelector('[class*="post-content"], [class*="post-body"]'); | |
| const content = postBody ? postBody.innerText.trim() : (text('main') || meta('meta[property="og:description"]')); | |
| const ogImage = meta('meta[property="og:image"]'); | |
| let body = '## ' + title + '\n\n'; | |
| body += '**By:** ' + author + '\n\n---\n\n' + content + '\n'; | |
| if (ogImage) body += '\n---\n### Cover\n\n'; | |
| return { | |
| source: 'beehiiv', path: '0_inbox/beehiiv', | |
| author, handle: '', | |
| title, | |
| filename: dateStr + ' ' + title + (author ? ' — ' + author : ''), | |
| body, tags: ['beehiiv'] | |
| }; | |
| }, | |
| /* ── arXiv ── */ | |
| 'arxiv.org': () => { | |
| const title = (text('.title') || meta('meta[property="og:title"]')).replace(/^Title:\s*/i, ''); | |
| const authors = (text('.authors') || '').replace(/^Authors:\s*/i, ''); | |
| const abstract = (text('.abstract') || meta('meta[property="og:description"]')).replace(/^Abstract:\s*/i, ''); | |
| const paperId = (url.match(/\d{4}\.\d{4,5}/) || [])[0] || ''; | |
| const subjects = text('.subjects') || ''; | |
| const submitDate = text('.dateline') || ''; | |
| let body = '## ' + title + '\n\n'; | |
| body += '**Authors:** ' + authors + '\n'; | |
| body += '**Paper:** ' + paperId + '\n'; | |
| if (submitDate) body += '**Date:** ' + submitDate + '\n'; | |
| if (subjects) body += '**Subjects:** ' + subjects + '\n'; | |
| body += '\n### Abstract\n\n' + abstract + '\n'; | |
| body += '\n---\n**PDF:** https://arxiv.org/pdf/' + paperId + '\n'; | |
| return { | |
| source: 'arxiv', path: '0_inbox/arxiv', | |
| author: authors.split(',')[0].trim(), handle: paperId, | |
| title, | |
| filename: dateStr + ' ' + title, | |
| body, tags: ['arxiv', 'paper'] | |
| }; | |
| }, | |
| /* ── Hugging Face ── */ | |
| 'huggingface.co': () => { | |
| // Detect page type: model, dataset, space, paper, or profile | |
| const pathParts = url.replace('https://huggingface.co/', '').split('/'); | |
| const isModel = !['datasets','spaces','papers','blog','docs'].includes(pathParts[0]) && pathParts.length >= 2; | |
| const isDataset = pathParts[0] === 'datasets'; | |
| const isSpace = pathParts[0] === 'spaces'; | |
| const isPaper = pathParts[0] === 'papers'; | |
| const pageTitle = meta('meta[property="og:title"]') || document.title.replace(' - Hugging Face', '').trim(); | |
| const pageDesc = meta('meta[property="og:description"]') || ''; | |
| if (isPaper) { | |
| // HF Papers page | |
| const paperTitle = text('h1') || pageTitle; | |
| const abstract = text('.pb-8 p') || text('[class*="abstract"]') || pageDesc; | |
| const paperAuthors = [...document.querySelectorAll('a[href*="/papers?author="]')] | |
| .map(a => a.innerText.trim()).filter(Boolean).join(', ') || ''; | |
| const paperId = pathParts[1] || ''; | |
| let body = '## ' + paperTitle + '\n\n'; | |
| if (paperAuthors) body += '**Authors:** ' + paperAuthors + '\n'; | |
| body += '**Paper:** ' + paperId + '\n'; | |
| body += '\n### Abstract\n\n' + abstract + '\n'; | |
| body += '\n---\n**arXiv:** https://arxiv.org/abs/' + paperId + '\n'; | |
| return { | |
| source: 'hf-paper', path: '0_inbox/huggingface', | |
| author: paperAuthors.split(',')[0].trim(), handle: paperId, | |
| title: paperTitle, | |
| filename: dateStr + ' ' + paperTitle, | |
| body, tags: ['huggingface', 'paper'] | |
| }; | |
| } | |
| if (isModel) { | |
| // Model card | |
| const modelName = pathParts.slice(0, 2).join('/'); | |
| const modelCard = text('.prose') || text('[class*="model-card"]') || ''; | |
| const downloads = text('[title*="downloads"]') || ''; | |
| const likes = text('[title*="likes"]') || ''; | |
| const tags = [...document.querySelectorAll('.tag-container a, [class*="tag"] a')] | |
| .map(a => a.innerText.trim()).filter(Boolean); | |
| const pipelineTag = text('[data-target="pipeline-tag"]') || ''; | |
| let body = '## ' + modelName + '\n\n'; | |
| if (pageDesc) body += '> ' + pageDesc + '\n\n'; | |
| if (pipelineTag) body += '**Pipeline:** ' + pipelineTag + '\n'; | |
| if (downloads) body += '**Downloads:** ' + downloads + '\n'; | |
| if (likes) body += '**Likes:** ' + likes + '\n'; | |
| if (tags.length) body += '**Tags:** ' + tags.join(', ') + '\n'; | |
| body += '\n---\n\n### Model Card\n\n' + truncate(modelCard, 500) + '\n'; | |
| return { | |
| source: 'hf-model', path: '0_inbox/huggingface', | |
| author: pathParts[0] || '', handle: modelName, | |
| title: modelName, | |
| filename: dateStr + ' ' + modelName, | |
| body, tags: ['huggingface', 'model', ...tags.slice(0, 5)] | |
| }; | |
| } | |
| if (isDataset) { | |
| const datasetName = pathParts.slice(1, 3).join('/'); | |
| const datasetCard = text('.prose') || text('[class*="dataset-card"]') || ''; | |
| const downloads = text('[title*="downloads"]') || ''; | |
| let body = '## ' + datasetName + '\n\n'; | |
| if (pageDesc) body += '> ' + pageDesc + '\n\n'; | |
| if (downloads) body += '**Downloads:** ' + downloads + '\n'; | |
| body += '\n---\n\n### Dataset Card\n\n' + truncate(datasetCard, 500) + '\n'; | |
| return { | |
| source: 'hf-dataset', path: '0_inbox/huggingface', | |
| author: pathParts[1] || '', handle: datasetName, | |
| title: datasetName, | |
| filename: dateStr + ' ' + datasetName, | |
| body, tags: ['huggingface', 'dataset'] | |
| }; | |
| } | |
| if (isSpace) { | |
| const spaceName = pathParts.slice(1, 3).join('/'); | |
| let body = '## ' + spaceName + '\n\n'; | |
| if (pageDesc) body += '> ' + pageDesc + '\n\n'; | |
| body += '[Open Space](' + url + ')\n'; | |
| return { | |
| source: 'hf-space', path: '0_inbox/huggingface', | |
| author: pathParts[1] || '', handle: spaceName, | |
| title: spaceName, | |
| filename: dateStr + ' ' + spaceName, | |
| body, tags: ['huggingface', 'space'] | |
| }; | |
| } | |
| // Fallback for other HF pages (blog, docs, profiles) | |
| const content = text('article') || text('main') || pageDesc; | |
| let body = '## ' + pageTitle + '\n\n' + content + '\n'; | |
| return { | |
| source: 'huggingface', path: '0_inbox/huggingface', | |
| author: '', handle: '', | |
| title: pageTitle, | |
| filename: dateStr + ' HF — ' + pageTitle, | |
| body, tags: ['huggingface'] | |
| }; | |
| }, | |
| /* ── ChatGPT ── */ | |
| 'chatgpt.com': () => { | |
| const title = document.title || 'ChatGPT Conversation'; | |
| const msgs = document.querySelectorAll('[data-message-author-role]'); | |
| let content = [...msgs] | |
| .map(msg => { | |
| const role = msg.getAttribute('data-message-author-role'); | |
| const txt = msg.innerText.trim(); | |
| return txt ? '**' + (role === 'user' ? 'You' : 'ChatGPT') + ':**\n' + txt : ''; | |
| }) | |
| .filter(Boolean) | |
| .join('\n\n'); | |
| if (!content) content = text('main') || ''; | |
| return { | |
| source: 'chatgpt', path: '0_inbox/chatgpt', | |
| author: 'ChatGPT', handle: '', | |
| title, | |
| filename: title + ' - ChatGPT', | |
| body: '## ' + title + '\n\n' + content, | |
| tags: ['chatgpt'] | |
| }; | |
| } | |
| }; | |
| /* ── Default fallback ── */ | |
| function defaultExtractor() { | |
| const title = meta('meta[property="og:title"]') || document.title; | |
| const author = meta('meta[name="author"]') || meta('meta[property="article:author"]') || ''; | |
| const published = meta('meta[property="article:published_time"]') || meta('meta[name="date"]') || ''; | |
| const desc = meta('meta[property="og:description"]') || meta('meta[name="description"]') || ''; | |
| const siteName = meta('meta[property="og:site_name"]') || host.split('.').slice(-2, -1)[0] || 'web'; | |
| const ogType = meta('meta[property="og:type"]'); | |
| const isBlog = !!document.querySelector('article') || ogType === 'article' || !!published; | |
| const articleEl = document.querySelector('article'); | |
| const content = articleEl ? articleEl.innerText.trim() : (text('main') || desc); | |
| const ogImage = meta('meta[property="og:image"]'); | |
| let body = '# ' + title + '\n\n'; | |
| if (author) body += '**By:** ' + author + '\n'; | |
| if (siteName) body += '**Source:** ' + siteName + '\n'; | |
| if (published) body += '**Published:** ' + published.split('T')[0] + '\n'; | |
| body += '\n---\n\n' + content + '\n'; | |
| if (ogImage) body += '\n---\n### Cover\n\n'; | |
| return { | |
| source: isBlog ? 'blog' : siteName, | |
| path: isBlog ? '0_inbox/blog' : '0_inbox/clips', | |
| author, handle: '', | |
| title, | |
| filename: dateStr + ' ' + title + (author ? ' — ' + author : ''), | |
| body, tags: [isBlog ? 'blog' : 'clip'] | |
| }; | |
| } | |
| /* ── Detect → Extract → Inject ── */ | |
| let extractor = null; | |
| for (const [domain, fn] of Object.entries(extractors)) { | |
| if (host.includes(domain)) { extractor = fn; break; } | |
| } | |
| // Fingerprint detection for custom-domain newsletters | |
| if (!extractor) { | |
| const platform = detectPlatform(); | |
| if (platform && extractors[platform]) extractor = extractors[platform]; | |
| } | |
| const d = extractor ? extractor() : defaultExtractor(); | |
| const tags = autoTags(d.body, d.tags); | |
| const topicTags = tags.filter(t => ['ai','dev','startup','data','security','infra','web3'].includes(t)); | |
| const suggested = topicTags.length ? topicTags[0] : ''; | |
| const catInput = prompt( | |
| 'Category for this ' + d.source + '?\n\n' + | |
| 'Auto-detected: ' + (topicTags.length ? topicTags.join(', ') : 'none') + '\n\n' + | |
| 'Common: ai, security, dev, startup, data, infra, web3\n' + | |
| '(or type your own)', | |
| suggested | |
| ); | |
| const category = (catInput || suggested || 'uncategorized').trim().toLowerCase() | |
| .replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'uncategorized'; | |
| const sourceFolder = d.path.replace('0_inbox/', ''); | |
| d.path = '0_inbox/' + year + '/' + month + '/' + sourceFolder + '/' + category; | |
| if (!d.tags.includes(category)) d.tags.push(category); | |
| inject('note-title', d.title); | |
| inject('note-filename', d.filename); | |
| inject('note-author', d.author); | |
| inject('note-handle', d.handle); | |
| inject('note-body', d.body); | |
| inject('note-source', d.source); | |
| inject('note-path', d.path); | |
| inject('note-tags', tags.filter(Boolean).join(', ')); | |
| inject('note-url', url); | |
| inject('note-date', dateStr); | |
| alert('\u2705 ' + d.source.toUpperCase() + ' clipped!\n\n"' + truncate(d.title, 12) + '"\n\n\u2192 ' + d.path + '/\n\nOpen Web Clipper now.'); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment