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 ## 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 ## Port: 3001
```bash ```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 ## Key Files
| File | Purpose | | 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 | | `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)/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]]/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)/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 ## @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 ```json
{ {
"compilerOptions": { "compilerOptions": {
"paths": { "paths": {
"@payload-config": [ "@payload-config": ["../../packages/core-cms/src/payload.config.ts"]
"../../packages/cms-core/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 This app (@repo/cms)
- **`admin/[[...segments]]/page.tsx`** -- Catch-all route that renders `RootPage` from `@payloadcms/next/views` └─ src/app/(payload)/layout.tsx
- **`admin/[[...segments]]/not-found.tsx`** -- 404 handler using `NotFoundPage` from `@payloadcms/next/views` loads config from @payload-config
- **`importMap.js`** -- Maps Payload component paths for the admin UI Payload CLI auto-generates admin routes
```
If you need to regenerate these files, Payload will do so automatically during dev/build.
## Type Generation ## Type Generation
To regenerate Payload TypeScript types after changing collections/globals: After adding/modifying collections in any feature's `/cms` folder:
```bash ```bash
cd apps/cms && pnpm generate:types cd apps/cms && pnpm generate:types
# Equivalent to: payload generate:types # Regenerates packages/core-cms/src/generated-types.ts
# Output goes to: packages/cms-core/src/payload-types.ts
``` ```
## Dependencies ## Dependencies
| Dependency | Purpose | | Dependency | Purpose |
|---|---| |---|---|
| `@repo/cms-core` | All Payload configuration (collections, globals, hooks, config) | | `@repo/core-cms` | Payload config + buildConfig |
| `@payloadcms/next` | Next.js integration for Payload (withPayload, admin UI views) | | `@payloadcms/next` | Next.js integration for Payload |
| `@payloadcms/ui` | Payload admin panel React components |
| `payload` | Payload CMS core | | `payload` | Payload CMS core |
| `next` | Next.js 15 framework | | `next` | Next.js 15 framework |
| `react` / `react-dom` | React 19 runtime | | `sharp` | Image processing |
| `sharp` | Image processing for Payload uploads |
## Cross-References ## Cross-References
- **ALL CMS configuration:** `packages/cms-core/` -- see `packages/cms-core/AGENTS.md` - **Feature collections:** each feature's `src/integrations/cms/` folder
- **CMS client for querying data:** `packages/cms-client/` -- see `packages/cms-client/AGENTS.md` - **CMS composition:** `packages/core-cms/AGENTS.md`
- **Core business logic:** `packages/core/` -- see `packages/core/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 ## 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 ## Port: 6006
@@ -14,20 +16,17 @@ pnpm dev --filter @repo/storybook # http://localhost:6006
### `.storybook/main.ts` ### `.storybook/main.ts`
```typescript Stories are discovered from `@repo/core-ui`:
import type { StorybookConfig } from "@storybook/react-vite";
```typescript
const config: StorybookConfig = { const config: StorybookConfig = {
framework: "@storybook/react-vite", framework: "@storybook/react-vite",
stories: ["../../../packages/core-ui/src/**/*.stories.@(ts|tsx)"], stories: ["../../../packages/core-ui/src/**/*.stories.@(ts|tsx)"],
addons: ["@storybook/addon-essentials"], addons: ["@storybook/addon-essentials"],
docs: { docs: { autodocs: "tag" },
autodocs: "tag",
},
async viteFinal(config) { async viteFinal(config) {
const { mergeConfig } = await import("vite"); const { mergeConfig } = await import("vite");
const tailwindPlugin = await import("@tailwindcss/vite"); const tailwindPlugin = await import("@tailwindcss/vite");
return mergeConfig(config, { return mergeConfig(config, {
plugins: [tailwindPlugin.default()], plugins: [tailwindPlugin.default()],
}); });
@@ -35,14 +34,15 @@ const config: StorybookConfig = {
}; };
``` ```
Key configuration details: Key settings:
- **`stories`** glob reaches into `packages/core-ui/src/` to find all `.stories.tsx` files - **`stories` glob** — reaches into `packages/core-ui/src/` for all `*.stories.tsx` files
- **`viteFinal`** adds the `@tailwindcss/vite` plugin so Tailwind v4 classes render correctly in stories - **`viteFinal`** adds Tailwind v4 plugin so classes render in Storybook
- **`autodocs: "tag"`** generates documentation pages for stories tagged with `"autodocs"` - **`autodocs: "tag"`** — auto-generates docs for tagged stories
- **`@storybook/addon-essentials`** includes Controls, Actions, Backgrounds, Viewport, Docs
### `.storybook/preview.ts` ### `.storybook/preview.ts`
Imports global styles:
```typescript ```typescript
import type { Preview } from "@storybook/react"; import type { Preview } from "@storybook/react";
import "../../../packages/core-ui/src/styles/globals.css"; 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 ## 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 | Sidebar path |
| 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` |
|---|---|---| |---|---|---|
| `Atoms/Button` | Button (5 variants: Default, Secondary, Destructive, Outline, Ghost) | `src/atoms/button/button.stories.tsx` | | Atom | `"Atoms/{ComponentName}"` | Atoms > ComponentName |
| `Atoms/Input` | Input (Default, Disabled) | `src/atoms/input/input.stories.tsx` | | Molecule | `"Molecules/{ComponentName}"` | Molecules > ComponentName |
| `Molecules/FormField` | FormField (Default, WithDescription, WithError) | `src/molecules/form-field/form-field.stories.tsx` | | 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 ## 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 http://localhost:6006/mcp
``` ```
### Available MCP tools: ### Available tools:
- **`list-all-documentation`** -- Lists all documented components and their stories - **`list-all-documentation`** Lists all component stories and their properties
- **`get-documentation`** -- Gets detailed documentation for a specific component (props, variants, usage) - **`get-documentation`** Gets detailed component info (props, variants, usage examples)
- **`run-story-tests`** -- Runs visual tests on stories to validate rendering - **`run-story-tests`** — Validates story rendering
### Installing addon-mcp ### Before building new components:
If `@storybook/addon-mcp` is not already installed: 1. Query `list-all-documentation` to check if a similar component exists
2. Query `get-documentation` to understand existing props and variants
```bash 3. After creating: `run-story-tests` to validate
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
## Dependencies ## Dependencies
| Dependency | Purpose | | Dependency | Purpose |
|---|---| |---|---|
| `@repo/core-ui` | Source of all component stories | | `@repo/core-ui` | Component source + stories |
| `@storybook/react-vite` | Storybook framework using Vite bundler | | `@storybook/react-vite` | Storybook with Vite bundler |
| `@storybook/addon-essentials` | Controls, Actions, Docs, Backgrounds, Viewport | | `@storybook/addon-essentials` | Controls, Actions, Docs, Backgrounds |
| `@tailwindcss/vite` | Vite plugin for Tailwind CSS v4 | | `@tailwindcss/vite` | Vite plugin for Tailwind v4 |
| `storybook` | Storybook core CLI and dev server | | `storybook` | Storybook CLI + dev server |
| `tailwindcss` | Tailwind CSS v4 engine | | `tailwindcss` | Tailwind CSS v4 |
| `vite` | Build tool / dev server | | `vite` | Build tool |
| `react` / `react-dom` | React 19 runtime | | `react` / `react-dom` | React 19 |
## Cross-References ## Cross-References
- **Component source:** `packages/core-ui/` -- see `packages/core-ui/AGENTS.md` - **Component source:** `packages/core-ui/AGENTS.md`
- **Tailwind tokens:** `packages/core-ui/src/styles/globals.css` - **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 ## 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 ## 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 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 ## Key Files
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `src/app/layout.tsx` | Root layout -- wraps children with `<Providers>`, sets HTML metadata | | `src/app/layout.tsx` | Root layout wraps app with `<TrpcProvider>` from `@repo/core-trpc/next` |
| `src/app/providers.tsx` | Client component that wraps the app with `<ApiProvider trpcUrl="/api/trpc">` | | `src/app/providers.tsx` | Client component for tRPC + React Query setup |
| `src/app/page.tsx` | Home page (server component by default) | | `src/app/page.tsx` | Home page — navigation + marketing content |
| `src/app/api/trpc/[trpc]/route.ts` | tRPC HTTP endpoint using the Next.js fetch adapter | | `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 ```typescript
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@repo/api"; import { appRouter } from "@repo/core-api";
const handler = (req: Request) => const handler = (req: Request) =>
fetchRequestHandler({ fetchRequestHandler({
@@ -38,149 +50,14 @@ const handler = (req: Request) =>
export { handler as GET, handler as POST }; export { handler as GET, handler as POST };
``` ```
How it works: The provider (in `src/app/providers.tsx`):
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:
```typescript ```typescript
// src/lib/payload.ts "use client";
import { getPayload } from "payload"; import { TrpcProvider } from "@repo/core-trpc/next";
import config from "@repo/cms-core/src/payload.config";
import { createPayloadClient, type PayloadClient } from "@repo/cms-client";
let cachedClient: PayloadClient | null = null; export function Providers({ children }: React.ReactNode) {
return <TrpcProvider>{children}</TrpcProvider>;
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>
);
} }
``` ```
@@ -188,16 +65,43 @@ export default async function ArticlesPage() {
| Dependency | Purpose | | Dependency | Purpose |
|---|---| |---|---|
| `@repo/api` | `appRouter` for the tRPC HTTP endpoint | | `@repo/core-api` | `appRouter` for tRPC endpoint |
| `@repo/api-client` | `ApiProvider` + `useTRPC()` for client-side data fetching | | `@repo/core-trpc/next` | Next.js tRPC client + provider |
| `@repo/ui` | Shared UI components (Button, Input, Label, FormField, etc.) | | `@repo/core-ui` | Design system components |
| `next` | Next.js 15 framework with App Router | | `@repo/auth`, `@repo/blog`, etc. | Feature packages (indirectly via core-api) |
| `react` / `react-dom` | React 19 runtime | | `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 ## Cross-References
- **tRPC routers:** `packages/api/` -- see `packages/api/AGENTS.md` - **Feature packages:** `packages/{auth,blog,media,marketing-pages,navigation}/`
- **tRPC client/hooks:** `packages/api-client/` -- see `packages/api-client/AGENTS.md` - **tRPC composition:** `packages/core-api/AGENTS.md`
- **UI components:** `packages/ui/` -- see `packages/ui/AGENTS.md` - **tRPC client + provider:** `packages/core-trpc/AGENTS.md`
- **CMS client:** `packages/cms-client/` -- see `packages/cms-client/AGENTS.md` - **UI components:** `packages/core-ui/AGENTS.md`
- **CMS config:** `packages/cms-core/` -- see `packages/cms-core/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 ## 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 ## 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 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 ## Key Files
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `src/routes/__root.tsx` | Root layout -- creates the root route, wraps with `<ApiProvider>` and `<Outlet>` | | `src/routes/__root.tsx` | Root layout — wraps all routes with `<TrpcProvider>` from `@repo/core-trpc/tanstack` |
| `src/routes/index.tsx` | Home page route (`/`) | | `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 ## 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 | | File | URL |
|---|---|---| |---|---|
| `src/routes/__root.tsx` | (all routes) | Root layout, wraps all child routes | | `src/routes/__root.tsx` | Root (all routes) |
| `src/routes/index.tsx` | `/` | Home page | | `src/routes/index.tsx` | `/` |
| `src/routes/about.tsx` | `/about` | Static page | | `src/routes/blog/index.tsx` | `/blog` |
| `src/routes/articles/index.tsx` | `/articles` | Article listing | | `src/routes/blog/$slug.tsx` | `/blog/:slug` |
| `src/routes/articles/$id.tsx` | `/articles/:id` | Single article (dynamic param) |
### Naming conventions: Naming conventions:
- `__root.tsx` -- special root layout file, always wraps all routes - `__root.tsx` special root layout
- `index.tsx` -- index route for its directory (e.g., `/articles/index.tsx` matches `/articles`) - `index.tsx` index route for its directory
- `$paramName.tsx` -- dynamic route segment (e.g., `$id.tsx` captures `:id`) - `$paramName.tsx` dynamic segment
- Nested folders create nested URL segments
## 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 // src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router"; import { TrpcProvider } from "@repo/core-trpc/tanstack";
import { ApiProvider } from "@repo/api-client"; import { Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({ export const Route = createRootRoute({
component: () => ( component: () => (
<ApiProvider trpcUrl="http://localhost:3000/api/trpc"> <TrpcProvider trpcUrl="http://localhost:3000/api/trpc">
<Outlet /> <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. ```typescript
// src/routes/blog/$slug.tsx
### Step 1: Create the route file
Create `src/routes/articles/index.tsx`:
```tsx
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useTRPC } from "@repo/api-client"; import { useTRPC } from "@repo/core-trpc";
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 { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
export const Route = createFileRoute("/articles/$id")({ export const Route = createFileRoute("/blog/$slug")({
component: ArticlePage, component: BlogPostPage,
}); });
function ArticlePage() { function BlogPostPage() {
const { id } = Route.useParams(); const { slug } = Route.useParams();
const trpc = useTRPC(); const trpc = useTRPC();
// Use the article ID from the URL parameter
// (Assuming a getArticle procedure exists on the content router)
const { data, isLoading } = useQuery( const { data, isLoading } = useQuery(
trpc.content.listArticles.queryOptions({ limit: 1 }) trpc.blog.getBySlug.queryOptions({ slug })
); );
if (isLoading) return <p>Loading...</p>; if (isLoading) return <p>Loading...</p>;
return <article>{data?.title}</article>;
return (
<main>
<h1>Article {id}</h1>
</main>
);
} }
``` ```
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 ## Dependencies
| Dependency | Purpose | | Dependency | Purpose |
|---|---| |---|---|
| `@repo/api` | `AppRouter` type (transitive via `@repo/api-client`) | | `@repo/core-api` | AppRouter type (via core-trpc) |
| `@repo/api-client` | `ApiProvider` + `useTRPC()` for client-side data fetching | | `@repo/core-trpc/tanstack` | TanStack tRPC client + provider |
| `@repo/ui` | Shared UI components | | `@repo/core-ui` | Design system components |
| `@tanstack/react-router` | TanStack Router for file-based routing | | `@tanstack/react-router` | File-based routing |
| `@tanstack/react-query` | Data fetching + caching |
| `react` / `react-dom` | React 19 runtime | | `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 ## Cross-References
- **tRPC routers:** `packages/api/` -- see `packages/api/AGENTS.md` - **Feature packages:** `packages/{auth,blog,media,marketing-pages,navigation}/`
- **tRPC client/hooks:** `packages/api-client/` -- see `packages/api-client/AGENTS.md` - **tRPC composition:** `packages/core-api/AGENTS.md`
- **UI components:** `packages/ui/` -- see `packages/ui/AGENTS.md` - **tRPC client + provider:** `packages/core-trpc/AGENTS.md`
- **Next.js app (serves the tRPC endpoint):** `apps/web-next/` -- see `apps/web-next/AGENTS.md` - **UI components:** `packages/core-ui/AGENTS.md`
- **Next.js app (serves tRPC):** `apps/web-next/AGENTS.md`