Documentation

Mantle adds a visual editing layer to your existing application. Install an SDK, connect your project, and start editing content directly in your browser.

Getting Started

Mantle provides SDK packages for different frameworks. The core package @mantle/sdk is framework-agnostic. For React and Next.js projects, use @mantle/react.

Installation

Terminal
npm install @mantle/react

Or if you're using the core SDK directly:

Terminal
npm install @mantle/sdk

Core Concepts

Mantle works by mapping editable regions in your UI to content identifiers. These mappings are defined through either React components (with @mantle/react) or HTML data attributes (with @mantle/sdk).

In edit mode, Mantle overlays editing controls on your running application. In production, the SDK adds zero overhead — components render as plain HTML elements.

React Integration

The @mantle/react package provides React components and hooks for editable content.

MantleProvider

Wrap your app (or any subtree) with MantleProvider to enable visual editing.

layout.tsx
import { MantleProvider } from '@mantle/react'

export default function Layout({ children }) {
  return (
    <MantleProvider>
      {children}
    </MantleProvider>
  )
}

MantleText

Makes text content editable. Renders as a span by default. Supports rich text editing when connected to the editor.

page.tsx
import { MantleText } from '@mantle/react'

function Hero() {
  return (
    <h1>
      <MantleText id="hero-title">
        Welcome to our site
      </MantleText>
    </h1>
  )
}

MantleImage

Makes images swappable through the visual editor.

page.tsx
import { MantleImage } from '@mantle/react'

function Hero() {
  return (
    <MantleImage
      id="hero-image"
      src="/hero.jpg"
      alt="Hero banner"
      width={1200}
      height={600}
    />
  )
}

MantleSection

Wraps a content section to enable reordering and section-level operations in the editor.

page.tsx
import { MantleSection } from '@mantle/react'

function FeaturesSection() {
  return (
    <MantleSection id="features" label="Features">
      <div className="grid grid-cols-3 gap-8">
        {/* Feature cards */}
      </div>
    </MantleSection>
  )
}

useMantle Hook

For more control, the useMantle hook gives you direct access to editable values.

component.tsx
import { useMantle } from '@mantle/react'

function PricingCard() {
  const price = useMantle('pricing-amount', '$29/mo')
  const label = useMantle('pricing-label', 'Pro Plan')

  return (
    <div>
      <h3>{label}</h3>
      <p>{price}</p>
    </div>
  )
}

Content Binding

If you're using the core SDK or prefer HTML attributes over React components, you can bind content with data attributes.

AttributePurpose
data-acms-idUnique identifier for the editable element
data-acms-typeContent type: text, rich-text, image, link
data-acms-labelHuman-readable label shown in the editor sidebar
data-acms-elementMaps root element to its element definition
index.html
<h1
  data-acms-id="hero-title"
  data-acms-type="text"
  data-acms-label="Hero Title"
>
  Welcome to our site
</h1>

<img
  data-acms-id="hero-image"
  data-acms-type="image"
  data-acms-label="Hero Image"
  src="/hero.jpg"
  alt="Hero"
/>

Connected Mode

Connected mode lets Mantle communicate with your running dev server in real time. When connected, edits in the Mantle editor are reflected immediately in your app.

