import type { DatabaseSync } from "node:sqlite"; // Moves favicon-shaped values out of `rich_content.thumbnailUrl` into the new // `faviconUrl` field. // // The extraction cascade used to end at the page's icon and then at a guessed // `${origin}/favicon.ico`, so `thumbnailUrl` was almost never empty — it just // held a 16×16 icon (or a 404) that the UI then cover-cropped into a 128×72 // box. The cascade now stops at real artwork, and an absent `thumbnailUrl` // means "no artwork", which is what lets the frontend draw a placeholder. // This migration gives rows written before that change the same meaning. // // Purely local — it classifies the already-stored URL and makes no network // calls, so `accentColor` is deliberately not backfilled: the frontend derives // a stable hue from the hostname whenever one is missing, and // `refreshDumpMetadata` fetches the real color on demand. // // Idempotent: rows that already carry a `faviconUrl` are skipped, so a fresh // database built from schema.sql is a no-op. /** * Whether a stored thumbnail URL is really a site icon. * * Deliberately loose. A false positive (a genuine cover image living under * `/assets/icons/`) renders contained on a tinted field instead of * cover-cropped — mildly wrong, never broken — so chasing them isn't worth the * extra rules. */ function isIconUrl(raw: string): boolean { let pathname: string; try { pathname = new URL(raw).pathname; } catch { return false; } return /favicon|apple-touch-icon|\/icons?\//i.test(pathname) || /\.(ico|svg)$/i.test(pathname); } export function up(db: DatabaseSync): void { const rows = db.prepare( `SELECT id, rich_content FROM dumps WHERE kind = 'url' AND rich_content IS NOT NULL;`, ).all() as { id: string; rich_content: string }[]; const update = db.prepare( `UPDATE dumps SET rich_content = ? WHERE id = ?;`, ); let patched = 0; for (const row of rows) { let rich: { thumbnailUrl?: string; faviconUrl?: string }; try { rich = JSON.parse(row.rich_content); } catch { continue; // malformed payload — leave it untouched } if (rich.faviconUrl || !rich.thumbnailUrl) continue; if (!isIconUrl(rich.thumbnailUrl)) continue; const { thumbnailUrl: _dropped, ...rest } = rich; update.run( JSON.stringify({ ...rest, faviconUrl: rich.thumbnailUrl }), row.id, ); patched++; } if (patched > 0) { console.log( `[migrate] 0011: reclassified ${patched} favicon thumbnail(s)`, ); } }