Docker production build runs without DATABASE_URL, so any page Next
tries to prerender at build (force-static / revalidate without a
dynamic segment) fails on the Prisma call. Homepage, genre page, and
sitemap previously had page-level revalidate, forcing prerender. Add
force-dynamic to each and move the revalidation inside an
unstable_cache wrapper around the Prisma query — result still cached
5m / 1h at runtime, but build no longer touches the DB.
Detail page (/manga/[slug]) keeps page-level revalidate since its
dynamic segment without generateStaticParams was never prerendered at
build anyway.
Update CLAUDE.md caching section to reflect the new strategy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace radius-based sliding window with fixed 5-page chunks. On entering
a chapter, fetch pages [current..current+4]. When user approaches within
3 pages of either the cached range's high or low edge, fetch the next
forward or backward chunk. Near chapter end, also prefetch the next
chapter's first chunk so the hand-off is seamless.
Pruning now also keeps chapter+1 when user is in the last
KEEP_PREV_CHAPTER_PAGES of current chapter — previously scrolling back
from a just-entered chapter would prune it immediately even though the
next forward scroll would re-fetch it.
Also disable window.history.scrollRestoration on reader mount. On
refresh while in an auto-appended chapter, the stored scrollY
references a taller document than reloads with only the URL chapter —
browser would clamp and land near the bottom. Manual mode lets the
useLayoutEffect resume logic be the source of truth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stable image URLs (post-signing removal) make page-level caching
safe again. Homepage, genre page, sitemap, and detail page now
revalidate on an interval instead of running Prisma on every hit.
Reader and search keep dynamic rendering (searchParams) but wrap
their Prisma queries in unstable_cache.
TTLs: home/genre/detail 5m, reader manga 5m, reader page meta 1h
(immutable), search 1m, sitemap 1h.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prior rendering let <img> flow naturally (h-auto), so subpixel aspect
mismatches between DB dims and natural file dims caused img to over-
flow the placeholder div — manifesting as gaps or content overlap
between consecutive pages. Switching to absolute inset-0 +
object-contain pins the img to div dimensions regardless.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Images now load direct from images.04080616.xyz. Removes read-side
signing (signUrl/signCoverUrls + callers), unlocking browser and
edge caching since URLs are stable. Presigned upload kept for /api/upload.
PageReader retries failed loads via onError as a safety net.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refreshing the chapter page was landing the user back at the top, because
only ?resume=1 triggered the saved-position restore. Added isPageReload()
helper (checks performance navigation entry type === 'reload') and OR'd
it with the resume flag. Refresh now restores to the last scroll
position; drawer/list clicks still go to top as intended.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(hover: hover) matches only when the primary input is mouse-like, which
excludes touch-laptops, iPads with a trackpad, and some other hybrid
configurations. Switch to (any-hover: hover) plus a one-shot mousemove
fallback so any mouse movement unlocks the hover-reveal behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously hover both revealed (on enter) AND hid (on leave) via the
persistent showUI state — so tapping the nav visible and then moving the
mouse away would hide it. Now hover toggles a separate transient
hoveringNav state; the nav is visible when (showUI || hoveringNav). On
mouseleave, hoveringNav clears and the nav returns to whatever showUI
was before — visible if the user tapped to show it, hidden if it was
scroll-auto-hidden.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the nav is hidden (auto-hidden after scrolling past 50px), a
fixed-position hover sensor at the top of the viewport catches mouseenter
and slides the nav back in. Moving the mouse away from the nav slides it
back out.
Gated by `@media (hover: hover)` via JS matchMedia so touch devices are
unaffected — the existing single-tap toggle stays the only way to show
the nav there.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iOS WebKit intermittently fails to resolve Tailwind's CSS-variable-based
color utilities (text-foreground, text-muted, bg-background) in this
specific stacking context (fixed + nested absolute + rounded-t-3xl +
overflow-hidden). Text was rendering with an unresolved color, making it
invisible but still selectable/copyable.
Inline hex values bypass the variable chain and force the resolved color.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iOS Safari fails to paint sticky descendants of overflow-hidden ancestors
until a repaint is triggered, causing the "Chapters / N total" header to
render invisibly. The sticky was unnecessary — the header sits outside
the drawer's scroll container, so it never actually needed to stick.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a tiny client component mounted in the root layout that listens for
pageshow with persisted=true (bfcache resume) and nudges scroll + forces
a reflow. Resolves the zoom-desync bug where viewportFit=cover +
maximumScale=1 leaves the visual viewport stale after device lock.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Throttled write inside the scroll rAF tick (every 200ms during active
scroll) captures mid-page ratio changes. Previously only page-boundary
crossings triggered writes, so refreshing mid-page restored to top of
that page instead of exact position.
- pagehide + visibilitychange + unmount flush captures the final 0-200ms
before tab close / bfcache / nav.
- hasScrolledRef guards writes so opening a chapter without scrolling
doesn't clobber a prior deep-progress bookmark for a different chapter.
- getBoundingClientRect replaces offsetTop/offsetHeight for subpixel
precision and positioning-ancestor independence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Resume scroll position only when arriving via 继续阅读 (?resume=1).
Plain chapter-list / drawer clicks now actively scroll to top on mount.
- Progress format extended to {chapter, page, ratio} for within-page
precision; legacy bare-number and {chapter, page} still read correctly.
- Tappable skeleton logo (sunflower outline, spins) while a page loads;
tap force-fetches a fresh signed URL.
- Viewport-priority image loading: second IntersectionObserver at margin 0
marks truly-visible pages, drives <img fetchpriority="high"> and fires
immediate single-page fetches that cut the batch queue.
- Bounded image cache: unmount previous chapter's <img> elements when
currentPage > 5 into the new chapter; placeholders stay for layout.
One AbortController per live chapter; unmount aborts in-flight batches.
- Hashed chapter IDs on the wire via hashids; DB PKs unchanged.
- Origin/Referer allowlist + rate limiting on all /api/* routes via a
withGuards(opts, handler) wrapper (eliminates 6-line boilerplate x5).
- robots.txt allows Googlebot/Bingbot/Slurp/DuckDuckBot/Baiduspider/
YandexBot only; disallows /api/ for all UAs.
- Extract pure helpers for future TDD: lib/scroll-ratio.ts (calcScrollRatio,
scrollOffsetFromRatio), lib/progress.ts (parseProgress + injectable
StorageLike), lib/rate-limit.ts (optional { now, store, ipOf } deps),
lib/api-guards.ts.
- New env keys: HASHIDS_SALT, ALLOWED_ORIGINS (wired into docker-compose).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CLAUDE.md is now self-contained: absorbed the still-accurate parts of
AGENTS.md (Next.js-is-different warning, port allocation, Nginx block,
SEO notes, Meilisearch as phase-2 plan) and dropped the stale bits
(pre-width/height schema, "public domain only" content policy, old
manga.04080616.xyz domain, aspirational build order).
Also added findings surfaced by auditing CLAUDE.md against the actual
code:
- Placeholder-based reader architecture and its invariants (must have
Page.width/height, scroll={false} on Links, history.replaceState for
URL sync, prev/next derived from currentChapterNum).
- docker-compose.yml only brings up the app; Postgres is external.
- localStorage key format sunnymh:last-read:<slug> + legacy bare-number
fallback that must be preserved.
- Immersive reader route: Header/BottomNav return null on /manga/[slug]/[n].
- signCoverUrls is the single source of truth for signed cover URLs.
- iOS Safari conventions: viewportFit cover, pt-safe/pb-safe, WebKit
momentum scrolling, overflow-x:hidden.
- Dev gotchas: allowedDevOrigins VPN whitelist, prisma/seed.ts is
docs-only, scraper is the real data pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The back-to-manga header Link and the end-of-manga "Back to Home" Link
were missing scroll={false}, so navigating away and returning (or going
home then re-entering via Continue Reading) could lose the resume scroll.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Once continuous scroll crosses a chapter boundary, the URL was stuck at
the originally-opened chapter so browser back / reload would jump the
user back there. Double-tap left/right also walked off the wrong chapter
since prevChapter/nextChapter were frozen at mount time.
- replaceState the URL as currentChapterNum changes (no server refetch).
- Derive prevChapter/nextChapter dynamically via useMemo on
currentChapterNum, dropping the now-redundant server-computed props.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chapter drawer now opens with the active row pre-scrolled to the center
of the list instead of always starting at chapter #1. useLayoutEffect
measures via getBoundingClientRect so the scroll lands before paint —
no visible jump.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the prepend/flushSync/scrollBy gymnastics with placeholder divs
sized by each page's width/height. Document height is correct from the
first paint, so resume + backward scroll just work — no scroll
compensation, no gesture fights, no forced aspect ratio distorting images.
- New /api/chapters/[id]/meta returns the dim skeleton for any chapter.
- Chapter page pre-fetches the starting chapter's meta server-side and
parallelizes the two Prisma queries via Promise.all.
- Reader renders placeholders with aspectRatio: w/h, lazy-loads image
URLs in batches via IntersectionObserver, and prefetches the next
chapter's meta ~3 pages from the end.
- Scroll tracker walks only the intersecting-pages set (~3–5 elements)
instead of every loaded page per rAF.
- scroll={false} on all Links into the reader + { scroll: false } on
double-tap router.push, plus a belt-and-suspenders rAF re-scroll, so
resume survives soft navigation and browser scroll-restoration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add width/height columns to Page (default 0, migration SQL committed).
- Backfill script ranges first 16KB of each R2 object and parses with
image-size. Probed all 26,209 existing pages successfully.
- Export keyFromPublicUrl from lib/r2 so the script reuses the existing
URL→key logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the user resumes at a mid-chapter page, the reader previously
skipped the earlier pages entirely. This adds a scroll-up prefetch so
those earlier pages appear smoothly as the user scrolls toward the top.
- prependBatch() mirrors fetchBatch but decrements the offset cursor
and prepends the new pages. prependExhaustedRef fires when the
cursor hits 0 (start of chapter).
- Trigger: scrollY < 2500px fires prepend — well before the user
reaches the top, so the DOM + images spawn ahead of the scroll
position.
- Scroll preservation: aspect-[3/4] on <img> reserves vertical space
before the image bytes arrive, so scrollHeight is accurate
immediately after React commits. A single scrollBy(delta) keeps the
previously-visible page visually anchored — no per-image jitter.
- Forward-fetch trigger indices + loadedCountRef are shifted by
batch.length on each prepend so next-batch prefetch still fires at
the correct page.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Upgrades the reader's last-read tracking from {chapter} to {chapter, page}.
- ReadingProgressButton: storage is now JSON {chapter, page}; legacy
bare-number values are read back as {chapter, page: 1}. Button label
is unchanged ("继续阅读 · #N title") — the extra precision lives in
the reader's first-fetch offset, not the label.
- PageReader: on mount with saved progress, seed offsetRef to
(page - 1) so the first /api/pages call starts AT the resumed page
instead of the beginning of the chapter. currentPageNum state is
initialized from storage too, so the first persist write is a no-op
that matches the saved value.
- Scroll tracker now also tracks currentPageNum (last page whose top
has crossed above viewport top+80), and persistence writes the
{chapter, page} pair on each change.
Known limitation: earlier pages of the resumed chapter aren't loaded
yet — a follow-up commit adds scroll-up prefetch for those.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reading progress
- New ReadingProgressButton client component that reads/writes
localStorage key "sunnymh:last-read:{slug}"
- PageReader writes the currently-visible chapter as the user scrolls
through continuous chapters
- Manga detail CTA now reads: "开始阅读" on first visit, or
"继续阅读 · #{N} {title}" when a prior chapter is stored (with
spacing and truncation for long titles)
Multiple genres
- lib/genres.ts with parseGenres() and collectGenres() helpers to
split "冒险, 恋爱, 魔幻" into a list and aggregate across a collection
- Manga detail renders one pill per genre
- GenreTabs filters via parseGenres(...).includes(activeGenre) so a
multi-genre manga appears under each of its genres
- Home / Genre pages compute the tab list from collectGenres(signedManga)
- Card captions (GenreTabs + TrendingCarousel) show "冒险 · 恋爱 · 魔幻"
with truncation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the fixed-position reader with a sticky layout that works
correctly on iPhone Safari and Edge, while also auto-appending the
next chapter's pages when the current one finishes.
Layout
- Swap all position:fixed for sticky (Header, BottomNav, reader top nav)
— fixed-positioning quirks broke the bottom nav in Edge and
prevented Safari's URL bar from collapsing on scroll
- Viewport: viewport-fit=cover + interactiveWidget=overlays-content
so manga extends edge-to-edge and the URL bar overlays content
without resizing the viewport
- Add pt-safe / pb-safe utilities; apply on nav bars so chrome
respects the notch and home-indicator
- Drop fixed-positioning bottom padding now that BottomNav is in flow
Continuous reading
- PageReader now receives the full chapter manifest (id + totalPages)
and auto-fetches the next chapter when the current one is done
- Subtle chapter divider strip appears between chapters in the scroll
- Top nav chapter title updates as the user scrolls into a new chapter
(rAF-throttled scroll listener, cached offsetTop)
- Double-tap on left/right viewport half navigates prev/next chapter
- End-of-manga footer fills the viewport with a Back-to-Manga action
Theme polish
- Light theme: white body/background, blue accent preserved for
chapter numbers, badges, active states
- Modern chapter drawer: white sheet, rounded-t-3xl, two-column
rows with chapter-number badge, blue highlight for current chapter
- Suppress hydration warnings for extension-injected attributes on
<html> and the search input
- Manga detail CTA localized to 开始阅读
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace scroll-direction nav reappear with a one-shot scroll-down hide
plus tap-on-image toggle. Distinguish tap from scroll on touch via
touchstart/touchmove tracking so swipes don't re-show the nav.
Discourage casual image saving with contextmenu prevent, draggable=false,
select-none, and -webkit-touch-callout:none. Add 10.8.0.2 to
allowedDevOrigins for VPN dev access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sign all image URLs server-side with 60s expiry presigned URLs
- Add /api/pages endpoint for batched page fetching (7 per batch)
- PageReader prefetches next batch when user scrolls to 3rd page
- Move chapter count badge outside overflow-hidden for 3D effect
- Fix missing URL signing on search and genre pages
- Extract signCoverUrls helper to reduce duplication
- Clamp API limit param to prevent abuse
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nav bars hide when scrolling down and reappear on scroll up.
Replace static page count with a chapter picker that opens as a
bottom sheet drawer for quick chapter navigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simple landing page for initial Portainer GitOps deployment.
Includes Dockerfile, docker-compose.yml with pull_policy: build,
and project documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>