docs(agents): write per-app AGENTS.md for cms, web-next, web-tanstack, storybook
This commit is contained in:
@@ -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
|
||||||
|
docker compose up -d postgres # Start PostgreSQL on port 5432
|
||||||
pnpm dev --filter @repo/cms # http://localhost:3001/admin
|
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`
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user