e72765df85
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.
84 lines
6.1 KiB
Markdown
84 lines
6.1 KiB
Markdown
Guidance for OpenCode (and Claude Code) working under `userscript/`. See root `AGENTS.md` for the project-wide architecture diagram, hard constraints, and design system.
|
||
|
||
### Userscript structure (single IIFE, `manga-bookmark.user.js`)
|
||
|
||
1. **Site adapters** — one per host, `detect(location, document)` return page `type` + IDs. Identify type/IDs from **URL regex** (most stable); pull `title`/`cover` from **`og:title`/`og:image` meta tags**, not CSS classes.
|
||
2. **API client** — `apiGet/apiPut/apiDelete` with bearer header; `localStorage` key `bmgr:manga:cache` for instant render + offline fallback.
|
||
3. **Progress logic** — auto-upsert `last_chapter` only when `chapterNum >= stored last_chapter_num` (re-reading old chapters must not regress progress; unparseable -> set current). Manual panel override forces any value.
|
||
4. **Retry queue** — every write go through `pushBookmark`/`pushDelete`, so
|
||
failed mutation park in `localStorage` (`bmgr:manga:queue`) and replayed on
|
||
next navigation, reconnect, or `refresh()`. Entries are markers
|
||
(`{key, op, sendStatus, attempts}`), never payloads — body read from
|
||
cache at send time, so one entry per key give ordering and coalescing for
|
||
free. `sendStatus` is **sticky**: while archive pending, later writes to
|
||
that key keep carrying bucket, which stop successful
|
||
in-between write from silently un-archiving series. `refresh()` drains
|
||
before it fetches and overlays anything still pending, so list never
|
||
flaps. 400 drops entry, 401 abort pass and keep queue, and
|
||
transient failures retry to cap of 10. Latest-chapter writes deliberately
|
||
stay out of queue. See
|
||
`docs/superpowers/specs/2026-07-27-offline-retry-queue-design.md`.
|
||
5. **UI** — rendered inside **Shadow DOM** root to isolate from site CSS
|
||
(critical on mobile). Three tabs (All / Favourites / Archived) and row of
|
||
link chips to web UI and both manga sites; `WEB_BASE` sits in CONFIG
|
||
block next to `API_BASE`. FAB is `7 × 44` edge tab whose *hit* area
|
||
widened to `28 × 72` by invisible `#hit` child; `#fab` must keep
|
||
`touch-action: none` and must **not** regain `overflow: hidden`. Since
|
||
`touch-action` resolved at gesture start, strip can't be both
|
||
browser-scrolled and script-dragged, so `makeDraggable` splits by intent: swipe
|
||
from `#hit` scrolls via `window.scrollBy`, hold of `ARM_MS` arms
|
||
reposition drag, visible sliver drags with no hold. See
|
||
`docs/superpowers/specs/2026-07-28-edge-tab-hitbox-design.md`.
|
||
6. **SPA navigation** — Asura is Astro, client-routed on comic/chapter pages: patch `history.pushState`/`replaceState` + listen `popstate`, re-run `detect()` on URL change so auto-update fire without reload. Demonic uses classic reloads (initial `document-idle` run suffice).
|
||
|
||
### Live URL shapes (verified 2026-07-26, may drift — re-check against live pages before trust)
|
||
|
||
- **asurascans.com**: series `/comics/<slug>` (slug carries trailing
|
||
site-wide build-hash suffix, e.g. `-059befe1`, that **rotates on every
|
||
redeploy**), chapter `/comics/<slug>/chapter/<n>`. `seriesId` must strip
|
||
hash (`/-[0-9a-f]{8}$/`, `stripBuildHash` in userscript,
|
||
`asuraBuildHash` in backend); URLs keep full slug — stale-hash
|
||
URLs 302 to current ones. Astro-rendered; chapter links present in raw
|
||
server HTML.
|
||
- **demonicscans.org**: series `/manga/<slug>` (slug may URL-encode punctuation, e.g. `%2527` for `'`), chapter `/title/<slug>/chapter/<n>/<page>` (older `chaptered.php?manga=<id>&chapter=<n>` form still exists as redirect, what series-page chapter-list anchors link through).
|
||
Encodings (incl. triple-encoded punctuation like `%25252D`) identical
|
||
on /manga/ and /title/ pages, so decode-once seriesIds match — verified
|
||
2026-07-28.
|
||
- **comix.to**: series `/title/<id>-<slug>`, chapter
|
||
`/title/<id>-<slug>/<uploadId>-chapter-<n>`. Only the leading `<id>` is
|
||
identity — the slug re-renders when a series is renamed (`comixSeriesId`).
|
||
An SPA that **never rewrites `og:title`**: the server-rendered head keeps
|
||
whatever document loaded first, so on a cold load `og:title` is the homepage's
|
||
"Comix — Read Comics online for free" and after an in-page hop it is the
|
||
*previous* series' name. `document.title` is the one thing client routing does
|
||
update, so titles come from there, with the chapter page's `" · Ch.<n>"` tail
|
||
stripped. Covers likewise: `og:image` is absent, so the cover is the `img`
|
||
whose `alt` matches the cleaned title — verified live 2026-08-08.
|
||
- **kagane.to**: series `/series/<uuid>`, reader
|
||
`/series/<uuid>/reader/<bookUuid>`. Reader URLs carry no chapter number, so
|
||
the number comes out of `og:title`. Two shapes exist: `"<Series> - Chapter
|
||
<n>[ - Episode <n>]"` and, for volume-numbered series, `"<Series> - Volume <v>
|
||
Chapter <n>"` with no episode name — both must yield a bare series title, or
|
||
the volume tail lands in the bookmark's title. Its covers are challenge- and
|
||
CORP-protected, so the web UI proxies them; the userscript still stores the
|
||
raw `og:image`. Behind a Cloudflare JS challenge, so the backend polls it
|
||
through the headless browser.
|
||
- **novelfull.com** (novel script): series `/<slug>.html`, chapter
|
||
`/<slug>/chapter-<n>[-<title-slug>].html`. No `og:*` tags at all — title from
|
||
`h3.title` (series) or `a.truyen-title` (chapter), cover from
|
||
`meta[name="image"]`. Behind a Cloudflare JS challenge no TLS fingerprint
|
||
clears, so the backend polls it through the headless browser.
|
||
- **lightnovelworld.net** (novel script): series `/novel/<slug>/`, chapter
|
||
`/<slug>-chapter-<n>/` — flat, at the site root. `h1.entry-title` is the clean
|
||
title on a series page and `<Title> Chapter <n>` on a chapter page. Chapter
|
||
pages carry no `og:image`. Its series page lists every chapter with an
|
||
absolute href, so the backend polls it with the plain TLS client.
|
||
|
||
### Second script: `novel-bookmark.user.js`
|
||
|
||
A copy of the manga script with two adapters, `LIBRARY = "novel"` and
|
||
`STORE_PREFIX = "bmgr:novel:"`. No migration loop (this script has no previous
|
||
installation to carry keys over from). Installed alongside the manga script;
|
||
both write to the same backend with the same `LIBRARY` column discriminating
|
||
them.
|