feat(userscript): render Covers from the public route (#60)
Both userscripts stop scraping Covers and stop putting one on the wire. A scraped address has no rendering path left now that the backend acquires, stores and serves every Cover from its own origin (ADR-0007), and keeping one would reintroduce third-party URLs into exactly the place #47 came from. - Adapters no longer read og:image / meta[name=image], and comix's img[alt] cover scan and novelfull's metaName helper are deleted. - apiPut strips `cover` off every outgoing body, so a cached row's address (already ours) never travels back either. The server ignores the field regardless. - Both card renderers swap in the existing `.cover.ph` placeholder when the image fails to load, so a failure looks designed rather than broken - the other half of what #47 reported. - The deleted scraping's test cases go with it: the comix cover cases, the img[alt] and meta[name] stub branches, and the stale og:image fixtures. Export lists are unchanged; nothing cover-specific was exported. The cover route itself is already public and uncredentialed, with the immutable cache directive and 404-for-unknown covered by the tests that landed with #59.
This commit is contained in:
@@ -56,9 +56,10 @@
|
||||
// ============================================================
|
||||
// Site adapters
|
||||
//
|
||||
// Page type + IDs come from URL regex (most stable); title/cover come from
|
||||
// og: meta tags. Verified live 2026-07-24 against asurascans.com and
|
||||
// demonicscans.org — see README "Adapter reference".
|
||||
// Page type + IDs come from URL regex (most stable); the title comes from
|
||||
// og: meta tags. Covers are never read here: the backend acquires and serves
|
||||
// them itself (ADR-0007). Verified live 2026-07-24 against asurascans.com
|
||||
// and demonicscans.org — see README "Adapter reference".
|
||||
// ============================================================
|
||||
|
||||
function meta(prop) {
|
||||
@@ -128,7 +129,6 @@
|
||||
site: this.site,
|
||||
seriesId: stripBuildHash(m[1]),
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: meta("og:image") || "",
|
||||
seriesUrl: loc.origin + "/comics/" + m[1],
|
||||
chapterLabel: "Chapter " + m[2],
|
||||
chapterNum: isNaN(num) ? null : num,
|
||||
@@ -143,7 +143,6 @@
|
||||
site: this.site,
|
||||
seriesId: stripBuildHash(m[1]),
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: meta("og:image") || "",
|
||||
seriesUrl: loc.origin + "/comics/" + m[1],
|
||||
chapterLabel: null,
|
||||
chapterNum: null,
|
||||
@@ -192,7 +191,6 @@
|
||||
site: this.site,
|
||||
seriesId: decodeURIComponent(m[1]),
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: meta("og:image") || "",
|
||||
seriesUrl: loc.origin + "/manga/" + m[1],
|
||||
chapterLabel: "Chapter " + m[2],
|
||||
chapterNum: isNaN(num) ? null : num,
|
||||
@@ -207,7 +205,6 @@
|
||||
site: this.site,
|
||||
seriesId: decodeURIComponent(m[1]),
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: meta("og:image") || "",
|
||||
seriesUrl: loc.origin + "/manga/" + m[1],
|
||||
chapterLabel: null,
|
||||
chapterNum: null,
|
||||
@@ -259,7 +256,6 @@
|
||||
site: this.site,
|
||||
seriesId: comixSeriesId(m[1]),
|
||||
title: pageTitle,
|
||||
cover: coverFromPage(pageTitle),
|
||||
seriesUrl: loc.origin + "/title/" + m[1],
|
||||
chapterLabel: "Chapter " + m[2],
|
||||
chapterNum: isNaN(num) ? null : num,
|
||||
@@ -274,7 +270,6 @@
|
||||
site: this.site,
|
||||
seriesId: comixSeriesId(m[1]),
|
||||
title: pageTitle,
|
||||
cover: coverFromPage(pageTitle),
|
||||
seriesUrl: loc.origin + "/title/" + m[1],
|
||||
chapterLabel: null,
|
||||
chapterNum: null,
|
||||
@@ -289,17 +284,6 @@
|
||||
return t.replace(/\s*·\s*Ch\.[\d.]+\s*$/i, "").trim();
|
||||
}
|
||||
|
||||
// comix serves no og:image, so this is the one adapter that has to read
|
||||
// the DOM for a cover. Matching on alt rather than a class keeps it off
|
||||
// the site's styling: the cover is the image whose alt is the title.
|
||||
// Do not "simplify" this into meta("og:image") — that returns null.
|
||||
function coverFromPage(title) {
|
||||
if (!title || !document.querySelectorAll) return "";
|
||||
for (const img of document.querySelectorAll("img[alt]")) {
|
||||
if (img.getAttribute("alt") === title) return img.getAttribute("src") || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
// Scoped to this series' own id prefix so a recommendation strip's links
|
||||
// cannot win the maximum. seriesId is passed in because the anchors alone
|
||||
@@ -342,7 +326,6 @@
|
||||
site: this.site,
|
||||
seriesId: m[1],
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: meta("og:image") || "",
|
||||
seriesUrl: loc.origin + "/series/" + m[1],
|
||||
chapterLabel: num === null ? null : "Chapter " + num,
|
||||
chapterNum: num,
|
||||
@@ -357,7 +340,6 @@
|
||||
site: this.site,
|
||||
seriesId: m[1],
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: meta("og:image") || "",
|
||||
seriesUrl: loc.origin + "/series/" + m[1],
|
||||
chapterLabel: null,
|
||||
chapterNum: null,
|
||||
@@ -498,6 +480,10 @@
|
||||
async function apiPut(key, obj, { sendStatus = false } = {}) {
|
||||
const body = Object.assign({}, obj);
|
||||
if (!sendStatus) delete body.status;
|
||||
// Covers belong to the backend, which acquires and serves them itself
|
||||
// (ADR-0007) and ignores an incoming one; a third-party address must never
|
||||
// go back on the wire.
|
||||
delete body.cover;
|
||||
const res = await fetch(API_BASE + "/bookmarks/" + encodeURIComponent(key), {
|
||||
method: "PUT",
|
||||
headers: authHeaders({ "Content-Type": "application/json" }),
|
||||
@@ -855,7 +841,6 @@
|
||||
series_id: p.seriesId,
|
||||
title: p.title || (existing && existing.title) || p.seriesId,
|
||||
series_url: p.seriesUrl || (existing && existing.series_url) || "",
|
||||
cover: p.cover || (existing && existing.cover) || "",
|
||||
last_chapter: p.chapterLabel || (existing && existing.last_chapter) || "",
|
||||
last_chapter_num:
|
||||
p.chapterNum != null ? p.chapterNum : existing ? existing.last_chapter_num : null,
|
||||
@@ -878,7 +863,6 @@
|
||||
series_id: p.seriesId,
|
||||
title: existing.title || p.title || p.seriesId,
|
||||
series_url: existing.series_url || p.seriesUrl || "",
|
||||
cover: existing.cover || p.cover || "",
|
||||
last_chapter: p.chapterLabel || existing.last_chapter || "",
|
||||
last_chapter_num: p.chapterNum != null ? p.chapterNum : existing.last_chapter_num,
|
||||
last_chapter_url: p.chapterUrl || "",
|
||||
@@ -1394,7 +1378,15 @@
|
||||
return el("div", { class: "item" + heat }, [
|
||||
el("a", { class: "go", href: cont }, [
|
||||
b.cover
|
||||
? el("img", { class: "cover", src: b.cover, loading: "lazy", alt: "" })
|
||||
? el("img", {
|
||||
class: "cover",
|
||||
src: b.cover,
|
||||
loading: "lazy",
|
||||
alt: "",
|
||||
// A Cover that will not load shows the designed placeholder
|
||||
// rather than the browser's broken-image glyph (#47).
|
||||
onerror: (e) => e.target.replaceWith(el("div", { class: "cover ph" })),
|
||||
})
|
||||
: el("div", { class: "cover ph" }),
|
||||
]),
|
||||
el("div", { class: "meta" }, [
|
||||
|
||||
Reference in New Issue
Block a user