@imjp/writenex-astro 1.5.0 → 1.6.1

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