@tanstack/create 0.61.4 → 0.61.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/create-app.js +64 -3
- package/dist/custom-add-ons/add-on.js +59 -1
- package/dist/custom-add-ons/starter.js +5 -1
- package/dist/frameworks/react/add-ons/clerk/info.json +9 -0
- package/dist/frameworks/react/add-ons/convex/info.json +16 -0
- package/dist/frameworks/react/add-ons/strapi/README.md +158 -8
- package/dist/frameworks/react/add-ons/strapi/assets/_dot_env.local.append +1 -1
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/block-renderer.tsx +55 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/index.ts +14 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/media.tsx +27 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/quote.tsx +19 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/rich-text.tsx +11 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/slider.tsx +28 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/markdown-content.tsx +74 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/pagination.tsx +120 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/search.tsx +35 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/strapi-image.tsx +47 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/data/loaders/articles.ts +106 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/data/loaders/index.ts +28 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/data/strapi-sdk.ts +9 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/lib/strapi-utils.ts +25 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.$articleId.tsx +170 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.tsx +269 -43
- package/dist/frameworks/react/add-ons/strapi/assets/src/types/strapi.ts +90 -0
- package/dist/frameworks/react/add-ons/strapi/info.json +3 -3
- package/dist/frameworks/react/add-ons/strapi/package.json +5 -2
- package/dist/frameworks.js +1 -0
- package/dist/index.js +1 -1
- package/dist/types/custom-add-ons/add-on.d.ts +9 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types.d.ts +192 -0
- package/dist/types.js +10 -0
- package/package.json +1 -1
- package/src/create-app.ts +77 -3
- package/src/custom-add-ons/add-on.ts +72 -1
- package/src/custom-add-ons/starter.ts +7 -1
- package/src/frameworks/react/add-ons/clerk/info.json +9 -0
- package/src/frameworks/react/add-ons/convex/info.json +16 -0
- package/src/frameworks/react/add-ons/strapi/README.md +158 -8
- package/src/frameworks/react/add-ons/strapi/assets/_dot_env.local.append +1 -1
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/block-renderer.tsx +55 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/index.ts +14 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/media.tsx +27 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/quote.tsx +19 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/rich-text.tsx +11 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/slider.tsx +28 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/markdown-content.tsx +74 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/pagination.tsx +120 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/search.tsx +35 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/strapi-image.tsx +47 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/data/loaders/articles.ts +106 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/data/loaders/index.ts +28 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/data/strapi-sdk.ts +9 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/lib/strapi-utils.ts +25 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.$articleId.tsx +170 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.tsx +269 -43
- package/src/frameworks/react/add-ons/strapi/assets/src/types/strapi.ts +90 -0
- package/src/frameworks/react/add-ons/strapi/info.json +3 -3
- package/src/frameworks/react/add-ons/strapi/package.json +5 -2
- package/src/frameworks.ts +1 -0
- package/src/index.ts +1 -1
- package/src/types.ts +14 -0
- package/tests/custom-add-ons/starter.test.ts +29 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/lib/strapiClient.ts +0 -7
- package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi_.$articleId.tsx +0 -78
- package/src/frameworks/react/add-ons/strapi/assets/src/lib/strapiClient.ts +0 -7
- package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi_.$articleId.tsx +0 -78
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @tanstack/create
|
|
2
2
|
|
|
3
|
+
## 0.61.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Improve the Strapi add-on scaffolding for reliability. ([#323](https://github.com/TanStack/cli/pull/323))
|
|
8
|
+
|
|
9
|
+
- Remove brittle post-create shell automation that attempted to clone and bootstrap a sibling Strapi server.
|
|
10
|
+
- Fix Strapi article detail routing to use a consistent file-based route path.
|
|
11
|
+
- Update Strapi add-on guidance to document manual/hosted Strapi setup expectations.
|
|
12
|
+
|
|
13
|
+
## 0.61.5
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Add a continuous development workflow for custom add-on authors. ([`b3cc585`](https://github.com/TanStack/cli/commit/b3cc5851d2b81613e3b024eb7981c440ee5183af))
|
|
18
|
+
|
|
19
|
+
- Add `tanstack add-on dev` to watch project files and continuously refresh `.add-on` outputs.
|
|
20
|
+
- Rebuild `.add-on` assets and `add-on.json` automatically when source files change.
|
|
21
|
+
- Document the new add-on development loop in the custom add-on guide.
|
|
22
|
+
|
|
23
|
+
- Improve scaffold customization and custom add-on authoring flow. ([`5fbf262`](https://github.com/TanStack/cli/commit/5fbf262fe3a0d070e6a78fa2f2a920b176b84480))
|
|
24
|
+
|
|
25
|
+
- Add `--examples` / `--no-examples` support to include or omit demo/example pages during app creation.
|
|
26
|
+
- Prompt for add-on-declared environment variables during interactive create and seed entered values into generated `.env.local`.
|
|
27
|
+
- Ensure custom add-on/starter metadata consistently includes a `version`, with safe backfill for older metadata files.
|
|
28
|
+
- Align bundled starter/example metadata and docs with current Start/file-router behavior.
|
|
29
|
+
|
|
3
30
|
## 0.61.4
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/dist/create-app.js
CHANGED
|
@@ -8,6 +8,42 @@ import { createTemplateFile } from './template-file.js';
|
|
|
8
8
|
import { installShadcnComponents } from './integrations/shadcn.js';
|
|
9
9
|
import { setupGit } from './integrations/git.js';
|
|
10
10
|
import { runSpecialSteps } from './special-steps/index.js';
|
|
11
|
+
function isDemoRoutePath(path) {
|
|
12
|
+
if (!path)
|
|
13
|
+
return false;
|
|
14
|
+
const normalized = path.replace(/\\/g, '/');
|
|
15
|
+
return (normalized.includes('/routes/demo/') ||
|
|
16
|
+
normalized.includes('/routes/demo.') ||
|
|
17
|
+
normalized.includes('/routes/example/') ||
|
|
18
|
+
normalized.includes('/routes/example.'));
|
|
19
|
+
}
|
|
20
|
+
function stripExamplesFromOptions(options) {
|
|
21
|
+
if (options.includeExamples !== false) {
|
|
22
|
+
return options;
|
|
23
|
+
}
|
|
24
|
+
const chosenAddOns = options.chosenAddOns
|
|
25
|
+
.filter((addOn) => addOn.type !== 'example')
|
|
26
|
+
.map((addOn) => {
|
|
27
|
+
const filteredRoutes = (addOn.routes || []).filter((route) => !isDemoRoutePath(route.path) &&
|
|
28
|
+
!(route.url && route.url.startsWith('/demo')));
|
|
29
|
+
return {
|
|
30
|
+
...addOn,
|
|
31
|
+
routes: filteredRoutes,
|
|
32
|
+
getFiles: async () => {
|
|
33
|
+
const files = await addOn.getFiles();
|
|
34
|
+
return files.filter((file) => !isDemoRoutePath(file));
|
|
35
|
+
},
|
|
36
|
+
getDeletedFiles: async () => {
|
|
37
|
+
const deletedFiles = await addOn.getDeletedFiles();
|
|
38
|
+
return deletedFiles.filter((file) => !isDemoRoutePath(file));
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
...options,
|
|
44
|
+
chosenAddOns,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
11
47
|
async function writeFiles(environment, options) {
|
|
12
48
|
const templateFileFromContent = createTemplateFile(environment, options);
|
|
13
49
|
async function writeFileBundle(bundle) {
|
|
@@ -170,6 +206,29 @@ async function runCommandsAndInstallDependencies(environment, options) {
|
|
|
170
206
|
}
|
|
171
207
|
await installShadcnComponents(environment, options.targetDir, options);
|
|
172
208
|
}
|
|
209
|
+
async function seedEnvValues(environment, options) {
|
|
210
|
+
const envVarValues = options.envVarValues || {};
|
|
211
|
+
const entries = Object.entries(envVarValues);
|
|
212
|
+
if (entries.length === 0)
|
|
213
|
+
return;
|
|
214
|
+
const envLocalPath = resolve(options.targetDir, '.env.local');
|
|
215
|
+
if (!environment.exists(envLocalPath)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let envContents = await environment.readFile(envLocalPath);
|
|
219
|
+
for (const [key, value] of entries) {
|
|
220
|
+
const escapedValue = value.replace(/\n/g, '\\n');
|
|
221
|
+
const nextLine = `${key}=${escapedValue}`;
|
|
222
|
+
const pattern = new RegExp(`^${key}=.*$`, 'm');
|
|
223
|
+
if (pattern.test(envContents)) {
|
|
224
|
+
envContents = envContents.replace(pattern, nextLine);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
envContents += `${envContents.endsWith('\n') ? '' : '\n'}${nextLine}\n`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
await environment.writeFile(envLocalPath, envContents);
|
|
231
|
+
}
|
|
173
232
|
function report(environment, options) {
|
|
174
233
|
const warnings = [];
|
|
175
234
|
for (const addOn of options.chosenAddOns) {
|
|
@@ -207,9 +266,11 @@ ${cdInstruction}% ${formatCommand(getPackageManagerScriptCommand(options.package
|
|
|
207
266
|
Please read the README.md file for information on testing, styling, adding routes, etc.${errorStatement}`);
|
|
208
267
|
}
|
|
209
268
|
export async function createApp(environment, options) {
|
|
269
|
+
const effectiveOptions = stripExamplesFromOptions(options);
|
|
210
270
|
environment.startRun();
|
|
211
|
-
await writeFiles(environment,
|
|
212
|
-
await
|
|
271
|
+
await writeFiles(environment, effectiveOptions);
|
|
272
|
+
await seedEnvValues(environment, effectiveOptions);
|
|
273
|
+
await runCommandsAndInstallDependencies(environment, effectiveOptions);
|
|
213
274
|
environment.finishRun();
|
|
214
|
-
report(environment,
|
|
275
|
+
report(environment, effectiveOptions);
|
|
215
276
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, watch } from 'node:fs/promises';
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, resolve } from 'node:path';
|
|
4
4
|
import { AddOnCompiledSchema } from '../types.js';
|
|
@@ -15,6 +15,19 @@ export const ADD_ON_IGNORE_FILES = [
|
|
|
15
15
|
const INFO_FILE = '.add-on/info.json';
|
|
16
16
|
const COMPILED_FILE = 'add-on.json';
|
|
17
17
|
const ASSETS_DIR = 'assets';
|
|
18
|
+
const ADD_ON_DEV_IGNORE_PREFIXES = [
|
|
19
|
+
'.add-on/',
|
|
20
|
+
'.git/',
|
|
21
|
+
'node_modules/',
|
|
22
|
+
'.turbo/',
|
|
23
|
+
'dist/',
|
|
24
|
+
];
|
|
25
|
+
function shouldIgnoreDevPath(path) {
|
|
26
|
+
const normalized = path.replace(/\\/g, '/');
|
|
27
|
+
if (normalized === COMPILED_FILE)
|
|
28
|
+
return true;
|
|
29
|
+
return ADD_ON_DEV_IGNORE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
30
|
+
}
|
|
18
31
|
export function camelCase(str) {
|
|
19
32
|
return str
|
|
20
33
|
.split(/(\.|-|\/)/)
|
|
@@ -78,6 +91,9 @@ export async function readOrGenerateAddOnInfo(options) {
|
|
|
78
91
|
},
|
|
79
92
|
dependsOn: options.chosenAddOns,
|
|
80
93
|
};
|
|
94
|
+
if (!info.version) {
|
|
95
|
+
info.version = '0.0.1';
|
|
96
|
+
}
|
|
81
97
|
return info;
|
|
82
98
|
}
|
|
83
99
|
export async function generateProject(persistedOptions) {
|
|
@@ -136,6 +152,48 @@ export async function initAddOn(environment) {
|
|
|
136
152
|
await updateAddOnInfo(environment);
|
|
137
153
|
await compileAddOn(environment);
|
|
138
154
|
}
|
|
155
|
+
export async function devAddOn(environment) {
|
|
156
|
+
await initAddOn(environment);
|
|
157
|
+
environment.info('Add-on dev mode is running.', 'Watching project files and recompiling add-on output on changes. Press Ctrl+C to stop.');
|
|
158
|
+
const abortController = new AbortController();
|
|
159
|
+
let debounceTimeout;
|
|
160
|
+
const rerun = async () => {
|
|
161
|
+
try {
|
|
162
|
+
await updateAddOnInfo(environment);
|
|
163
|
+
await compileAddOn(environment);
|
|
164
|
+
environment.info('Add-on updated.', 'Compiled add-on.json and refreshed .add-on');
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
+
environment.error('Failed to rebuild add-on.', message);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
process.once('SIGINT', () => {
|
|
172
|
+
abortController.abort();
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
for await (const event of watch(process.cwd(), {
|
|
176
|
+
recursive: true,
|
|
177
|
+
signal: abortController.signal,
|
|
178
|
+
})) {
|
|
179
|
+
const file = event.filename?.toString();
|
|
180
|
+
if (!file || shouldIgnoreDevPath(file))
|
|
181
|
+
continue;
|
|
182
|
+
if (debounceTimeout) {
|
|
183
|
+
clearTimeout(debounceTimeout);
|
|
184
|
+
}
|
|
185
|
+
debounceTimeout = setTimeout(() => {
|
|
186
|
+
void rerun();
|
|
187
|
+
}, 200);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
if (!message.includes('aborted')) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
139
197
|
export async function loadRemoteAddOn(url) {
|
|
140
198
|
const response = await fetch(url);
|
|
141
199
|
const jsonContent = await response.json();
|
|
@@ -7,7 +7,7 @@ import { compareFilesRecursively, createAppOptionsFromPersisted, createPackageAd
|
|
|
7
7
|
const INFO_FILE = 'starter-info.json';
|
|
8
8
|
const COMPILED_FILE = 'starter.json';
|
|
9
9
|
export async function readOrGenerateStarterInfo(options) {
|
|
10
|
-
|
|
10
|
+
const info = existsSync(INFO_FILE)
|
|
11
11
|
? JSON.parse((await readFile(INFO_FILE)).toString())
|
|
12
12
|
: {
|
|
13
13
|
id: `${options.projectName}-starter`,
|
|
@@ -31,6 +31,10 @@ export async function readOrGenerateStarterInfo(options) {
|
|
|
31
31
|
dependsOn: options.chosenAddOns,
|
|
32
32
|
typescript: true,
|
|
33
33
|
};
|
|
34
|
+
if (!info.version) {
|
|
35
|
+
info.version = '0.0.1';
|
|
36
|
+
}
|
|
37
|
+
return info;
|
|
34
38
|
}
|
|
35
39
|
async function loadCurrentStarterInfo(environment) {
|
|
36
40
|
const persistedOptions = await readCurrentProjectOptions(environment);
|
|
@@ -28,5 +28,14 @@
|
|
|
28
28
|
"jsName": "ClerkProvider",
|
|
29
29
|
"path": "src/integrations/clerk/provider.tsx"
|
|
30
30
|
}
|
|
31
|
+
],
|
|
32
|
+
"envVars": [
|
|
33
|
+
{
|
|
34
|
+
"name": "VITE_CLERK_PUBLISHABLE_KEY",
|
|
35
|
+
"description": "Clerk publishable key",
|
|
36
|
+
"required": true,
|
|
37
|
+
"secret": false,
|
|
38
|
+
"file": ".env.local"
|
|
39
|
+
}
|
|
31
40
|
]
|
|
32
41
|
}
|
|
@@ -23,5 +23,21 @@
|
|
|
23
23
|
"path": "src/integrations/convex/provider.tsx",
|
|
24
24
|
"jsName": "ConvexProvider"
|
|
25
25
|
}
|
|
26
|
+
],
|
|
27
|
+
"envVars": [
|
|
28
|
+
{
|
|
29
|
+
"name": "CONVEX_DEPLOYMENT",
|
|
30
|
+
"description": "Convex deployment name",
|
|
31
|
+
"required": false,
|
|
32
|
+
"secret": false,
|
|
33
|
+
"file": ".env.local"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "VITE_CONVEX_URL",
|
|
37
|
+
"description": "Convex deployment URL",
|
|
38
|
+
"required": true,
|
|
39
|
+
"secret": false,
|
|
40
|
+
"file": ".env.local"
|
|
41
|
+
}
|
|
26
42
|
]
|
|
27
43
|
}
|
|
@@ -1,14 +1,164 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Strapi CMS Integration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This add-on integrates Strapi CMS with your TanStack Start application using the official Strapi Client SDK.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Features
|
|
6
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 (create manually or use hosted Strapi)
|
|
40
|
+
├── src/api/ # Content types
|
|
41
|
+
├── config/ # Strapi configuration
|
|
42
|
+
└── package.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Quick Start
|
|
46
|
+
|
|
47
|
+
Create your Strapi project separately (or use an existing hosted Strapi instance), then point this app to it with `VITE_STRAPI_URL`.
|
|
48
|
+
|
|
49
|
+
**1. Set up Strapi:**
|
|
50
|
+
|
|
51
|
+
Follow the Strapi quick-start guide to create a local project, or use your existing Strapi deployment:
|
|
52
|
+
|
|
53
|
+
- https://docs.strapi.io/dev-docs/quick-start
|
|
54
|
+
|
|
55
|
+
If you created a local Strapi project in a sibling `server` directory, continue with:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd ../server
|
|
59
|
+
npm install # or pnpm install / yarn install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**2. Start the Strapi server:**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run develop # Starts at http://localhost:1337
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**3. Create an admin account:**
|
|
69
|
+
|
|
70
|
+
Open http://localhost:1337/admin and create your first admin user.
|
|
71
|
+
|
|
72
|
+
**4. Create content:**
|
|
73
|
+
|
|
74
|
+
In the Strapi admin panel, go to Content Manager > Article and create some articles.
|
|
75
|
+
|
|
76
|
+
**5. Start your TanStack app (in another terminal):**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd ../client # or your project name
|
|
80
|
+
npm run dev # Starts at http://localhost:3000
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**6. View the demo:**
|
|
84
|
+
|
|
85
|
+
Navigate to http://localhost:3000/demo/strapi to see your articles.
|
|
86
|
+
|
|
87
|
+
### Environment Variables
|
|
88
|
+
|
|
89
|
+
The following environment variable is pre-configured in `.env.local`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
VITE_STRAPI_URL="http://localhost:1337"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For production, update this to your deployed Strapi URL.
|
|
96
|
+
|
|
97
|
+
### Demo Pages
|
|
98
|
+
|
|
99
|
+
| URL | Description |
|
|
100
|
+
|-----|-------------|
|
|
101
|
+
| `/demo/strapi` | Articles list with search and pagination |
|
|
102
|
+
| `/demo/strapi/:articleId` | Article detail with block rendering |
|
|
103
|
+
|
|
104
|
+
### Search and Pagination
|
|
105
|
+
|
|
106
|
+
- **Search**: Type in the search box to filter articles by title or description
|
|
107
|
+
- **Pagination**: Navigate between pages using the pagination controls
|
|
108
|
+
- **URL State**: Search and page are stored in the URL (`?query=term&page=2`)
|
|
109
|
+
|
|
110
|
+
### Block Types Supported
|
|
111
|
+
|
|
112
|
+
| Block | Component | Description |
|
|
113
|
+
|-------|-----------|-------------|
|
|
114
|
+
| `shared.rich-text` | RichText | Markdown content |
|
|
115
|
+
| `shared.quote` | Quote | Blockquote with author |
|
|
116
|
+
| `shared.media` | Media | Single image/video |
|
|
117
|
+
| `shared.slider` | Slider | Image gallery grid |
|
|
118
|
+
|
|
119
|
+
### Dependencies
|
|
120
|
+
|
|
121
|
+
| Package | Purpose |
|
|
122
|
+
|---------|---------|
|
|
123
|
+
| `@strapi/client` | Official Strapi SDK |
|
|
124
|
+
| `react-markdown` | Markdown rendering |
|
|
125
|
+
| `remark-gfm` | GitHub Flavored Markdown |
|
|
126
|
+
| `use-debounce` | Debounced search input |
|
|
127
|
+
|
|
128
|
+
### Running Both Servers
|
|
129
|
+
|
|
130
|
+
Open two terminal windows from the parent directory:
|
|
131
|
+
|
|
132
|
+
**Terminal 1 - Strapi:**
|
|
7
133
|
```bash
|
|
8
|
-
|
|
9
|
-
cd my-strapi-project
|
|
10
|
-
pnpm dev
|
|
134
|
+
cd server && npm run develop
|
|
11
135
|
```
|
|
12
136
|
|
|
13
|
-
|
|
14
|
-
|
|
137
|
+
**Terminal 2 - TanStack Start:**
|
|
138
|
+
```bash
|
|
139
|
+
cd client && npm run dev # or your project name
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Customization
|
|
143
|
+
|
|
144
|
+
**Change page size:**
|
|
145
|
+
Edit `src/data/loaders/articles.ts` and modify `PAGE_SIZE`.
|
|
146
|
+
|
|
147
|
+
**Add new block types:**
|
|
148
|
+
1. Create component in `src/components/blocks/`
|
|
149
|
+
2. Export from `src/components/blocks/index.ts`
|
|
150
|
+
3. Add case to `block-renderer.tsx` switch statement
|
|
151
|
+
4. Update populate in articles loader
|
|
152
|
+
|
|
153
|
+
**Add new content types:**
|
|
154
|
+
1. Add types to `src/types/strapi.ts`
|
|
155
|
+
2. Create loader in `src/data/loaders/`
|
|
156
|
+
3. Create route in `src/routes/demo/`
|
|
157
|
+
|
|
158
|
+
### Learn More
|
|
159
|
+
|
|
160
|
+
- [Strapi Documentation](https://docs.strapi.io/)
|
|
161
|
+
- [Strapi Client SDK](https://www.npmjs.com/package/@strapi/client)
|
|
162
|
+
- [Strapi Cloud Template Blog](https://github.com/strapi/strapi-cloud-template-blog)
|
|
163
|
+
- [TanStack Start Documentation](https://tanstack.com/start/latest)
|
|
164
|
+
- [TanStack Router Search Params](https://tanstack.com/router/latest/docs/framework/react/guide/search-params)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Strapi configuration
|
|
2
|
-
VITE_STRAPI_URL="http://localhost:1337
|
|
2
|
+
VITE_STRAPI_URL="http://localhost:1337"
|
|
@@ -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
|
+
}
|
|
@@ -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";
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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,0 +1,74 @@
|
|
|
1
|
+
import Markdown from "react-markdown";
|
|
2
|
+
import remarkGfm from "remark-gfm";
|
|
3
|
+
|
|
4
|
+
interface MarkdownContentProps {
|
|
5
|
+
content: string | undefined | null;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const styles = {
|
|
10
|
+
h1: "text-3xl font-bold mb-6 text-white",
|
|
11
|
+
h2: "text-2xl font-bold mb-4 text-white",
|
|
12
|
+
h3: "text-xl font-bold mb-3 text-white",
|
|
13
|
+
p: "mb-4 leading-relaxed text-gray-300",
|
|
14
|
+
a: "text-cyan-400 hover:underline",
|
|
15
|
+
ul: "list-disc pl-6 mb-4 space-y-2 text-gray-300",
|
|
16
|
+
ol: "list-decimal pl-6 mb-4 space-y-2 text-gray-300",
|
|
17
|
+
li: "leading-relaxed",
|
|
18
|
+
blockquote: "border-l-4 border-cyan-400 pl-4 italic text-gray-400 my-4",
|
|
19
|
+
code: "bg-slate-800 px-2 py-1 rounded text-cyan-400 text-sm font-mono",
|
|
20
|
+
pre: "bg-slate-800 p-4 rounded-lg overflow-x-auto mb-4",
|
|
21
|
+
table: "w-full border-collapse mb-4",
|
|
22
|
+
th: "border border-slate-700 p-2 bg-slate-800 text-left text-white",
|
|
23
|
+
td: "border border-slate-700 p-2 text-gray-300",
|
|
24
|
+
img: "max-w-full h-auto rounded-lg my-4",
|
|
25
|
+
hr: "border-slate-700 my-8",
|
|
26
|
+
strong: "text-white font-semibold",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function MarkdownContent({ content, className = "" }: MarkdownContentProps) {
|
|
30
|
+
if (!content) return null;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={`prose prose-invert max-w-none ${className}`}>
|
|
34
|
+
<Markdown
|
|
35
|
+
remarkPlugins={[remarkGfm]}
|
|
36
|
+
components={{
|
|
37
|
+
h1: ({ children }) => <h1 className={styles.h1}>{children}</h1>,
|
|
38
|
+
h2: ({ children }) => <h2 className={styles.h2}>{children}</h2>,
|
|
39
|
+
h3: ({ children }) => <h3 className={styles.h3}>{children}</h3>,
|
|
40
|
+
p: ({ children }) => <p className={styles.p}>{children}</p>,
|
|
41
|
+
a: ({ href, children }) => (
|
|
42
|
+
<a href={href} className={styles.a} target="_blank" rel="noopener noreferrer">
|
|
43
|
+
{children}
|
|
44
|
+
</a>
|
|
45
|
+
),
|
|
46
|
+
ul: ({ children }) => <ul className={styles.ul}>{children}</ul>,
|
|
47
|
+
ol: ({ children }) => <ol className={styles.ol}>{children}</ol>,
|
|
48
|
+
li: ({ children }) => <li className={styles.li}>{children}</li>,
|
|
49
|
+
blockquote: ({ children }) => <blockquote className={styles.blockquote}>{children}</blockquote>,
|
|
50
|
+
code: ({ className, children }) => {
|
|
51
|
+
const isCodeBlock = className?.includes("language-");
|
|
52
|
+
if (isCodeBlock) {
|
|
53
|
+
return (
|
|
54
|
+
<pre className={styles.pre}>
|
|
55
|
+
<code className="text-sm font-mono text-gray-300">{children}</code>
|
|
56
|
+
</pre>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return <code className={styles.code}>{children}</code>;
|
|
60
|
+
},
|
|
61
|
+
pre: ({ children }) => <>{children}</>,
|
|
62
|
+
table: ({ children }) => <table className={styles.table}>{children}</table>,
|
|
63
|
+
th: ({ children }) => <th className={styles.th}>{children}</th>,
|
|
64
|
+
td: ({ children }) => <td className={styles.td}>{children}</td>,
|
|
65
|
+
img: ({ src, alt }) => <img src={src} alt={alt || ""} className={styles.img} />,
|
|
66
|
+
hr: () => <hr className={styles.hr} />,
|
|
67
|
+
strong: ({ children }) => <strong className={styles.strong}>{children}</strong>,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{content}
|
|
71
|
+
</Markdown>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|