Setup

  1. Start your dev server as usual (e.g. npm run dev)
  2. In your Mantle project settings, set the Dev Server URL to your local address (e.g. http://localhost:3000)
  3. Enable Connected Mode in project settings
  4. Open the editor — Mantle loads your dev server in the editing iframe and communicates changes via postMessage

Project Configuration

Connected projects have these additional fields:

{
  "framework": "nextjs",       // or "what", "vite", "other"
  "devServerUrl": "http://localhost:3000",
  "sourceDirectory": "./src",
  "isConnected": true
}

What Framework Integration

The @mantle/what package provides deep integration with the What Framework, including a Vite plugin and a signal-based adapter.

Installation

Terminal
npm install @mantle/what

Vite Plugin

The Vite plugin auto-injects the Mantle SDK in development mode and hooks into What Framework's signal registry for live editing.

vite.config.ts
import { defineConfig } from 'vite'
import { mantlePlugin } from '@mantle/what/vite'

export default defineConfig({
  plugins: [
    mantlePlugin()
  ]
})

Component Authoring

What Framework components use signal() for reactive state. To make content editable with Mantle, signal variable names must match their corresponding data-acms-id values.

Component Structure

A Mantle-compatible What component follows this pattern: declare signals for editable content, then reference them in JSX with the matching data-acms-* attributes.

HeroSection.tsx
import { signal } from 'what-core'

export function HeroSection() {
  const headline = signal('Build Something Amazing')
  const subtitle = signal('Ship faster with visual editing')

  return (
    <section data-acms-element="hero-section" class="py-20 px-4">
      <h1
        data-acms-id="headline"
        data-acms-type="text"
        data-acms-label="Headline"
      >
        {headline()}
      </h1>
      <p
        data-acms-id="subtitle"
        data-acms-type="text"
        data-acms-label="Subtitle"
      >
        {subtitle()}
      </p>
    </section>
  )
}

Required Attributes

AttributeRequired OnPurpose
data-acms-elementRoot elementMaps component to its element definition (use the component slug)
data-acms-idEach editable nodeMust match the signal variable name exactly
data-acms-typeEach editable nodeContent type: text, rich-text, image
data-acms-labelEach editable nodeHuman-readable label for the editor sidebar

Signal Naming Rules

The signal variable name is how Mantle maps editor changes back to your source code. The adapter supports several naming conventions:

  • Exact match (highest priority): const headline = signal(...) data-acms-id="headline"
  • Dot notation: const heroTitle = signal(...) data-acms-id="hero.title"
  • Underscore notation: const hero_title = signal(...) data-acms-id="hero.title"

Control Flow

What Framework uses <Show>, <For>, and <Switch> instead of ternaries and .map(). Components run once — only signal-reading nodes re-render.

FeatureList.tsx
import { signal } from 'what-core'
import { For, Show } from 'what-core'

export function FeatureList() {
  const title = signal('Features')
  const showBadge = signal(true)
  const features = signal([
    { name: 'Visual Editor', desc: 'Edit content in the browser' },
    { name: 'AI Generation', desc: 'Generate sections with AI' },
  ])

  return (
    <section data-acms-element="feature-list" class="py-16">
      <h2 data-acms-id="title" data-acms-type="text" data-acms-label="Title">
        {title()}
      </h2>
      <Show when={showBadge()}>
        <span class="badge">New</span>
      </Show>
      <div class="grid grid-cols-2 gap-6">
        <For each={features()}>
          {(feature) => (
            <div class="p-4 border rounded-lg">
              <h3>{feature.name}</h3>
              <p>{feature.desc}</p>
            </div>
          )}
        </For>
      </div>
    </section>
  )
}

JSX Differences from React

  • Use class instead of className
  • HTML attributes use their native names (for, tabindex, etc.)
  • Signal values are accessed by calling the signal: {count()} not {count}
  • Event handlers use native names: onclick not onClick

DevTools Integration

What Framework exposes a global window.__WHAT_DEVTOOLS__ object in development mode. Mantle's adapter hooks into this to discover signals, track components, and map editable content to live state.

Signal Registry

The DevTools global provides a registries.signals map containing every signal in the running app. Each entry has:

DevTools Signal Entry
{
  id: number,           // Unique signal ID
  name: string,         // Variable name (e.g. "headline")
  value: unknown,       // Current value
  componentId?: number  // ID of owning component (for scoping)
}

How Mantle Uses DevTools

  1. Signal discovery: On acms:init, the adapter reads all registered signals and builds a map from data-acms-id values to signal IDs.
  2. Live updates: When the editor sends a content change, the adapter calls signal.set(newValue) — the framework reactively updates only the affected DOM nodes.
  3. Event subscription: The adapter listens for signal:created and signal:updated events to keep its map current as components mount and unmount.

Component Scoping

When two components have signals with the same name (e.g. both have a title signal), the adapter uses componentId scoping to match the correct signal. This prevents false matches in pages with multiple instances of similar components.

Vite Plugin Configuration

The @mantle/what Vite plugin handles SDK injection and adapter setup automatically. Here are the available configuration options:

vite.config.ts
import { defineConfig } from 'vite'
import { mantlePlugin } from '@mantle/what/vite'

export default defineConfig({
  plugins: [
    mantlePlugin({
      // Mantle project slug (required for connected mode)
      projectSlug: 'my-project',

      // Mantle editor URL (default: https://usemantle.app)
      editorUrl: 'https://usemantle.app',

      // Auto-inject SDK script in dev mode (default: true)
      inject: true,

      // Enable dev overlay with element outlines (default: true)
      devOverlay: true,
    })
  ]
})

What the Plugin Does

  • Script injection: Adds the Mantle SDK <script> tag to your HTML in development mode. Removed automatically in production builds.
  • Adapter registration: Registers the What Framework signal adapter so the SDK can read and write signal values.
  • DevTools bridge: Connects to window.__WHAT_DEVTOOLS__ for signal discovery and component tracking.
  • HMR integration: Re-maps signals after hot module replacement to keep editing state consistent.

Production Behavior

In production builds, the plugin is a no-op. No SDK code is bundled, no data attributes are stripped — your components render as-is with zero overhead. The data-acms-* attributes remain in the HTML but are inert without the SDK.

Store Integration

What Framework stores provide shared reactive state across components. Mantle can bind to store properties in addition to signals, enabling editing of shared content like navigation items, theme settings, or global configuration.

Binding to Store Properties

Use dot notation in data-acms-id to reference nested store properties. The adapter resolves store bindings when a matching signal is not found.

SiteHeader.tsx
import { createStore } from 'what-core'

const siteConfig = createStore('siteConfig', {
  title: 'My Website',
  tagline: 'Built with What Framework',
  logo: '/logo.svg',
})

export function SiteHeader() {
  return (
    <header data-acms-element="site-header" class="flex items-center p-4">
      <img
        data-acms-id="siteConfig.logo"
        data-acms-type="image"
        data-acms-label="Site Logo"
        src={siteConfig.logo}
        alt="Logo"
      />
      <div>
        <h1
          data-acms-id="siteConfig.title"
          data-acms-type="text"
          data-acms-label="Site Title"
        >
          {siteConfig.title}
        </h1>
        <p
          data-acms-id="siteConfig.tagline"
          data-acms-type="text"
          data-acms-label="Tagline"
        >
          {siteConfig.tagline}
        </p>
      </div>
    </header>
  )
}

How Store Editing Works

  1. The adapter builds a store map from registries.stores in DevTools
  2. When a data-acms-id like siteConfig.title doesn't match any signal, the adapter checks store bindings
  3. Edits call store.set(path, newValue) which reactively updates all components reading that property
  4. When saving to GitHub, the content writer updates the store's default values in your source file

Store vs Signal

Use signals for content scoped to a single component (headings, body text, images). Use stores for content shared across components (site title, navigation, theme). Both are fully editable in the Mantle visual editor.

PostMessage Protocol

The Mantle editor communicates with the preview iframe using postMessage. All messages are prefixed with acms:.

MessageDirectionDescription
acms:initEditor → PreviewInitialize the SDK in the preview frame
acms:content-updateEditor → PreviewPush a content change to the preview
acms:element-selectedPreview → EditorUser clicked an editable element
acms:readyPreview → EditorSDK initialized and ready

To listen for messages from the editor in your custom integration:

custom-adapter.ts
window.addEventListener('message', (event) => {
  if (typeof event.data !== 'string') return
  if (!event.data.startsWith('acms:')) return

  const [, type] = event.data.split(':')
  const payload = JSON.parse(event.data.slice(event.data.indexOf('{')) || '{}')

  switch (type) {
    case 'content-update':
      // Apply content change
      break
    case 'init':
      // Initialize SDK
      break
  }
})

Need help? Sign up and reach us through the dashboard.