8.0 KiB
Web Spec
Scope
This spec applies to web/**.
Read this file before changing the Astro site, React app islands, authenticated app routes, API clients, i18n, or responsive layout.
Current Stack
- Astro 6 for static public pages and route files.
- Web production build uses Astro server output with the
@astrojs/nodeadapter so client-owned dynamic shell routes such as/{locale}/history/:idcan be refreshed directly. - React 19 for interactive client UI.
- React Router DOM for the authenticated business app shell.
- Tailwind CSS 4 through
@tailwindcss/vite. - TypeScript strict mode.
- Local i18n from
web/src/i18n/utils.ts. - Backend API base for production:
https://api.meeyao.com. - Local development API access uses the Vite
/apiproxy inweb/astro.config.mjs.
Do not introduce a second frontend framework, a second router, or scattered API URL construction for web code.
Route Architecture
Public pages are Astro pages under web/src/pages/{locale}/ and use Marketing.astro.
Authenticated pages are Astro route shells that all render DashboardAppPage.astro. The actual logged-in application is a single React Router app:
DashboardApp.tsxowns React Router routes for dashboard, store, history, notifications, profile, settings, and divination pages.AppShell.tsxowns the persistent sidebar, mobile drawer, route guard, authenticated session recovery, and authenticated layout.- Business page components render only their page body. They must not wrap themselves in
AppShell. - Sidebar navigation must use React Router navigation so the shell remains mounted and only the right-side content changes.
- Direct browser refresh on each existing business route must still render the app shell through Astro.
Login and public marketing/legal pages are not part of the authenticated app shell.
Auth Rules
- Login and registration are the same email-code flow. The backend auto-registers new email accounts.
- Test credentials for local verification:
test@example.comwith code123456. - Auth state is stored by
web/src/lib/auth.tsunder one local storage key. - Every authenticated route must recover or refresh the session before showing business content.
AppShell.tsxis the single owner of authenticated app session recovery. Do not add another client wrapper that also refreshes the session around every authenticated route.- Missing, expired, invalid, or refresh-failed tokens must clear local auth and redirect to
/{locale}/login. - Do not add silent success paths for auth failures.
API Rules
- All API paths live in
web/src/lib/api-routes.ts. - Shared request behavior lives in
web/src/lib/api-client.ts. - Auth/session behavior lives in
web/src/lib/auth.ts. - Business API functions live in
web/src/lib/api.ts. - Shared authenticated read caching lives in
web/src/lib/data-client.tsandweb/src/lib/resources.ts. - Components must call typed API helper functions, not inline
fetch('/api/...'). - Components that need profile, points, packages, history, notifications, or unread-count data should use the resource hooks/functions from
web/src/lib/resources.tsinstead of starting their own duplicate GET lifecycle. - Dashboard-visible user, points, notification, and history data must come from the backend. Do not hardcode those values.
- Production API host is
https://api.meeyao.com; local dev should use same-origin/apiand the Vite proxy.
Authenticated Data Resource Pattern
Use this pattern for backend reads that are reused across authenticated pages:
// lib/api.ts: transport-only business API
export function getPointsBalance(): Promise<PointsBalance> {
return authFetch<PointsBalance>(API_ROUTES.points.balance);
}
// lib/resources.ts: cache policy + hook/function surface
export function usePoints() {
return useResource({
key: pointsBalanceKey,
ttlMs: 60_000,
fetcher: getPointsBalance,
staleWhileRevalidate: true,
});
}
Resource contracts:
lib/api.tsremains transport-only: no per-endpoint ad hoc memory cache there.lib/resources.tsowns resource keys, TTLs, in-flight dedupe, stale-while-revalidate behavior, prefetch, and mutation invalidation.clearAuth()must clear the shared data cache so authenticated data cannot leak across users.- Resource hooks must support disabled/optional keys for pages where an id may be absent; do not create a fetcher that intentionally rejects during normal render.
- Active hooks must refetch after invalidation when they still need the resource.
Invalidation matrix:
- Profile, avatar, or settings write -> set the profile resource with the returned backend profile.
- Divination run or follow-up completion -> invalidate points and the relevant history list/thread resources.
- Notification mark-read -> patch the notification list and decrement unread count when the item changes from unread to read.
- Mark-all-notifications-read -> patch the notification list and set unread count to zero.
- Logout, expired refresh, or invalid auth -> clear auth and clear all resource data.
Wrong vs correct:
// Wrong: every page starts an independent duplicate GET.
useEffect(() => {
getUserProfile().then(setProfile);
}, []);
// Correct: subscribe to the shared profile resource.
const profileState = useProfile();
Layout Rules
- Build mobile-first, then add
sm:,md:,lg:, andxl:refinements. - Business pages must not require horizontal scrolling at common phone widths such as
390x844. - Use responsive stacks for fixed-width desktop columns:
flex-col lg:flex-row,w-full lg:w-[...]. - Keep the authenticated shell as
h-screenwith the main content scrollable. - Mobile sidebar must be reachable through the menu button and must not hide the page content permanently.
- Public header mobile navigation must expose feature, pricing, about, login, and language switching.
Mobile Guided Overlays
- Keep one dimming strategy per viewport. Do not combine a full-screen dark overlay with a spotlight element that also uses an oversized outer shadow on the same mobile viewport.
- Mobile spotlight targets should fit inside the phone viewport. If a desktop tutorial highlights a tall panel, use a smaller mobile-only target such as the rows or controls that the step actually explains.
- Tooltip placement and arrow direction must match: a tooltip above the target uses a bottom arrow pointing down; a tooltip below the target uses a top arrow pointing up.
- When the app shell owns scrolling, compute mobile overlay coordinates relative to the page component host and visible scroll container, not the document body.
i18n Rules
- Supported locales:
zh,zh_Hant,en. - Routes are prefixed by locale, including the default locale.
- User-visible text should come from
web/src/i18n/utils.tsor locale-specific content assets. - Do not add user-facing strings in only one locale.
Design Source
- Pencil design files under
web/design/are the visual source for login and public page design. - If UI implementation diverges from Pencil, inspect the design first and keep the code aligned unless the user explicitly asks to change the design.
- Assets in
web/public/imagesandweb/public/legalare symlinks toweb/design/assets; do not duplicate them.
Verification
Before finishing meaningful web changes:
- Run
npm run buildinweb/. - Use Chrome DevTools on
http://localhost:4322/for at least one desktop and one phone viewport when layout or routing changed. - For authenticated changes, verify the test account can log in and lands on
/{locale}/dashboard. - Verify direct unauthenticated access to a business route redirects to login.
- Verify sidebar navigation changes content without a full document reload.
Forbidden
- Do not hardcode API URLs or endpoint paths in components.
- Do not add
.envas a required file for normal local development. - Do not reintroduce page-refresh sidebar navigation inside the authenticated app.
- Do not wrap business page components in nested cards or duplicate
AppShell. - Do not add fallback/mock success behavior for failed API calls.