/** * Previews any page no other provider claims, for `rich-content-service.ts`, * from whatever metadata its HTML carries. * * @module */ import type { RichContent } from "../../../shared/dump.ts"; import type { RichContentProvider } from "./provider.ts"; import { siteHost } from "../../../shared/url.ts"; import { extractBestIcon, extractFirstContentImage, extractJsonLd, extractLargeImage, extractMaskIconColor, extractMetaName, extractOgTag, extractPageTitle, extractThemeColor, normalizeCssColor, } from "../html.ts"; import { fetchWithTimeout, readTextCapped } from "../fetch.ts"; /** * Matches every URL, so it comes last in the service's list. Each field falls * back from OG tags to Twitter tags, JSON-LD and the markup itself. The icon * and brand color feed the placeholder the frontend draws when there is no * thumbnail. */ export const genericProvider: RichContentProvider = { name: "generic", matches(_url: string): boolean { return true; }, async fetch(url: string): Promise { const res = await fetchWithTimeout(url); const contentType = res.headers.get("content-type") ?? ""; if (!contentType.startsWith("text/html")) { await res.body?.cancel(); return { type: "generic", url }; } const html = await readTextCapped(res); const ld = extractJsonLd(html); // An og:url naming another page (often the home page) means the OG tags // are the site's defaults, not this page's: its title and image are // skipped. const ogUrl = extractOgTag(html, "url"); const useOg = !ogUrl || (() => { try { const ogPath = new URL(ogUrl).pathname.replace(/\/+$/, "") || "/"; const pagePath = new URL(url).pathname.replace(/\/+$/, "") || "/"; return ogPath === pagePath; } catch { return true; } })(); const title = (useOg ? extractOgTag(html, "title") : undefined) ?? extractMetaName(html, "twitter:title") ?? ld.title ?? extractPageTitle(html); const siteName = extractOgTag(html, "site_name") ?? siteHost(url); const description = extractOgTag(html, "description") ?? extractMetaName(html, "twitter:description") ?? ld.description ?? extractMetaName(html, "description"); // Never the favicon: a 16×16 icon cropped into a thumbnail looks broken, // and the placeholder drawn instead shows the icon properly. const thumbnailUrl = (useOg ? extractOgTag(html, "image") : undefined) ?? extractMetaName(html, "twitter:image") ?? ld.thumbnailUrl ?? extractLargeImage(html, url) ?? extractFirstContentImage(html, url); // `/favicon.ico` is a guess. When it doesn't load, the placeholder shows // the site's initial. const faviconUrl = extractBestIcon(html, url) ?? `${new URL(url).origin}/favicon.ico`; const accentColor = normalizeCssColor( extractThemeColor(html) ?? extractMetaName(html, "msapplication-TileColor") ?? extractMaskIconColor(html), ); return { type: "generic", url, title, description, thumbnailUrl, faviconUrl, accentColor, siteName, }; }, };