233 lines
16 KiB
Markdown
233 lines
16 KiB
Markdown
# Architecture
|
|
|
|
**Analysis Date:** 2026-04-18
|
|
|
|
## Pattern Overview
|
|
|
|
**Overall:** Server-first Next.js App Router monolith with localized page routes, thin API handlers, Prisma-backed data access, and small client-side interaction islands.
|
|
|
|
**Key Characteristics:**
|
|
- Use `src/app/layout.tsx` and `src/app/[locale]/layout.tsx` as the primary composition roots, with most route files implemented as async server components.
|
|
- Keep browser-only interaction in explicit client components such as `src/app/[locale]/projects/ProjectsPageClient.tsx`, `src/app/[locale]/projects/ProjectsResultsClient.tsx`, `src/components/search/AISearchBar.tsx`, and `src/components/signals/SignalFeedClient.tsx`.
|
|
- Centralize most project and home-page reads in `src/hooks/useProjects.ts` and `src/hooks/useHome.ts`, even though these modules are named like React hooks.
|
|
- Validate external input with Zod schemas from `src/lib/validations.ts` before querying or mutating the database.
|
|
- Use Prisma as the only persistence client through `src/lib/prisma.ts`, with schema ownership in `prisma/schema.prisma`.
|
|
|
|
## Layers
|
|
|
|
**Routing and Layout Layer:**
|
|
- Purpose: Resolve locale-aware routes, shared layout chrome, SEO metadata, and global CSS.
|
|
- Location: `src/app/layout.tsx`, `src/app/[locale]/layout.tsx`, `src/app/[locale]/page.tsx`, `src/app/[locale]/projects/page.tsx`, `src/app/[locale]/projects/[id]/page.tsx`, `src/app/[locale]/signals/page.tsx`, `src/app/[locale]/about/page.tsx`, `src/app/sitemap.ts`, `src/app/robots.ts`, `src/middleware.ts`, `src/i18n/request.ts`
|
|
- Contains: App Router entry points, `generateMetadata`, `generateStaticParams`, `revalidate` exports, locale middleware, and SEO documents.
|
|
- Depends on: `next-intl`, server query modules in `src/hooks`, UI components in `src/components`, and shared helpers in `src/lib`.
|
|
- Used by: The Next.js runtime.
|
|
|
|
**UI Composition Layer:**
|
|
- Purpose: Render reusable view fragments for pages and route-specific UI sections.
|
|
- Location: `src/components/home/*`, `src/components/layout/*`, `src/components/locale/*`, `src/components/project/*`, `src/components/search/*`, `src/components/signals/*`
|
|
- Contains: Mostly presentational components, plus a few server components that call translations or helpers directly, such as `src/components/project/ProjectList.tsx` and `src/components/project/ProjectSidebar.tsx`.
|
|
- Depends on: Translation APIs, route props, and typed data returned by `src/hooks/useProjects.ts` or `src/hooks/useHome.ts`.
|
|
- Used by: Route entry points under `src/app/[locale]`.
|
|
|
|
**Client Interaction Layer:**
|
|
- Purpose: Own browser-only state, URL mutation, fetch loops, local storage, and interactive filters/search.
|
|
- Location: `src/app/VercelMetrics.tsx`, `src/app/[locale]/projects/ProjectsPageClient.tsx`, `src/app/[locale]/projects/ProjectsResultsClient.tsx`, `src/components/layout/AnnouncementBar.tsx`, `src/components/locale/LocaleSwitcher.tsx`, `src/components/project/TagFilterPanel.tsx`, `src/components/search/AISearchBar.tsx`, `src/components/search/HomeSearchBar.tsx`, `src/components/search/AISearchResults.tsx`, `src/components/signals/SignalFeedClient.tsx`
|
|
- Contains: `useState`/`useEffect`-driven UI state, `window.history.replaceState`, `window.location.href`, `localStorage`, and client-side `fetch` calls to `/api/*`.
|
|
- Depends on: Page props, Next navigation APIs, and JSON APIs implemented under `src/app/api`.
|
|
- Used by: Server-rendered pages and layouts.
|
|
|
|
**Query and Read Model Layer:**
|
|
- Purpose: Encapsulate database reads, filtering, retry logic, shape transformations, and cached read models for projects, tags, and home-page aggregates.
|
|
- Location: `src/hooks/useProjects.ts`, `src/hooks/useHome.ts`
|
|
- Contains: Prisma queries, pagination logic, tag-category grouping, home-page aggregate builders, and result-type definitions such as `ProjectWithFlatTags`, `FilterTagCategoryGroup`, and `HomePageData`.
|
|
- Depends on: `src/lib/prisma.ts`, `src/lib/cache.ts`, `src/lib/tag-taxonomy.ts`, and Prisma types from `@prisma/client`.
|
|
- Used by: Page routes such as `src/app/[locale]/page.tsx` and list APIs such as `src/app/api/projects/route.ts`.
|
|
|
|
**API and Mutation Layer:**
|
|
- Purpose: Expose JSON endpoints for list/detail reads, AI search proxying, signals feed reads, authenticated tag maintenance, project-tag resets, and signal ingestion.
|
|
- Location: `src/app/api/projects/route.ts`, `src/app/api/projects/[slug]/route.ts`, `src/app/api/search/ai/route.ts`, `src/app/api/signals/route.ts`, `src/app/api/tags/route.ts`, `src/app/api/tags/maintenance/route.ts`, `src/app/api/tags/maintenance/service.ts`, `src/app/api/tags/reset-projects/route.ts`, `src/app/api/webhook/signals/route.ts`
|
|
- Contains: `GET` and `POST` route handlers, request parsing, validation, response shaping, cache revalidation, and transaction-scoped maintenance logic.
|
|
- Depends on: `src/lib/validations.ts`, `src/lib/auth.ts`, `src/lib/prisma.ts`, `src/lib/signal-hotness.ts`, and the query layer in `src/hooks`.
|
|
- Used by: Client components, external webhook callers, and automation clients.
|
|
|
|
**Domain Utility Layer:**
|
|
- Purpose: Hold cross-route domain logic that is not tied to a single route.
|
|
- Location: `src/lib/auth.ts`, `src/lib/cache.ts`, `src/lib/prisma.ts`, `src/lib/signal-hotness.ts`, `src/lib/slug.ts`, `src/lib/tag-taxonomy.ts`, `src/lib/validations.ts`, `src/lib/github/badges.ts`
|
|
- Contains: API-key verification, cache fallback helpers, Prisma singleton setup, signal hotness scoring, slug generation, tag taxonomy inference, schema validation, and GitHub link parsing.
|
|
- Depends on: Standard library APIs, Prisma, and Zod.
|
|
- Used by: Both pages and API handlers.
|
|
|
|
**Persistence Layer:**
|
|
- Purpose: Define and migrate the database model and seed baseline data.
|
|
- Location: `prisma/schema.prisma`, `prisma/migrations/*`, `prisma/seed.ts`
|
|
- Contains: PostgreSQL schema for `Project`, `Tag`, `ProjectTag`, `ExternalLink`, and `Signal`, plus migrations and seed data.
|
|
- Depends on: Prisma CLI and environment-provided `DATABASE_URL`.
|
|
- Used by: `src/lib/prisma.ts` at runtime and Prisma tooling during migrations/seed.
|
|
|
|
## Data Flow
|
|
|
|
**Localized Page Render:**
|
|
|
|
1. `src/middleware.ts` forces a locale-prefixed pathname and excludes `/api`, `/_next`, and static assets from locale routing.
|
|
2. `src/app/[locale]/layout.tsx` validates the locale, calls `setRequestLocale(locale)`, loads messages through `next-intl`, and wraps the tree with `NextIntlClientProvider`.
|
|
3. Route pages such as `src/app/[locale]/page.tsx`, `src/app/[locale]/projects/page.tsx`, and `src/app/[locale]/projects/[id]/page.tsx` fetch data from `src/hooks/useProjects.ts` or `src/hooks/useHome.ts`.
|
|
4. Reusable server and client components render the data, with ISR enabled through `export const revalidate = 300` on the main content routes.
|
|
|
|
**Traditional Projects Browse Flow:**
|
|
|
|
1. `src/app/[locale]/projects/page.tsx` reads `searchParams`, normalizes filters, and calls `getProjects`, `getFixedProjectTypeFilters`, and `getTagCategoryGroups` from `src/hooks/useProjects.ts`.
|
|
2. `src/app/[locale]/projects/ProjectsPageClient.tsx` and `src/components/project/TagFilterPanel.tsx` manage filter-panel visibility and query-string updates in the browser.
|
|
3. `src/app/[locale]/projects/ProjectsResultsClient.tsx` fetches updated pages from `src/app/api/projects/route.ts` for client-side pagination and sorting.
|
|
4. `src/app/api/projects/route.ts` validates query parameters with Zod, then delegates to `getProjects`.
|
|
|
|
**AI Search Flow:**
|
|
|
|
1. `src/components/search/AISearchBar.tsx` toggles between traditional and AI mode and forwards the query back to the page-level handler.
|
|
2. `src/app/[locale]/projects/ProjectsResultsClient.tsx` requests `POST /api/search/ai`.
|
|
3. `src/app/api/search/ai/route.ts` validates the request with `ProjectQuerySchema`, forwards a GET request to the external N8N webhook URL from `N8N_AI_SEARCH_WEBHOOK`, then hydrates returned IDs by calling `getProjectsByIds` from `src/hooks/useProjects.ts`.
|
|
4. The route sorts and filters the hydrated projects again before returning JSON to the client.
|
|
|
|
**Signals Feed Flow:**
|
|
|
|
1. `src/app/[locale]/signals/page.tsx` renders shell content and mounts `src/components/signals/SignalFeedClient.tsx`.
|
|
2. `SignalFeedClient` holds search text, debounce state, source filter, sort mode, and pagination cursor in component state.
|
|
3. The client calls `GET /api/signals` on `src/app/api/signals/route.ts`.
|
|
4. `src/app/api/signals/route.ts` validates the query, queries Prisma, computes or reads hotness metadata via `src/lib/signal-hotness.ts`, and returns cursor-based pagination.
|
|
|
|
**Signal Ingestion Flow:**
|
|
|
|
1. External automation posts to `src/app/api/webhook/signals/route.ts`.
|
|
2. The handler validates the top-level payload and each item with `SignalWebhookPayloadSchema` and `SignalIngestionInputSchema` from `src/lib/validations.ts`.
|
|
3. `src/lib/auth.ts` verifies `apiKey` using `crypto.timingSafeEqual`.
|
|
4. The handler upserts signals in Prisma, computing fallback hotness when needed and downgrading if `hotScore`/`isHot` columns are absent.
|
|
|
|
**Tag Maintenance Flow:**
|
|
|
|
1. Clients call `POST /api/tags/maintenance` or `POST /api/tags/reset-projects`.
|
|
2. The route validates input, authenticates with `isValidApiKey`, and runs Prisma mutations.
|
|
3. `src/app/api/tags/maintenance/route.ts` delegates transactional merge/update logic to `src/app/api/tags/maintenance/service.ts`.
|
|
4. Successful mutations call `revalidatePath` for affected localized pages.
|
|
|
|
## State Management
|
|
|
|
**Server State:**
|
|
- Persistent application state lives in PostgreSQL, modeled in `prisma/schema.prisma` and accessed only through Prisma in `src/lib/prisma.ts`.
|
|
- Page-level read models are built in `src/hooks/useProjects.ts` and `src/hooks/useHome.ts`.
|
|
|
|
**Cached Server State:**
|
|
- `unstable_cache` is used in `src/hooks/useProjects.ts`, `src/hooks/useHome.ts`, and `src/app/api/tags/route.ts`.
|
|
- `src/lib/cache.ts` provides `runWithCacheFallback` so cached fetchers can fall back to direct database reads when Incremental Cache is unavailable.
|
|
|
|
**Client UI State:**
|
|
- Local component state, not a global store, drives interactivity.
|
|
- `src/app/[locale]/projects/ProjectsPageClient.tsx` stores filter-panel expansion.
|
|
- `src/app/[locale]/projects/ProjectsResultsClient.tsx` stores AI/traditional pagination, loading flags, and sort/limit state.
|
|
- `src/components/signals/SignalFeedClient.tsx` stores debounced search, source filters, sort mode, cursor, and loading state.
|
|
- `src/components/layout/AnnouncementBar.tsx` stores dismissal state in `localStorage`.
|
|
|
|
**URL State:**
|
|
- Search, pagination, sort, AI mode, and taxonomy filters are encoded in the query string on `src/app/[locale]/projects/page.tsx`.
|
|
- The projects UI treats the URL as the source of truth and synchronizes local state to it through `router.push` and `window.history.replaceState`.
|
|
|
|
**Translation State:**
|
|
- Locale selection is path-based and enforced by `src/middleware.ts`.
|
|
- Message bundles load from `src/messages/en.json` and `src/messages/zh.json` via `src/i18n/request.ts`.
|
|
|
|
## Key Abstractions
|
|
|
|
**Project Read Model:**
|
|
- Purpose: Present projects with flattened tags and related links, independent of Prisma join-table shape.
|
|
- Examples: `ProjectWithFlatTags` and `getProjects` in `src/hooks/useProjects.ts`, `getProjectBySlug` in `src/hooks/useProjects.ts`
|
|
- Pattern: Read-model transformation from Prisma includes to page/API-friendly objects.
|
|
|
|
**Tag Taxonomy:**
|
|
- Purpose: Normalize and classify project tags into fixed categories used by filters and maintenance APIs.
|
|
- Examples: `src/lib/tag-taxonomy.ts`, `getTagCategoryGroups` in `src/hooks/useProjects.ts`
|
|
- Pattern: Central domain vocabulary with inference helpers shared across pages and mutations.
|
|
|
|
**Signal View Model:**
|
|
- Purpose: Convert raw `Signal` rows, localized text fields, JSON sections, and hotness metadata into feed-ready objects.
|
|
- Examples: `toSignalView` and `parseSections` in `src/app/api/signals/route.ts`
|
|
- Pattern: API-specific presentation mapping on top of Prisma rows.
|
|
|
|
**Maintenance Service:**
|
|
- Purpose: Keep complex tag merge/update rules out of the route handler.
|
|
- Examples: `executeTagMaintenance` and `TagMaintenanceApiError` in `src/app/api/tags/maintenance/service.ts`
|
|
- Pattern: Route delegates orchestration to a transaction-aware service module.
|
|
|
|
**Auth Guard for Internal APIs:**
|
|
- Purpose: Reuse constant-time API-key validation across maintenance and webhook routes.
|
|
- Examples: `isValidApiKey` in `src/lib/auth.ts`, used by `src/app/api/tags/maintenance/route.ts`, `src/app/api/tags/reset-projects/route.ts`, and `src/app/api/webhook/signals/route.ts`
|
|
- Pattern: Small shared guard function, not middleware-based auth.
|
|
|
|
## Entry Points
|
|
|
|
**Root App Shell:**
|
|
- Location: `src/app/layout.tsx`
|
|
- Triggers: Every page render.
|
|
- Responsibilities: Global HTML/body shell, global CSS import, and conditional Vercel analytics injection through `src/app/VercelMetrics.tsx`.
|
|
|
|
**Localized App Shell:**
|
|
- Location: `src/app/[locale]/layout.tsx`
|
|
- Triggers: Every localized page render under `/<locale>/*`.
|
|
- Responsibilities: Locale validation, translations, site navigation, announcement bar, footer, and `NextIntlClientProvider`.
|
|
|
|
**Home Route:**
|
|
- Location: `src/app/[locale]/page.tsx`
|
|
- Triggers: `GET /zh` and `GET /en`
|
|
- Responsibilities: Render home hero, stats, rankings, tag insights, recent timeline, and featured projects.
|
|
|
|
**Projects Route:**
|
|
- Location: `src/app/[locale]/projects/page.tsx`
|
|
- Triggers: `GET /<locale>/projects`
|
|
- Responsibilities: Parse URL filters, render filter/search UI, fetch paginated project data, and mount the projects results client island.
|
|
|
|
**Project Detail Route:**
|
|
- Location: `src/app/[locale]/projects/[id]/page.tsx`
|
|
- Triggers: `GET /<locale>/projects/:slug`
|
|
- Responsibilities: Fetch a single project, derive related projects, render sidebar/details, and generate route metadata.
|
|
|
|
**Signals Route:**
|
|
- Location: `src/app/[locale]/signals/page.tsx`
|
|
- Triggers: `GET /<locale>/signals`
|
|
- Responsibilities: Render the signals feed shell and mount the client-side feed loader.
|
|
|
|
**API Routes:**
|
|
- Location: `src/app/api/*`
|
|
- Triggers: Browser fetches and external webhook clients.
|
|
- Responsibilities: JSON read endpoints, AI search proxying, signals feed pagination, authenticated tag maintenance, project tag resets, and signal ingestion.
|
|
|
|
**Locale Middleware:**
|
|
- Location: `src/middleware.ts`
|
|
- Triggers: All non-API, non-static requests.
|
|
- Responsibilities: Apply the `next-intl` locale prefix policy and route matching.
|
|
|
|
## Error Handling
|
|
|
|
**Strategy:** Validate early, return structured JSON for API errors, use `notFound()` for invalid page resources, and degrade some read paths to empty or fallback results instead of hard-failing the page.
|
|
|
|
**Patterns:**
|
|
- Use Zod validation in `src/app/api/projects/route.ts`, `src/app/api/search/ai/route.ts`, `src/app/api/signals/route.ts`, `src/app/api/tags/maintenance/route.ts`, `src/app/api/tags/reset-projects/route.ts`, and `src/app/api/webhook/signals/route.ts`.
|
|
- Use `notFound()` in `src/app/[locale]/layout.tsx`, `src/app/[locale]/projects/[id]/page.tsx`, and `src/app/[locale]/[...catchAll]/page.tsx` for route-level misses.
|
|
- Retry transient database errors in `src/hooks/useProjects.ts` via `withDbRetry`.
|
|
- Degrade to empty or zero-count results in `src/hooks/useProjects.ts` and `src/hooks/useHome.ts` when some DB reads fail.
|
|
- Wrap service-specific failures in `TagMaintenanceApiError` in `src/app/api/tags/maintenance/service.ts`.
|
|
|
|
## Cross-Cutting Concerns
|
|
|
|
**Logging:** Use `console.error` and `console.warn` directly in route handlers and server query modules, for example in `src/app/api/search/ai/route.ts`, `src/app/api/signals/route.ts`, `src/app/api/webhook/signals/route.ts`, `src/hooks/useProjects.ts`, and `src/hooks/useHome.ts`.
|
|
|
|
**Validation:** Use shared Zod schemas from `src/lib/validations.ts` for external request bodies and query parameters.
|
|
|
|
**Authentication:** Protect internal mutation/webhook endpoints with API-key checks from `src/lib/auth.ts`. No session or user-account auth layer is present in the inspected files.
|
|
|
|
**Internationalization:** Use `next-intl` with locale-prefixed routing through `src/middleware.ts`, request config in `src/i18n/request.ts`, and message bundles in `src/messages/*.json`.
|
|
|
|
**Caching and Revalidation:** Use ISR-style route revalidation (`revalidate = 300`), `unstable_cache`, and `revalidatePath` after tag mutations.
|
|
|
|
**Observability:** The only built-in runtime instrumentation found is Vercel analytics in `src/app/VercelMetrics.tsx`. No separate tracing or background job framework was detected in inspected files.
|
|
|
|
---
|
|
|
|
*Architecture analysis: 2026-04-18*
|