@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.
@@ -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 |