Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/create/src/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,16 +234,26 @@ async function runCommandsAndInstallDependencies(

function report(environment: Environment, options: Options) {
const warnings: Array<string> = []
// TODO: nextSteps displays post-creation instructions for add-ons that require additional setup
// (e.g., starting a sibling server). Decide if this belongs in core or if add-ons should use README instead.
const nextSteps: Array<string> = []
for (const addOn of options.chosenAddOns) {
if (addOn.warning) {
warnings.push(addOn.warning)
}
if (addOn.nextSteps) {
nextSteps.push(`${addOn.name}:\n${addOn.nextSteps}`)
}
}

if (warnings.length > 0) {
environment.warn('Warnings', warnings.join('\n'))
}

if (nextSteps.length > 0) {
environment.info('Next Steps', nextSteps.join('\n\n'))
}

// Format errors
let errorStatement = ''
if (environment.getErrors().length) {
Expand Down
160 changes: 152 additions & 8 deletions packages/create/src/frameworks/react/add-ons/strapi/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,158 @@
## Setting up Strapi
## Strapi CMS Integration

The current setup shows an example of how to use Strapi with an articles collection which is part of the example structure & data.
This add-on integrates Strapi CMS with your TanStack Start application using the official Strapi Client SDK. The Strapi server is created as a sibling directory during setup.

- Create a local running copy of the strapi admin
### Features

- Article listing with search and pagination
- Article detail pages with dynamic block rendering
- Rich text, quotes, media, and image slider blocks
- Markdown content rendering with GitHub Flavored Markdown
- Responsive image handling with error fallbacks
- URL-based search and pagination (shareable/bookmarkable)
- Graceful error handling with helpful setup instructions

### Project Structure

```
parent/
├── client/ # TanStack Start frontend (your project name)
│ ├── src/
│ │ ├── components/
│ │ │ ├── blocks/ # Block rendering components
│ │ │ ├── markdown-content.tsx
│ │ │ ├── pagination.tsx
│ │ │ ├── search.tsx
│ │ │ └── strapi-image.tsx
│ │ ├── data/
│ │ │ ├── loaders/ # Server functions
│ │ │ └── strapi-sdk.ts
│ │ ├── lib/
│ │ │ └── strapi-utils.ts
│ │ ├── routes/demo/
│ │ │ ├── strapi.tsx # Articles list
│ │ │ └── strapi_.$articleId.tsx # Article detail
│ │ └── types/
│ │ └── strapi.ts
│ ├── .env.local
│ └── package.json
└── server/ # Strapi CMS backend (auto-created)
├── src/api/ # Content types
├── config/ # Strapi configuration
└── package.json
```

### Quick Start

The Strapi server is automatically cloned from the official [Strapi Cloud Template Blog](https://github.com/strapi/strapi-cloud-template-blog).

**1. Install Strapi dependencies:**

```bash
cd ../server
npm install # or pnpm install / yarn install
```

**2. Start the Strapi server:**

```bash
pnpm dlx create-strapi@latest my-strapi-project
cd my-strapi-project
pnpm dev
npm run develop # Starts at http://localhost:1337
```

- Login and publish the example articles to see them on the strapi demo page.
- Set the `VITE_STRAPI_URL` environment variable in your `.env.local`. (For local it should be http://localhost:1337/api)
**3. Create an admin account:**

Open http://localhost:1337/admin and create your first admin user.

**4. Create content:**

In the Strapi admin panel, go to Content Manager > Article and create some articles.

**5. Start your TanStack app (in another terminal):**

```bash
cd ../client # or your project name
npm run dev # Starts at http://localhost:3000
```

**6. View the demo:**

Navigate to http://localhost:3000/demo/strapi to see your articles.

### Environment Variables

The following environment variable is pre-configured in `.env.local`:

```bash
VITE_STRAPI_URL="http://localhost:1337"
```

For production, update this to your deployed Strapi URL.

### Demo Pages

| URL | Description |
|-----|-------------|
| `/demo/strapi` | Articles list with search and pagination |
| `/demo/strapi/:articleId` | Article detail with block rendering |

### Search and Pagination

- **Search**: Type in the search box to filter articles by title or description
- **Pagination**: Navigate between pages using the pagination controls
- **URL State**: Search and page are stored in the URL (`?query=term&page=2`)

### Block Types Supported

| Block | Component | Description |
|-------|-----------|-------------|
| `shared.rich-text` | RichText | Markdown content |
| `shared.quote` | Quote | Blockquote with author |
| `shared.media` | Media | Single image/video |
| `shared.slider` | Slider | Image gallery grid |

### Dependencies

| Package | Purpose |
|---------|---------|
| `@strapi/client` | Official Strapi SDK |
| `react-markdown` | Markdown rendering |
| `remark-gfm` | GitHub Flavored Markdown |
| `use-debounce` | Debounced search input |

### Running Both Servers

Open two terminal windows from the parent directory:

**Terminal 1 - Strapi:**
```bash
cd server && npm run develop
```

**Terminal 2 - TanStack Start:**
```bash
cd client && npm run dev # or your project name
```

### Customization

**Change page size:**
Edit `src/data/loaders/articles.ts` and modify `PAGE_SIZE`.

**Add new block types:**
1. Create component in `src/components/blocks/`
2. Export from `src/components/blocks/index.ts`
3. Add case to `block-renderer.tsx` switch statement
4. Update populate in articles loader

**Add new content types:**
1. Add types to `src/types/strapi.ts`
2. Create loader in `src/data/loaders/`
3. Create route in `src/routes/demo/`

### Learn More

- [Strapi Documentation](https://docs.strapi.io/)
- [Strapi Client SDK](https://www.npmjs.com/package/@strapi/client)
- [Strapi Cloud Template Blog](https://github.com/strapi/strapi-cloud-template-blog)
- [TanStack Start Documentation](https://tanstack.com/start/latest)
- [TanStack Router Search Params](https://tanstack.com/router/latest/docs/framework/react/guide/search-params)
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Strapi configuration
VITE_STRAPI_URL="http://localhost:1337/api"
VITE_STRAPI_URL="http://localhost:1337"
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { RichText } from "./rich-text";
import { Quote } from "./quote";
import { Media } from "./media";
import { Slider } from "./slider";

import type { IRichText } from "./rich-text";
import type { IQuote } from "./quote";
import type { IMedia } from "./media";
import type { ISlider } from "./slider";

// Union type of all block types
export type Block = IRichText | IQuote | IMedia | ISlider;

interface BlockRendererProps {
blocks: Array<Block>;
}

/**
* BlockRenderer - Renders dynamic content blocks from Strapi
*
* Usage:
* ```tsx
* <BlockRenderer blocks={article.blocks} />
* ```
*/
export function BlockRenderer({ blocks }: Readonly<BlockRendererProps>) {
if (!blocks || blocks.length === 0) return null;

const renderBlock = (block: Block) => {
switch (block.__component) {
case "shared.rich-text":
return <RichText {...block} />;
case "shared.quote":
return <Quote {...block} />;
case "shared.media":
return <Media {...block} />;
case "shared.slider":
return <Slider {...block} />;
default:
// Log unknown block types in development
console.warn("Unknown block type:", (block as any).__component);
return null;
}
};

return (
<div className="space-y-6">
{blocks.map((block, index) => (
<div key={`${block.__component}-${block.id}-${index}`}>
{renderBlock(block)}
</div>
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export { BlockRenderer } from "./block-renderer";
export type { Block } from "./block-renderer";

export { RichText } from "./rich-text";
export type { IRichText } from "./rich-text";

export { Quote } from "./quote";
export type { IQuote } from "./quote";

export { Media } from "./media";
export type { IMedia } from "./media";

export { Slider } from "./slider";
export type { ISlider } from "./slider";
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { StrapiImage } from "@/components/strapi-image";
import type { TImage } from "@/types/strapi";

export interface IMedia {
__component: "shared.media";
id: number;
file?: TImage;
}

export function Media({ file }: Readonly<IMedia>) {
if (!file) return null;

return (
<figure className="my-8">
<StrapiImage
src={file.url}
alt={file.alternativeText || ""}
className="rounded-lg w-full"
/>
{file.alternativeText && (
<figcaption className="mt-2 text-center text-sm text-gray-500">
{file.alternativeText}
</figcaption>
)}
</figure>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface IQuote {
__component: "shared.quote";
id: number;
body: string;
title?: string;
}

export function Quote({ body, title }: Readonly<IQuote>) {
return (
<blockquote className="border-l-4 border-cyan-400 pl-6 py-4 my-6 bg-slate-800/30 rounded-r-lg">
<p className="text-xl italic text-gray-300 leading-relaxed">{body}</p>
{title && (
<cite className="block mt-4 text-cyan-400 not-italic font-medium">
— {title}
</cite>
)}
</blockquote>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MarkdownContent } from "@/components/markdown-content";

export interface IRichText {
__component: "shared.rich-text";
id: number;
body: string;
}

export function RichText({ body }: Readonly<IRichText>) {
return <MarkdownContent content={body} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { StrapiImage } from "@/components/strapi-image";
import type { TImage } from "@/types/strapi";

export interface ISlider {
__component: "shared.slider";
id: number;
files?: Array<TImage>;
}

export function Slider({ files }: Readonly<ISlider>) {
if (!files || files.length === 0) return null;

return (
<div className="my-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{files.map((file, index) => (
<figure key={file.id || index}>
<StrapiImage
src={file.url}
alt={file.alternativeText || ""}
className="rounded-lg w-full h-48 object-cover"
/>
</figure>
))}
</div>
</div>
);
}
Loading