Skip to content
Astro Integration

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.

terminal
# Install the integration
npx astro add @imjp/writenex-astro

# Start your dev server
astro dev

# Open the editor
http://localhost:4321/_writenex

That'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:

typescript
// 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

writenex.config.ts
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.

collection({
  name: "blog",
  path: "src/content/blog",
  schema: { /* fields */ }
})

singleton()

For single-item content like site settings, about page, or global configuration.

singleton({
  name: "settings",
  path: "src/content/settings.json",
  schema: { /* fields */ }
})

Both patterns are valid

Pattern A — raw object

defineConfig({
  collections: [{
    name: "blog",
    schema: {
      title: fields.text(...), // ✅
    },
  }],
})

Pattern B — collection() (recommended)

defineConfig({
  collections: [
    collection({
      name: "blog",
      schema: {
        title: fields.text(...), // ✅
      }
    }),
  ],
})

25+ Field Types

Text Fields

text

Single or multi-line text input

slug

URL-friendly slug with auto-generation

url

URL input with validation

Number Fields

number

Numeric input for decimals

integer

Whole number input

Selection Fields

select

Dropdown selection from options

multiselect

Multi-select with checkboxes

checkbox

Boolean toggle

Date & Time

date

Date picker

datetime

Date and time picker

File & Media

image

Image upload with preview

file

File upload for documents

Structured

object

Nested group of fields

array

List of items with same schema

blocks

List of items with different types

Reference

relationship

Reference to another collection

pathReference

Reference to a file path

Content

markdoc

Markdoc rich content

mdx

MDX content with components

child

Child document content

Advanced

conditional

Conditional field display

cloudImage

Cloud-hosted image (future)

ignored

Skip 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

OptionTypeApplies To
isRequiredbooleanAll fields
minnumbernumber, integer
maxnumbernumber, integer
minLengthnumbertext, url
maxLengthnumbertext, url
patternstringtext, slug
patternDescriptionstringtext, 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 SchemaFields 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

Default

Images 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.png

Reference: ![Alt](./my-post/hero.jpg)

Public

Images stored in the public directory. Best for shared images used across multiple content items.

public/
└── images/
    └── blog/
        └── my-post-hero.jpg

Reference: ![Alt](/images/blog/my-post-hero.jpg)

Keyboard Shortcuts

Familiar shortcuts for efficient editing.

New contentAlt + N
SaveCtrl/Cmd + S
Open previewCtrl/Cmd + P
Show shortcuts helpCtrl/Cmd + /
Refresh contentCtrl/Cmd + Shift + R
Close modalEscape

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.

Open Editor