@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 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.1.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
+ }