34 Commits

Author SHA1 Message Date
33087cc5b3 Make list pages fully dynamic; cache only at query level
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>
2026-04-16 20:56:26 +08:00
0ccb9debbb Reader: chunk-based image prefetch, disable browser scroll-restoration
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>
2026-04-16 20:51:21 +08:00
90f8f50166 Enable ISR on list pages; cache reader + search DB queries
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>
2026-04-16 20:12:51 +08:00
0923dc1dce Reader: contain img inside aspect-ratio placeholder
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>
2026-04-16 20:12:41 +08:00
dea57e6b28 Drop R2 URL signing; serve images via custom domain
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>
2026-04-16 19:29:11 +08:00
95135942a2 Reader: treat browser refresh as implicit resume
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>
2026-04-15 22:29:14 +08:00
fb2b032d73 Reader: broaden hover-nav detection to hybrid devices
(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>
2026-04-15 22:27:57 +08:00
6eff1f68fc Reader: nav hover reveals transiently, restores pre-hover state on leave
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>
2026-04-15 22:25:37 +08:00
f3d71a5423 Reader: hover-reveal / hover-hide top nav on desktop
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>
2026-04-15 22:24:31 +08:00
6ea021d284 Inline drawer-header colors to fix iOS invisible text
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>
2026-04-15 22:21:13 +08:00
f6fa5d9d6a Remove sticky top-0 from chapter drawer header
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>
2026-04-15 22:18:45 +08:00
9681dddc2e Fix iOS Safari zoom-in on lock/unlock
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>
2026-04-15 22:11:59 +08:00
cd1fd6ad64 Reader: make position tracking accurate to sub-200ms
- 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>
2026-04-15 22:05:35 +08:00
b993de43bc Reader: fix resume bug, add loading skeleton, scraping protection, bounded image cache
- 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>
2026-04-15 21:48:15 +08:00
f2ef775f70 Consolidate project docs into CLAUDE.md, remove AGENTS.md
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>
2026-04-12 18:25:10 +08:00
61c73c9e2d Add scroll={false} to the last two reader-entry Links
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>
2026-04-12 18:24:56 +08:00
6c750e70ce Update site msg 2026-04-12 15:51:57 +08:00
3e4b87329a Added logo and changed theme color from light blue to green 2026-04-12 15:45:46 +08:00
1c74348fae Sync URL and prev/next nav with the chapter being viewed
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>
2026-04-12 14:40:13 +08:00
cad59143a3 Center current chapter in drawer on open
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>
2026-04-12 13:04:35 +08:00
43a2a6d3f8 Rewrite reader around known-dimension page placeholders
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>
2026-04-12 13:01:41 +08:00
0a1365a743 Store page dimensions to reserve layout space upfront
- 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>
2026-04-12 13:01:27 +08:00
3745f1f316 Add backward prefetch for resumed mid-chapter reads
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>
2026-04-12 11:00:01 +08:00
0c6425f0ff Track reading progress at the page level and resume there
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>
2026-04-12 10:59:40 +08:00
26b620de2f Add reading-progress resume + multi-genre support
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>
2026-04-12 10:27:28 +08:00
06dcf0a649 Rewrite reader for iOS Safari with continuous multi-chapter flow
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>
2026-04-12 10:17:54 +08:00
c099673f6b Switch nav toggle to tap-only and harden image protection
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>
2026-04-12 07:56:39 +08:00
57255e2624 Add signed R2 URLs, batched page fetching, and 3D chapter badges
- 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>
2026-04-11 21:15:01 +08:00
e7dc39738c Add auto-hide nav bars on scroll and chapter selector bottom sheet
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>
2026-04-11 18:38:54 +08:00
6257f6c79d Switch domain from manga.04080616.xyz to www.04080616.xyz
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:48:07 +08:00
09fb507e05 Second Commit 2026-04-11 16:59:44 +08:00
f1c649429e First Commit 2026-03-25 22:20:37 +08:00
9bd75381a7 Add hello world landing page and Docker deployment config
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>
2026-03-24 22:37:08 +08:00
c7374493c5 Initial commit from Create Next App 2026-03-24 22:20:52 +08:00