v3: fix migration 0011 mistaking real og:images for favicons
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 40s

0011 tested for "favicon" anywhere in the path, so mirtitles.org's declared
og:image — /wp-content/uploads/2022/04/mir-logo-favicon.png — was moved into
faviconUrl and its thumbnailUrl cleared. Without a thumbnail, hasThumbnail() is
false and the journal mosaic demoted the dump from an image card to a
pull-quote. A blanket .svg match did the same to real artwork (tanibis.net's
header.svg).

0012 judges an icon by its filename rather than the whole path: named
favicon…/apple-touch-icon…/icon…, living in an icons/ directory, or ending in
.ico. That spares mir-logo-favicon.png and also catches icon_SEARCH.png, which
0011 missed. It runs in both directions, and is deliberately narrow when
reversing — a value is only promoted back to thumbnailUrl if it matched 0011's
rule but not this one, so rows written by a normal fetch aren't disturbed.

On the dev database: 4 thumbnails restored, 1 icon reclassified, real favicons
left alone, and a second pass rewrites nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-08-11 19:51:49 +00:00
parent 1cb904d2cf
commit 7303509b6f
2 changed files with 105 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
import type { DatabaseSync } from "node:sqlite";
// Repairs 0011, which was too eager about what counts as a site icon.
//
// 0011 tested for "favicon" anywhere in the path, so a legitimate `og:image`
// like mirtitles.org's `/wp-content/uploads/2022/04/mir-logo-favicon.png` was
// moved into `faviconUrl` and its `thumbnailUrl` cleared — which also demoted
// the dump from an image card to a pull-quote in the journal mosaic. A blanket
// `.svg` match had the same effect on real artwork.
//
// `isIconUrl` here looks at the *filename* rather than the whole path: an icon
// is named `favicon…`, `apple-touch-icon…` or `icon…`, lives in an `icons/`
// directory, or ends in `.ico`. That both spares `mir-logo-favicon.png` and
// catches `icon_SEARCH.png`, which 0011 missed.
//
// Applied in both directions, and deliberately narrow when reversing: a value
// is only promoted back to `thumbnailUrl` if it matches 0011's rule but not
// this one, so rows written by a normal fetch aren't disturbed.
/** 0011's rule, kept verbatim so its false positives can be identified. */
function wasIconUrl0011(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);
}
/** Whether a URL names a site icon, judged on its filename and directory. */
function isIconUrl(raw: string): boolean {
let pathname: string;
try {
pathname = new URL(raw).pathname;
} catch {
return false;
}
const segments = pathname.split("/").filter(Boolean);
const filename = segments.pop() ?? "";
if (/\.ico$/i.test(filename)) return true;
if (segments.some((s) => /^(favicons?|icons?)$/i.test(s))) return true;
const basename = filename.replace(/\.[^.]+$/, "");
return /^(favicon|apple-touch-icon|icon)(?=$|[-_.\d])/i.test(basename);
}
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 restored = 0;
let reclassified = 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
}
// Artwork 0011 mistook for an icon: put it back.
if (
rich.faviconUrl && !rich.thumbnailUrl &&
wasIconUrl0011(rich.faviconUrl) && !isIconUrl(rich.faviconUrl)
) {
const { faviconUrl: _dropped, ...rest } = rich;
update.run(
JSON.stringify({ ...rest, thumbnailUrl: rich.faviconUrl }),
row.id,
);
restored++;
continue;
}
// An icon 0011's rule didn't recognize: move it now.
if (
rich.thumbnailUrl && !rich.faviconUrl && isIconUrl(rich.thumbnailUrl)
) {
const { thumbnailUrl: _dropped, ...rest } = rich;
update.run(
JSON.stringify({ ...rest, faviconUrl: rich.thumbnailUrl }),
row.id,
);
reclassified++;
}
}
if (restored > 0 || reclassified > 0) {
console.log(
`[migrate] 0012: restored ${restored} thumbnail(s), reclassified ${reclassified} icon(s)`,
);
}
}