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
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:
@@ -10,6 +10,7 @@ import { up as up0008ChatMessages } from "./migrations/0008_chat_messages.ts";
|
||||
import { up as up0009ChatReply } from "./migrations/0009_chat_reply.ts";
|
||||
import { up as up0010YoutubeEmbedStart } from "./migrations/0010_youtube_embed_start.ts";
|
||||
import { up as up0011SplitFaviconThumbnail } from "./migrations/0011_split_favicon_thumbnail.ts";
|
||||
import { up as up0012FixFaviconReclassification } from "./migrations/0012_fix_favicon_reclassification.ts";
|
||||
|
||||
interface Migration {
|
||||
name: string;
|
||||
@@ -31,6 +32,10 @@ const MIGRATIONS: Migration[] = [
|
||||
{ name: "0009_chat_reply", up: up0009ChatReply },
|
||||
{ name: "0010_youtube_embed_start", up: up0010YoutubeEmbedStart },
|
||||
{ name: "0011_split_favicon_thumbnail", up: up0011SplitFaviconThumbnail },
|
||||
{
|
||||
name: "0012_fix_favicon_reclassification",
|
||||
up: up0012FixFaviconReclassification,
|
||||
},
|
||||
];
|
||||
|
||||
export function runMigrations(db: DatabaseSync): void {
|
||||
|
||||
100
api/db/migrations/0012_fix_favicon_reclassification.ts
Normal file
100
api/db/migrations/0012_fix_favicon_reclassification.ts
Normal 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)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user