Skip to content

Admin Content Management

Manage CMS content from an admin app — hydrate your own objects, bind managed galleries, and drop in a content manager UI.

InlineCMS isn’t only for the public site. The same content store powers an admin experience: hydrate the objects your app already owns with their CMS fields, render managed galleries, and drop in a full management UI behind a button — without building a separate dashboard.

The model is enrichment: your application owns each object’s identity (a property, a listing, a product — its id and core data). InlineCMS overlays CMS-managed fields keyed by (type, id, fieldPath). A field like property.images need not exist on your object at all — it lives entirely in InlineCMS and is merged in at read time. InlineCMS never creates objects; it only decorates ids you hand it.

APIWhat it does
useInlineContent(type, object)Returns your object merged with its CMS overlay (scalars + collections).
<InlineImages data value />A managed image/video collection bound to an object field. View + manage in one.
<InlineObjects data value />A managed list of rich objects (amenities, features, specs) bound to an object field. View + manage in one.
<InlineCMSContentManager data />A drop-in management surface (content + schema) scoped to an object.

All three read and write the same (type, id, path) store, so an edit made in the admin manager shows up on the public site, and vice-versa.

These building blocks ship for both React (@inlinecms/react) and Vue (@inlinecms/vue) with the same names and props — useCMSObject, useInlineContent, <InlineImages>, <InlineObjects>, and <InlineCMSContentManager>. The examples below are React; the Vue equivalents are identical aside from framework idiom (composables return refs; props are kebab-case in templates). See the Vue SDK reference for Vue signatures, and apps/vue-demo for a complete working page.

Wrap your admin app in the provider, just like the public site. In an admin tool you typically authenticate your users separately, so editorAccess: 'handoff' (a short-lived token handed off from your auth) or 'manual' (an in-UI sign-in) both work — the content manager will prompt for sign-in if no session exists.

import { InlineCMSProvider } from '@inlinecms/react'
export function AdminApp({ children }) {
return (
<InlineCMSProvider
config={{
apiKey: import.meta.env.VITE_INLINECMS_KEY,
apiUrl: import.meta.env.VITE_INLINECMS_API,
editorAccess: 'manual',
}}
>
{children}
</InlineCMSProvider>
)
}

useInlineContent registers the object and returns it merged with its CMS overlay. Scalar fields resolve to their value; collection fields resolve to an array (so .map() is always safe), and the raw object data you passed in is preserved.

import { useInlineContent } from '@inlinecms/react'
function PropertyHeader({ property }) {
// property = { id: 'prop_8c4f', name: 'Beachfront Stunner', location: '…' }
const content = useInlineContent('property', property)
return (
<header>
<h1>{content.name}</h1>
<div className="gallery">
{content.images.map((img) => (
<img key={img.url} src={img.url} alt={img.caption ?? ''} />
))}
</div>
</header>
)
}

content.$cms carries editing metadata: { loading, canEdit, fields, collections } — useful when you want to branch on whether the current user can edit, or show a loading state during hydration.

A field surfaces here once it exists in the type’s schema — which happens automatically the first time you reference it with <InlineImages> or declare it in the manager’s Schema tab. Until then, your object’s own data passes through untouched.

<InlineImages> binds a collection field to an object and renders it. For visitors it’s a gallery; for signed-in editors it doubles as the management surface (upload, caption, reorder, remove) — no separate component.

import { InlineImages } from '@inlinecms/react'
<InlineImages data={property} value="images" /> {/* gallery + inline manage */}
<InlineImages data={property} value="images" mode="manage" /> {/* open straight into the grid */}
<InlineImages data={property} value="images" render="button" /> {/* a button that opens the manager */}

Items are stored as ordered rows, each a { url, type, caption } media object — so captions, order, and individual publish state are preserved per image rather than as one opaque blob.

Some fields aren’t a single value or a gallery — they’re a list of small, repeating objects: a resort’s amenities, a plan’s features, a product’s specs. Each has a title and, often, an icon or badge. <InlineObjects> manages exactly that — the same enrichment model as <InlineImages>, but each item is a structured { title, icon?, badge?, description? } (an ObjectItemValue) instead of a media file.

import { InlineObjects } from '@inlinecms/react'
<InlineObjects data={resort} value="amenities" itemLabel="amenity" />

