hensei-web/prd/migrate-next-to-sveltekit-runes.md
Justin Edmund 2d02f88622 Fix Link component legacy behavior and tab switching
- Remove legacyBehavior prop and nested <a> tags from Link components
- Modernize to Next.js 13+ Link API with className directly on Link
- Convert external links to plain <a> tags (LinkItem, Discord link)
- Remove unnecessary passHref props from Header components
- Fix tab switching by mapping string values to GridType enum

The tab switching issue was caused by trying to parse string values
("characters", "weapons", "summons") as integers when they needed to
be mapped to GridType enum values.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 15:58:12 -07:00

9.5 KiB
Raw Blame History

PRD: Migrate Web App from Next.js to SvelteKit (Svelte 5 + Runes)

Context

The app is a small but interactive site for building, saving, and sharing Granblue Fantasy team compositions. It currently runs on Next.js 14 using a hybrid of App Router and legacy Pages, with next-intl for i18n, Valtio for state, Radix-based UI wrappers, and Tiptap for rich text. The backend is a Rails API and will not change.

Problem / Opportunity

  • The UI layer carries Next-specific complexity (hybrid routing, middleware composition, Valtio, React wrappers around primitives) for a scope that aligns well with SvelteKits simpler mental model.
  • Svelte 5 (Runes) and SvelteKit provide small, fast bundles and ergonomic state management for highly interactive UIs like the party editor.
  • A rewrite would impose upfront cost but could yield better performance, maintainability, and developer experience.

Goals

  • Achieve feature and URL parity (including localized routing and auth/method guards) with the current Next.js app.
  • Reuse domain types, data contracts, and i18n content; minimize churn to the Rails API layer.
  • Maintain or improve perceived performance, accessibility, and UX of primitives (menus, dialogs, tooltips, toasts).

NonGoals

  • No changes to the Rails API endpoints, auth semantics, or data shape.
  • No new product features beyond parity (performance and a11y improvements are in scope).

Current State (Summary)

  • Framework: Next.js 14 with app/ and pages/ side by side.
  • Routing/i18n: app/[locale]/… with next-intl and “as-needed” default-locale prefixing; middleware composes i18n with auth/method guards.
  • State: Valtio global stores for accountState and appState (party/grid/editor flows).
  • Data: axios-based utils/api.tsx, app route handlers under app/api/** proxy to Rails; server helpers in app/lib/* with zod validation.
  • UI: Components under components/** using SCSS modules, Radix-based wrappers, custom editor based on Tiptap React + custom extensions.
  • URL State: nuqs binds filters to query params.
  • Storybook: React/Next preset.

Feature/Parity Requirements

  • Public routes: /, /new, /teams, /p/[party] (+ tab segments), /[username], /about, /updates, /roadmap with locale variants under /ja and default-locale “as-needed” behavior.
  • Auth/method gates: protect /saved and /profile; 401 JSON for unauthorized API mutations; preserve method-based rules under /api/**.
  • Party editor: weapons/summons/characters, job/skills/accessory, guidebooks, toggles, remix/delete, editability rules by cookie/user.
  • Search & filters: team discovery with element/raid/recency, advanced filters from cookie, pagination (append/replace).
  • i18n: reuse JSON namespaces; preserve current translation keys and locale cookie behavior.
  • Primitives: dropdown menu, dialog, tooltip, toast, popover, select, switch, slider, command, segmented control with keyboard a11y.
  • Editor: Tiptap with mentions, link, lists, headings, youtube, highlight, placeholder.

Proposed Direction

Rebuild the web app on SvelteKit 2 and Svelte 5 Runes. Keep the Rails API contract intact. Adopt Svelte stores for global state and Runes for local component state. Replace React-specific libraries with Svelte-native equivalents while reusing JSON messages, type declarations, API contracts, and Tiptap core extensions.

Target Architecture (SvelteKit)

  • Routing: src/routes/[lang=locale]/… to mirror localized segments; top-level / redirects to /new.
  • Hooks: hooks.server.ts handles locale detection/redirects and reproduces auth/method gates; handleFetch attaches bearer tokens from cookies.
  • Data loading: +layout.server.ts/+page.server.ts for SSR (e.g., version, lists); +page.ts for client/SSR-unified fetches.
  • Endpoints: src/routes/api/**/+server.ts proxy to Rails with zod validation and consistent status codes.
  • State: Svelte writable stores for account and app (party, grid, search, jobs, skills, version), with typed helper actions; local UI state via runes.
  • Theming: small store plus SSR-safe initial theme; apply class/data-theme on <html>.
  • i18n: svelte-i18n (recommended) loading existing JSON bundles; locale cookie + accept-language fallback; “as-needed” default-locale paths.

