@dani-builder/strapi-client 0.1.0 → 0.2.0
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/AGENTS.md +117 -0
- package/docs/api-guide.md +223 -0
- package/docs/block-reference.md +601 -0
- package/docs/content-types.md +393 -0
- package/package.json +10 -8
package/AGENTS.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @dani-builder/strapi-client
|
|
2
|
+
|
|
3
|
+
TypeScript client library for the Daniworks Builder CMS (Strapi v5). Provides typed API functions, populate builders, and media utilities for SSR-safe data fetching.
|
|
4
|
+
|
|
5
|
+
## Import Paths
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createStrapiClient, getPage, getMediaUrl } from '@dani-builder/strapi-client'; // everything
|
|
9
|
+
import type { PageDocument, HeroBlock } from '@dani-builder/strapi-client/types'; // types only
|
|
10
|
+
import { createStrapiClient, isStrapiError } from '@dani-builder/strapi-client/client'; // client factory
|
|
11
|
+
import { getPagePopulate, coreBlockPopulates } from '@dani-builder/strapi-client/populate'; // populate builders
|
|
12
|
+
import { getPage, listArticles, postInquiry } from '@dani-builder/strapi-client/api'; // API functions
|
|
13
|
+
import { getMediaUrl, getAssetUrl } from '@dani-builder/strapi-client/utils'; // media helpers
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const client = createStrapiClient({ baseUrl: 'http://localhost:1337', apiToken: '...' });
|
|
20
|
+
const page = await getPage(client, documentId); // Smart endpoint (server-side populate)
|
|
21
|
+
const page2 = await getPageBySlug(client, 'about'); // REST with client-side populate
|
|
22
|
+
const global = await getGlobal(client, { locale: 'ko' }); // Smart endpoint
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Key Concepts
|
|
26
|
+
|
|
27
|
+
- **Smart vs REST** — Smart endpoints (`/api/builder/*`) use server-side populate via plugin. REST endpoints use client-side populate builders. Prefer Smart when available.
|
|
28
|
+
- **Dynamic Zone discrimination** — Use `block.__component` to switch on block type (e.g. `'blocks.hero'`).
|
|
29
|
+
- **Populate builders** — `getPagePopulate()`, `getGlobalPopulate()`, etc. build nested populate objects. Pass `customBlocks` to extend.
|
|
30
|
+
- **Media URL resolution** — `getMediaUrl(url, mediaHost)` resolves relative paths. `getAssetUrl(asset, format, mediaHost)` gets a specific format with fallback.
|
|
31
|
+
|
|
32
|
+
## Critical Rules
|
|
33
|
+
|
|
34
|
+
1. **Variants are exact enum values** — DO NOT invent variant names. Only use values defined in the type (e.g. `'side-by-side' | 'media-extending' | 'fullscreen-media' | 'stacked-media'` for HeroBlock). See [Block Reference](./docs/block-reference.md).
|
|
35
|
+
2. **Media policy varies by field** — Some fields accept images+videos, others images-only. Check the block's media policy before rendering `<video>`. See [Media Policy Summary](./docs/block-reference.md#media-policy-summary).
|
|
36
|
+
3. **All media fields are nullable** — Always render defensively (`asset && <img ... />`). Even required media in sub-components may be null at the API level.
|
|
37
|
+
4. **BlocksContent is structured JSON** — `Article.body`, `FAQ.answer`, and `ContentBlock.body` are `BlockNode[]` arrays, NOT HTML or Markdown. Use a Blocks renderer.
|
|
38
|
+
5. **Section.theme is a Tailwind preset key** — Map to your design tokens (e.g. `'dark'` → dark background + light text). DO NOT apply arbitrary CSS colors.
|
|
39
|
+
6. **`name` fields are admin-only** — Blocks with `name` (Features, Slider, Highlights, etc.) use it for CMS identification. DO NOT display it in the frontend.
|
|
40
|
+
7. **Series uses `/api/all-series`** — The pluralName differs from singularName. DO NOT use `/api/series`.
|
|
41
|
+
8. **Inquiry has no draftAndPublish** — `postInquiry` creates immediately-visible records. No publish step needed.
|
|
42
|
+
|
|
43
|
+
## MCP Content Creation Guide
|
|
44
|
+
|
|
45
|
+
LLM(Claude Code)이 MCP를 통해 Strapi CMS에 직접 페이지/콘텐츠를 등록하는 워크플로우.
|
|
46
|
+
|
|
47
|
+
### 추천 MCP 서버
|
|
48
|
+
|
|
49
|
+
**`@bschauer/strapi-mcp-server`** (npm: `@bschauer/strapi-mcp-server`)
|
|
50
|
+
- Strapi v5 호환, API Token 인증, `strapi_rest` 도구로 REST CRUD 지원
|
|
51
|
+
- `strapi-mcp` (l33tdawg)는 v5에서 create 실패 — 사용 금지
|
|
52
|
+
|
|
53
|
+
### 설정 순서
|
|
54
|
+
|
|
55
|
+
1. Strapi Admin → Settings → API Tokens → **Full access / Unlimited** 토큰 생성
|
|
56
|
+
2. `~/.mcp/strapi-mcp-server.config.json`에 서버 등록:
|
|
57
|
+
```json
|
|
58
|
+
{ "my-cms": { "api_url": "http://localhost:1337", "api_key": "<token>", "version": "5.*" } }
|
|
59
|
+
```
|
|
60
|
+
3. 프로젝트 루트 `.mcp.json`에 MCP 서버 설정 (상세: 프로젝트 CLAUDE.md 참조)
|
|
61
|
+
4. **`npx daniworks-builder sync` 필수** — schema뿐 아니라 routes/controllers/services도 생성하여 REST CRUD 엔드포인트 활성화
|
|
62
|
+
|
|
63
|
+
### 페이지 생성 패턴
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
// POST api/pages (via strapi_rest, server="my-cms")
|
|
67
|
+
{
|
|
68
|
+
"data": {
|
|
69
|
+
"title": "서비스 소개",
|
|
70
|
+
"slug": "services",
|
|
71
|
+
"locale": "ko",
|
|
72
|
+
"headerMode": "offset",
|
|
73
|
+
"blocks": [
|
|
74
|
+
{
|
|
75
|
+
"__component": "blocks.hero",
|
|
76
|
+
"variant": "side-by-side",
|
|
77
|
+
"section": {
|
|
78
|
+
"heading": "최고의 서비스",
|
|
79
|
+
"subheading": "고객에게 가치를 전달합니다",
|
|
80
|
+
"theme": "dark"
|
|
81
|
+
},
|
|
82
|
+
"ctaLink": { "label": "시작하기", "href": "/contact" }
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"__component": "blocks.features",
|
|
86
|
+
"variant": "simple-grid",
|
|
87
|
+
"section": {
|
|
88
|
+
"heading": "주요 기능",
|
|
89
|
+
"theme": "default"
|
|
90
|
+
},
|
|
91
|
+
"items": [
|
|
92
|
+
{ "label": "빠른 개발", "description": "CMS 기반 빠른 페이지 구축" },
|
|
93
|
+
{ "label": "유연한 레이아웃", "description": "15가지 블록 조합" }
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 핵심 주의사항
|
|
102
|
+
|
|
103
|
+
9. **`data` 래퍼 필수** — POST/PUT body는 `{ "data": { ... } }`. 이중 래핑 금지.
|
|
104
|
+
10. **`locale` 필수** — i18n 활성 CT는 `locale` 누락 시 `Invalid key` 에러.
|
|
105
|
+
11. **블록 텍스트는 `section` 안에** — heading/subheading/theme은 블록이 아닌 `section` 서브컴포넌트에 위치. Hero에 `title` 필드 없음.
|
|
106
|
+
12. **`__component` 필수** — Dynamic zone의 각 블록 객체에 반드시 포함.
|
|
107
|
+
13. **컴포넌트 스키마 먼저 확인** — `strapi_get_components`로 실제 필드명 확인 후 데이터 작성. 필드명 추측 금지.
|
|
108
|
+
14. **미디어는 별도 업로드** — `strapi_upload_media` → ID 획득 → PUT으로 연결.
|
|
109
|
+
15. **Deep populate는 Smart API** — `api/builder/pages/:docId`로 모든 중첩 데이터 조회.
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
| File | Content |
|
|
114
|
+
|------|---------|
|
|
115
|
+
| [docs/api-guide.md](./docs/api-guide.md) | Client setup, 19 API functions, Smart vs REST, populate builders, error handling, i18n |
|
|
116
|
+
| [docs/block-reference.md](./docs/block-reference.md) | 14 blocks + Section: variants, fields, media policies, constraints, sub-components |
|
|
117
|
+
| [docs/content-types.md](./docs/content-types.md) | 11 content types: fields, relations, i18n, DZ unions, BlocksContent structure |
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# API Guide
|
|
2
|
+
|
|
3
|
+
## Client Setup
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createStrapiClient } from '@dani-builder/strapi-client/client';
|
|
7
|
+
|
|
8
|
+
const client = createStrapiClient({
|
|
9
|
+
baseUrl: 'http://localhost:1337', // Strapi server URL
|
|
10
|
+
apiToken: process.env.STRAPI_TOKEN, // optional Bearer token
|
|
11
|
+
mediaHost: 'https://cdn.example.com', // optional CDN override for media URLs
|
|
12
|
+
defaultLocale: 'ko', // optional default locale
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`createStrapiClient` returns an Axios instance with:
|
|
17
|
+
- `Authorization: Bearer <token>` header (if `apiToken` provided)
|
|
18
|
+
- `Content-Type: application/json`
|
|
19
|
+
- `qs` params serializer (Strapi-compatible nested query format)
|
|
20
|
+
|
|
21
|
+
## API Functions
|
|
22
|
+
|
|
23
|
+
| Function | Method | Endpoint | Returns | Notes |
|
|
24
|
+
|----------|--------|----------|---------|-------|
|
|
25
|
+
| `getPage(client, docId, opts?)` | GET | `/api/builder/pages/:docId` | `PageDocument` | Smart endpoint |
|
|
26
|
+
| `getPageBySlug(client, slug, opts?)` | GET | `/api/pages` | `PageDocument \| null` | REST, filter by slug |
|
|
27
|
+
| `listPages(client, opts?)` | GET | `/api/builder/pages` | `{ data, meta }` | Smart, no blocks |
|
|
28
|
+
| `listPagesRest(client, opts?)` | GET | `/api/pages` | `{ data, meta }` | REST, with SEO populate |
|
|
29
|
+
| `getArticle(client, docId, opts?)` | GET | `/api/articles/:docId` | `ArticleDocument` | REST, full populate |
|
|
30
|
+
| `getArticleBySlug(client, slug, opts?)` | GET | `/api/articles` | `ArticleDocument \| null` | REST, filter by slug |
|
|
31
|
+
| `listArticles(client, opts?)` | GET | `/api/articles` | `{ data, meta }` | Filter: category, tag, author, series |
|
|
32
|
+
| `getGlobal(client, opts?)` | GET | `/api/builder/global` | `GlobalDocument` | Smart endpoint |
|
|
33
|
+
| `getGlobalRest(client, opts?)` | GET | `/api/global` | `GlobalDocument` | REST, client-side populate |
|
|
34
|
+
| `getNavigation(client, opts?)` | GET | `/api/navigation` | `NavigationDocument` | REST |
|
|
35
|
+
| `getSiteSettings(client)` | GET | `/api/site-setting` | `SiteSettingsDocument` | No locale, no draftAndPublish |
|
|
36
|
+
| `listFaqs(client, opts?)` | GET | `/api/faqs` | `{ data, meta }` | Filter: category; sort: sortOrder:asc |
|
|
37
|
+
| `listCategories(client, opts?)` | GET | `/api/categories` | `{ data, meta }` | Sort: sortOrder:asc |
|
|
38
|
+
| `listTags(client, opts?)` | GET | `/api/tags` | `{ data, meta }` | Sort: name:asc |
|
|
39
|
+
| `listAuthors(client, opts?)` | GET | `/api/authors` | `{ data, meta }` | With avatar populate |
|
|
40
|
+
| `getAuthorBySlug(client, slug)` | GET | `/api/authors` | `AuthorDocument \| null` | Filter by slug |
|
|
41
|
+
| `listSeries(client, opts?)` | GET | `/api/all-series` | `{ data, meta }` | Note: `/all-series` not `/series` |
|
|
42
|
+
| `getSeriesBySlug(client, slug, locale?)` | GET | `/api/all-series` | `SeriesDocument \| null` | Filter by slug |
|
|
43
|
+
| `postInquiry(client, data)` | POST | `/api/inquiries` | `InquiryDocument` | Body: `InquiryFormData` |
|
|
44
|
+
|
|
45
|
+
## Smart vs REST
|
|
46
|
+
|
|
47
|
+
**Smart endpoints** are provided by the Builder plugin and apply server-side populate (optimal field selection):
|
|
48
|
+
|
|
49
|
+
| Smart Endpoint | Equivalent REST |
|
|
50
|
+
|---------------|----------------|
|
|
51
|
+
| `/api/builder/pages` | `/api/pages` (list, no blocks) |
|
|
52
|
+
| `/api/builder/pages/:docId` | `/api/pages/:docId` (single, full blocks) |
|
|
53
|
+
| `/api/builder/global` | `/api/global` (full populate) |
|
|
54
|
+
|
|
55
|
+
**When to use which:**
|
|
56
|
+
- **Smart** — Default choice. Populate is optimized server-side; smaller payloads.
|
|
57
|
+
- **REST** — When you need slug-based lookup (`getPageBySlug`), custom populate, or the Smart endpoint is unavailable.
|
|
58
|
+
|
|
59
|
+
`getPageBySlug` uses REST because Smart endpoints only support documentId lookup, not slug filters.
|
|
60
|
+
|
|
61
|
+
## Populate Builders
|
|
62
|
+
|
|
63
|
+
Populate builders construct the nested `populate` parameter for REST queries:
|
|
64
|
+
|
|
65
|
+
| Builder | Used For |
|
|
66
|
+
|---------|----------|
|
|
67
|
+
| `getPagePopulate(customBlocks?)` | Page with all DZ blocks deep-populated |
|
|
68
|
+
| `getPageListPopulate()` | Page list — SEO only, no blocks |
|
|
69
|
+
| `getGlobalPopulate(customBlocks?)` | Global with footer, seo, siteWideBlocks, home, favicon |
|
|
70
|
+
| `getArticlePopulate()` | Article with seo, cover, gallery, relations |
|
|
71
|
+
| `getArticleListPopulate()` | Article list with cover, categories, tags, author |
|
|
72
|
+
| `getNavigationPopulate()` | Navigation with logos and nav groups |
|
|
73
|
+
| `getSiteSettingsPopulate()` | SiteSettings with analytics, chatbot, verifications |
|
|
74
|
+
| `getFaqPopulate()` | FAQ with category |
|
|
75
|
+
|
|
76
|
+
**Extending with custom blocks:**
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const customBlocks = {
|
|
80
|
+
'blocks.custom-block': {
|
|
81
|
+
populate: { section: { populate: { backgroundImage: true } }, items: true },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const populate = getPagePopulate(customBlocks);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Low-level populate objects** are also exported:
|
|
88
|
+
- `coreBlockPopulates` — Record of all 14 block UIDs + `elements.section` with their populate
|
|
89
|
+
- `seoPopulate` — SEO component with ogImage
|
|
90
|
+
- `footerPopulate` — Footer with socialLinks, otherLinks, sitemap, logo
|
|
91
|
+
|
|
92
|
+
## Query Patterns
|
|
93
|
+
|
|
94
|
+
### Fetch a page by slug
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const page = await getPageBySlug(client, 'about', { locale: 'ko' });
|
|
98
|
+
if (!page) return notFound();
|
|
99
|
+
// page.blocks is PageBlocks[] — discriminate with __component
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### List articles with filters
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const { data: articles, meta } = await listArticles(client, {
|
|
106
|
+
category: 'tech', // filter by category slug
|
|
107
|
+
tag: 'react', // filter by tag slug
|
|
108
|
+
page: 1,
|
|
109
|
+
pageSize: 10,
|
|
110
|
+
sort: 'firstPublishedAt:desc',
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Fetch global layout data
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const global = await getGlobal(client, { locale: 'ko' });
|
|
118
|
+
// global.footer — Footer component
|
|
119
|
+
// global.siteWideBlocks — SiteWideBlocks[] (max 4 block types)
|
|
120
|
+
// global.seo — default SEO
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Submit inquiry
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const inquiry = await postInquiry(client, {
|
|
127
|
+
name: 'John Doe',
|
|
128
|
+
email: 'john@example.com',
|
|
129
|
+
subject: 'General inquiry',
|
|
130
|
+
message: 'Hello...',
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Media URL Resolution
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { getMediaUrl, getAssetUrl } from '@dani-builder/strapi-client/utils';
|
|
138
|
+
|
|
139
|
+
// Resolve relative URL to absolute
|
|
140
|
+
const url = getMediaUrl('/uploads/photo.jpg', 'https://cdn.example.com');
|
|
141
|
+
// → 'https://cdn.example.com/uploads/photo.jpg'
|
|
142
|
+
|
|
143
|
+
// Already-absolute URLs pass through
|
|
144
|
+
getMediaUrl('https://external.com/img.png');
|
|
145
|
+
// → 'https://external.com/img.png'
|
|
146
|
+
|
|
147
|
+
// Get a specific format with fallback to original
|
|
148
|
+
const thumbUrl = getAssetUrl(article.cover, 'medium', mediaHost);
|
|
149
|
+
// Available formats: 'thumbnail' | 'small' | 'medium' | 'large'
|
|
150
|
+
// Falls back to original URL if requested format doesn't exist
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Error Handling
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { isStrapiError } from '@dani-builder/strapi-client/client';
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const page = await getPage(client, docId);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (isStrapiError(error)) {
|
|
162
|
+
// error.response.data.error has: { status, name, message, details }
|
|
163
|
+
console.error(error.response!.data.error.message);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`StrapiError` shape:
|
|
170
|
+
```ts
|
|
171
|
+
{
|
|
172
|
+
data: null;
|
|
173
|
+
error: {
|
|
174
|
+
status: number; // HTTP status code
|
|
175
|
+
name: string; // e.g. 'NotFoundError'
|
|
176
|
+
message: string; // Human-readable message
|
|
177
|
+
details: Record<string, unknown>;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## i18n Support
|
|
183
|
+
|
|
184
|
+
| Content Type | i18n | Base Type |
|
|
185
|
+
|-------------|------|-----------|
|
|
186
|
+
| Page | yes | `StrapiI18nDocument` |
|
|
187
|
+
| Article | yes | `StrapiI18nDocument` |
|
|
188
|
+
| FAQ | yes | `StrapiI18nDocument` |
|
|
189
|
+
| Category | yes | `StrapiI18nDocument` |
|
|
190
|
+
| Tag | yes | `StrapiI18nDocument` |
|
|
191
|
+
| Series | yes | `StrapiI18nDocument` |
|
|
192
|
+
| Navigation | yes | `StrapiI18nDocument` |
|
|
193
|
+
| Global | yes | `StrapiI18nDocument` |
|
|
194
|
+
| Author | no | `StrapiDocument` |
|
|
195
|
+
| Inquiry | no | `StrapiDocument` |
|
|
196
|
+
| SiteSettings | no | `StrapiDocument` |
|
|
197
|
+
|
|
198
|
+
Pass `locale` in query options for i18n-enabled types. Non-i18n types ignore the locale parameter.
|
|
199
|
+
|
|
200
|
+
## Project Wrapping Pattern
|
|
201
|
+
|
|
202
|
+
Wrap the client in a project-level `lib/strapi/` module to centralize configuration:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// lib/strapi/client.ts
|
|
206
|
+
import { createStrapiClient } from '@dani-builder/strapi-client/client';
|
|
207
|
+
|
|
208
|
+
export const strapi = createStrapiClient({
|
|
209
|
+
baseUrl: process.env.STRAPI_URL!,
|
|
210
|
+
apiToken: process.env.STRAPI_TOKEN!,
|
|
211
|
+
mediaHost: process.env.STRAPI_MEDIA_HOST,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// lib/strapi/queries.ts
|
|
215
|
+
import { strapi } from './client';
|
|
216
|
+
import { getPage, getGlobal, listArticles } from '@dani-builder/strapi-client/api';
|
|
217
|
+
|
|
218
|
+
export const fetchPage = (docId: string, locale?: string) =>
|
|
219
|
+
getPage(strapi, docId, { locale });
|
|
220
|
+
|
|
221
|
+
export const fetchGlobal = (locale?: string) =>
|
|
222
|
+
getGlobal(strapi, { locale });
|
|
223
|
+
```
|
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
# Block Reference
|
|
2
|
+
|
|
3
|
+
## Quick Reference Table
|
|
4
|
+
|
|
5
|
+
| Block | `__component` | Variants | Media Policy | Items Type |
|
|
6
|
+
|-------|--------------|----------|-------------|-----------|
|
|
7
|
+
| Hero | `blocks.hero` | `side-by-side`, `media-extending`, `fullscreen-media`, `stacked-media` | heroMedia: images+videos | — |
|
|
8
|
+
| Features | `blocks.features` | `simple-grid`, `dual-column`, `media-aside`, `media-aside-compact` | media: images only | `FeatureItem[]` |
|
|
9
|
+
| CTA | `blocks.cta` | `inline-spread`, `inline-centered`, `inline-stacked`, `card-stacked`, `card-centered`, `card-horizontal` | ctaMedia: images+videos | — |
|
|
10
|
+
| Slider | `blocks.slider` | `cover-flow`, `card-slider` | — | `SliderItem[]` |
|
|
11
|
+
| InquiryForm | `blocks.inquiry-form` | — | — | — |
|
|
12
|
+
| FAQ | `blocks.faq` | — | — | `FaqDocument[]` (relation) |
|
|
13
|
+
| Highlights | `blocks.highlights` | `dual-column`, `triple-column`, `alternating` | media: images+videos | `HighlightItem[]` |
|
|
14
|
+
| BentoGrid | `blocks.bento-grid` | — (layout via `columns` + item spans) | — | `BentoItem[]` |
|
|
15
|
+
| Testimonials | `blocks.testimonials` | `centered`, `side-by-side`, `card-grid`, `subtle-grid` | media: images+videos | `TestimonialItem[]` |
|
|
16
|
+
| LogoCloud | `blocks.logo-cloud` | `inline`, `grid`, `split` | — | `LogoItem[]` |
|
|
17
|
+
| Stats | `blocks.stats` | `simple`, `card-grid`, `split` | media: images+videos | `StatItem[]` |
|
|
18
|
+
| Pricing | `blocks.pricing` | `two-column`, `three-column`, `single`, `with-comparison` | — | `PricingTier[]` |
|
|
19
|
+
| Content | `blocks.content` | `prose`, `wide`, `full-width` | — | — |
|
|
20
|
+
| Team | `blocks.team` | `grid`, `large-image`, `compact` | — | `TeamMember[]` |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Media Policy Summary
|
|
25
|
+
|
|
26
|
+
### Images + Videos
|
|
27
|
+
|
|
28
|
+
These fields accept both image and video uploads. Render `<video>` or `<img>` based on `asset.mime`.
|
|
29
|
+
|
|
30
|
+
| Field | Component |
|
|
31
|
+
|-------|-----------|
|
|
32
|
+
| `heroMedia` | `blocks.hero` |
|
|
33
|
+
| `ctaMedia` | `blocks.cta` |
|
|
34
|
+
| `media` | `blocks.highlights` |
|
|
35
|
+
| `media` | `blocks.testimonials` |
|
|
36
|
+
| `media` | `blocks.stats` |
|
|
37
|
+
| `media` | `shared.highlight-item` |
|
|
38
|
+
| `media` | `shared.slider-item` |
|
|
39
|
+
|
|
40
|
+
### Images Only
|
|
41
|
+
|
|
42
|
+
These fields reject video uploads. Always render as `<img>`.
|
|
43
|
+
|
|
44
|
+
| Field | Component |
|
|
45
|
+
|-------|-----------|
|
|
46
|
+
| `media` | `blocks.features` |
|
|
47
|
+
| `backgroundImage` | `elements.section` |
|
|
48
|
+
| `logo` | `elements.footer` |
|
|
49
|
+
| `logo` | `shared.logo-item` |
|
|
50
|
+
| `avatar` | `shared.testimonial-item` |
|
|
51
|
+
| `logo` | `shared.testimonial-item` |
|
|
52
|
+
| `media` | `shared.bento-item` |
|
|
53
|
+
| `photo` | `shared.team-member` |
|
|
54
|
+
| `ogImage` | `shared.seo` |
|
|
55
|
+
| `mainLogo` | Navigation CT |
|
|
56
|
+
| `darkLogo` | Navigation CT |
|
|
57
|
+
| `favicon` | Global CT |
|
|
58
|
+
| `cover` | Article CT |
|
|
59
|
+
| `gallery` | Article CT |
|
|
60
|
+
| `avatar` | Author CT |
|
|
61
|
+
| `cover` | Series CT |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Section (`elements.section`)
|
|
66
|
+
|
|
67
|
+
Every block has an optional `section: Section | null` field. Section provides the heading, theme, and background for the block. It can also appear standalone in `Page.blocks` as a separator.
|
|
68
|
+
|
|
69
|
+
| Field | Type | Description |
|
|
70
|
+
|-------|------|-------------|
|
|
71
|
+
| `heading` | `string \| null` | Section heading |
|
|
72
|
+
| `subheading` | `string \| null` | Subheading |
|
|
73
|
+
| `kickerText` | `string \| null` | Small text above heading |
|
|
74
|
+
| `anchorId` | `string \| null` | Anchor ID for in-page navigation |
|
|
75
|
+
| `headerAlign` | `'auto' \| 'left' \| 'center' \| 'right' \| null` | Heading alignment |
|
|
76
|
+
| `theme` | `'default' \| 'light' \| 'dark' \| 'accent' \| 'muted' \| 'contrast'` | Tailwind preset key |
|
|
77
|
+
| `backgroundImage` | `StrapiMedia \| null` | Background image (images only) |
|
|
78
|
+
| `backgroundImageOpacity` | `number \| null` | Opacity 0–1 for background image overlay |
|
|
79
|
+
|
|
80
|
+
**Constraints:**
|
|
81
|
+
- `theme` maps to a Tailwind preset in the frontend theme config. DO NOT use arbitrary CSS colors.
|
|
82
|
+
- `backgroundImage` is images-only. DO NOT attempt to use video backgrounds here.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Hero (`blocks.hero`)
|
|
87
|
+
|
|
88
|
+
**`__component`: `'blocks.hero'`**
|
|
89
|
+
**Used in:** Page.blocks, Global.siteWideBlocks
|
|
90
|
+
|
|
91
|
+
### Variants
|
|
92
|
+
|
|
93
|
+
| Value | Layout |
|
|
94
|
+
|-------|--------|
|
|
95
|
+
| `side-by-side` | Text on one side, media on the other — split layout |
|
|
96
|
+
| `media-extending` | Media extends beyond container bounds |
|
|
97
|
+
| `fullscreen-media` | Full-viewport background media with text overlay |
|
|
98
|
+
| `stacked-media` | Media below the text content |
|
|
99
|
+
|
|
100
|
+
### Fields
|
|
101
|
+
|
|
102
|
+
| Field | Type | Description |
|
|
103
|
+
|-------|------|-------------|
|
|
104
|
+
| `variant` | enum | Layout variant (see above) |
|
|
105
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
106
|
+
| `heroMedia` | `StrapiMedia \| null` | Hero media — **images+videos** |
|
|
107
|
+
| `ctaText` | `string \| null` | Supplementary CTA text |
|
|
108
|
+
| `ctaLink` | `Link \| null` | Primary CTA link |
|
|
109
|
+
| `secondaryLink` | `Link \| null` | Secondary link |
|
|
110
|
+
| `headerMode` | `'none' \| 'transparent-black' \| 'transparent-white'` | Header overlay mode for navigation |
|
|
111
|
+
|
|
112
|
+
**Constraints:**
|
|
113
|
+
- `heroMedia` accepts images+videos. Check `mime` to decide between `<img>` and `<video>`.
|
|
114
|
+
- `headerMode` controls how the page header (navigation) renders over this hero. `none` = default header, `transparent-*` = transparent with light/dark text.
|
|
115
|
+
- DO NOT confuse `headerMode` here with `Page.headerMode` (`'overlay' | 'offset'`).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Features (`blocks.features`)
|
|
120
|
+
|
|
121
|
+
**`__component`: `'blocks.features'`**
|
|
122
|
+
**Used in:** Page.blocks, Global.siteWideBlocks
|
|
123
|
+
|
|
124
|
+
### Variants
|
|
125
|
+
|
|
126
|
+
| Value | Layout |
|
|
127
|
+
|-------|--------|
|
|
128
|
+
| `simple-grid` | Icon+text items in a uniform grid |
|
|
129
|
+
| `dual-column` | Two-column grid layout |
|
|
130
|
+
| `media-aside` | Items on one side, media image on the other |
|
|
131
|
+
| `media-aside-compact` | Compact version of media-aside |
|
|
132
|
+
|
|
133
|
+
### Fields
|
|
134
|
+
|
|
135
|
+
| Field | Type | Description |
|
|
136
|
+
|-------|------|-------------|
|
|
137
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
138
|
+
| `variant` | enum | Layout variant |
|
|
139
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
140
|
+
| `media` | `StrapiMedia \| null` | Supporting image — **images only** |
|
|
141
|
+
| `items` | `FeatureItem[]` | Feature items list |
|
|
142
|
+
|
|
143
|
+
**Constraints:**
|
|
144
|
+
- `media` is images-only and only relevant for `media-aside` / `media-aside-compact` variants.
|
|
145
|
+
- DO NOT render the `media` field for `simple-grid` or `dual-column` variants.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## CTA (`blocks.cta`)
|
|
150
|
+
|
|
151
|
+
**`__component`: `'blocks.cta'`**
|
|
152
|
+
**Used in:** Page.blocks
|
|
153
|
+
|
|
154
|
+
### Variants
|
|
155
|
+
|
|
156
|
+
| Value | Layout |
|
|
157
|
+
|-------|--------|
|
|
158
|
+
| `inline-spread` | Text and buttons spread horizontally |
|
|
159
|
+
| `inline-centered` | Centered text with buttons below |
|
|
160
|
+
| `inline-stacked` | Text and buttons stacked vertically |
|
|
161
|
+
| `card-stacked` | Card container with stacked content |
|
|
162
|
+
| `card-centered` | Card container with centered content |
|
|
163
|
+
| `card-horizontal` | Card container with horizontal layout |
|
|
164
|
+
|
|
165
|
+
### Fields
|
|
166
|
+
|
|
167
|
+
| Field | Type | Description |
|
|
168
|
+
|-------|------|-------------|
|
|
169
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
170
|
+
| `variant` | enum | Layout variant |
|
|
171
|
+
| `ctaMedia` | `StrapiMedia \| null` | CTA media — **images+videos** |
|
|
172
|
+
| `ctaText` | `string \| null` | Supplementary text |
|
|
173
|
+
| `ctaLink` | `Link \| null` | Primary CTA link |
|
|
174
|
+
| `secondaryLink` | `Link \| null` | Secondary link |
|
|
175
|
+
|
|
176
|
+
**Constraints:**
|
|
177
|
+
- `ctaMedia` accepts images+videos. Most relevant for `card-*` variants.
|
|
178
|
+
- `inline-*` variants are borderless/background-less; `card-*` variants have a card container.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Slider (`blocks.slider`)
|
|
183
|
+
|
|
184
|
+
**`__component`: `'blocks.slider'`**
|
|
185
|
+
**Used in:** Page.blocks, Global.siteWideBlocks
|
|
186
|
+
|
|
187
|
+
### Variants
|
|
188
|
+
|
|
189
|
+
| Value | Layout |
|
|
190
|
+
|-------|--------|
|
|
191
|
+
| `cover-flow` | Cover-flow style with center-focused active slide |
|
|
192
|
+
| `card-slider` | Card-based slider/carousel |
|
|
193
|
+
|
|
194
|
+
### Fields
|
|
195
|
+
|
|
196
|
+
| Field | Type | Description |
|
|
197
|
+
|-------|------|-------------|
|
|
198
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
199
|
+
| `variant` | enum | Slider style |
|
|
200
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
201
|
+
| `items` | `SliderItem[]` | Slider items (media required per item) |
|
|
202
|
+
|
|
203
|
+
**Constraints:**
|
|
204
|
+
- Each `SliderItem.media` is required (images+videos). Always render media for each slide.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## InquiryForm (`blocks.inquiry-form`)
|
|
209
|
+
|
|
210
|
+
**`__component`: `'blocks.inquiry-form'`**
|
|
211
|
+
**Used in:** Page.blocks, Global.siteWideBlocks
|
|
212
|
+
|
|
213
|
+
### Fields
|
|
214
|
+
|
|
215
|
+
| Field | Type | Description |
|
|
216
|
+
|-------|------|-------------|
|
|
217
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
218
|
+
| `successMessage` | `string \| null` | Message displayed after successful submission |
|
|
219
|
+
|
|
220
|
+
**Constraints:**
|
|
221
|
+
- The form UI is rendered by the frontend (not from CMS data). Submit to `postInquiry(client, data)`.
|
|
222
|
+
- Form fields: `name` (required), `email`, `phone`, `subject`, `message` — see `InquiryFormData` type.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## FAQ (`blocks.faq`)
|
|
227
|
+
|
|
228
|
+
**`__component`: `'blocks.faq'`**
|
|
229
|
+
**Used in:** Page.blocks
|
|
230
|
+
|
|
231
|
+
### Fields
|
|
232
|
+
|
|
233
|
+
| Field | Type | Description |
|
|
234
|
+
|-------|------|-------------|
|
|
235
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
236
|
+
| `items` | `FaqDocument[]` | FAQ entries (relation: oneToMany → `api::faq.faq`) |
|
|
237
|
+
|
|
238
|
+
**Constraints:**
|
|
239
|
+
- `items` are full FAQ documents (with `question`, `answer`, `category`, `sortOrder`).
|
|
240
|
+
- `answer` is `BlocksContent | null` — use a Blocks renderer, not markdown.
|
|
241
|
+
- No variant — use a single accordion/expandable pattern.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Highlights (`blocks.highlights`)
|
|
246
|
+
|
|
247
|
+
**`__component`: `'blocks.highlights'`**
|
|
248
|
+
**Used in:** Page.blocks
|
|
249
|
+
|
|
250
|
+
### Variants
|
|
251
|
+
|
|
252
|
+
| Value | Layout |
|
|
253
|
+
|-------|--------|
|
|
254
|
+
| `dual-column` | Two-column image+text grid |
|
|
255
|
+
| `triple-column` | Three-column grid |
|
|
256
|
+
| `alternating` | Zigzag layout — alternating image/text sides |
|
|
257
|
+
|
|
258
|
+
### Fields
|
|
259
|
+
|
|
260
|
+
| Field | Type | Description |
|
|
261
|
+
|-------|------|-------------|
|
|
262
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
263
|
+
| `variant` | enum | Layout variant |
|
|
264
|
+
| `media` | `StrapiMedia \| null` | Block-level supporting media — **images+videos** |
|
|
265
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
266
|
+
| `items` | `HighlightItem[]` | Highlight items (each has required media) |
|
|
267
|
+
|
|
268
|
+
**Constraints:**
|
|
269
|
+
- Block-level `media` is a supporting visual, not per-item media.
|
|
270
|
+
- Each `HighlightItem.media` is required (images+videos).
|
|
271
|
+
- For `alternating` variant, items alternate between text-left/media-right and media-left/text-right.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## BentoGrid (`blocks.bento-grid`)
|
|
276
|
+
|
|
277
|
+
**`__component`: `'blocks.bento-grid'`**
|
|
278
|
+
**Used in:** Page.blocks
|
|
279
|
+
|
|
280
|
+
### Fields
|
|
281
|
+
|
|
282
|
+
| Field | Type | Description |
|
|
283
|
+
|-------|------|-------------|
|
|
284
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
285
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
286
|
+
| `columns` | `number` | Grid columns (2–4, default 3) |
|
|
287
|
+
| `items` | `BentoItem[]` | Grid items with colSpan/rowSpan |
|
|
288
|
+
|
|
289
|
+
**Constraints:**
|
|
290
|
+
- No variant — layout is controlled by `columns` + each item's `colSpan`/`rowSpan`.
|
|
291
|
+
- Use CSS Grid: `grid-template-columns: repeat(columns, 1fr)`.
|
|
292
|
+
- `BentoItem.media` is images-only.
|
|
293
|
+
- Each item's `colSpan` (1–4) and `rowSpan` (1–3) define its grid area.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Testimonials (`blocks.testimonials`)
|
|
298
|
+
|
|
299
|
+
**`__component`: `'blocks.testimonials'`**
|
|
300
|
+
**Used in:** Page.blocks
|
|
301
|
+
|
|
302
|
+
### Variants
|
|
303
|
+
|
|
304
|
+
| Value | Layout |
|
|
305
|
+
|-------|--------|
|
|
306
|
+
| `centered` | Single/few quotes centered, large format |
|
|
307
|
+
| `side-by-side` | Quote + supporting media in split layout |
|
|
308
|
+
| `card-grid` | Multiple testimonials as cards in a grid |
|
|
309
|
+
| `subtle-grid` | Multiple testimonials in a minimal grid |
|
|
310
|
+
|
|
311
|
+
### Fields
|
|
312
|
+
|
|
313
|
+
| Field | Type | Description |
|
|
314
|
+
|-------|------|-------------|
|
|
315
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
316
|
+
| `variant` | enum | Layout variant |
|
|
317
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
318
|
+
| `media` | `StrapiMedia \| null` | Block-level supporting media — **images+videos** |
|
|
319
|
+
| `items` | `TestimonialItem[]` | Testimonial items |
|
|
320
|
+
|
|
321
|
+
**Constraints:**
|
|
322
|
+
- Block-level `media` is primarily used with `side-by-side` variant.
|
|
323
|
+
- `TestimonialItem.avatar` and `TestimonialItem.logo` are images-only.
|
|
324
|
+
- `TestimonialItem.rating` is 1–5 integer or null. Render as star rating when present.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## LogoCloud (`blocks.logo-cloud`)
|
|
329
|
+
|
|
330
|
+
**`__component`: `'blocks.logo-cloud'`**
|
|
331
|
+
**Used in:** Page.blocks
|
|
332
|
+
|
|
333
|
+
### Variants
|
|
334
|
+
|
|
335
|
+
| Value | Layout |
|
|
336
|
+
|-------|--------|
|
|
337
|
+
| `inline` | Logos in a single horizontal row (4–8 logos) |
|
|
338
|
+
| `grid` | Logos in a grid layout (8+ logos) |
|
|
339
|
+
| `split` | Text on left (section heading/subheading) + logo grid on right |
|
|
340
|
+
|
|
341
|
+
### Fields
|
|
342
|
+
|
|
343
|
+
| Field | Type | Description |
|
|
344
|
+
|-------|------|-------------|
|
|
345
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
346
|
+
| `variant` | enum | Layout variant |
|
|
347
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
348
|
+
| `items` | `LogoItem[]` | Logo items |
|
|
349
|
+
|
|
350
|
+
**Constraints:**
|
|
351
|
+
- No block-level media. `split` variant uses `section.heading`/`subheading` for text.
|
|
352
|
+
- `LogoItem.logo` is required (images only).
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Stats (`blocks.stats`)
|
|
357
|
+
|
|
358
|
+
**`__component`: `'blocks.stats'`**
|
|
359
|
+
**Used in:** Page.blocks
|
|
360
|
+
|
|
361
|
+
### Variants
|
|
362
|
+
|
|
363
|
+
| Value | Layout |
|
|
364
|
+
|-------|--------|
|
|
365
|
+
| `simple` | Horizontal row of stats (use section.theme for dark/light) |
|
|
366
|
+
| `card-grid` | Stats displayed as cards in a grid |
|
|
367
|
+
| `split` | Stats on one side + media on the other |
|
|
368
|
+
|
|
369
|
+
### Fields
|
|
370
|
+
|
|
371
|
+
| Field | Type | Description |
|
|
372
|
+
|-------|------|-------------|
|
|
373
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
374
|
+
| `variant` | enum | Layout variant |
|
|
375
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
376
|
+
| `media` | `StrapiMedia \| null` | Supporting media — **images+videos** |
|
|
377
|
+
| `items` | `StatItem[]` | Stat items |
|
|
378
|
+
|
|
379
|
+
**Constraints:**
|
|
380
|
+
- `media` is only relevant for `split` variant. DO NOT render it for `simple` or `card-grid`.
|
|
381
|
+
- `StatItem.value` is a string (e.g. `"10,000+"`, `"99.9%"`). DO NOT parse it as a number.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Pricing (`blocks.pricing`)
|
|
386
|
+
|
|
387
|
+
**`__component`: `'blocks.pricing'`**
|
|
388
|
+
**Used in:** Page.blocks
|
|
389
|
+
|
|
390
|
+
### Variants
|
|
391
|
+
|
|
392
|
+
| Value | Layout |
|
|
393
|
+
|-------|--------|
|
|
394
|
+
| `two-column` | Side-by-side comparison of 2 plans |
|
|
395
|
+
| `three-column` | Three plans (Good/Better/Best) |
|
|
396
|
+
| `single` | Single plan detail view |
|
|
397
|
+
| `with-comparison` | Plans + feature comparison table below |
|
|
398
|
+
|
|
399
|
+
### Fields
|
|
400
|
+
|
|
401
|
+
| Field | Type | Description |
|
|
402
|
+
|-------|------|-------------|
|
|
403
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
404
|
+
| `variant` | enum | Layout variant |
|
|
405
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
406
|
+
| `items` | `PricingTier[]` | Pricing tiers |
|
|
407
|
+
|
|
408
|
+
**Constraints:**
|
|
409
|
+
- `PricingTier.price` is a string (e.g. `"$29"`, `"Free"`, `"Contact us"`). DO NOT parse as number.
|
|
410
|
+
- `PricingTier.featureList` is richtext (Markdown). Render as a bullet list.
|
|
411
|
+
- `PricingTier.highlighted` marks the recommended plan — apply visual emphasis (ribbon, border, etc.).
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Content (`blocks.content`)
|
|
416
|
+
|
|
417
|
+
**`__component`: `'blocks.content'`**
|
|
418
|
+
**Used in:** Page.blocks
|
|
419
|
+
|
|
420
|
+
### Variants
|
|
421
|
+
|
|
422
|
+
| Value | Layout |
|
|
423
|
+
|-------|--------|
|
|
424
|
+
| `prose` | Reading-optimized width (max-w-prose). For long-form text. |
|
|
425
|
+
| `wide` | Wider container. For content with images/tables. |
|
|
426
|
+
| `full-width` | Full container width. For notices, terms. |
|
|
427
|
+
|
|
428
|
+
### Fields
|
|
429
|
+
|
|
430
|
+
| Field | Type | Description |
|
|
431
|
+
|-------|------|-------------|
|
|
432
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
433
|
+
| `variant` | enum | Content width variant |
|
|
434
|
+
| `body` | `BlocksContent` | Rich text body — **required** |
|
|
435
|
+
|
|
436
|
+
**Constraints:**
|
|
437
|
+
- `body` is `BlocksContent` (structured JSON), NOT HTML or Markdown. Use a Blocks renderer.
|
|
438
|
+
- No `name`, no `items`, no `media` — the Blocks editor supports inline images.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Team (`blocks.team`)
|
|
443
|
+
|
|
444
|
+
**`__component`: `'blocks.team'`**
|
|
445
|
+
**Used in:** Page.blocks
|
|
446
|
+
|
|
447
|
+
### Variants
|
|
448
|
+
|
|
449
|
+
| Value | Layout |
|
|
450
|
+
|-------|--------|
|
|
451
|
+
| `grid` | Standard grid — circular/square photos + name/role/bio. For 4–12 members. |
|
|
452
|
+
| `large-image` | Large photo emphasis. For leadership/key members. |
|
|
453
|
+
| `compact` | Small photos + minimal info. For large teams (12+). |
|
|
454
|
+
|
|
455
|
+
### Fields
|
|
456
|
+
|
|
457
|
+
| Field | Type | Description |
|
|
458
|
+
|-------|------|-------------|
|
|
459
|
+
| `name` | `string \| null` | Admin label — DO NOT display |
|
|
460
|
+
| `variant` | enum | Layout variant |
|
|
461
|
+
| `section` | `Section \| null` | Section wrapper |
|
|
462
|
+
| `items` | `TeamMember[]` | Team members |
|
|
463
|
+
|
|
464
|
+
**Constraints:**
|
|
465
|
+
- `TeamMember.photo` is images-only.
|
|
466
|
+
- `TeamMember.socialLinks` is `Link[]` — render as icon links.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Shared Sub-Components
|
|
471
|
+
|
|
472
|
+
### FeatureItem (`shared.feature-item`)
|
|
473
|
+
|
|
474
|
+
| Field | Type | Description |
|
|
475
|
+
|-------|------|-------------|
|
|
476
|
+
| `label` | `string \| null` | Item label |
|
|
477
|
+
| `description` | `string \| null` | Markdown richtext |
|
|
478
|
+
| `icon` | `string \| null` | Icon picker value |
|
|
479
|
+
| `infoText` | `string \| null` | Supplementary info text |
|
|
480
|
+
| `link` | `Link \| null` | Optional link |
|
|
481
|
+
|
|
482
|
+
### HighlightItem (`shared.highlight-item`)
|
|
483
|
+
|
|
484
|
+
| Field | Type | Description |
|
|
485
|
+
|-------|------|-------------|
|
|
486
|
+
| `label` | `string \| null` | Item label |
|
|
487
|
+
| `description` | `string \| null` | Markdown richtext |
|
|
488
|
+
| `media` | `StrapiMedia` | **Required** — images+videos |
|
|
489
|
+
| `link` | `Link \| null` | Optional link |
|
|
490
|
+
|
|
491
|
+
### SliderItem (`shared.slider-item`)
|
|
492
|
+
|
|
493
|
+
| Field | Type | Description |
|
|
494
|
+
|-------|------|-------------|
|
|
495
|
+
| `label` | `string \| null` | Item label |
|
|
496
|
+
| `description` | `string \| null` | Description text |
|
|
497
|
+
| `link` | `Link \| null` | Optional link |
|
|
498
|
+
| `media` | `StrapiMedia` | **Required** — images+videos |
|
|
499
|
+
|
|
500
|
+
### BentoItem (`shared.bento-item`)
|
|
501
|
+
|
|
502
|
+
| Field | Type | Description |
|
|
503
|
+
|-------|------|-------------|
|
|
504
|
+
| `title` | `string \| null` | Item title |
|
|
505
|
+
| `description` | `string \| null` | Description text |
|
|
506
|
+
| `media` | `StrapiMedia \| null` | Screenshot/illustration — images only |
|
|
507
|
+
| `icon` | `string \| null` | Decorative icon |
|
|
508
|
+
| `colSpan` | `number` | Column span (1–4, default 1) |
|
|
509
|
+
| `rowSpan` | `number` | Row span (1–3, default 1) |
|
|
510
|
+
| `link` | `Link \| null` | Optional link |
|
|
511
|
+
|
|
512
|
+
### TestimonialItem (`shared.testimonial-item`)
|
|
513
|
+
|
|
514
|
+
| Field | Type | Description |
|
|
515
|
+
|-------|------|-------------|
|
|
516
|
+
| `quote` | `string` | **Required** — testimonial text |
|
|
517
|
+
| `authorName` | `string \| null` | Author name |
|
|
518
|
+
| `authorRole` | `string \| null` | Author title/role |
|
|
519
|
+
| `avatar` | `StrapiMedia \| null` | Profile image — images only |
|
|
520
|
+
| `logo` | `StrapiMedia \| null` | Company logo — images only |
|
|
521
|
+
| `rating` | `number \| null` | Star rating 1–5 |
|
|
522
|
+
|
|
523
|
+
### LogoItem (`shared.logo-item`)
|
|
524
|
+
|
|
525
|
+
| Field | Type | Description |
|
|
526
|
+
|-------|------|-------------|
|
|
527
|
+
| `logo` | `StrapiMedia` | **Required** — images only |
|
|
528
|
+
| `companyName` | `string \| null` | Company name (for alt text/accessibility) |
|
|
529
|
+
| `link` | `Link \| null` | Optional link |
|
|
530
|
+
|
|
531
|
+
### StatItem (`shared.stat-item`)
|
|
532
|
+
|
|
533
|
+
| Field | Type | Description |
|
|
534
|
+
|-------|------|-------------|
|
|
535
|
+
| `value` | `string` | **Required** — formatted value string |
|
|
536
|
+
| `label` | `string \| null` | Short label |
|
|
537
|
+
| `description` | `string \| null` | Supplementary description |
|
|
538
|
+
|
|
539
|
+
### PricingTier (`shared.pricing-tier`)
|
|
540
|
+
|
|
541
|
+
| Field | Type | Description |
|
|
542
|
+
|-------|------|-------------|
|
|
543
|
+
| `planName` | `string` | **Required** — plan name |
|
|
544
|
+
| `price` | `string` | **Required** — formatted price string |
|
|
545
|
+
| `billingPeriod` | `string \| null` | e.g. "/mo", "/year" |
|
|
546
|
+
| `description` | `string \| null` | Plan description |
|
|
547
|
+
| `featureList` | `string \| null` | Markdown bullet list |
|
|
548
|
+
| `ctaLink` | `Link \| null` | CTA link |
|
|
549
|
+
| `highlighted` | `boolean` | Recommended plan flag |
|
|
550
|
+
|
|
551
|
+
### TeamMember (`shared.team-member`)
|
|
552
|
+
|
|
553
|
+
| Field | Type | Description |
|
|
554
|
+
|-------|------|-------------|
|
|
555
|
+
| `memberName` | `string` | **Required** — member name |
|
|
556
|
+
| `role` | `string \| null` | Title/role |
|
|
557
|
+
| `bio` | `string \| null` | Short bio |
|
|
558
|
+
| `photo` | `StrapiMedia \| null` | Profile photo — images only |
|
|
559
|
+
| `socialLinks` | `Link[]` | Social media links |
|
|
560
|
+
|
|
561
|
+
### Link (`shared.link`)
|
|
562
|
+
|
|
563
|
+
| Field | Type | Description |
|
|
564
|
+
|-------|------|-------------|
|
|
565
|
+
| `label` | `string` | **Required** — link label |
|
|
566
|
+
| `href` | `string` | **Required** — URL |
|
|
567
|
+
| `newWindow` | `boolean` | Open in new window (default false) |
|
|
568
|
+
| `icon` | `string \| null` | Icon picker value |
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## DZ Discrimination Pattern
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
import type { PageBlocks, SiteWideBlocks } from '@dani-builder/strapi-client/types';
|
|
576
|
+
|
|
577
|
+
function renderBlock(block: PageBlocks) {
|
|
578
|
+
switch (block.__component) {
|
|
579
|
+
case 'blocks.hero': return <HeroSection block={block} />;
|
|
580
|
+
case 'blocks.features': return <FeaturesSection block={block} />;
|
|
581
|
+
case 'blocks.cta': return <CtaSection block={block} />;
|
|
582
|
+
case 'blocks.slider': return <SliderSection block={block} />;
|
|
583
|
+
case 'blocks.inquiry-form': return <InquiryFormSection block={block} />;
|
|
584
|
+
case 'blocks.faq': return <FaqSection block={block} />;
|
|
585
|
+
case 'blocks.highlights': return <HighlightsSection block={block} />;
|
|
586
|
+
case 'blocks.bento-grid': return <BentoGridSection block={block} />;
|
|
587
|
+
case 'blocks.testimonials': return <TestimonialsSection block={block} />;
|
|
588
|
+
case 'blocks.logo-cloud': return <LogoCloudSection block={block} />;
|
|
589
|
+
case 'blocks.stats': return <StatsSection block={block} />;
|
|
590
|
+
case 'blocks.pricing': return <PricingSection block={block} />;
|
|
591
|
+
case 'blocks.content': return <ContentSection block={block} />;
|
|
592
|
+
case 'blocks.team': return <TeamSection block={block} />;
|
|
593
|
+
case 'elements.section': return <SectionDivider section={block} />;
|
|
594
|
+
default: return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**PageBlocks union** (15 members): `Section | HeroBlock | FeaturesBlock | CtaBlock | SliderBlock | InquiryFormBlock | FaqBlock | HighlightsBlock | BentoGridBlock | TestimonialsBlock | LogoCloudBlock | StatsBlock | PricingBlock | ContentBlock | TeamBlock`
|
|
600
|
+
|
|
601
|
+
**SiteWideBlocks union** (4 members): `SliderBlock | InquiryFormBlock | HeroBlock | FeaturesBlock`
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Content Types
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Content Type | Kind | draftAndPublish | i18n | Slug | Key Relations |
|
|
6
|
+
|-------------|------|----------------|------|------|---------------|
|
|
7
|
+
| Page | collection | yes | yes | `slug` | blocks (DZ), seo |
|
|
8
|
+
| Article | collection | yes | yes | `slug` | categories, tags, author, series, related_posts |
|
|
9
|
+
| FAQ | collection | yes | yes | — | category |
|
|
10
|
+
| Inquiry | collection | no | no | — | — |
|
|
11
|
+
| Category | collection | no | yes | `slug` | — |
|
|
12
|
+
| Tag | collection | no | yes | `slug` | — |
|
|
13
|
+
| Author | collection | no | no | `slug` | — |
|
|
14
|
+
| Series | collection | no | yes | `slug` | — |
|
|
15
|
+
| Navigation | single | yes | yes | — | globalNavigation, primaryLink, secondaryLink |
|
|
16
|
+
| Global | single | yes | yes | — | home (Page), footer, seo, siteWideBlocks (DZ) |
|
|
17
|
+
| SiteSettings | single | no | no | — | analyticsSettings, chatbot, siteVerifications |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Collection Types
|
|
22
|
+
|
|
23
|
+
### Page
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
interface Page {
|
|
27
|
+
title: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
seo: Seo | null;
|
|
30
|
+
blocks: PageBlocks[]; // DZ — 15 block types
|
|
31
|
+
headerMode: 'overlay' | 'offset';
|
|
32
|
+
}
|
|
33
|
+
type PageDocument = StrapiI18nDocument & Page;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Field | Type | Description |
|
|
37
|
+
|-------|------|-------------|
|
|
38
|
+
| `title` | `string` | Page title |
|
|
39
|
+
| `slug` | `string` | URL slug |
|
|
40
|
+
| `seo` | `Seo \| null` | SEO metadata |
|
|
41
|
+
| `blocks` | `PageBlocks[]` | Dynamic Zone — see [Block Reference](./block-reference.md) |
|
|
42
|
+
| `headerMode` | `'overlay' \| 'offset'` | How the navigation header relates to the first block |
|
|
43
|
+
|
|
44
|
+
`headerMode`:
|
|
45
|
+
- `overlay` — Header overlaps the first block (hero with transparent header)
|
|
46
|
+
- `offset` — Header takes its own space above the first block
|
|
47
|
+
|
|
48
|
+
### Article
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
interface Article {
|
|
52
|
+
title: string;
|
|
53
|
+
slug: string;
|
|
54
|
+
body: BlocksContent;
|
|
55
|
+
summary: string | null;
|
|
56
|
+
excerpt: string | null;
|
|
57
|
+
cover: StrapiMedia | null;
|
|
58
|
+
gallery: StrapiMedia[];
|
|
59
|
+
reading_time: number;
|
|
60
|
+
firstPublishedAt: string | null;
|
|
61
|
+
seo: Seo | null;
|
|
62
|
+
categories: CategoryDocument[];
|
|
63
|
+
tags: TagDocument[];
|
|
64
|
+
author: AuthorDocument | null;
|
|
65
|
+
series: SeriesDocument | null;
|
|
66
|
+
series_order: number;
|
|
67
|
+
related_posts: ArticleDocument[];
|
|
68
|
+
}
|
|
69
|
+
type ArticleDocument = StrapiI18nDocument & Article;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Field | Type | Description |
|
|
73
|
+
|-------|------|-------------|
|
|
74
|
+
| `title` | `string` | Article title |
|
|
75
|
+
| `slug` | `string` | URL slug |
|
|
76
|
+
| `body` | `BlocksContent` | Article body — structured JSON (NOT HTML) |
|
|
77
|
+
| `summary` | `string \| null` | Short summary for cards/lists |
|
|
78
|
+
| `excerpt` | `string \| null` | Longer excerpt for previews |
|
|
79
|
+
| `cover` | `StrapiMedia \| null` | Cover image — images only |
|
|
80
|
+
| `gallery` | `StrapiMedia[]` | Image gallery — images only |
|
|
81
|
+
| `reading_time` | `number` | Estimated reading time in minutes |
|
|
82
|
+
| `firstPublishedAt` | `string \| null` | Original publish date (ISO string) |
|
|
83
|
+
| `seo` | `Seo \| null` | SEO metadata |
|
|
84
|
+
| `categories` | `CategoryDocument[]` | Related categories |
|
|
85
|
+
| `tags` | `TagDocument[]` | Related tags |
|
|
86
|
+
| `author` | `AuthorDocument \| null` | Article author |
|
|
87
|
+
| `series` | `SeriesDocument \| null` | Article series |
|
|
88
|
+
| `series_order` | `number` | Order within series |
|
|
89
|
+
| `related_posts` | `ArticleDocument[]` | Related articles |
|
|
90
|
+
|
|
91
|
+
- `summary` vs `excerpt`: `summary` is a 1–2 sentence blurb for card UIs; `excerpt` is a longer preview paragraph.
|
|
92
|
+
- `body` is `BlocksContent` — use a Blocks renderer component.
|
|
93
|
+
|
|
94
|
+
### FAQ
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface Faq {
|
|
98
|
+
question: string | null;
|
|
99
|
+
answer: BlocksContent | null;
|
|
100
|
+
sortOrder: number | null;
|
|
101
|
+
category: CategoryDocument | null;
|
|
102
|
+
}
|
|
103
|
+
type FaqDocument = StrapiI18nDocument & Faq;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Field | Type | Description |
|
|
107
|
+
|-------|------|-------------|
|
|
108
|
+
| `question` | `string \| null` | FAQ question |
|
|
109
|
+
| `answer` | `BlocksContent \| null` | Answer — structured JSON (NOT HTML) |
|
|
110
|
+
| `sortOrder` | `number \| null` | Display order |
|
|
111
|
+
| `category` | `CategoryDocument \| null` | Related category |
|
|
112
|
+
|
|
113
|
+
### Inquiry
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
interface Inquiry {
|
|
117
|
+
name: string;
|
|
118
|
+
email: string | null;
|
|
119
|
+
phone: string | null;
|
|
120
|
+
subject: string | null;
|
|
121
|
+
message: string | null;
|
|
122
|
+
}
|
|
123
|
+
type InquiryDocument = StrapiDocument & Inquiry; // no i18n, no draftAndPublish
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Field | Type | Description |
|
|
127
|
+
|-------|------|-------------|
|
|
128
|
+
| `name` | `string` | Submitter name (required) |
|
|
129
|
+
| `email` | `string \| null` | Email address |
|
|
130
|
+
| `phone` | `string \| null` | Phone number |
|
|
131
|
+
| `subject` | `string \| null` | Inquiry subject |
|
|
132
|
+
| `message` | `string \| null` | Message body |
|
|
133
|
+
|
|
134
|
+
**POST body type:**
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
interface InquiryFormData {
|
|
138
|
+
name: string; // required
|
|
139
|
+
email?: string;
|
|
140
|
+
phone?: string;
|
|
141
|
+
subject?: string;
|
|
142
|
+
message?: string;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Inquiry is POST-only from the frontend. Records are created immediately (no publish step).
|
|
147
|
+
|
|
148
|
+
### Category
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
interface Category {
|
|
152
|
+
name: string;
|
|
153
|
+
slug: string;
|
|
154
|
+
description: string | null;
|
|
155
|
+
sortOrder: number | null;
|
|
156
|
+
}
|
|
157
|
+
type CategoryDocument = StrapiI18nDocument & Category;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Field | Type | Description |
|
|
161
|
+
|-------|------|-------------|
|
|
162
|
+
| `name` | `string` | Category name |
|
|
163
|
+
| `slug` | `string` | URL slug |
|
|
164
|
+
| `description` | `string \| null` | Category description |
|
|
165
|
+
| `sortOrder` | `number \| null` | Display order |
|
|
166
|
+
|
|
167
|
+
### Tag
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
interface Tag {
|
|
171
|
+
name: string;
|
|
172
|
+
slug: string;
|
|
173
|
+
color: string | null;
|
|
174
|
+
description: string | null;
|
|
175
|
+
}
|
|
176
|
+
type TagDocument = StrapiI18nDocument & Tag;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
| Field | Type | Description |
|
|
180
|
+
|-------|------|-------------|
|
|
181
|
+
| `name` | `string` | Tag name |
|
|
182
|
+
| `slug` | `string` | URL slug |
|
|
183
|
+
| `color` | `string \| null` | Hex color string (color picker) |
|
|
184
|
+
| `description` | `string \| null` | Tag description |
|
|
185
|
+
|
|
186
|
+
### Author
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
interface Author {
|
|
190
|
+
name: string;
|
|
191
|
+
slug: string;
|
|
192
|
+
role: string | null;
|
|
193
|
+
bio: string | null; // Markdown richtext
|
|
194
|
+
avatar: StrapiMedia | null;
|
|
195
|
+
email: string | null;
|
|
196
|
+
}
|
|
197
|
+
type AuthorDocument = StrapiDocument & Author; // no i18n
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
| Field | Type | Description |
|
|
201
|
+
|-------|------|-------------|
|
|
202
|
+
| `name` | `string` | Author name |
|
|
203
|
+
| `slug` | `string` | URL slug |
|
|
204
|
+
| `role` | `string \| null` | Title/role |
|
|
205
|
+
| `bio` | `string \| null` | Bio (Markdown richtext) |
|
|
206
|
+
| `avatar` | `StrapiMedia \| null` | Profile image — images only |
|
|
207
|
+
| `email` | `string \| null` | Contact email |
|
|
208
|
+
|
|
209
|
+
### Series
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
interface Series {
|
|
213
|
+
name: string;
|
|
214
|
+
slug: string;
|
|
215
|
+
description: string | null;
|
|
216
|
+
cover: StrapiMedia | null;
|
|
217
|
+
}
|
|
218
|
+
type SeriesDocument = StrapiI18nDocument & Series;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
| Field | Type | Description |
|
|
222
|
+
|-------|------|-------------|
|
|
223
|
+
| `name` | `string` | Series name |
|
|
224
|
+
| `slug` | `string` | URL slug |
|
|
225
|
+
| `description` | `string \| null` | Series description |
|
|
226
|
+
| `cover` | `StrapiMedia \| null` | Cover image — images only |
|
|
227
|
+
|
|
228
|
+
API endpoint: `/api/all-series` (pluralName `all-series` differs from singularName `series`).
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Single Types
|
|
233
|
+
|
|
234
|
+
### Navigation
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
interface Navigation {
|
|
238
|
+
mainLogo: StrapiMedia | null;
|
|
239
|
+
darkLogo: StrapiMedia | null;
|
|
240
|
+
globalNavigation: NavigationGroup[];
|
|
241
|
+
primaryLink: Link | null;
|
|
242
|
+
secondaryLink: Link | null;
|
|
243
|
+
align: 'left' | 'center' | 'right';
|
|
244
|
+
}
|
|
245
|
+
type NavigationDocument = StrapiI18nDocument & Navigation;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
| Field | Type | Description |
|
|
249
|
+
|-------|------|-------------|
|
|
250
|
+
| `mainLogo` | `StrapiMedia \| null` | Main logo — images only |
|
|
251
|
+
| `darkLogo` | `StrapiMedia \| null` | Dark mode logo — images only |
|
|
252
|
+
| `globalNavigation` | `NavigationGroup[]` | Nav groups with nested links |
|
|
253
|
+
| `primaryLink` | `Link \| null` | Primary action link (e.g. "Get Started") |
|
|
254
|
+
| `secondaryLink` | `Link \| null` | Secondary link (e.g. "Login") |
|
|
255
|
+
| `align` | `'left' \| 'center' \| 'right'` | Logo/nav alignment |
|
|
256
|
+
|
|
257
|
+
### Global
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
interface Global {
|
|
261
|
+
sitename: string;
|
|
262
|
+
seo: Seo | null;
|
|
263
|
+
home: PageDocument | null;
|
|
264
|
+
footer: Footer | null;
|
|
265
|
+
siteWideBlocks: SiteWideBlocks[];
|
|
266
|
+
darkModeEnabled: boolean;
|
|
267
|
+
defaultTheme: 'light' | 'dark' | null;
|
|
268
|
+
favicon: StrapiMedia | null;
|
|
269
|
+
}
|
|
270
|
+
type GlobalDocument = StrapiI18nDocument & Global;
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
| Field | Type | Description |
|
|
274
|
+
|-------|------|-------------|
|
|
275
|
+
| `sitename` | `string` | Site name |
|
|
276
|
+
| `seo` | `Seo \| null` | Default SEO (fallback for pages without SEO) |
|
|
277
|
+
| `home` | `PageDocument \| null` | Relation to homepage Page |
|
|
278
|
+
| `footer` | `Footer \| null` | Footer component |
|
|
279
|
+
| `siteWideBlocks` | `SiteWideBlocks[]` | DZ — blocks rendered on every page |
|
|
280
|
+
| `darkModeEnabled` | `boolean` | Whether dark mode is enabled |
|
|
281
|
+
| `defaultTheme` | `'light' \| 'dark' \| null` | Default color scheme |
|
|
282
|
+
| `favicon` | `StrapiMedia \| null` | Site favicon — images only |
|
|
283
|
+
|
|
284
|
+
`siteWideBlocks` accepts only 4 block types: `SliderBlock | InquiryFormBlock | HeroBlock | FeaturesBlock`.
|
|
285
|
+
|
|
286
|
+
### SiteSettings
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
interface SiteSettings {
|
|
290
|
+
analyticsSettings: AnalyticsSetting[];
|
|
291
|
+
chatbot: ChatbotSetting | null;
|
|
292
|
+
siteVerifications: SiteVerification[];
|
|
293
|
+
}
|
|
294
|
+
type SiteSettingsDocument = StrapiDocument & SiteSettings; // no i18n
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
| Field | Type | Description |
|
|
298
|
+
|-------|------|-------------|
|
|
299
|
+
| `analyticsSettings` | `AnalyticsSetting[]` | Analytics providers (repeatable) |
|
|
300
|
+
| `chatbot` | `ChatbotSetting \| null` | Chatbot configuration |
|
|
301
|
+
| `siteVerifications` | `SiteVerification[]` | Search engine verification meta tags |
|
|
302
|
+
|
|
303
|
+
API endpoint: `/api/site-setting` (singularName `site-setting`).
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Relation Map
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
Page ──blocks──▶ [DZ: 14 blocks + Section]
|
|
311
|
+
│ │
|
|
312
|
+
│ blocks.faq ──items──▶ FAQ ──category──▶ Category
|
|
313
|
+
│
|
|
314
|
+
└──seo──▶ Seo
|
|
315
|
+
|
|
316
|
+
Article ──┬──categories──▶ Category[]
|
|
317
|
+
├──tags──▶ Tag[]
|
|
318
|
+
├──author──▶ Author
|
|
319
|
+
├──series──▶ Series
|
|
320
|
+
├──related_posts──▶ Article[]
|
|
321
|
+
└──seo──▶ Seo
|
|
322
|
+
|
|
323
|
+
Global ──┬──home──▶ Page
|
|
324
|
+
├──footer──▶ Footer ──sitemap──▶ NavigationGroup[] ──links──▶ Link[]
|
|
325
|
+
├──siteWideBlocks──▶ [DZ: 4 block types]
|
|
326
|
+
└──seo──▶ Seo
|
|
327
|
+
|
|
328
|
+
Navigation ──globalNavigation──▶ NavigationGroup[] ──links──▶ Link[]
|
|
329
|
+
|
|
330
|
+
SiteSettings ──┬──analyticsSettings──▶ AnalyticsSetting[]
|
|
331
|
+
├──chatbot──▶ ChatbotSetting
|
|
332
|
+
└──siteVerifications──▶ SiteVerification[]
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## BlocksContent Structure
|
|
338
|
+
|
|
339
|
+
`BlocksContent` is the JSON output of Strapi's Blocks editor. It is an array of `BlockNode` objects.
|
|
340
|
+
|
|
341
|
+
**Used in:** `Article.body`, `FAQ.answer`, `ContentBlock.body`
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
type BlocksContent = BlockNode[];
|
|
345
|
+
|
|
346
|
+
type BlockNode =
|
|
347
|
+
| BlockParagraph // { type: 'paragraph', children: BlockInlineNode[] }
|
|
348
|
+
| BlockHeading // { type: 'heading', level: 1-6, children: BlockInlineNode[] }
|
|
349
|
+
| BlockList // { type: 'list', format: 'ordered'|'unordered', children: BlockListItem[] }
|
|
350
|
+
| BlockQuote // { type: 'quote', children: BlockInlineNode[] }
|
|
351
|
+
| BlockCode // { type: 'code', children: BlockTextChild[] }
|
|
352
|
+
| BlockImage; // { type: 'image', image: StrapiMedia, children: [{ type: 'text', text: '' }] }
|
|
353
|
+
|
|
354
|
+
type BlockInlineNode = BlockTextChild | BlockLinkChild;
|
|
355
|
+
|
|
356
|
+
// BlockTextChild: { type: 'text', text: string, bold?, italic?, underline?, strikethrough?, code? }
|
|
357
|
+
// BlockLinkChild: { type: 'link', url: string, children: BlockTextChild[] }
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
DO NOT treat BlocksContent as HTML or Markdown. Use a dedicated renderer that walks the node tree.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Document Base Fields
|
|
365
|
+
|
|
366
|
+
### StrapiDocument
|
|
367
|
+
|
|
368
|
+
All content type documents extend this:
|
|
369
|
+
|
|
370
|
+
| Field | Type | Description |
|
|
371
|
+
|-------|------|-------------|
|
|
372
|
+
| `id` | `number` | Internal database ID |
|
|
373
|
+
| `documentId` | `string` | Stable document ID (use for API queries) |
|
|
374
|
+
| `createdAt` | `string` | ISO date string |
|
|
375
|
+
| `updatedAt` | `string` | ISO date string |
|
|
376
|
+
| `publishedAt` | `string \| null` | Publish date (null if draft) |
|
|
377
|
+
|
|
378
|
+
### StrapiI18nDocument
|
|
379
|
+
|
|
380
|
+
Extends `StrapiDocument` with:
|
|
381
|
+
|
|
382
|
+
| Field | Type | Description |
|
|
383
|
+
|-------|------|-------------|
|
|
384
|
+
| `locale` | `string` | Locale code (e.g. `'ko'`, `'en'`) |
|
|
385
|
+
|
|
386
|
+
### StrapiComponent
|
|
387
|
+
|
|
388
|
+
Base for all component instances:
|
|
389
|
+
|
|
390
|
+
| Field | Type | Description |
|
|
391
|
+
|-------|------|-------------|
|
|
392
|
+
| `id` | `number` | Component instance ID |
|
|
393
|
+
| `__component` | `string` | Component UID (e.g. `'blocks.hero'`, `'shared.link'`) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dani-builder/strapi-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "TypeScript client for Daniworks Builder CMS (Strapi v5)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
|
-
"dist"
|
|
34
|
+
"dist",
|
|
35
|
+
"AGENTS.md",
|
|
36
|
+
"docs"
|
|
35
37
|
],
|
|
36
38
|
"publishConfig": {
|
|
37
39
|
"access": "public"
|
|
@@ -41,16 +43,16 @@
|
|
|
41
43
|
"url": "https://github.com/hojinzs/daniworks-builder-v2.git",
|
|
42
44
|
"directory": "packages/strapi-client"
|
|
43
45
|
},
|
|
44
|
-
"scripts": {
|
|
45
|
-
"build": "tsc"
|
|
46
|
-
},
|
|
47
46
|
"dependencies": {
|
|
48
47
|
"axios": "^1.7.0",
|
|
49
48
|
"qs": "^6.13.0"
|
|
50
49
|
},
|
|
51
50
|
"devDependencies": {
|
|
52
|
-
"@repo/typescript-config": "workspace:*",
|
|
53
51
|
"@types/qs": "^6.9.0",
|
|
54
|
-
"typescript": "5.9.2"
|
|
52
|
+
"typescript": "5.9.2",
|
|
53
|
+
"@repo/typescript-config": "0.0.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc"
|
|
55
57
|
}
|
|
56
|
-
}
|
|
58
|
+
}
|