docs(agents): write per-app AGENTS.md for cms, web-next, web-tanstack, storybook

This commit is contained in:
2026-05-05 10:00:28 +02:00
parent 0ede53998f
commit 2c6c836206
4 changed files with 232 additions and 381 deletions

View File

@@ -1,108 +1,97 @@
# apps/cms -- Payload CMS Admin Shell
# AGENTS.md — apps/cms
**Thin shell** hosting the Payload CMS admin panel via Next.js. All CMS configuration (collections, globals, hooks, access control, `payload.config.ts`) lives in `@repo/core-cms`, which aggregates collections from feature packages.
## Purpose
**THIN SHELL ONLY** -- this app exists solely to serve the Payload CMS admin panel via Next.js. All CMS logic (collections, globals, hooks, access control, `payload.config.ts`) lives in `@repo/cms-core`. This app contains no custom CMS code beyond Next.js routing boilerplate that Payload generates automatically.
This app exists solely to serve the Payload Admin UI. It contains no custom CMS code beyond Next.js routing boilerplate. All business knowledge lives in feature packages (`@repo/auth`, `@repo/blog`, etc.), which export their collections/globals via subpath exports (`.../cms`). `@repo/core-cms` composes them into a single Payload config.
## Port: 3001
```bash
pnpm dev --filter @repo/cms # http://localhost:3001/admin
docker compose up -d postgres # Start PostgreSQL on port 5432
pnpm dev --filter @repo/cms # http://localhost:3001/admin
```
Requires PostgreSQL running first:
```bash
docker compose up -d postgres # Starts PostgreSQL on port 5432
```
## Hard Rules
- **NEVER** add collections, globals, hooks, or access control in this app -- put them in `@repo/cms-core`
- **NEVER** import from `@repo/core/infrastructure`
- **NEVER** modify auto-generated files (marked with "THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD")
- All CMS configuration changes go in `packages/cms-core/`
## Key Files
| File | Purpose |
|---|---|
| `next.config.mjs` | Next.js config wrapped with `withPayload()` from `@payloadcms/next` |
| `next.config.mjs` | Minimal config wrapped with `withPayload()` from `@payloadcms/next` |
| `tsconfig.json` | TypeScript config with `@payload-config` path alias |
| `src/app/(payload)/layout.tsx` | Auto-generated Payload root layout (DO NOT MODIFY) |
| `src/app/(payload)/admin/[[...segments]]/page.tsx` | Auto-generated catch-all admin page (DO NOT MODIFY) |
| `src/app/(payload)/admin/[[...segments]]/not-found.tsx` | Auto-generated 404 page (DO NOT MODIFY) |
| `src/app/(payload)/importMap.js` | Auto-generated Payload import map (DO NOT MODIFY) |
| `src/app/(payload)/custom.scss` | Custom SCSS overrides for admin panel styling |
## Hard Rules
- **NEVER** add collections, globals, or hooks in this app — put them in feature packages
- **NEVER** create custom CMS logic here — use `@repo/core-cms`
- **NEVER** modify auto-generated files under `src/app/(payload)/`
- All Payload config changes go in `packages/core-cms/src/payload.config.ts`
## @payload-config Alias
The `tsconfig.json` defines a path alias that points to the config in `@repo/cms-core`:
The `tsconfig.json` points to `@repo/core-cms`:
```json
{
"compilerOptions": {
"paths": {
"@payload-config": [
"../../packages/cms-core/src/payload.config.ts"
]
"@payload-config": ["../../packages/core-cms/src/payload.config.ts"]
}
}
}
```
When Payload or auto-generated files import `@payload-config`, it resolves to `packages/cms-core/src/payload.config.ts`. This is how the thin shell delegates all configuration to `@repo/cms-core`.
When Payload imports `@payload-config`, it resolves to the composed config from `@repo/core-cms`, which in turn imports feature collections.
## next.config.mjs
## Composition flow
The Next.js config is minimal -- just the `withPayload` wrapper:
```javascript
import { withPayload } from "@payloadcms/next/withPayload";
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withPayload(nextConfig);
```
Feature 1 (@repo/blog)
└─ src/integrations/cms/collections/articles.ts
└─ exported as ./cms
`withPayload()` adds the necessary webpack aliases, module resolution, and middleware for Payload to work within Next.js.
Feature 2 (@repo/auth)
└─ src/integrations/cms/collections/users.ts
└─ exported as ./cms
## Auto-Generated Files
Feature 3 (@repo/navigation)
└─ src/integrations/cms/globals/header.ts
└─ exported as ./cms
The files under `src/app/(payload)/` are generated by Payload and should NOT be manually edited:
Core CMS (@repo/core-cms)
└─ src/payload.config.ts
imports all feature /cms exports
calls buildConfig({ collections, globals })
- **`layout.tsx`** -- Wraps the admin panel with `RootLayout` from `@payloadcms/next/layouts`, injects config and importMap
- **`admin/[[...segments]]/page.tsx`** -- Catch-all route that renders `RootPage` from `@payloadcms/next/views`
- **`admin/[[...segments]]/not-found.tsx`** -- 404 handler using `NotFoundPage` from `@payloadcms/next/views`
- **`importMap.js`** -- Maps Payload component paths for the admin UI
If you need to regenerate these files, Payload will do so automatically during dev/build.
This app (@repo/cms)
└─ src/app/(payload)/layout.tsx
loads config from @payload-config
Payload CLI auto-generates admin routes
```
## Type Generation
To regenerate Payload TypeScript types after changing collections/globals:
After adding/modifying collections in any feature's `/cms` folder:
```bash
cd apps/cms && pnpm generate:types
# Equivalent to: payload generate:types
# Output goes to: packages/cms-core/src/payload-types.ts
# Regenerates packages/core-cms/src/generated-types.ts
```
## Dependencies
| Dependency | Purpose |
|---|---|
| `@repo/cms-core` | All Payload configuration (collections, globals, hooks, config) |
| `@payloadcms/next` | Next.js integration for Payload (withPayload, admin UI views) |
| `@payloadcms/ui` | Payload admin panel React components |
| `@repo/core-cms` | Payload config + buildConfig |
| `@payloadcms/next` | Next.js integration for Payload |
| `payload` | Payload CMS core |
| `next` | Next.js 15 framework |
| `react` / `react-dom` | React 19 runtime |
| `sharp` | Image processing for Payload uploads |
| `sharp` | Image processing |
## Cross-References
- **ALL CMS configuration:** `packages/cms-core/` -- see `packages/cms-core/AGENTS.md`
- **CMS client for querying data:** `packages/cms-client/` -- see `packages/cms-client/AGENTS.md`
- **Core business logic:** `packages/core/` -- see `packages/core/AGENTS.md`
- **Feature collections:** each feature's `src/integrations/cms/` folder
- **CMS composition:** `packages/core-cms/AGENTS.md`

View File

@@ -1,8 +1,10 @@
# apps/storybook -- Centralized Storybook
# AGENTS.md — apps/storybook
Centralized Storybook instance pulling stories from `@repo/core-ui`. Provides visual component development, documentation, and MCP integration for AI agents.
## Purpose
Centralized Storybook instance that pulls and renders all stories from `packages/core-ui`. Provides a visual development environment, component documentation, and MCP integration for AI agents.
Visual testing and documentation hub for the design system. All stories live colocated with their components in `@repo/core-ui`. Storybook serves as the single source of truth for component usage.
## Port: 6006
@@ -14,20 +16,17 @@ pnpm dev --filter @repo/storybook # http://localhost:6006
### `.storybook/main.ts`
```typescript
import type { StorybookConfig } from "@storybook/react-vite";
Stories are discovered from `@repo/core-ui`:
```typescript
const config: StorybookConfig = {
framework: "@storybook/react-vite",
stories: ["../../../packages/core-ui/src/**/*.stories.@(ts|tsx)"],
addons: ["@storybook/addon-essentials"],
docs: {
autodocs: "tag",
},
docs: { autodocs: "tag" },
async viteFinal(config) {
const { mergeConfig } = await import("vite");
const tailwindPlugin = await import("@tailwindcss/vite");
return mergeConfig(config, {
plugins: [tailwindPlugin.default()],
});
@@ -35,14 +34,15 @@ const config: StorybookConfig = {
};
```
Key configuration details:
- **`stories`** glob reaches into `packages/core-ui/src/` to find all `.stories.tsx` files
- **`viteFinal`** adds the `@tailwindcss/vite` plugin so Tailwind v4 classes render correctly in stories
- **`autodocs: "tag"`** generates documentation pages for stories tagged with `"autodocs"`
- **`@storybook/addon-essentials`** includes Controls, Actions, Backgrounds, Viewport, Docs
Key settings:
- **`stories` glob** — reaches into `packages/core-ui/src/` for all `*.stories.tsx` files
- **`viteFinal`** adds Tailwind v4 plugin so classes render in Storybook
- **`autodocs: "tag"`** — auto-generates docs for tagged stories
### `.storybook/preview.ts`
Imports global styles:
```typescript
import type { Preview } from "@storybook/react";
import "../../../packages/core-ui/src/styles/globals.css";
@@ -59,77 +59,75 @@ const preview: Preview = {
};
```
Key details:
- Imports `globals.css` from `@repo/ui` so all Tailwind v4 `@theme` tokens are available
- Control matchers auto-detect color and date props for appropriate editor widgets
## Story Organization
Stories are organized by Atomic Design level via the `title` field in story metadata. The title determines the sidebar hierarchy in Storybook.
Stories are organized by Atomic Design level via the `title` field:
### Story Title Convention
| Level | Title format | Example | Sidebar path |
|---|---|---|---|
| Atom | `"Atoms/{ComponentName}"` | `"Atoms/Button"` | Atoms > Button |
| Molecule | `"Molecules/{ComponentName}"` | `"Molecules/FormField"` | Molecules > FormField |
| Organism | `"Organisms/{ComponentName}"` | `"Organisms/DataTable"` | Organisms > DataTable |
| Template | `"Templates/{ComponentName}"` | `"Templates/DashboardLayout"` | Templates > DashboardLayout |
### Existing Stories
| Story title | Component | Location in `@repo/core-ui` |
| Level | Title format | Sidebar path |
|---|---|---|
| `Atoms/Button` | Button (5 variants: Default, Secondary, Destructive, Outline, Ghost) | `src/atoms/button/button.stories.tsx` |
| `Atoms/Input` | Input (Default, Disabled) | `src/atoms/input/input.stories.tsx` |
| `Molecules/FormField` | FormField (Default, WithDescription, WithError) | `src/molecules/form-field/form-field.stories.tsx` |
| Atom | `"Atoms/{ComponentName}"` | Atoms > ComponentName |
| Molecule | `"Molecules/{ComponentName}"` | Molecules > ComponentName |
| Organism | `"Organisms/{ComponentName}"` | Organisms > ComponentName |
| Template | `"Templates/{ComponentName}"` | Templates > ComponentName |
Example story file (`packages/core-ui/src/atoms/button/button.stories.tsx`):
```typescript
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./button";
const meta = {
title: "Atoms/Button",
component: Button,
tags: ["autodocs"],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { children: "Click me" },
};
export const Variant: Story = {
args: { children: "Secondary", variant: "secondary" },
};
```
## MCP Integration
When Storybook is running, the MCP (Model Context Protocol) endpoint is available at:
When Storybook runs, the MCP endpoint is available at:
```
http://localhost:6006/mcp
```
### Available MCP tools:
### Available tools:
- **`list-all-documentation`** -- Lists all documented components and their stories
- **`get-documentation`** -- Gets detailed documentation for a specific component (props, variants, usage)
- **`run-story-tests`** -- Runs visual tests on stories to validate rendering
- **`list-all-documentation`** Lists all component stories and their properties
- **`get-documentation`** Gets detailed component info (props, variants, usage examples)
- **`run-story-tests`** — Validates story rendering
### Installing addon-mcp
### Before building new components:
If `@storybook/addon-mcp` is not already installed:
```bash
npx storybook add @storybook/addon-mcp
```
This adds the addon to `.storybook/main.ts` and enables the MCP endpoint.
### Using MCP in workflows
Always query MCP before creating new UI components:
1. **Before creating:** `list-all-documentation` to check if a similar component exists
2. **Before extending:** `get-documentation` to understand existing props and variants
3. **After creating:** `run-story-tests` to validate the new story renders correctly
1. Query `list-all-documentation` to check if a similar component exists
2. Query `get-documentation` to understand existing props and variants
3. After creating: `run-story-tests` to validate
## Dependencies
| Dependency | Purpose |
|---|---|
| `@repo/core-ui` | Source of all component stories |
| `@storybook/react-vite` | Storybook framework using Vite bundler |
| `@storybook/addon-essentials` | Controls, Actions, Docs, Backgrounds, Viewport |
| `@tailwindcss/vite` | Vite plugin for Tailwind CSS v4 |
| `storybook` | Storybook core CLI and dev server |
| `tailwindcss` | Tailwind CSS v4 engine |
| `vite` | Build tool / dev server |
| `react` / `react-dom` | React 19 runtime |
| `@repo/core-ui` | Component source + stories |
| `@storybook/react-vite` | Storybook with Vite bundler |
| `@storybook/addon-essentials` | Controls, Actions, Docs, Backgrounds |
| `@tailwindcss/vite` | Vite plugin for Tailwind v4 |
| `storybook` | Storybook CLI + dev server |
| `tailwindcss` | Tailwind CSS v4 |
| `vite` | Build tool |
| `react` / `react-dom` | React 19 |
## Cross-References
- **Component source:** `packages/core-ui/` -- see `packages/core-ui/AGENTS.md`
- **Tailwind tokens:** `packages/core-ui/src/styles/globals.css`
- **Component source:** `packages/core-ui/AGENTS.md`
- **Storybook docs:** `.storybook/` folder

