Inline-edit CMS engine

01 — Try it

There is no demo mode. This is the app.

Every keystroke you make on this page runs the same code path a production app runs: engine, transport, adapter, your database.

  1. Turn on edit mode. Every field on this page becomes editable in place, headings and body copy included.E
  2. Click anything and type. No admin panel, no modal, no second URL to remember.
  3. Save every dirty field in one round trip. Volatile adapters skip this and save on blur.S
  4. Open the database panel below. The row you just changed is sitting in Postgres.
  5. Reload. It is still there, because PGlite persists to IndexedDB, in your tab, with no server.

02 — Where it went

Your edit is a row. Here it is.

Content is rows, not commits. That is the difference between this and a git-backed CMS: it works for apps, not only static sites.

database
your database6 rows
hero.headline
hero.tagline
hero.intro
shared.message
cards[0].title
cards[1].title
adapter callslive

    03 — Beyond text

    Insert a row. Drag it. Delete it. Drop in an image.

    The same engine that edits a field also creates, reorders and deletes records. Every op updates the snapshot immediately and rolls back if the adapter rejects the write. Turn on edit mode to get the controls.

    04 — Bindings

    Edit in React. Watch Vue and Svelte.

    These are three real islands on one page, each a different framework, all subscribed to the same engine. Type into any of them and the other two re-render before the save even lands.

    const engine = createCmsEngine({ transport: adapterTransport(adapter) })framework-free, zero framework imports
    import { ContentEditSpan, usePageContext } from "better-content/react";
    
    export default function NoteCard() {
      const { hasUnsavedChanges } = usePageContext();
    
      return (
        <article className="island" data-framework="react">
          <header className="island__bar">
            <span className="island__dot"></span>
            <span className="island__name">React</span>
            <code className="island__hook">&lt;ContentEditSpan /&gt;</code>
            <span className="island__state" data-dirty={hasUnsavedChanges || undefined}>
              {hasUnsavedChanges ? "unsaved" : "synced"}
            </span>
          </header>
          <div className="island__body">
            <p className="island__path">page / shared.message</p>
            <ContentEditSpan
              as="p"
              className="island__text"
              collection="page"
              itemId="shared"
              fieldKey="message"
            />
          </div>
        </article>
      );
    }

    05 — Adapters

    Seven methods stand between us and your database.

    There is no hosted service to sign up for and no proprietary backend to migrate off. Implement these seven and better-content talks to whatever you already run.

    interface DataAdapter

    fetchCollection(collection, q?)read
    fetchById(collection, id)read
    create(collection, data)write
    createWithId(collection, id, data)write
    update(collection, id, patch)write
    upsert(collection, id, patch)write
    delete(collection, id)write

    An afternoon of work for a database you already know.

    import {
      adapterTransport,
      createCmsEngine,
      type ClientStorageAdapter,
      type CmsEngine,
    } from "better-content/core";
    import { loadItemMap } from "better-content/server";
    import { loadAdapter } from "./db";
    import { statusStore } from "./status";
    
    // Uploaded images become data URLs so the whole demo stays inside your tab.
    // In a real app this would be a storage adapter like cloudinary.
    const dataUrlStorage: ClientStorageAdapter = {
      upload: (file) =>
        new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.onload = () => resolve({ url: reader.result as string });
          reader.onerror = () => reject(new Error("Could not read file"));
          reader.readAsDataURL(file);
        }),
    };
    
    // ONE engine for the whole page. Every island below, whatever its
    // framework, binds to this exact object. The engine itself never
    // imports React, Vue, or Svelte.
    export const ready: Promise<CmsEngine> = (async () => {
      const adapter = await loadAdapter();
    
      const initialItems = await loadItemMap(adapter, {
        page: {},
        cards: { query: { orderBy: [{ field: "order", direction: "asc" }] } },
      });
    
      return createCmsEngine({
        transport: adapterTransport(adapter),
        storage: dataUrlStorage,
        notify: { success: statusStore.set, error: statusStore.set },
        initialItems,
      });
    })();

    Take it home.

    Scaffold a Next.js, Nuxt, SvelteKit or Astro app wired to Postgres or Firestore. MIT licensed, pre-1.0, no account required.

    Read the docs