lightnovelworld: chapter slug != series slug, so poller 404s on renamed novels #77
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
VPS poller log, 2026-08-10:
Root cause
Not a URL parse bug — the parse is fine, the assumption behind it is wrong.
novel-bookmark.user.js(lightnovelworld.detect, L143-158) synthesises the seriesidentity from the chapter path:
/<slug>-chapter-<n>/->seriesId = <slug>,seriesUrl = https://lightnovelworld.net/novel/<slug>/. That assumes the chapter slugand the series slug are the same string.
On lightnovelworld they can diverge — a renamed novel keeps its old chapter slugs.
Verified live 2026-08-10:
https://lightnovelworld.net/my-longevity-simulation-chapter-1/https://lightnovelworld.net/novel/my-longevity-simulation/https://lightnovelworld.net/novel/immortality-simulator/<h1 class="entry-title">Immortality Simulator</h1>The chapter page's own breadcrumb points at the series:
and that series page's 1234 chapter anchors are all
https://lightnovelworld.net/my-longevity-simulation-chapter-<n>/.So for this novel: series slug
immortality-simulator, chapter slugmy-longevity-simulation. Two independent facts, and the userscript stores only one.Consequences
Poller 404s forever.
series_urlis written once at creation (ADR-0003) and nevercorrected, so this series never gets a Latest Chapter or a cover, and burns a fetch
every cooldown.
Fixing only
series_urlis not enough. Both chapter scanners key the chapterpattern off the series slug:
backend/internal/latest/sites.go:128-137—lnwSlugRepulls<slug>out of/novel/<slug>/and buildslightnovelworld\.net/<slug>-chapter-([0-9.]+)/.novel-bookmark.user.js:177-182— same regex, fromseriesId.With
series_url = /novel/immortality-simulator/the page fetches 200 but matcheszero anchors (they all say
my-longevity-simulation-chapter-N). The scan needs thechapter slug, which the series URL does not carry.
Duplicate rows. Bookmarking from the series page yields key
lightnovelworld:immortality-simulator; from a chapter page,lightnovelworld:my-longevity-simulation. One novel, two Series rows.Fix direction
The chapter page already links its own series — read it instead of synthesising it.
In
lightnovelworld.detect's chapter branch, takeseriesUrl/seriesIdfrom thebreadcrumb anchor (
a[href*="/novel/"], orlink[rel=canonical]on the series page),falling back to the current slug guess when absent. That fixes new bookmarks and the
duplicate-key hazard.
The scanner still needs the chapter slug as a separate fact. Cheapest option that keeps
the existing store shape: on lightnovelworld, drop the per-series scoping and match
lightnovelworld\.net/[a-z0-9-]+-chapter-([0-9.]+)/against the series page only —the page lists one novel's chapters, unlike novelfull/asura which carry
recommendation strips (a "latest updates" sidebar would need checking first). Otherwise
the chapter slug has to be stored alongside
series_id, which is a schema change.Existing rows are wrong on disk either way and need a one-off correction
(
series_url+ key) or a re-bookmark.Scope
userscript/novel-bookmark.user.js(lightnovelworld.detect,latestChapterFromAnchors)backend/internal/latest/sites.go(lnwSlugRe,latestChapterFromlnw branch)userscript/test/novel-logic.test.js,backend/internal/latest/sites_test.go— both currently use
a-will-eternal, where the two slugs happen to coincide, which iswhy this slipped through. Add a divergent-slug fixture.
Research addendum — live survey of lightnovelworld, 2026-08-11
Full note committed at
docs/research/lightnovelworld-chapter-vs-series-slug.md(41 distinct novels sampled, plain
curl, no Cloudflare challenge on any request).It confirms the diagnosis and corrects three assumptions in the issue body.
Confirmed
/my-longevity-simulation-chapter-1/200,/novel/immortality-simulator/200,/novel/my-longevity-simulation/404, 0 redirects. The constructed series URL ispermanently dead — no alias, no 301.
<a href='https://lightnovelworld.net/novel/immortality-simulator/' aria-label='All Chapter'>(note single-quoted attributes). Microdata breadcrumb
position 2is an equally goodsecond, and its
<span itemprop="name">yields the correct series title directly.Correction 1 — the unscoped regex is SAFE, and the existing fixture is wrong
lnwSeriesFixtureinsites_test.go:75-80carries anovergeared-chapter-9999/anchorwith the comment "The last anchor is another series'." No sampled lightnovelworld
series page contains a foreign chapter anchor. Across 28 real series pages, 27 had
exactly one distinct chapter-slug prefix and the 28th had two that both belong to that
same novel. Recommendation strips on a series page link
/novel/<slug>/only, which thepattern cannot match.
The one widget that would carry foreign chapter links — "Latest Reading" — is an inert
client-side mustache template (
#series-historyisdisplay:nonewith an empty<ul>,rows live in
#series-history-tplwithhref="#/{{number}}") populated from thevisitor's local history. A server-side fetch never sees content there.
So: relax the regex, and fix the fixture rather than preserving the scoping. Caveat
worth a comment in the code: this is an empirical guarantee, not a structural one like
asura/novelfull. Narrowing the match to the
ul.clstylechapter-list container wouldrestore a structural guarantee cheaply.
Correction 2 — no stored-slug fix can work, because a slug can change mid-series
/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/carries two chapterslugs in one page:
…-one-skill-notfor chapters 1–99,…-one-skill-not-them-allfor100–423. Both resolve, both breadcrumb back to the same series. This kills the
"store the chapter slug alongside
series_id" alternative outright — one novel legitimatelyhas more than one. The unscoped regex is the only option that handles it.
Correction 3 — the divergence has no derivable rule, in either direction
Not "chapters keep the old title". It goes both ways:
immortality-simulatormy-longevity-simulationthe-sword-illuminates-the-great-wildernessradiant-blade-of-the-wildernessa-villains-will-to-survivethe-villain-wants-to-live3 of 41 sampled novels (~7%) diverge at their latest chapter. There is no
"Alternative names" field on lightnovelworld series pages (the info panel exposes only
Author / Released / Type / Status), so the mapping exists only in the chapter anchors.
Neither slug is computable from the other.
Also settled
for
immortality-simulator, 2666 forgreed-all-for-what./novel/<slug>/chapters/and/chapters/page-2both 404; there is no pagination to follow.anchored on the literal host is safe.
/<chapter-slug>/(no-chapter-N) 302sto chapter 1, which still requires parsing that chapter page.
Open decision for the implementer
series_idis currently the chapter slug, so the row key islightnovelworld:my-longevity-simulation.sites.gopolls off the storedseries_url,not
series_id, so storing the correctseries_urlalone is sufficient to fix the pollerwithout a re-key. Re-keying to the real series slug additionally fixes the duplicate-row
hazard (bookmarking from the series page vs. a chapter page) but requires migrating
existing rows. Worth a maintainer call before implementation — flagging rather than
deciding it here.
Note
fetchableSeriesURL(poller.go:321-324) only checks the hostname, so it happilyaccepts the dead
/novel/<chapter-slug>/URL; the gate is not a backstop for this.Spec published as #80 (
ready-for-agent). Design settled in a grilling session on 2026-08-11: identity is read from the chapter page'sAll Chapterpointer rather than derived from the address, the Poll's scan is unscoped and truncated before the comment thread, and existing rows are repaired client-side when the Reader next opens a chapter page. ADR-0008 records the decision;docs/research/lightnovelworld-chapter-vs-series-slug.mdcarries the evidence. Two corrections landed against this issue's own text: theul.clstylecontainer named in the research note is the hidden empty template, not the chapter list, and the suggestedseries_url-only repair was rejected because it keeps an identity the Site does not guarantee.