@imjp/writenex-astro 1.4.0 → 1.6.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/README.md +894 -60
- package/dist/{chunk-HNS5YKP3.js → chunk-E4PQLKAH.js} +69 -28
- package/dist/chunk-E4PQLKAH.js.map +1 -0
- package/dist/client/index.d.ts +19 -0
- package/dist/config/index.js +1 -1
- package/dist/fields/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/fields/README.md +985 -0
- package/src/fields/resolve.ts +86 -32
- package/src/types/config.ts +1 -0
- package/dist/chunk-HNS5YKP3.js.map +0 -1
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
# Fields API Documentation
|
|
2
|
+
|
|
3
|
+
A TypeScript-first builder pattern for defining content schema fields in Astro projects.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Fields API provides a fluent, type-safe way to define the schema for your content collections. Instead of plain JSON objects, you use builder functions like `fields.text()`, `fields.select()`, etc.
|
|
8
|
+
|
|
9
|
+
### Why Use the Fields API?
|
|
10
|
+
|
|
11
|
+
- **Type Safety** - Full TypeScript inference with autocomplete
|
|
12
|
+
- **IDE Support** - Documented config options with hover tooltips
|
|
13
|
+
- **Validation** - Built-in validation rules for each field type
|
|
14
|
+
- **Composable** - Nest fields within objects, arrays, and blocks
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// writenex.config.ts
|
|
20
|
+
import { defineConfig, collection, fields } from "@imjp/writenex-astro/config";
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
collections: [
|
|
24
|
+
collection({
|
|
25
|
+
name: "blog",
|
|
26
|
+
path: "src/content/blog",
|
|
27
|
+
schema: {
|
|
28
|
+
title: fields.text({ label: "Title", validation: { isRequired: true } }),
|
|
29
|
+
slug: fields.slug({ name: { label: "Slug" } }),
|
|
30
|
+
publishedAt: fields.date({ label: "Published Date" }),
|
|
31
|
+
draft: fields.checkbox({ label: "Draft", defaultValue: true }),
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
The Fields API is included with `@imjp/writenex-astro`. Import it from:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { fields, collection, singleton, defineConfig } from "@imjp/writenex-astro/config";
|
|
44
|
+
// or
|
|
45
|
+
import { fields, collection, singleton, defineConfig } from "@writenex/astro/config";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Field Types
|
|
49
|
+
|
|
50
|
+
### Text Fields
|
|
51
|
+
|
|
52
|
+
#### `fields.text()`
|
|
53
|
+
|
|
54
|
+
Single or multi-line text input.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
fields.text({ label: "Title" })
|
|
58
|
+
fields.text({ label: "Description", multiline: true })
|
|
59
|
+
fields.text({
|
|
60
|
+
label: "Bio",
|
|
61
|
+
multiline: true,
|
|
62
|
+
placeholder: "Tell us about yourself...",
|
|
63
|
+
validation: {
|
|
64
|
+
isRequired: true,
|
|
65
|
+
minLength: 10,
|
|
66
|
+
maxLength: 500
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Config:**
|
|
72
|
+
| Option | Type | Description |
|
|
73
|
+
|--------|------|-------------|
|
|
74
|
+
| `label` | `string` | Display label |
|
|
75
|
+
| `description` | `string` | Help text |
|
|
76
|
+
| `multiline` | `boolean` | Multi-line textarea (default: false) |
|
|
77
|
+
| `placeholder` | `string` | Placeholder text |
|
|
78
|
+
| `defaultValue` | `string` | Default value |
|
|
79
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
80
|
+
| `validation.minLength` | `number` | Minimum character count |
|
|
81
|
+
| `validation.maxLength` | `number` | Maximum character count |
|
|
82
|
+
| `validation.pattern` | `string` | Regex pattern |
|
|
83
|
+
| `validation.patternDescription` | `string` | Pattern error message |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
#### `fields.slug()`
|
|
88
|
+
|
|
89
|
+
URL-friendly slug field with auto-generation support.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
fields.slug({ label: "URL Slug" })
|
|
93
|
+
fields.slug({
|
|
94
|
+
name: { label: "Name Slug", placeholder: "my-page" },
|
|
95
|
+
pathname: { label: "URL Path", placeholder: "/pages/" }
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Config:**
|
|
100
|
+
| Option | Type | Description |
|
|
101
|
+
|--------|------|-------------|
|
|
102
|
+
| `label` | `string` | Display label |
|
|
103
|
+
| `name.label` | `string` | Name field label |
|
|
104
|
+
| `name.placeholder` | `string` | Name placeholder |
|
|
105
|
+
| `pathname.label` | `string` | Path field label |
|
|
106
|
+
| `pathname.placeholder` | `string` | Path placeholder |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
#### `fields.url()`
|
|
111
|
+
|
|
112
|
+
URL input with validation.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
fields.url({ label: "Website" })
|
|
116
|
+
fields.url({
|
|
117
|
+
label: "GitHub Profile",
|
|
118
|
+
placeholder: "https://github.com/username",
|
|
119
|
+
validation: { isRequired: true }
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Config:**
|
|
124
|
+
| Option | Type | Description |
|
|
125
|
+
|--------|------|-------------|
|
|
126
|
+
| `label` | `string` | Display label |
|
|
127
|
+
| `placeholder` | `string` | Placeholder URL |
|
|
128
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### Number Fields
|
|
133
|
+
|
|
134
|
+
#### `fields.number()`
|
|
135
|
+
|
|
136
|
+
Numeric input for decimals.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
fields.number({ label: "Price" })
|
|
140
|
+
fields.number({
|
|
141
|
+
label: "Rating",
|
|
142
|
+
placeholder: 4.5,
|
|
143
|
+
validation: { min: 0, max: 5 }
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Config:**
|
|
148
|
+
| Option | Type | Description |
|
|
149
|
+
|--------|------|-------------|
|
|
150
|
+
| `label` | `string` | Display label |
|
|
151
|
+
| `placeholder` | `number` | Placeholder value |
|
|
152
|
+
| `defaultValue` | `number` | Default value |
|
|
153
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
154
|
+
| `validation.min` | `number` | Minimum value |
|
|
155
|
+
| `validation.max` | `number` | Maximum value |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
#### `fields.integer()`
|
|
160
|
+
|
|
161
|
+
Whole number input.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
fields.integer({ label: "Quantity" })
|
|
165
|
+
fields.integer({
|
|
166
|
+
label: "Year",
|
|
167
|
+
validation: { min: 1900, max: 2100 }
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Config:**
|
|
172
|
+
| Option | Type | Description |
|
|
173
|
+
|--------|------|-------------|
|
|
174
|
+
| `label` | `string` | Display label |
|
|
175
|
+
| `placeholder` | `number` | Placeholder value |
|
|
176
|
+
| `defaultValue` | `number` | Default value |
|
|
177
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
178
|
+
| `validation.min` | `number` | Minimum value |
|
|
179
|
+
| `validation.max` | `number` | Maximum value |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Selection Fields
|
|
184
|
+
|
|
185
|
+
#### `fields.select()`
|
|
186
|
+
|
|
187
|
+
Dropdown selection from options.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
fields.select({
|
|
191
|
+
label: "Status",
|
|
192
|
+
options: ["draft", "published", "archived"],
|
|
193
|
+
defaultValue: "draft"
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
fields.select({
|
|
197
|
+
label: "Category",
|
|
198
|
+
options: ["technology", "lifestyle", "travel"],
|
|
199
|
+
validation: { isRequired: true }
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Config:**
|
|
204
|
+
| Option | Type | Description |
|
|
205
|
+
|--------|------|-------------|
|
|
206
|
+
| `label` | `string` | Display label |
|
|
207
|
+
| `options` | `string[]` | Selectable options (required) |
|
|
208
|
+
| `defaultValue` | `string` | Default option |
|
|
209
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
#### `fields.multiselect()`
|
|
214
|
+
|
|
215
|
+
Multi-select with checkboxes or multi-select UI.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
fields.multiselect({
|
|
219
|
+
label: "Tags",
|
|
220
|
+
options: ["javascript", "typescript", "react", "node"],
|
|
221
|
+
defaultValue: ["javascript"]
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
fields.multiselect({
|
|
225
|
+
label: "Topics",
|
|
226
|
+
options: ["frontend", "backend", "devops", "mobile"],
|
|
227
|
+
validation: { isRequired: true }
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Config:**
|
|
232
|
+
| Option | Type | Description |
|
|
233
|
+
|--------|------|-------------|
|
|
234
|
+
| `label` | `string` | Display label |
|
|
235
|
+
| `options` | `string[]` | Selectable options (required) |
|
|
236
|
+
| `defaultValue` | `string[]` | Default selections |
|
|
237
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
#### `fields.checkbox()`
|
|
242
|
+
|
|
243
|
+
Boolean toggle.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
fields.checkbox({ label: "Published" })
|
|
247
|
+
fields.checkbox({
|
|
248
|
+
label: "Featured",
|
|
249
|
+
defaultValue: false
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Config:**
|
|
254
|
+
| Option | Type | Description |
|
|
255
|
+
|--------|------|-------------|
|
|
256
|
+
| `label` | `string` | Display label |
|
|
257
|
+
| `defaultValue` | `boolean` | Default state (default: false) |
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### Date & Time Fields
|
|
262
|
+
|
|
263
|
+
#### `fields.date()`
|
|
264
|
+
|
|
265
|
+
Date picker.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
fields.date({ label: "Published Date" })
|
|
269
|
+
fields.date({
|
|
270
|
+
label: "Event Date",
|
|
271
|
+
defaultValue: "2024-01-15"
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Config:**
|
|
276
|
+
| Option | Type | Description |
|
|
277
|
+
|--------|------|-------------|
|
|
278
|
+
| `label` | `string` | Display label |
|
|
279
|
+
| `defaultValue` | `string` | Default date (YYYY-MM-DD) |
|
|
280
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
#### `fields.datetime()`
|
|
285
|
+
|
|
286
|
+
Date and time picker.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
fields.datetime({ label: "Publish At" })
|
|
290
|
+
fields.datetime({
|
|
291
|
+
label: "Event Date & Time",
|
|
292
|
+
defaultValue: "2024-01-15T09:00"
|
|
293
|
+
})
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Config:**
|
|
297
|
+
| Option | Type | Description |
|
|
298
|
+
|--------|------|-------------|
|
|
299
|
+
| `label` | `string` | Display label |
|
|
300
|
+
| `defaultValue` | `string` | Default datetime (ISO format) |
|
|
301
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
### File & Media Fields
|
|
306
|
+
|
|
307
|
+
#### `fields.image()`
|
|
308
|
+
|
|
309
|
+
Image upload with preview.
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
fields.image({ label: "Hero Image" })
|
|
313
|
+
fields.image({
|
|
314
|
+
label: "Thumbnail",
|
|
315
|
+
directory: "public/images/blog",
|
|
316
|
+
publicPath: "/images/blog"
|
|
317
|
+
})
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Config:**
|
|
321
|
+
| Option | Type | Description |
|
|
322
|
+
|--------|------|-------------|
|
|
323
|
+
| `label` | `string` | Display label |
|
|
324
|
+
| `directory` | `string` | Storage directory |
|
|
325
|
+
| `publicPath` | `string` | Public URL path |
|
|
326
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
#### `fields.file()`
|
|
331
|
+
|
|
332
|
+
File upload for documents.
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
fields.file({ label: "Attachment" })
|
|
336
|
+
fields.file({
|
|
337
|
+
label: "PDF Document",
|
|
338
|
+
directory: "public/files",
|
|
339
|
+
publicPath: "/files"
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Config:**
|
|
344
|
+
| Option | Type | Description |
|
|
345
|
+
|--------|------|-------------|
|
|
346
|
+
| `label` | `string` | Display label |
|
|
347
|
+
| `directory` | `string` | Storage directory |
|
|
348
|
+
| `publicPath` | `string` | Public URL path |
|
|
349
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
### Structured Fields
|
|
354
|
+
|
|
355
|
+
#### `fields.object()`
|
|
356
|
+
|
|
357
|
+
Nested group of fields.
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
fields.object({
|
|
361
|
+
label: "Author",
|
|
362
|
+
fields: {
|
|
363
|
+
name: fields.text({ label: "Name" }),
|
|
364
|
+
email: fields.url({ label: "Email" }),
|
|
365
|
+
bio: fields.text({ label: "Bio", multiline: true }),
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Config:**
|
|
371
|
+
| Option | Type | Description |
|
|
372
|
+
|--------|------|-------------|
|
|
373
|
+
| `label` | `string` | Display label |
|
|
374
|
+
| `fields` | `Record<string, FieldDefinition>` | Nested fields (required) |
|
|
375
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
#### `fields.array()`
|
|
380
|
+
|
|
381
|
+
List of items with the same schema.
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
fields.array({
|
|
385
|
+
label: "Tags",
|
|
386
|
+
itemField: fields.text({ label: "Tag" }),
|
|
387
|
+
itemLabel: "Tag"
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
fields.array({
|
|
391
|
+
label: "Links",
|
|
392
|
+
itemField: fields.object({
|
|
393
|
+
fields: {
|
|
394
|
+
title: fields.text({ label: "Title" }),
|
|
395
|
+
url: fields.url({ label: "URL" }),
|
|
396
|
+
}
|
|
397
|
+
}),
|
|
398
|
+
itemLabel: "Link"
|
|
399
|
+
})
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Config:**
|
|
403
|
+
| Option | Type | Description |
|
|
404
|
+
|--------|------|-------------|
|
|
405
|
+
| `label` | `string` | Display label |
|
|
406
|
+
| `itemField` | `FieldDefinition` | Schema for each item (required) |
|
|
407
|
+
| `itemLabel` | `string` | Label for items in editor |
|
|
408
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
#### `fields.blocks()`
|
|
413
|
+
|
|
414
|
+
List of items with different block types.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
fields.blocks({
|
|
418
|
+
label: "Content Blocks",
|
|
419
|
+
blockTypes: {
|
|
420
|
+
paragraph: {
|
|
421
|
+
label: "Paragraph",
|
|
422
|
+
fields: {
|
|
423
|
+
text: fields.text({ label: "Text", multiline: true })
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
quote: {
|
|
427
|
+
label: "Quote",
|
|
428
|
+
fields: {
|
|
429
|
+
text: fields.text({ label: "Quote" }),
|
|
430
|
+
attribution: fields.text({ label: "Attribution" })
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
image: {
|
|
434
|
+
label: "Image",
|
|
435
|
+
fields: {
|
|
436
|
+
src: fields.image({ label: "Image" }),
|
|
437
|
+
caption: fields.text({ label: "Caption" })
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
itemLabel: "Block"
|
|
442
|
+
})
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Config:**
|
|
446
|
+
| Option | Type | Description |
|
|
447
|
+
|--------|------|-------------|
|
|
448
|
+
| `label` | `string` | Display label |
|
|
449
|
+
| `blockTypes` | `Record<string, BlockDefinition>` | Block type definitions (required) |
|
|
450
|
+
| `itemLabel` | `string` | Label for blocks in editor |
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
### Reference Fields
|
|
455
|
+
|
|
456
|
+
#### `fields.relationship()`
|
|
457
|
+
|
|
458
|
+
Reference to another collection item.
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
fields.relationship({
|
|
462
|
+
label: "Author",
|
|
463
|
+
collection: "authors"
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
fields.relationship({
|
|
467
|
+
label: "Related Posts",
|
|
468
|
+
collection: "blog"
|
|
469
|
+
})
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Config:**
|
|
473
|
+
| Option | Type | Description |
|
|
474
|
+
|--------|------|-------------|
|
|
475
|
+
| `label` | `string` | Display label |
|
|
476
|
+
| `collection` | `string` | Target collection name (required) |
|
|
477
|
+
| `validation.isRequired` | `boolean` | Field is required |
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
#### `fields.pathReference()`
|
|
482
|
+
|
|
483
|
+
Reference to a file path.
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
fields.pathReference({ label: "Template" })
|
|
487
|
+
fields.pathReference({
|
|
488
|
+
label: "Layout",
|
|
489
|
+
contentTypes: [".astro", ".mdx"]
|
|
490
|
+
})
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**Config:**
|
|
494
|
+
| Option | Type | Description |
|
|
495
|
+
|--------|------|-------------|
|
|
496
|
+
| `label` | `string` | Display label |
|
|
497
|
+
| `contentTypes` | `string[]` | Allowed file extensions |
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
### Content Fields
|
|
502
|
+
|
|
503
|
+
#### `fields.markdoc()`
|
|
504
|
+
|
|
505
|
+
Markdoc rich content.
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
fields.markdoc({ label: "Content" })
|
|
509
|
+
fields.markdoc({
|
|
510
|
+
label: "Article Body",
|
|
511
|
+
validation: { isRequired: true }
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
#### `fields.mdx()`
|
|
518
|
+
|
|
519
|
+
MDX content with component support.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
fields.mdx({ label: "Content" })
|
|
523
|
+
fields.mdx({
|
|
524
|
+
label: "Documentation",
|
|
525
|
+
validation: { isRequired: true }
|
|
526
|
+
})
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
### Conditional Fields
|
|
532
|
+
|
|
533
|
+
#### `fields.conditional()`
|
|
534
|
+
|
|
535
|
+
Show a field based on another field's value.
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
fields.conditional({
|
|
539
|
+
label: "CTA Button",
|
|
540
|
+
matchField: "hasCTA",
|
|
541
|
+
matchValue: true,
|
|
542
|
+
showField: fields.object({
|
|
543
|
+
fields: {
|
|
544
|
+
text: fields.text({ label: "Button Text" }),
|
|
545
|
+
url: fields.url({ label: "Link URL" }),
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
// With checkbox condition
|
|
551
|
+
fields.conditional({
|
|
552
|
+
label: "External Link",
|
|
553
|
+
matchField: "linkType",
|
|
554
|
+
matchValue: "external",
|
|
555
|
+
showField: fields.url({ label: "URL" })
|
|
556
|
+
})
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Config:**
|
|
560
|
+
| Option | Type | Description |
|
|
561
|
+
|--------|------|-------------|
|
|
562
|
+
| `label` | `string` | Display label |
|
|
563
|
+
| `matchField` | `string` | Field name to check (required) |
|
|
564
|
+
| `matchValue` | `unknown` | Value to match (required) |
|
|
565
|
+
| `showField` | `FieldDefinition` | Field to show when matched (required) |
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
### Child & Nested Fields
|
|
570
|
+
|
|
571
|
+
#### `fields.child()`
|
|
572
|
+
|
|
573
|
+
Child document content.
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
fields.child({ label: "Page Content" })
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
### Cloud & Placeholder Fields
|
|
582
|
+
|
|
583
|
+
#### `fields.cloudImage()`
|
|
584
|
+
|
|
585
|
+
Cloud-hosted image (future support).
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
fields.cloudImage({ label: "Profile Picture" })
|
|
589
|
+
fields.cloudImage({
|
|
590
|
+
label: "Avatar",
|
|
591
|
+
provider: "cloudinary"
|
|
592
|
+
})
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
#### `fields.empty()`
|
|
598
|
+
|
|
599
|
+
Placeholder field (renders nothing).
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
fields.empty({ label: "Reserved" })
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
#### `fields.emptyContent()`
|
|
608
|
+
|
|
609
|
+
Placeholder for empty content area.
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
fields.emptyContent()
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
#### `fields.emptyDocument()`
|
|
618
|
+
|
|
619
|
+
Placeholder for empty document section.
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
fields.emptyDocument()
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
#### `fields.ignored()`
|
|
628
|
+
|
|
629
|
+
Field is skipped in forms (useful for computed fields).
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
fields.ignored({ label: "Internal ID" })
|
|
633
|
+
fields.ignored()
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## Validation
|
|
639
|
+
|
|
640
|
+
All fields support validation options:
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
fields.text({
|
|
644
|
+
label: "Title",
|
|
645
|
+
validation: {
|
|
646
|
+
isRequired: true,
|
|
647
|
+
minLength: 3,
|
|
648
|
+
maxLength: 100,
|
|
649
|
+
pattern: "^[A-Za-z]",
|
|
650
|
+
patternDescription: "Must start with a letter"
|
|
651
|
+
}
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
fields.number({
|
|
655
|
+
label: "Price",
|
|
656
|
+
validation: {
|
|
657
|
+
isRequired: true,
|
|
658
|
+
min: 0,
|
|
659
|
+
max: 10000
|
|
660
|
+
}
|
|
661
|
+
})
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
| Validation Option | Type | Applies To |
|
|
665
|
+
|-------------------|------|------------|
|
|
666
|
+
| `isRequired` | `boolean` | All fields |
|
|
667
|
+
| `min` | `number` | `number`, `integer` |
|
|
668
|
+
| `max` | `number` | `number`, `integer` |
|
|
669
|
+
| `minLength` | `number` | `text`, `url` |
|
|
670
|
+
| `maxLength` | `number` | `text`, `url` |
|
|
671
|
+
| `pattern` | `string` | `text`, `slug` |
|
|
672
|
+
| `patternDescription` | `string` | `text`, `slug` |
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## Collections vs Singletons
|
|
677
|
+
|
|
678
|
+
### `collection()`
|
|
679
|
+
|
|
680
|
+
For multi-item content (blog posts, docs, products):
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
collection({
|
|
684
|
+
name: "blog",
|
|
685
|
+
path: "src/content/blog",
|
|
686
|
+
schema: {
|
|
687
|
+
title: fields.text({ label: "Title", validation: { isRequired: true } }),
|
|
688
|
+
slug: fields.slug({ name: { label: "Slug" } }),
|
|
689
|
+
body: fields.mdx({ label: "Content" }),
|
|
690
|
+
}
|
|
691
|
+
})
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### `singleton()`
|
|
695
|
+
|
|
696
|
+
For single-item content (site settings, about page):
|
|
697
|
+
|
|
698
|
+
```typescript
|
|
699
|
+
singleton({
|
|
700
|
+
name: "settings",
|
|
701
|
+
path: "src/content/settings.json",
|
|
702
|
+
schema: {
|
|
703
|
+
siteName: fields.text({ label: "Site Name" }),
|
|
704
|
+
tagline: fields.text({ label: "Tagline" }),
|
|
705
|
+
logo: fields.image({ label: "Logo" }),
|
|
706
|
+
}
|
|
707
|
+
})
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## Real-World Examples
|
|
713
|
+
|
|
714
|
+
### Blog Post Schema
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
collection({
|
|
718
|
+
name: "blog",
|
|
719
|
+
path: "src/content/blog",
|
|
720
|
+
filePattern: "{slug}.md",
|
|
721
|
+
previewUrl: "/blog/{slug}",
|
|
722
|
+
schema: {
|
|
723
|
+
title: fields.text({
|
|
724
|
+
label: "Title",
|
|
725
|
+
validation: { isRequired: true, maxLength: 100 }
|
|
726
|
+
}),
|
|
727
|
+
slug: fields.slug({
|
|
728
|
+
name: { label: "Slug" },
|
|
729
|
+
pathname: { label: "Path", placeholder: "/blog/" }
|
|
730
|
+
}),
|
|
731
|
+
description: fields.text({
|
|
732
|
+
label: "Description",
|
|
733
|
+
multiline: true,
|
|
734
|
+
validation: { maxLength: 300 }
|
|
735
|
+
}),
|
|
736
|
+
publishedAt: fields.date({ label: "Published Date" }),
|
|
737
|
+
updatedAt: fields.datetime({ label: "Last Updated" }),
|
|
738
|
+
author: fields.relationship({ label: "Author", collection: "authors" }),
|
|
739
|
+
heroImage: fields.image({ label: "Hero Image" }),
|
|
740
|
+
tags: fields.multiselect({
|
|
741
|
+
label: "Tags",
|
|
742
|
+
options: ["javascript", "typescript", "react", "astro", "node"]
|
|
743
|
+
}),
|
|
744
|
+
draft: fields.checkbox({ label: "Draft", defaultValue: true }),
|
|
745
|
+
body: fields.mdx({ label: "Content", validation: { isRequired: true } }),
|
|
746
|
+
}
|
|
747
|
+
})
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Documentation Schema
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
collection({
|
|
754
|
+
name: "docs",
|
|
755
|
+
path: "src/content/docs",
|
|
756
|
+
filePattern: "{slug}/index.md",
|
|
757
|
+
previewUrl: "/docs/{slug}",
|
|
758
|
+
schema: {
|
|
759
|
+
title: fields.text({ label: "Title", validation: { isRequired: true } }),
|
|
760
|
+
description: fields.text({ label: "Description" }),
|
|
761
|
+
order: fields.integer({ label: "Sort Order" }),
|
|
762
|
+
category: fields.select({
|
|
763
|
+
label: "Category",
|
|
764
|
+
options: ["getting-started", "guides", "api-reference", "tutorials"]
|
|
765
|
+
}),
|
|
766
|
+
children: fields.child({ label: "Child Pages" }),
|
|
767
|
+
body: fields.markdoc({ label: "Content" }),
|
|
768
|
+
}
|
|
769
|
+
})
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Product Catalog Schema
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
collection({
|
|
776
|
+
name: "products",
|
|
777
|
+
path: "src/content/products",
|
|
778
|
+
filePattern: "{slug}.md",
|
|
779
|
+
previewUrl: "/products/{slug}",
|
|
780
|
+
schema: {
|
|
781
|
+
name: fields.text({ label: "Product Name", validation: { isRequired: true } }),
|
|
782
|
+
slug: fields.slug({ name: { label: "URL Slug" } }),
|
|
783
|
+
price: fields.number({ label: "Price" }),
|
|
784
|
+
compareAtPrice: fields.number({ label: "Compare At Price" }),
|
|
785
|
+
sku: fields.text({ label: "SKU" }),
|
|
786
|
+
description: fields.text({ label: "Description", multiline: true }),
|
|
787
|
+
images: fields.array({
|
|
788
|
+
label: "Product Images",
|
|
789
|
+
itemField: fields.image({ label: "Image" }),
|
|
790
|
+
itemLabel: "Image"
|
|
791
|
+
}),
|
|
792
|
+
category: fields.relationship({ label: "Category", collection: "categories" }),
|
|
793
|
+
tags: fields.multiselect({
|
|
794
|
+
label: "Tags",
|
|
795
|
+
options: ["new", "sale", "featured", "bestseller"]
|
|
796
|
+
}),
|
|
797
|
+
inStock: fields.checkbox({ label: "In Stock", defaultValue: true }),
|
|
798
|
+
featured: fields.checkbox({ label: "Featured Product" }),
|
|
799
|
+
specs: fields.object({
|
|
800
|
+
label: "Specifications",
|
|
801
|
+
fields: {
|
|
802
|
+
weight: fields.text({ label: "Weight" }),
|
|
803
|
+
dimensions: fields.text({ label: "Dimensions" }),
|
|
804
|
+
material: fields.text({ label: "Material" }),
|
|
805
|
+
}
|
|
806
|
+
}),
|
|
807
|
+
}
|
|
808
|
+
})
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Author Profile Schema
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
collection({
|
|
815
|
+
name: "authors",
|
|
816
|
+
path: "src/content/authors",
|
|
817
|
+
filePattern: "{slug}.md",
|
|
818
|
+
previewUrl: "/authors/{slug}",
|
|
819
|
+
schema: {
|
|
820
|
+
name: fields.text({ label: "Name", validation: { isRequired: true } }),
|
|
821
|
+
slug: fields.slug({ name: { label: "Slug" } }),
|
|
822
|
+
avatar: fields.image({ label: "Avatar" }),
|
|
823
|
+
role: fields.select({
|
|
824
|
+
label: "Role",
|
|
825
|
+
options: ["author", "editor", "contributor", "admin"],
|
|
826
|
+
defaultValue: "author"
|
|
827
|
+
}),
|
|
828
|
+
bio: fields.text({ label: "Bio", multiline: true }),
|
|
829
|
+
social: fields.object({
|
|
830
|
+
label: "Social Links",
|
|
831
|
+
fields: {
|
|
832
|
+
twitter: fields.url({ label: "Twitter" }),
|
|
833
|
+
github: fields.url({ label: "GitHub" }),
|
|
834
|
+
linkedin: fields.url({ label: "LinkedIn" }),
|
|
835
|
+
website: fields.url({ label: "Website" }),
|
|
836
|
+
}
|
|
837
|
+
}),
|
|
838
|
+
email: fields.url({ label: "Email" }),
|
|
839
|
+
featured: fields.checkbox({ label: "Featured Author", defaultValue: false }),
|
|
840
|
+
}
|
|
841
|
+
})
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## Migration from Plain Schema
|
|
847
|
+
|
|
848
|
+
If you have an existing plain schema config, here's how to migrate:
|
|
849
|
+
|
|
850
|
+
### Before (Plain Schema)
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
export default defineConfig({
|
|
854
|
+
collections: [
|
|
855
|
+
{
|
|
856
|
+
name: "blog",
|
|
857
|
+
path: "src/content/blog",
|
|
858
|
+
schema: {
|
|
859
|
+
title: { type: "string", required: true },
|
|
860
|
+
description: { type: "string" },
|
|
861
|
+
pubDate: { type: "date", required: true },
|
|
862
|
+
draft: { type: "boolean", default: false },
|
|
863
|
+
tags: { type: "array", items: "string" },
|
|
864
|
+
heroImage: { type: "image" },
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
});
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### After (Fields API)
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
import { defineConfig, collection, fields } from "@imjp/writenex-astro/config";
|
|
875
|
+
|
|
876
|
+
export default defineConfig({
|
|
877
|
+
collections: [
|
|
878
|
+
collection({
|
|
879
|
+
name: "blog",
|
|
880
|
+
path: "src/content/blog",
|
|
881
|
+
schema: {
|
|
882
|
+
title: fields.text({ label: "Title", validation: { isRequired: true } }),
|
|
883
|
+
description: fields.text({ label: "Description" }),
|
|
884
|
+
pubDate: fields.date({ label: "Published Date", validation: { isRequired: true } }),
|
|
885
|
+
draft: fields.checkbox({ label: "Draft", defaultValue: false }),
|
|
886
|
+
tags: fields.array({ label: "Tags", itemField: fields.text({ label: "Tag" }) }),
|
|
887
|
+
heroImage: fields.image({ label: "Hero Image" }),
|
|
888
|
+
},
|
|
889
|
+
}),
|
|
890
|
+
],
|
|
891
|
+
});
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Type Mapping
|
|
895
|
+
|
|
896
|
+
| Plain Schema | Fields API |
|
|
897
|
+
|--------------|------------|
|
|
898
|
+
| `type: "string"` | `fields.text()` |
|
|
899
|
+
| `type: "number"` | `fields.number()` |
|
|
900
|
+
| `type: "boolean"` | `fields.checkbox()` |
|
|
901
|
+
| `type: "date"` | `fields.date()` |
|
|
902
|
+
| `type: "array"` | `fields.array({ itemField: ... })` |
|
|
903
|
+
| `type: "object"` | `fields.object({ fields: ... })` |
|
|
904
|
+
| `type: "image"` | `fields.image()` |
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
## Troubleshooting
|
|
909
|
+
|
|
910
|
+
### Config file not loading
|
|
911
|
+
|
|
912
|
+
1. Ensure `writenex.config.ts` is in your project root
|
|
913
|
+
2. Check the file has proper exports: `export default defineConfig({ ... })`
|
|
914
|
+
3. Restart the dev server after making changes
|
|
915
|
+
|
|
916
|
+
### Field types not rendering correctly
|
|
917
|
+
|
|
918
|
+
1. Verify the field type is spelled correctly (e.g., `fields.text`, not `fields.string`)
|
|
919
|
+
2. Check that required config properties are provided (e.g., `options` for `select`)
|
|
920
|
+
3. For `object` and `array`, ensure `fields` or `itemField` is properly nested
|
|
921
|
+
|
|
922
|
+
### Validation not working
|
|
923
|
+
|
|
924
|
+
1. Ensure `validation` object is inside the field config, not outside
|
|
925
|
+
2. Check that validation rules match the field type (e.g., `min`/`max` for numbers)
|
|
926
|
+
3. Remember `isRequired` only validates on form submission
|
|
927
|
+
|
|
928
|
+
### Collection not found for relationship
|
|
929
|
+
|
|
930
|
+
1. Verify the `collection` name matches exactly (case-sensitive)
|
|
931
|
+
2. Ensure the referenced collection is also defined in your config
|
|
932
|
+
3. Check that the referenced collection has at least one item
|
|
933
|
+
|
|
934
|
+
### Auto-generated slug not working
|
|
935
|
+
|
|
936
|
+
1. Ensure the `slug` field exists and is properly configured
|
|
937
|
+
2. Check if there's a `title` field - slug generation often depends on it
|
|
938
|
+
3. Verify the slug field config has proper name/pathname labels
|
|
939
|
+
|
|
940
|
+
---
|
|
941
|
+
|
|
942
|
+
## API Reference
|
|
943
|
+
|
|
944
|
+
### Exports
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
// From @imjp/writenex-astro/config or @writenex/astro/config
|
|
948
|
+
import {
|
|
949
|
+
fields, // Field builder object
|
|
950
|
+
collection, // Multi-item content helper
|
|
951
|
+
singleton, // Single-item content helper
|
|
952
|
+
defineConfig, // Config definition function
|
|
953
|
+
} from "@imjp/writenex-astro/config";
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### Field Builder Methods
|
|
957
|
+
|
|
958
|
+
| Method | Description |
|
|
959
|
+
|--------|-------------|
|
|
960
|
+
| `fields.text()` | Single/multi-line text |
|
|
961
|
+
| `fields.slug()` | URL-friendly slug |
|
|
962
|
+
| `fields.url()` | URL input |
|
|
963
|
+
| `fields.number()` | Decimal number |
|
|
964
|
+
| `fields.integer()` | Whole number |
|
|
965
|
+
| `fields.select()` | Dropdown selection |
|
|
966
|
+
| `fields.multiselect()` | Multi-select |
|
|
967
|
+
| `fields.checkbox()` | Boolean toggle |
|
|
968
|
+
| `fields.date()` | Date picker |
|
|
969
|
+
| `fields.datetime()` | Date & time picker |
|
|
970
|
+
| `fields.image()` | Image upload |
|
|
971
|
+
| `fields.file()` | File upload |
|
|
972
|
+
| `fields.object()` | Nested fields group |
|
|
973
|
+
| `fields.array()` | List of items |
|
|
974
|
+
| `fields.blocks()` | Multiple block types |
|
|
975
|
+
| `fields.relationship()` | Reference to other collection |
|
|
976
|
+
| `fields.pathReference()` | File path reference |
|
|
977
|
+
| `fields.markdoc()` | Markdoc content |
|
|
978
|
+
| `fields.mdx()` | MDX content |
|
|
979
|
+
| `fields.conditional()` | Conditional field display |
|
|
980
|
+
| `fields.child()` | Child document |
|
|
981
|
+
| `fields.cloudImage()` | Cloud image (future) |
|
|
982
|
+
| `fields.empty()` | Placeholder field |
|
|
983
|
+
| `fields.emptyContent()` | Empty content placeholder |
|
|
984
|
+
| `fields.emptyDocument()` | Empty document placeholder |
|
|
985
|
+
| `fields.ignored()` | Skip from forms |
|