For visitors it renders the list; for signed-in editors it becomes the editor — add, edit, reorder, remove, and upload icon/badge images, with the same Save draft / Publish flow as galleries.

It’s deliberately forgiving about what your data already looks like:

  • Host fallback — if your object already carries an amenities array, <InlineObjects> shows it untouched. { icon, label }, { name, … }, or even plain strings are normalized to { title, icon, … }, so there’s nothing to migrate to see your existing list.
  • Import to edit — the first time an editor opens the manager, a one-click Import from page pulls those host items into the CMS so they become editable. (Identity stays borrowed — InlineCMS still never originates the parent object.)
  • Icons are emoji or images — type 🌊 or paste/upload an image URL; it renders as text or an <img> automatically.
  • Reuse across objects — the manager surfaces a Used elsewhere row: distinct values already used on other objects of the same type (e.g. amenities from your other resorts), de-duplicated case-insensitively, derived client-side from the store. Click one to add it — your vocabulary converges without a separate taxonomy to maintain.

Pick which sub-fields the editor exposes and the view’s column count:

<InlineObjects data={resort} value="amenities" fields={['title', 'icon']} columns={2} />

The same component ships for Vue:

<script setup>
import { InlineObjects } from '@inlinecms/vue'
</script>
<template>
<InlineObjects :data="resort" value="amenities" item-label="amenity" />
</template>

Items are stored as ordered collection rows, each an { title, icon?, badge?, description? } object — so order and per-item publish state are preserved, exactly like image collections.

<InlineCMSContentManager> is the headline: a complete, object-scoped management UI you can place anywhere in your admin app. render="button" adds an unobtrusive trigger that opens a side drawer; render="panel" renders it inline.

import { InlineCMSContentManager } from '@inlinecms/react'
function PropertyAdminRow({ property }) {
return (
<div className="row">
<span>{property.name}</span>
<InlineCMSContentManager data={property} render="button" />
</div>
)
}

Clicking the button:

  1. Prompts for sign-in if there’s no editor session.
  2. Opens a Content tab — every CMS field for this object, editable in place (galleries via <InlineImages>, scalars via the right input for their type).
  3. Shows a Schema tab to admins — add new fields, set or change their type, and resolve any fields whose type couldn’t be inferred.

Because InlineCMS has no roster of its own (it only knows the ids you give it), pass your full list via roster to get an object switcher inside the manager — the admin app owns the list, so it decides the scope:

<InlineCMSContentManager data={properties[0]} roster={properties} render="button" />

Fields can be declared two ways, and the server keeps the union of both:

  • From code — referencing a field with <InlineImages value="images" /> (or useInlineContent) registers it automatically.
  • From the manager — an admin adds a field in the Schema tab.

When you add a field, pick a type. Collections use friendly sugar that maps to a base type plus cardinality:

You pickStored asResolves to
text{ type: 'text', many: false }string
image{ type: 'image', many: false }one { url, … }
images{ type: 'image', many: true }{ url, … }[]
list{ type: 'text', many: true }string[]
objects{ type: 'object', many: true }{ title, icon?, … }[] (managed by <InlineObjects>)
richtext, url, number, boolean, date, selectscalartheir value

Each field records where it came from (code or admin). An admin change to a field’s type wins and sticks — a later code re-declaration won’t overwrite it.

Edits are optimistic and local first. The manager and <InlineImages> expose Save draft and Publish actions; programmatically, cms.saveAllDrafts() and cms.publishAll() flush every pending change across page content, object fields, and collections. Visitors see published values; signed-in editors see their drafts.

The admin surface is just the SDK talking to the same REST API as the public site:

EndpointPurpose
GET /v1/objects?types=propertyUnified read — scalars + collections per object (server-side hydration).
GET / PATCH /v1/object-fieldsScalar overlay values.
GET / PATCH /v1/object-collectionsOrdered collection items (galleries, lists).
GET / POST / PATCH /v1/object-typesThe object-type schema registry.

Two tiers, enforced on the server:

  • Content — editing an object’s fields and collections requires the editor or admin role.
  • Schema — adding fields or changing their types requires admin.

The manager hides affordances the current user can’t use, but the API is the source of truth — a write without permission is rejected regardless of the UI.

Was this page helpful? Your feedback goes straight to the docs team.