Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
4.1 kB · 105 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106// Reads standard.site link-tag hints from the page head and reports them to// the background worker. Kept dependency-free; built as an IIFE.
// No exports — this only marks the file as a module, so src/content.test.ts// can import it and so the declaration below stays out of the global scope.export {}
// The Navigation API is not in TypeScript's DOM lib yet (5.9 ships only// NavigationHistoryEntry), so declare the sliver we use. Access is guarded by// `typeof`, which is safe for an identifier the browser does not define.declare const navigation: EventTarget | undefined
function hint(rel: string): string | undefined { const el = document.querySelector<HTMLLinkElement>(`link[rel="${rel}"]`) const href = el?.getAttribute('href')?.trim() return href && href.startsWith('at://') ? href : undefined}
// After the extension reloads, orphaned copies of this script keep running in// open tabs but their chrome.runtime binding is dead: runtime.id is undefined// and sendMessage throws synchronously ("Extension context invalidated").function orphaned(): boolean { return !chrome.runtime?.id}
function readHints(): { pubHint?: string; docHint?: string } { return { pubHint: hint('site.standard.publication'), docHint: hint('site.standard.document'), }}
// Detection is keyed on the page URL and the two hints; re-sending an// unchanged triple would only make the worker redo the same lookups. Chrome// fires a navigatesuccess for the initial page load too — measured, it lands// just after the load event — so without this every page would report twice.let lastReported: string | undefined
function report() { if (orphaned()) return const hints = readHints() const key = `${location.href}\n${hints.pubHint ?? ''}\n${hints.docHint ?? ''}` if (key === lastReported) return lastReported = key chrome.runtime.sendMessage({ type: 'page-hints', ...hints }).catch(() => { // background worker not ready; it will probe on demand instead })}
report()
// The background worker asks for hints again on manual refresh.chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg?.type === 'read-hints') { sendResponse(readHints()) }})
// SPA navigations swap head tags without a page load.//// The Navigation API reports each navigation once — pushState, replaceState,// fragment changes and back/forward included — so this script costs the pages// it runs on one listener instead of a callback per DOM mutation. Measured on// bsky.app (headless Chrome 149, ~20s of scrolling and one in-app link click),// the observer this replaces ran 66 times over 448 mutation records to notice// one URL change; the listener below runs twice.//// `navigatesuccess` fires once the navigation has committed, so location.href// is already the new one. Routers often swap the head tags a tick later, so// read again after the page settles; report() drops that second read unless// something actually changed.const SETTLE_MS = 400let settleTimer: ReturnType<typeof setTimeout> | undefined
if (typeof navigation !== 'undefined') { const onNavigated = () => { if (orphaned()) { navigation.removeEventListener('navigatesuccess', onNavigated) clearTimeout(settleTimer) return } console.debug('[substandard] spa navigation', location.href) report() clearTimeout(settleTimer) settleTimer = setTimeout(report, SETTLE_MS) } navigation.addEventListener('navigatesuccess', onNavigated)} else { // Browsers without the Navigation API (it is Chrome-only as of writing). // A content script runs in an isolated world, where patching pushState is // invisible to the page, so there is nothing cheaper to fall back to than // the mutation watch this replaces. let lastUrl = location.href const observer = new MutationObserver(() => { if (orphaned()) { observer.disconnect() return } if (location.href !== lastUrl) { lastUrl = location.href report() } }) observer.observe(document, { subtree: true, childList: true })}