Skip to content

Instantly share code, notes, and snippets.

@brandontan
Last active March 23, 2026 09:25
Show Gist options
  • Select an option

  • Save brandontan/48498ad449248d0e20ea2e86d9d01c94 to your computer and use it in GitHub Desktop.

Select an option

Save brandontan/48498ad449248d0e20ea2e86d9d01c94 to your computer and use it in GitHub Desktop.
Universal Web Clipper for Obsidian
(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 => '![](' + i.src + ')')
.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 => '![](' + 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(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/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![](' + image.src + ')\n\n';
if (ytUrl) {
const videoId = (ytUrl.match(/[?&]v=([^&]+)/) || [])[1] || '';
body += '---\n### YouTube\n[![YT](https://img.youtube.com/vi/' + videoId + '/0.jpg)](' + 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 = '![' + title + '](' + url + ')\n\n';
if (thumb) body += '![thumbnail](' + thumb + ')\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![](' + ogImage + ')\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![](' + ogImage + ')\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