View File

@@ -1,8 +1,10 @@
# apps/web-next -- Next.js 15 Reference App
# AGENTS.md — apps/web-next
Next.js 15 reference application using App Router. Demonstrates consuming feature packages via tRPC, using `@repo/core-trpc/next` for client setup, and importing UI components from `@repo/core-ui`.
## Purpose
Next.js 15 reference application using App Router. Demonstrates how to consume `@repo/api-client` for tRPC data fetching, `@repo/ui` for components, and `@repo/api` for the tRPC HTTP endpoint. This is a thin app -- business logic lives in `@repo/core`, UI components live in `@repo/ui`.
Thin app showcasing how features work end-to-end. Business logic lives in feature packages (`@repo/auth`, `@repo/blog`, etc.); UI primitives live in `@repo/core-ui`; this app is mostly routes, layouts, and component composition.
## Port: 3000
@@ -10,22 +12,32 @@ Next.js 15 reference application using App Router. Demonstrates how to consume `
pnpm dev --filter @repo/web-next # http://localhost:3000
```
Requires `@repo/cms` and PostgreSQL running to fetch live data:
```bash
docker compose up -d postgres # PostgreSQL on port 5432
pnpm dev --filter @repo/cms # Payload admin on port 3001
pnpm dev --filter @repo/web-next # Next.js on port 3000
```
## Key Files
| File | Purpose |
|---|---|
| `src/app/layout.tsx` | Root layout -- wraps children with `<Providers>`, sets HTML metadata |
| `src/app/providers.tsx` | Client component that wraps the app with `<ApiProvider trpcUrl="/api/trpc">` |
| `src/app/page.tsx` | Home page (server component by default) |
| `src/app/api/trpc/[trpc]/route.ts` | tRPC HTTP endpoint using the Next.js fetch adapter |
| `src/app/layout.tsx` | Root layout wraps app with `<TrpcProvider>` from `@repo/core-trpc/next` |
| `src/app/providers.tsx` | Client component for tRPC + React Query setup |
| `src/app/page.tsx` | Home page — navigation + marketing content |
| `src/app/blog/[slug]/page.tsx` | Dynamic blog post route |
| `src/app/api/trpc/[trpc]/route.ts` | tRPC fetch adapter endpoint |
| `e2e/` | Playwright end-to-end tests |
## tRPC Endpoint Setup
## tRPC Setup
The file `src/app/api/trpc/[trpc]/route.ts` creates a catch-all API route that handles all tRPC requests:
The tRPC endpoint handler (in `src/app/api/trpc/[trpc]/route.ts`):
```typescript
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@repo/api";
import { appRouter } from "@repo/core-api";
const handler = (req: Request) =>
fetchRequestHandler({
@@ -38,149 +50,14 @@ const handler = (req: Request) =>
export { handler as GET, handler as POST };
```
How it works:
1. Next.js catch-all route `[trpc]` matches any path under `/api/trpc/`
2. `fetchRequestHandler` from tRPC's fetch adapter processes the request
3. `appRouter` from `@repo/api` contains all registered routers
4. `createContext` provides the context object to all procedures (currently empty `{}`)
5. Both GET (for queries) and POST (for mutations/batched queries) are exported
## Provider Setup
The `<ApiProvider>` from `@repo/api-client` must wrap the entire app. Since it uses React hooks, it lives in a `"use client"` component:
```tsx
// src/app/providers.tsx
"use client";
import { ApiProvider } from "@repo/api-client";
export function Providers({ children }: { children: React.ReactNode }) {
return <ApiProvider trpcUrl="/api/trpc">{children}</ApiProvider>;
}
```
```tsx
// src/app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
```
## Recipe: Adding a New Page with Data Fetching
This example adds an `/articles` page that lists published articles.
### Step 1: Create the page route
Create `src/app/articles/page.tsx`:
```tsx
import { ArticleList } from "./article-list";
export default function ArticlesPage() {
return (
<main>
<h1>Articles</h1>
<ArticleList />
</main>
);
}
```
### Step 2: Create the client component with data fetching
Create `src/app/articles/article-list.tsx`:
```tsx
"use client";
import { useTRPC } from "@repo/api-client";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@repo/ui";
export function ArticleList() {
const trpc = useTRPC();
const { data, isLoading, error } = useQuery(
trpc.content.listArticles.queryOptions({ status: "published", limit: 20 })
);
if (isLoading) return <p>Loading articles...</p>;
if (error) return <p>Error loading articles: {error.message}</p>;
return (
<ul>
{data?.map((article) => (
<li key={article.id}>
<h2>{article.title}</h2>
<Button variant="outline" size="sm">
Read more
</Button>
</li>
))}
</ul>
);
}
```
Key patterns:
- The page component (`page.tsx`) is a server component by default -- no `"use client"` needed
- Data-fetching components that use `useTRPC()` must be client components (`"use client"`)
- Import UI components from `@repo/ui`, never recreate them locally
## Payload Initialization Pattern (Server-Side Local API)
For server-side access to Payload CMS data (e.g., in server components, API routes, or server actions), create a Payload client initializer:
The provider (in `src/app/providers.tsx`):
```typescript
// src/lib/payload.ts
import { getPayload } from "payload";
import config from "@repo/cms-core/src/payload.config";
import { createPayloadClient, type PayloadClient } from "@repo/cms-client";
"use client";
import { TrpcProvider } from "@repo/core-trpc/next";
let cachedClient: PayloadClient | null = null;
export async function getPayloadClient(): Promise<PayloadClient> {
if (cachedClient) return cachedClient;
const payload = await getPayload({ config });
cachedClient = createPayloadClient({ mode: "local", payload });
return cachedClient;
}
```
Usage in a server component:
```tsx
// src/app/articles/page.tsx (server component)
import { getPayloadClient } from "@/lib/payload";
export default async function ArticlesPage() {
const client = await getPayloadClient();
const result = await client.find("articles", {
where: { status: { equals: "published" } },
sort: "-publishedAt",
limit: 20,
});
return (
<main>
<h1>Articles</h1>
<ul>
{result.docs.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
</main>
);
export function Providers({ children }: React.ReactNode) {
return <TrpcProvider>{children}</TrpcProvider>;
}
```
@@ -188,16 +65,43 @@ export default async function ArticlesPage() {
| Dependency | Purpose |
|---|---|
| `@repo/api` | `appRouter` for the tRPC HTTP endpoint |
| `@repo/api-client` | `ApiProvider` + `useTRPC()` for client-side data fetching |
| `@repo/ui` | Shared UI components (Button, Input, Label, FormField, etc.) |
| `next` | Next.js 15 framework with App Router |
| `react` / `react-dom` | React 19 runtime |
| `@repo/core-api` | `appRouter` for tRPC endpoint |
| `@repo/core-trpc/next` | Next.js tRPC client + provider |
| `@repo/core-ui` | Design system components |
| `@repo/auth`, `@repo/blog`, etc. | Feature packages (indirectly via core-api) |
| `next` | Next.js 15 framework |
| `@trpc/server` | tRPC server (fetch adapter) |
## Test conventions
- Unit tests colocated: `src/app/blog/article-list.test.tsx`
- Vitest environment: `jsdom`
- e2e tests in `e2e/` folder: `*.spec.ts`
- Run: `pnpm test --filter @repo/web-next` (units) or `pnpm test:e2e` (Playwright)
## E2E Test Setup
Playwright config in `e2e/playwright.config.ts`:
```typescript
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
webServer: {
command: "pnpm dev",
port: 3000,
reuseExistingServer: !process.env.CI,
},
use: { ...devices["Desktop Chrome"].use },
});
```
Run: `pnpm test:e2e` starts the dev server and runs all `.spec.ts` files.
## Cross-References
- **tRPC routers:** `packages/api/` -- see `packages/api/AGENTS.md`
- **tRPC client/hooks:** `packages/api-client/` -- see `packages/api-client/AGENTS.md`
- **UI components:** `packages/ui/` -- see `packages/ui/AGENTS.md`
- **CMS client:** `packages/cms-client/` -- see `packages/cms-client/AGENTS.md`
- **CMS config:** `packages/cms-core/` -- see `packages/cms-core/AGENTS.md`
- **Feature packages:** `packages/{auth,blog,media,marketing-pages,navigation}/`
- **tRPC composition:** `packages/core-api/AGENTS.md`
- **tRPC client + provider:** `packages/core-trpc/AGENTS.md`
- **UI components:** `packages/core-ui/AGENTS.md`

View File

@@ -1,8 +1,10 @@
# apps/web-tanstack -- TanStack Start Reference App
# AGENTS.md — apps/web-tanstack
TanStack Start reference application using TanStack Router with file-based routing. Demonstrates that feature packages are framework-agnostic by consuming the same features as Next.js (via `@repo/core-api`) using TanStack's architecture instead.
## Purpose
TanStack Start reference application using TanStack Router with file-based routing. Demonstrates how to consume `@repo/api-client` for tRPC data fetching and `@repo/ui` for components. Like `apps/web-next`, this is a thin app -- business logic lives in `@repo/core`, UI components live in `@repo/ui`.
Proof that features are framework-portable. This app consumes the exact same feature packages and tRPC routers as `apps/web-next`, but through TanStack Start's server/client architecture instead of Next.js App Router.
## Port: 3002
@@ -10,149 +12,107 @@ TanStack Start reference application using TanStack Router with file-based routi
pnpm dev --filter @repo/web-tanstack # http://localhost:3002
```
Requires tRPC endpoint (from `apps/web-next` or another backend):
```bash
pnpm dev --filter @repo/web-next # Serves tRPC at http://localhost:3000/api/trpc
pnpm dev --filter @repo/web-tanstack # http://localhost:3002
```
## Key Files
| File | Purpose |
|---|---|
| `src/routes/__root.tsx` | Root layout -- creates the root route, wraps with `<ApiProvider>` and `<Outlet>` |
| `src/routes/index.tsx` | Home page route (`/`) |
| `src/routes/__root.tsx` | Root layout — wraps all routes with `<TrpcProvider>` from `@repo/core-trpc/tanstack` |
| `src/routes/index.tsx` | Home page (`/`) |
| `src/routes/blog/index.tsx` | Blog listing (`/blog`) |
| `src/routes/blog/$slug.tsx` | Dynamic blog post (`/blog/:slug`) |
| `e2e/` | Playwright end-to-end tests |
## File-Based Routing
TanStack Router uses file-based routing where file paths in `src/routes/` map directly to URL paths:
TanStack Router uses file-based routing where file paths map directly to URL routes:
| File | URL | Description |
|---|---|---|
| `src/routes/__root.tsx` | (all routes) | Root layout, wraps all child routes |
| `src/routes/index.tsx` | `/` | Home page |
| `src/routes/about.tsx` | `/about` | Static page |
| `src/routes/articles/index.tsx` | `/articles` | Article listing |
| `src/routes/articles/$id.tsx` | `/articles/:id` | Single article (dynamic param) |
| File | URL |
|---|---|
| `src/routes/__root.tsx` | Root (all routes) |
| `src/routes/index.tsx` | `/` |
| `src/routes/blog/index.tsx` | `/blog` |
| `src/routes/blog/$slug.tsx` | `/blog/:slug` |
### Naming conventions:
- `__root.tsx` -- special root layout file, always wraps all routes
- `index.tsx` -- index route for its directory (e.g., `/articles/index.tsx` matches `/articles`)
- `$paramName.tsx` -- dynamic route segment (e.g., `$id.tsx` captures `:id`)
- Nested folders create nested URL segments
Naming conventions:
- `__root.tsx` special root layout
- `index.tsx` index route for its directory
- `$paramName.tsx` dynamic segment
## Provider Setup
## tRPC Setup
The `<ApiProvider>` wraps the entire app in `__root.tsx`:
The root route wraps with `<TrpcProvider>`:
```tsx
```typescript
// src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { ApiProvider } from "@repo/api-client";
import { TrpcProvider } from "@repo/core-trpc/tanstack";
import { Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => (
<ApiProvider trpcUrl="http://localhost:3000/api/trpc">
<TrpcProvider trpcUrl="http://localhost:3000/api/trpc">
<Outlet />
</ApiProvider>
</TrpcProvider>
),
});
```
Note: The `trpcUrl` points to the Next.js app's tRPC endpoint at `http://localhost:3000/api/trpc`. In production, this should be configured via environment variables.
Note: `trpcUrl` must point to a running tRPC endpoint (e.g., from `apps/web-next`).
## Recipe: Adding a New Route with Data Fetching
## Fetching data in routes
This example adds an `/articles` route that lists published articles.
### Step 1: Create the route file
Create `src/routes/articles/index.tsx`:
```tsx
```typescript
// src/routes/blog/$slug.tsx
import { createFileRoute } from "@tanstack/react-router";
import { useTRPC } from "@repo/api-client";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@repo/ui";
export const Route = createFileRoute("/articles/")({
component: ArticlesPage,
});
function ArticlesPage() {
const trpc = useTRPC();
const { data, isLoading, error } = useQuery(
trpc.content.listArticles.queryOptions({ status: "published", limit: 20 })
);
if (isLoading) return <p>Loading articles...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<main>
<h1>Articles</h1>
<ul>
{data?.map((article) => (
<li key={article.id}>
<h2>{article.title}</h2>
<Button variant="outline" size="sm">
Read more
</Button>
</li>
))}
</ul>
</main>
);
}
```
### Step 2: Add a dynamic route for individual articles
Create `src/routes/articles/$id.tsx`:
```tsx
import { createFileRoute } from "@tanstack/react-router";
import { useTRPC } from "@repo/api-client";
import { useTRPC } from "@repo/core-trpc";
import { useQuery } from "@tanstack/react-query";
export const Route = createFileRoute("/articles/$id")({
component: ArticlePage,
export const Route = createFileRoute("/blog/$slug")({
component: BlogPostPage,
});
function ArticlePage() {
const { id } = Route.useParams();
function BlogPostPage() {
const { slug } = Route.useParams();
const trpc = useTRPC();
// Use the article ID from the URL parameter
// (Assuming a getArticle procedure exists on the content router)
const { data, isLoading } = useQuery(
trpc.content.listArticles.queryOptions({ limit: 1 })
trpc.blog.getBySlug.queryOptions({ slug })
);
if (isLoading) return <p>Loading...</p>;
return (
<main>
<h1>Article {id}</h1>
</main>
);
return <article>{data?.title}</article>;
}
```
Key patterns:
- Every route file exports a `Route` created via `createFileRoute(path)(...)`
- The `component` property defines the React component for that route
- Use `Route.useParams()` to access dynamic parameters (e.g., `$id`)
- Data fetching uses the same `useTRPC()` + `useQuery()` pattern as Next.js
- Import UI components from `@repo/ui`, never recreate them locally
## Dependencies
| Dependency | Purpose |
|---|---|
| `@repo/api` | `AppRouter` type (transitive via `@repo/api-client`) |
| `@repo/api-client` | `ApiProvider` + `useTRPC()` for client-side data fetching |
| `@repo/ui` | Shared UI components |
| `@tanstack/react-router` | TanStack Router for file-based routing |
| `@repo/core-api` | AppRouter type (via core-trpc) |
| `@repo/core-trpc/tanstack` | TanStack tRPC client + provider |
| `@repo/core-ui` | Design system components |
| `@tanstack/react-router` | File-based routing |
| `@tanstack/react-query` | Data fetching + caching |
| `react` / `react-dom` | React 19 runtime |
## Test conventions
- e2e tests in `e2e/` folder: `*.spec.ts`
- Playwright config in `e2e/playwright.config.ts`
- Run: `pnpm test:e2e` (both Next.js and TanStack)
Parallel to `apps/web-next` e2e: validates that features work across frameworks.
## Cross-References
- **tRPC routers:** `packages/api/` -- see `packages/api/AGENTS.md`
- **tRPC client/hooks:** `packages/api-client/` -- see `packages/api-client/AGENTS.md`
- **UI components:** `packages/ui/` -- see `packages/ui/AGENTS.md`
- **Next.js app (serves the tRPC endpoint):** `apps/web-next/` -- see `apps/web-next/AGENTS.md`
- **Feature packages:** `packages/{auth,blog,media,marketing-pages,navigation}/`
- **tRPC composition:** `packages/core-api/AGENTS.md`
- **tRPC client + provider:** `packages/core-trpc/AGENTS.md`
- **UI components:** `packages/core-ui/AGENTS.md`
- **Next.js app (serves tRPC):** `apps/web-next/AGENTS.md`