Changeset 0101d0a in Klonkt for src/services/EmbedResolver.js
- Timestamp:
- 07/28/2026 08:22:37 AM (6 weeks ago)
- Branches:
- main
- Children:
- c52dc82
- Parents:
- b258a79
- File:
-
- 1 edited
-
src/services/EmbedResolver.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/EmbedResolver.js
rb258a79 r0101d0a 35 35 function decodeEntities(s) { 36 36 return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'"); 37 } 38 39 /** 40 * OpenGraph, the one that actually carries link previews on the open web. 41 * oEmbed is the richer protocol but most sites simply do not implement it; 42 * og:image / og:title is what Mastodon and everyone else reads, so it is the 43 * fallback that makes thumbnails appear at all. Same page fetch as the oEmbed 44 * discovery, so it costs nothing extra. 45 */ 46 export function findOpenGraph(html) { 47 if (!html || typeof html !== 'string') return null; 48 const meta = {}; 49 for (const tag of html.match(/<meta\b[^>]*>/gi) || []) { 50 const key = (tag.match(/\b(?:property|name)\s*=\s*["']([^"']+)["']/i) || [])[1]; 51 if (!key) continue; 52 const k = key.toLowerCase(); 53 if (!/^(og:image|og:title|og:site_name|og:description|twitter:image|twitter:title)$/.test(k)) continue; 54 const val = (tag.match(/\bcontent\s*=\s*["']([^"']*)["']/i) || [])[1]; 55 if (val && !meta[k]) meta[k] = decodeEntities(val); 56 } 57 const image = meta['og:image'] || meta['twitter:image']; 58 const title = meta['og:title'] || meta['twitter:title']; 59 if (!image && !title) return null; 60 return { image: image && /^https?:\/\//i.test(image) ? image : null, title: title || null, site: meta['og:site_name'] || null }; 37 61 } 38 62 … … 118 142 } 119 143 120 // 3. oEmbed: the generic path for everything else. 121 if (io.getPage && io.getJSON) { 144 // 3. oEmbed, then OpenGraph. One page fetch serves both: oEmbed is the richer 145 // protocol, OpenGraph is the one most of the web actually ships. 146 if (io.getPage) { 122 147 const page = await io.getPage(url).catch(() => null); 123 const endpoint = findOEmbedEndpoint(page); 124 if (endpoint) { 125 const o = await io.getJSON(endpoint).catch(() => null); 126 const card = fromOEmbed(url, o); 127 if (card) return card; 148 if (page) { 149 const endpoint = findOEmbedEndpoint(page); 150 if (endpoint && io.getJSON) { 151 const o = await io.getJSON(endpoint).catch(() => null); 152 const card = fromOEmbed(url, o); 153 if (card) return card; 154 } 155 const og = findOpenGraph(page); 156 if (og) { 157 return { 158 kind: 'opengraph', 159 url, 160 title: og.title, 161 author: og.site ? { name: og.site, handle: null, icon: null } : null, 162 provider: og.site, 163 html: null, 164 media: og.image ? [{ url: og.image, type: 'image/*' }] : [], 165 }; 166 } 128 167 } 129 168 } … … 140 179 141 180 const MAX_BODY = 512_000; // an oEmbed page/endpoint is small; refuse the rest 142 143 async function safeText(safeFetch, url, accept) { 181 const UA = 'Mozilla/5.0 (compatible; Klonkt/1.0; +https://klonkt.com)'; 182 183 async function safeText(safeFetch, url, accept, extra = {}) { 144 184 try { 145 const r = await safeFetch(url, { headers: { Accept: accept } });185 const r = await safeFetch(url, { headers: { Accept: accept, ...extra } }); 146 186 if (!r.ok) return null; 147 187 if (Number(r.headers.get('content-length') || 0) > MAX_BODY) return null; … … 163 203 try { return JSON.parse(body); } catch { return null; } // an HTML page is simply not AP 164 204 }, 165 getPage: (u) => safeText(safeFetch, u, 'text/html'), 205 // Plenty of sites only hand out their OpenGraph tags to something that 206 // looks like a browser, so the page fetch identifies itself. 207 getPage: (u) => safeText(safeFetch, u, 'text/html', { 'User-Agent': UA }), 166 208 getJSON: async (u) => { 167 209 const body = await safeText(safeFetch, u, 'application/json'); … … 179 221 } 180 222 181 export default { resolveEmbed, findOEmbedEndpoint, looksLikeAPObject, fromOEmbed, fromAPObject, liveIO };223 export default { resolveEmbed, findOEmbedEndpoint, findOpenGraph, looksLikeAPObject, fromOEmbed, fromAPObject, liveIO };
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)