Skip to content

Commit 6807462

Browse files
feat: add Strapi CMS add-on
1 parent c529055 commit 6807462

File tree

24 files changed

+1273
-104
lines changed

24 files changed

+1273
-104
lines changed

packages/create/src/create-app.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,16 +234,26 @@ async function runCommandsAndInstallDependencies(
234234

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

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

253+
if (nextSteps.length > 0) {
254+
environment.info('Next Steps', nextSteps.join('\n\n'))
255+
}
256+
247257
// Format errors
248258
let errorStatement = ''
249259
if (environment.getErrors().length) {
Lines changed: 152 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,158 @@
1-
## Setting up Strapi
1+
## Strapi CMS Integration
22

3-
The current setup shows an example of how to use Strapi with an articles collection which is part of the example structure & data.
3+
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.
44

5-
- Create a local running copy of the strapi admin
5+
### Features
6+
7+
- Article listing with search and pagination
8+
- Article detail pages with dynamic block rendering
9+
- Rich text, quotes, media, and image slider blocks
10+
- Markdown content rendering with GitHub Flavored Markdown
11+
- Responsive image handling with error fallbacks
12+
- URL-based search and pagination (shareable/bookmarkable)
13+
- Graceful error handling with helpful setup instructions
14+
15+
### Project Structure
16+
17+
```
18+
parent/
19+
├── client/ # TanStack Start frontend (your project name)
20+
│ ├── src/
21+
│ │ ├── components/
22+
│ │ │ ├── blocks/ # Block rendering components
23+
│ │ │ ├── markdown-content.tsx
24+
│ │ │ ├── pagination.tsx
25+
│ │ │ ├── search.tsx
26+
│ │ │ └── strapi-image.tsx
27+
│ │ ├── data/
28+
│ │ │ ├── loaders/ # Server functions
29+
│ │ │ └── strapi-sdk.ts
30+
│ │ ├── lib/
31+
│ │ │ └── strapi-utils.ts
32+
│ │ ├── routes/demo/
33+
│ │ │ ├── strapi.tsx # Articles list
34+
│ │ │ └── strapi_.$articleId.tsx # Article detail
35+
│ │ └── types/
36+
│ │ └── strapi.ts
37+
│ ├── .env.local
38+
│ └── package.json
39+
└── server/ # Strapi CMS backend (auto-created)
40+
├── src/api/ # Content types
41+
├── config/ # Strapi configuration
42+
└── package.json
43+
```
44+
45+
### Quick Start
46+
47+
The Strapi server is automatically cloned from the official [Strapi Cloud Template Blog](https://github.com/strapi/strapi-cloud-template-blog).
48+
49+
**1. Install Strapi dependencies:**
50+
51+
```bash
52+
cd ../server
53+
npm install # or pnpm install / yarn install
54+
```
55+
56+
**2. Start the Strapi server:**
657

758
```bash
8-
pnpm dlx create-strapi@latest my-strapi-project
9-
cd my-strapi-project
10-
pnpm dev
59+
npm run develop # Starts at http://localhost:1337
1160
```
1261

13-
- Login and publish the example articles to see them on the strapi demo page.
14-
- Set the `VITE_STRAPI_URL` environment variable in your `.env.local`. (For local it should be http://localhost:1337/api)
62+
**3. Create an admin account:**
63+
64+
Open http://localhost:1337/admin and create your first admin user.
65+
66+
**4. Create content:**
67+
68+
In the Strapi admin panel, go to Content Manager > Article and create some articles.
69+
70+
**5. Start your TanStack app (in another terminal):**
71+
72+
```bash
73+
cd ../client # or your project name
74+
npm run dev # Starts at http://localhost:3000
75+
```
76+
77+
**6. View the demo:**
78+
79+
Navigate to http://localhost:3000/demo/strapi to see your articles.
80+
81+
### Environment Variables
82+
83+
The following environment variable is pre-configured in `.env.local`:
84+
85+
```bash
86+
VITE_STRAPI_URL="http://localhost:1337"
87+
```
88+
89+
For production, update this to your deployed Strapi URL.
90+
91+
### Demo Pages
92+
93+
| URL | Description |
94+
|-----|-------------|
95+
| `/demo/strapi` | Articles list with search and pagination |
96+
| `/demo/strapi/:articleId` | Article detail with block rendering |
97+
98+
### Search and Pagination
99+
100+
- **Search**: Type in the search box to filter articles by title or description
101+
- **Pagination**: Navigate between pages using the pagination controls
102+
- **URL State**: Search and page are stored in the URL (`?query=term&page=2`)
103+
104+
### Block Types Supported
105+
106+
| Block | Component | Description |
107+
|-------|-----------|-------------|
108+
| `shared.rich-text` | RichText | Markdown content |
109+
| `shared.quote` | Quote | Blockquote with author |
110+
| `shared.media` | Media | Single image/video |
111+
| `shared.slider` | Slider | Image gallery grid |
112+
113+
### Dependencies
114+
115+
| Package | Purpose |
116+
|---------|---------|
117+
| `@strapi/client` | Official Strapi SDK |
118+
| `react-markdown` | Markdown rendering |
119+
| `remark-gfm` | GitHub Flavored Markdown |
120+
| `use-debounce` | Debounced search input |
121+
122+
### Running Both Servers
123+
124+
Open two terminal windows from the parent directory:
125+
126+
**Terminal 1 - Strapi:**
127+
```bash
128+
cd server && npm run develop
129+
```
130+
131+
**Terminal 2 - TanStack Start:**
132+
```bash
133+
cd client && npm run dev # or your project name
134+
```
135+
136+
### Customization
137+
138+
**Change page size:**
139+
Edit `src/data/loaders/articles.ts` and modify `PAGE_SIZE`.
140+
141+
**Add new block types:**
142+
1. Create component in `src/components/blocks/`
143+
2. Export from `src/components/blocks/index.ts`
144+
3. Add case to `block-renderer.tsx` switch statement
145+
4. Update populate in articles loader
146+
147+
**Add new content types:**
148+
1. Add types to `src/types/strapi.ts`
149+
2. Create loader in `src/data/loaders/`
150+
3. Create route in `src/routes/demo/`
151+
152+
### Learn More
153+
154+
- [Strapi Documentation](https://docs.strapi.io/)
155+
- [Strapi Client SDK](https://www.npmjs.com/package/@strapi/client)
156+
- [Strapi Cloud Template Blog](https://github.com/strapi/strapi-cloud-template-blog)
157+
- [TanStack Start Documentation](https://tanstack.com/start/latest)
158+
- [TanStack Router Search Params](https://tanstack.com/router/latest/docs/framework/react/guide/search-params)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# Strapi configuration
2-
VITE_STRAPI_URL="http://localhost:1337/api"
2+
VITE_STRAPI_URL="http://localhost:1337"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { RichText } from "./rich-text";
2+
import { Quote } from "./quote";
3+
import { Media } from "./media";
4+
import { Slider } from "./slider";
5+
6+
import type { IRichText } from "./rich-text";
7+
import type { IQuote } from "./quote";
8+
import type { IMedia } from "./media";
9+
import type { ISlider } from "./slider";
10+
11+
// Union type of all block types
12+
export type Block = IRichText | IQuote | IMedia | ISlider;
13+
14+
interface BlockRendererProps {
15+
blocks: Array<Block>;
16+
}
17+
18+
/**
19+
* BlockRenderer - Renders dynamic content blocks from Strapi
20+
*
21+
* Usage:
22+
* ```tsx
23+
* <BlockRenderer blocks={article.blocks} />
24+
* ```
25+
*/
26+
export function BlockRenderer({ blocks }: Readonly<BlockRendererProps>) {
27+
if (!blocks || blocks.length === 0) return null;
28+
29+
const renderBlock = (block: Block) => {
30+
switch (block.__component) {
31+
case "shared.rich-text":
32+
return <RichText {...block} />;
33+
case "shared.quote":
34+
return <Quote {...block} />;
35+
case "shared.media":
36+
return <Media {...block} />;
37+
case "shared.slider":
38+
return <Slider {...block} />;
39+
default:
40+
// Log unknown block types in development
41+
console.warn("Unknown block type:", (block as any).__component);
42+
return null;
43+
}
44+
};
45+
46+
return (
47+
<div className="space-y-6">
48+
{blocks.map((block, index) => (
49+
<div key={`${block.__component}-${block.id}-${index}`}>
50+
{renderBlock(block)}
51+
</div>
52+
))}
53+
</div>
54+
);
55+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export { BlockRenderer } from "./block-renderer";
2+
export type { Block } from "./block-renderer";
3+
4+
export { RichText } from "./rich-text";
5+
export type { IRichText } from "./rich-text";
6+
7+
export { Quote } from "./quote";
8+
export type { IQuote } from "./quote";
9+
10+
export { Media } from "./media";
11+
export type { IMedia } from "./media";
12+
13+
export { Slider } from "./slider";
14+
export type { ISlider } from "./slider";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { StrapiImage } from "@/components/strapi-image";
2+
import type { TImage } from "@/types/strapi";
3+
4+
export interface IMedia {
5+
__component: "shared.media";
6+
id: number;
7+
file?: TImage;
8+
}
9+
10+
export function Media({ file }: Readonly<IMedia>) {
11+
if (!file) return null;
12+
13+
return (
14+
<figure className="my-8">
15+
<StrapiImage
16+
src={file.url}
17+
alt={file.alternativeText || ""}
18+
className="rounded-lg w-full"
19+
/>
20+
{file.alternativeText && (
21+
<figcaption className="mt-2 text-center text-sm text-gray-500">
22+
{file.alternativeText}
23+
</figcaption>
24+
)}
25+
</figure>
26+
);
27+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export interface IQuote {
2+
__component: "shared.quote";
3+
id: number;
4+
body: string;
5+
title?: string;
6+
}
7+
8+
export function Quote({ body, title }: Readonly<IQuote>) {
9+
return (
10+
<blockquote className="border-l-4 border-cyan-400 pl-6 py-4 my-6 bg-slate-800/30 rounded-r-lg">
11+
<p className="text-xl italic text-gray-300 leading-relaxed">{body}</p>
12+
{title && (
13+
<cite className="block mt-4 text-cyan-400 not-italic font-medium">
14+
{title}
15+
</cite>
16+
)}
17+
</blockquote>
18+
);
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { MarkdownContent } from "@/components/markdown-content";
2+
3+
export interface IRichText {
4+
__component: "shared.rich-text";
5+
id: number;
6+
body: string;
7+
}
8+
9+
export function RichText({ body }: Readonly<IRichText>) {
10+
return <MarkdownContent content={body} />;
11+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { StrapiImage } from "@/components/strapi-image";
2+
import type { TImage } from "@/types/strapi";
3+
4+
export interface ISlider {
5+
__component: "shared.slider";
6+
id: number;
7+
files?: Array<TImage>;
8+
}
9+
10+
export function Slider({ files }: Readonly<ISlider>) {
11+
if (!files || files.length === 0) return null;
12+
13+
return (
14+
<div className="my-8">
15+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
16+
{files.map((file, index) => (
17+
<figure key={file.id || index}>
18+
<StrapiImage
19+
src={file.url}
20+
alt={file.alternativeText || ""}
21+
className="rounded-lg w-full h-48 object-cover"
22+
/>
23+
</figure>
24+
))}
25+
</div>
26+
</div>
27+
);
28+
}

0 commit comments

Comments
 (0)