fix(userscript): read comix titles from document.title, parse kagane volume chapters
comix.to is an SPA whose client router rewrites document.title but never
touches the server-rendered og:title. The adapter read og:title, so a
bookmark taken after a cold load got the homepage's title ("Comix - Read
Comics online for free") and one taken after an in-page hop got the
previous series' title. Titles now come from document.title, with the
chapter page's " - Ch.<n>" tail stripped.
comix also serves no og:image at all, which is why every comix bookmark
fell back to the monogram placeholder. The cover is now the img whose alt
matches the cleaned title.
Both fixes need the page to have finished its client-side route change,
and comix fills document.title a beat after the URL changes - later than
the nav watcher's 300ms snapshot. The watcher therefore also re-detects
when the detect() signature changes, not only when the URL does.
kagane reader URLs carry no chapter number, so it comes out of og:title.
Volume-numbered series render "<Series> - Volume <v> Chapter <n>" with no
episode name, a shape the suffix regex did not match. One unmatched title
caused both reported symptoms: the volume tail stayed in the stored title
("SP Baby - Volume 1 Chapter 1"), and chapterNum came back null so no
chapter was ever recorded for the series. The regex now takes an optional
"Volume <v> " segment.
All three page shapes were captured live on 2026-08-08 and are pinned as
regression tests in userscript/test/logic.test.js.
This commit is contained in:
@@ -242,6 +242,12 @@
|
||||
matches: (loc) => /(^|\.)comix\.to$/.test(loc.hostname),
|
||||
detect(loc) {
|
||||
const path = loc.pathname;
|
||||
// comix client-routes without ever rewriting og:title — the head keeps
|
||||
// whatever the first server-rendered document carried, so a bookmark
|
||||
// taken after a client route got the homepage's title, then the
|
||||
// previous series'. document.title is the one thing its router does
|
||||
// update. Verified live 2026-08-08; do not "restore" meta("og:title").
|
||||
const pageTitle = cleanTitle(document.title);
|
||||
// /title/<id>-<slug>/<uploadId>-chapter-<n>. Several uploads (different
|
||||
// groups or languages) share one chapter number; the number is the
|
||||
// progress identity, the upload id is not.
|
||||
@@ -252,8 +258,8 @@
|
||||
type: "chapter",
|
||||
site: this.site,
|
||||
seriesId: comixSeriesId(m[1]),
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: coverFromPage(),
|
||||
title: pageTitle,
|
||||
cover: coverFromPage(pageTitle),
|
||||
seriesUrl: loc.origin + "/title/" + m[1],
|
||||
chapterLabel: "Chapter " + m[2],
|
||||
chapterNum: isNaN(num) ? null : num,
|
||||
@@ -267,8 +273,8 @@
|
||||
type: "series",
|
||||
site: this.site,
|
||||
seriesId: comixSeriesId(m[1]),
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: coverFromPage(),
|
||||
title: pageTitle,
|
||||
cover: coverFromPage(pageTitle),
|
||||
seriesUrl: loc.origin + "/title/" + m[1],
|
||||
chapterLabel: null,
|
||||
chapterNum: null,
|
||||
@@ -277,7 +283,7 @@
|
||||
}
|
||||
return { type: "other" };
|
||||
|
||||
// comix chapter og:title is "<Title> · Ch.<n>"; series is clean.
|
||||
// comix chapter document.title is "<Title> · Ch.<n>"; series is clean.
|
||||
function cleanTitle(t) {
|
||||
if (!t) return "";
|
||||
return t.replace(/\s*·\s*Ch\.[\d.]+\s*$/i, "").trim();
|
||||
@@ -287,8 +293,7 @@
|
||||
// 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() {
|
||||
const title = cleanTitle(meta("og:title"));
|
||||
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") || "";
|
||||
@@ -313,6 +318,14 @@
|
||||
},
|
||||
};
|
||||
|
||||
// Kagane builds the reader og:title suffix out of the book's metadata, so
|
||||
// every combination occurs: the volume part appears only when the book has a
|
||||
// volume_no, the episode part only when it has a non-empty title. All four
|
||||
// shapes captured live 2026-08-08 — "SP Baby - Volume 1 Chapter 1" is the one
|
||||
// the old trailing-space regex missed, which left both the number and the
|
||||
// series title wrong.
|
||||
const KAGANE_CHAPTER_SUFFIX = /\s-\s(?:Volume\s[\d.]+\s)?Chapter\s([\d.]+)(?:\s-\s.*)?$/i;
|
||||
|
||||
const kagane = {
|
||||
site: "kagane",
|
||||
matches: (loc) => /(^|\.)kagane\.to$/.test(loc.hostname),
|
||||
@@ -353,9 +366,8 @@
|
||||
}
|
||||
return { type: "other" };
|
||||
|
||||
// Reader og:title is "<Title> - Chapter <n> - <episode name>".
|
||||
function chapterNumFromTitle(t) {
|
||||
const m = t && t.match(/\s-\sChapter\s([\d.]+)\s/);
|
||||
const m = t && t.match(KAGANE_CHAPTER_SUFFIX);
|
||||
if (!m) return null;
|
||||
const num = parseFloat(m[1]);
|
||||
return isNaN(num) ? null : num;
|
||||
@@ -363,7 +375,7 @@
|
||||
|
||||
function cleanTitle(t) {
|
||||
if (!t) return "";
|
||||
return t.replace(/\s-\sChapter\s[\d.]+\s-\s.*$/i, "").trim();
|
||||
return t.replace(KAGANE_CHAPTER_SUFFIX, "").trim();
|
||||
}
|
||||
},
|
||||
// Reader hrefs are uuids with no number in them, so no maximum can be taken
|
||||
@@ -1450,8 +1462,15 @@
|
||||
}
|
||||
|
||||
let lastUrl = location.href;
|
||||
function onNavigate() {
|
||||
let lastPageSig = "";
|
||||
|
||||
function setPage() {
|
||||
state.page = detect();
|
||||
lastPageSig = JSON.stringify(state.page);
|
||||
}
|
||||
|
||||
function onNavigate() {
|
||||
setPage();
|
||||
render();
|
||||
maybeAutoUpdate();
|
||||
maybeCaptureLatestOnSeriesPage();
|
||||
@@ -1484,7 +1503,14 @@
|
||||
wrap("pushState");
|
||||
wrap("replaceState");
|
||||
window.addEventListener("popstate", fire);
|
||||
setInterval(fire, 1500); // catch routes that bypass history
|
||||
// Also catches routes that bypass history — and comix, which fills
|
||||
// document.title a beat after the route changes, so the 300ms snapshot
|
||||
// above can still hold the previous page's title. Re-detect whenever what
|
||||
// we would read has changed, not only when the URL has.
|
||||
setInterval(() => {
|
||||
fire();
|
||||
if (JSON.stringify(detect()) !== lastPageSig) onNavigate();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -1563,7 +1589,7 @@
|
||||
|
||||
function init() {
|
||||
buildUI();
|
||||
state.page = detect();
|
||||
setPage();
|
||||
render();
|
||||
installNavWatcher();
|
||||
installLongPress();
|
||||
|
||||
Reference in New Issue
Block a user