Visual Editor for
Astro Content Collections
TypeScript-first Fields API with 25+ field types. Zero config, auto-discovery, WYSIWYG editing, and version history.
Quick Start
Get up and running in under a minute.
# Install the integration
npx astro add @imjp/writenex-astro
# Start your dev server
astro dev
# Open the editor
http://localhost:4321/_writenexThat's it! Writenex will auto-discover your content collections and you can start editing.
Everything You Need
A complete editing experience for your Astro content collections.
Fields API
TypeScript-first builder pattern with 25+ field types. Get full autocomplete and type safety for your content schema.
Zero Config
Auto-detects your content collections from the src/content folder. Just install and start editing with no setup required.
Smart Schema Detection
Identifies your frontmatter fields from existing content. Creates ready-to-use forms for titles, dates, tags, and more.
Image Upload
Drag and drop images with colocated or public storage options. Files are placed next to your content or in the public folder.
Version History
Creates automatic shadow copies on save. Restore any earlier version with one click and keep your work protected.
Validation
Built-in validation rules for required fields, min/max values, string lengths, and regex patterns.
Fields API
TypeScript-first builder pattern for defining content schema fields. Full autocomplete, type safety, and 25+ field types.
Imports
Import the Fields API from the config module:
// Import from the config module
import { defineConfig, collection, singleton, fields } from "@imjp/writenex-astro/config";
// Or use the @imjp namespace
import { defineConfig, collection, singleton, fields } from "@imjp/writenex-astro/config";Basic Example
import { defineConfig, collection, fields } from "@imjp/writenex-astro/config";
export default defineConfig({
collections: [
collection({
name: "blog",
path: "src/content/blog",
filePattern: "{slug}.md",
previewUrl: "/blog/{slug}",
schema: {
title: fields.text({ label: "Title", validation: { isRequired: true } }),
slug: fields.slug({ name: { label: "Slug" } }),
description: fields.text({ label: "Description", multiline: true }),
publishedAt: fields.date({ label: "Published Date" }),
heroImage: fields.image({ label: "Hero Image" }),
tags: fields.multiselect({
label: "Tags",
options: ["javascript", "typescript", "react", "astro"]
}),
draft: fields.checkbox({ label: "Draft", defaultValue: true }),
body: fields.mdx({ label: "Content", validation: { isRequired: true } }),
},
}),
],
});collection() vs singleton()
Both patterns below work — defineConfig auto-resolves fields.*() objects in either case. Using collection() is recommended for better TypeScript inference.
collection()
For multi-item content like blog posts, documentation pages, or products.
name: "blog",
path: "src/content/blog",
schema: { /* fields */ }
})
singleton()
For single-item content like site settings, about page, or global configuration.
name: "settings",
path: "src/content/settings.json",
schema: { /* fields */ }
})
Both patterns are valid
Pattern A — raw object
collections: [{
name: "blog",
schema: {
title: fields.text(...), // ✅
},
}],
})
Pattern B — collection() (recommended)
collections: [
collection({
name: "blog",
schema: {
title: fields.text(...), // ✅
}
}),
],
})
25+ Field Types
Text Fields
textSingle or multi-line text input
slugURL-friendly slug with auto-generation
urlURL input with validation
Number Fields
numberNumeric input for decimals
integerWhole number input
Selection Fields
selectDropdown selection from options
multiselectMulti-select with checkboxes
checkboxBoolean toggle
Date & Time
dateDate picker
datetimeDate and time picker
File & Media
imageImage upload with preview
fileFile upload for documents
Structured
objectNested group of fields
arrayList of items with same schema
blocksList of items with different types
Reference
relationshipReference to another collection
pathReferenceReference to a file path
Content
markdocMarkdoc rich content
mdxMDX content with components
childChild document content
Advanced
conditionalConditional field display
cloudImageCloud-hosted image (future)
ignoredSkip field from forms
Text Fields
fields.text()
Single or multi-line text input with validation support.
fields.text({ label: "Title" })
fields.text({ label: "Description", multiline: true })
fields.text({
label: "Bio",
multiline: true,
placeholder: "Tell us about yourself...",
validation: {
isRequired: true,
minLength: 10,
maxLength: 500
}
})fields.slug()
URL-friendly slug with auto-generation from title.
fields.slug({ label: "URL Slug" })
fields.slug({
name: { label: "Name Slug", placeholder: "my-page" },
pathname: { label: "URL Path", placeholder: "/pages/" }
})fields.url()
URL input with validation.
fields.url({ label: "Website" })
fields.url({
label: "GitHub Profile",
placeholder: "https://github.com/username",
validation: { isRequired: true }
})Selection Fields
fields.select()
Dropdown selection from predefined options.
fields.select({
label: "Status",
options: ["draft", "published", "archived"],
defaultValue: "draft"
})fields.multiselect()
Multi-select with checkboxes or multi-select UI.
fields.multiselect({
label: "Tags",
options: ["javascript", "typescript", "react", "node"],
defaultValue: ["javascript"]
})fields.checkbox()
Boolean toggle switch.
fields.checkbox({ label: "Published" })
fields.checkbox({
label: "Featured",
defaultValue: false
})Structured Fields
fields.object()
Nested group of fields for complex data structures.
fields.object({
label: "Author",
fields: {
name: fields.text({ label: "Name" }),
email: fields.url({ label: "Email" }),
bio: fields.text({ label: "Bio", multiline: true }),
}
})fields.array()
List of items with the same schema.
fields.array({
label: "Tags",
itemField: fields.text({ label: "Tag" }),
itemLabel: "Tag"
})
fields.array({
label: "Links",
itemField: fields.object({
fields: {
title: fields.text({ label: "Title" }),
url: fields.url({ label: "URL" }),
}
}),
itemLabel: "Link"
})fields.blocks()
List of items with different block types (paragraphs, quotes, images, etc.).
fields.blocks({
label: "Content Blocks",
blockTypes: {
paragraph: {
label: "Paragraph",
fields: {
text: fields.text({ label: "Text", multiline: true })
}
},
quote: {
label: "Quote",
fields: {
text: fields.text({ label: "Quote" }),
attribution: fields.text({ label: "Attribution" })
}
},
image: {
label: "Image",
fields: {
src: fields.image({ label: "Image" }),
caption: fields.text({ label: "Caption" })
}
}
},
itemLabel: "Block"
})Validation
All fields support validation options for data integrity.
fields.text({
label: "Title",
validation: {
isRequired: true,
minLength: 3,
maxLength: 100,
pattern: "^[A-Za-z]",
patternDescription: "Must start with a letter"
}
})
fields.number({
label: "Price",
validation: {
isRequired: true,
min: 0,
max: 10000
}
})
fields.integer({
label: "Year",
validation: { min: 1900, max: 2100 }
})Validation Options
| Option | Type | Applies To |
|---|---|---|
| isRequired | boolean | All fields |
| min | number | number, integer |
| max | number | number, integer |
| minLength | number | text, url |
| maxLength | number | text, url |
| pattern | string | text, slug |
| patternDescription | string | text, slug |
Migration from Plain Schema
Upgrade your existing plain schema config to the Fields API.
Before (Plain Schema)
export default defineConfig({
collections: [
{
name: "blog",
path: "src/content/blog",
schema: {
title: { type: "string", required: true },
description: { type: "string" },
pubDate: { type: "date", required: true },
draft: { type: "boolean", default: false },
tags: { type: "array", items: "string" },
heroImage: { type: "image" },
},
},
],
});After (Fields API)
import { defineConfig, collection, fields } from "@imjp/writenex-astro/config";
export default defineConfig({
collections: [
collection({
name: "blog",
path: "src/content/blog",
schema: {
title: fields.text({ label: "Title", validation: { isRequired: true } }),
description: fields.text({ label: "Description" }),
pubDate: fields.date({ label: "Published Date", validation: { isRequired: true } }),
draft: fields.checkbox({ label: "Draft", defaultValue: false }),
tags: fields.array({ label: "Tags", itemField: fields.text({ label: "Tag" }) }),
heroImage: fields.image({ label: "Hero Image" }),
},
}),
],
});Type Mapping
| Plain Schema | Fields API |
|---|---|
| type: "string" | fields.text() |
| type: "number" | fields.number() |
| type: "boolean" | fields.checkbox() |
| type: "date" | fields.date() |
| type: "array" | fields.array({ itemField: ... }) |
| type: "object" | fields.object({ fields: ... }) |
| type: "image" | fields.image() |
Image Strategies
Choose how images are stored in your project.
Colocated
DefaultImages stored alongside content files in a folder with the same name. Best for content-specific images.
src/content/blog/
├── my-post.md
└── my-post/
├── hero.jpg
└── diagram.pngReference: 
Public
Images stored in the public directory. Best for shared images used across multiple content items.
public/
└── images/
└── blog/
└── my-post-hero.jpgReference: 
Keyboard Shortcuts
Familiar shortcuts for efficient editing.
Production Safe
@imjp/writenex-astro is disabled by default in production builds. The editor only runs during development to prevent accidental exposure of filesystem write access.
If you need to enable it for staging environments, use the allowProduction: true option with proper authentication.
Ready to Get Started?
Add @imjp/writenex-astro to your project and start editing visually.
Looking for a Standalone Editor?
Try Writenex Editor - a free WYSIWYG markdown editor that works in your browser. No sign-up required.