Content files & fields

A content file is a TypeScript module whose exports are plain data, and JSON and markdown files work the same way (both are covered below). editsy infers the editing form from the values, so there is no schema to author.

Two approaches

defineContent({...}) for a single object (a page, the footer, settings) and defineCollection([...]) for repeated items (posts, projects, events):

import { defineContent, f } from "editsy";

export default defineContent({
  hero: {
    heading: "A tidy little website.",
    launched: "2026-07-02",
    body: f.markdown("Some **rich** text."),
  },
});

The wrappers are optional. A plain export default {...} works too (as const and satisfies included), which is how you adopt editsy on an existing site without touching its files. The wrappers add type-checking that keeps functions and JSX out of content.

Named exports are content too: export const events = [...] reads and writes exactly like a default export, and a file with several exports edits as one form with a section per export. Helper functions in the same file are simply left alone.

What the values become

  • short string → text input
  • long string or one with line breaks → textarea
  • "2026-10-09" → date picker
  • number or boolean → matching inputs
  • string[] → a tag list
  • array of objects → a collection with add, duplicate, reorder, delete
  • nested object → a grouped fieldset

Field key names become form labels, so heroImage shows as "Hero Image". Name keys like labels.

Give collections a template

A collection can declare what a new item looks like:

export default defineCollection([...posts], {
  template: {
    title: "A brand new post",
    date: "2026-01-01",
    summary: f.markdown("Say something worth reading."),
  },
});

That's exactly what the editor's add button creates: your defaults, your f.* annotations, ready to fill in. Without a template, adding copies the shape of the first existing item, and an empty collection can't grow from the editor at all, so the template is worth its six lines.

Field annotations

When inference can't guess, wrap the value: f.markdown() for rich text (WYSIWYG in the editor, markdown in the file, rendered safely through editsy's Markdown component), f.image() for an image picker over your public folder, f.url() for links, f.date(), f.textarea(), and f.select() for one value from a fixed set:

status: f.select("upcoming", ["upcoming", "sold out", "past"]),

That renders as a dropdown, and it's type-safe: TypeScript refuses a value that isn't one of the options. Every f.* helper returns its argument unchanged, so your site consumes plain data.

JSON files too

Plain .json files under your content globs are editable the same way: an object is a page, an array of objects is a collection. Handy for i18n dictionaries and config-style content. Field types are inferred (JSON has no annotations), and saves keep strict, valid JSON.

And markdown files

The shape most sites already keep posts in:

---
title: Hello world
date: 2026-07-03
tags: [intro, news]
---

The **body**, edited as rich text.

Frontmatter keys become form fields (strings, numbers, booleans, dates, lists of tags), and the body gets the full rich-text editor. Saves are surgical here too: change the title and only the title's bytes change, with quoting and list style preserved. Frontmatter that doesn't map to a form field (nested maps, block scalars) is flagged rather than guessed at, and left alone by saves.

f.html(), and why it's rarely the right choice

For sites that already store HTML fragments, f.html() edits an HTML string as rich text. Unlike f.markdown(), it is not sanitized: whatever gets saved is exactly what your site renders, so if you use dangerouslySetInnerHTML (or similar) with it, anyone who can save that field can inject markup for every visitor. Fine for a single trusted maintainer; not fine once you have multiple editors or a shared GitHub token. Prefer f.markdown() for new content, and sanitize the value yourself (e.g. with DOMPurify) if you do use f.html() with more than one editor.

The one rule

Values must be JSON-serializable literals: strings, numbers, booleans, arrays, plain objects. No functions, no JSX, no computed values, no spreads. That constraint is what makes reliable file round-tripping possible, and npx editsy check enforces it.