Dependency Mapping (Equivalents)

  • Framework/Core:
    • next@sveltejs/kit
    • react, react-domsvelte@5
    • @svgr/webpackvite-svg-loader (or inline SVG)
  • Routing/URL State:
    • next/navigation → SvelteKit goto, $page, afterNavigate
    • nuqs$page.url.searchParams + small helper or sveltekit-search-params
  • Data/Query:
    • @tanstack/react-query@tanstack/svelte-query@5
    • axios → keep or migrate to fetch; keep zod
  • i18n:
    • next-intlsvelte-i18n (reuse JSON); remove next-i18next
    • i18next* libs → only if choosing an i18next-based Svelte binding (otherwise remove)
  • UI/A11y:
    • @radix-ui/*bits-ui primitives and/or shadcn-svelte
    • tippy.jssvelte-tippy (or small custom)
    • cmdkcmdk-svelte/cmdk-sv
  • Theming:
    • next-themes → small Svelte store + cookie bootstrap
  • Editor:
    • @tiptap/react → Tiptap core with Svelte integration (e.g., svelte-tiptap); reuse custom extensions
  • Typeahead/Select/Infinite:
    • react-bootstrap-typeaheadsvelte-select
    • react-infinite-scroll-componentsvelte-infinite-loading or IO-based action
  • Misc:
    • cookies-nextevent.cookies server-side; js-cookie client
    • react-use/usehooks-tssvelte-use
    • remixicon-reactsvelte-remixicon or unplugin-icons (ri:*)
    • react-linkifylinkifyjs/linkify-html
    • SCSS → svelte-preprocess with component-scoped styles and global imports
    • Storybook: @storybook/nextjs@storybook/sveltekit

Reuse vs. Rewrite

  • Reuse
    • i18n bundles under public/locales/{en,ja}
    • Domain types types/*.d.ts (move to src/lib/types)
    • Pure utilities in utils/* (parsers, enums, formatters)
    • API contracts and zod schemas (app/lib/api-utils.ts)
    • Tiptap custom extensions (extensions/CustomMention, CustomSuggestion)
    • Public assets and SVGs
  • Rewrite
    • All React components and Valtio stores as Svelte components/stores
    • Next middleware as hooks.server.ts (auth + locale); API handlers as +server.ts
    • URL/query state helpers (replace nuqs)
    • Theming (replace next-themes)
    • Storybook stories
    • Font handling (move pages/fonts/gk-variable.woff2 to static/ and add @font-face)

Routing & i18n Details

  • Localized route group [lang=locale] constrained to en|ja (from existing i18n.config.ts).
  • “As-needed” default-locale behavior via redirects in handle and locale-aware links.
  • Preserve existing route structure and query semantics (e.g., /teams?element=…&raid=…).

Auth & Cookies

  • Mirror middleware.ts logic: protect /saved and /profile; method-guard mixed routes; return 401 JSON for API.
  • Server: use event.cookies and set locals.user; client: js-cookie where necessary.
  • Use handleFetch to inject Authorization header from account cookie token.

State & Data

  • Global state: writable stores mirroring initialAccountState and initialAppState with typed actions; local UI state via runes.
  • Queries: @tanstack/svelte-query for client/SSR-safe caching; SSR data via +page.server.ts where beneficial.
  • Invalidation: use SvelteKit invalidate/invalidateAll in place of Next revalidatePath.

UI & Editor

  • Primitives: build on Bits-UI/shadcn-svelte; verify keyboard navigation and focus management; carry over SCSS look-and-feel.
  • Editor: use Tiptap core with Svelte wrapper; reuse mention/suggestion logic and toolbar feature set (bold/italic/strike/heading/list/link/youtube/highlight/placeholder).

Performance & SEO

  • Expect smaller bundles and faster hydration from Svelte runtime.
  • Maintain SSR for primary pages and content; keep linkable, crawlable party pages and listings.
  • Optimize images/SVGs via Vite pipeline; keep existing public assets.

Tradeoffs (Pros / Cons)

  • Pros: performance, simpler state and data flows, clearer SSR/CSR boundaries, componentscoped styling, fast DX.
  • Cons: full component rewrite, parity tuning for UI primitives, community-maintained editor bindings, team rampup, Storybook conversion.

Alternatives Considered

  • Stay on Next.js and complete the ongoing App Router + nextintl consolidation (lowest risk and cost).
  • Prototype SvelteKit for the party editor only (dualstack) and evaluate before committing.

Open Questions

  • Keep proxy endpoints under /api/** or talk to Rails directly with CORS? (Proxy simplifies auth and error shaping.)
  • UI library choice finalization: BitsUI vs shadcnsvelte (or a minimal custom layer where needed).
  • Icon strategy: unplugin-icons vs direct SVG imports.

Acceptance Criteria

  • Route and locale parity with auth/method guards and “asneeded” defaultlocale behavior.
  • Party editor parity (create/update/remix/delete; grids; jobs/skills/accessories; guidebooks; toggles).
  • Querylinked filters and pagination on /teams, /saved, /[username].
  • Editor parity (mentions, link, lists, headings, youtube, highlight, placeholder) and equivalent styling.
  • Primitives parity with keyboard a11y and visual consistency.
  • i18n keys resolve from existing JSON bundles; theme persists SSR + client.

Success Measures

  • Timetointeractive and bundle size improved vs. Next build.
  • No regression in error rates for API interactions (remix/delete/save).
  • Positive qualitative feedback on responsiveness in editor/grid interactions.