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.
The building blocks
Section titled “The building blocks”| API | What 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> )}Hydrate your own objects
Section titled “Hydrate your own objects”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.
Bind a managed gallery
Section titled “Bind a managed gallery”<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.
Bind a managed object list
Section titled “Bind a managed object list”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
amenitiesarray,<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.
Drop in the content manager
Section titled “Drop in the content manager”<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:
- Prompts for sign-in if there’s no editor session.
- Opens a Content tab — every CMS field for this object, editable in place (galleries via
<InlineImages>, scalars via the right input for their type). - Shows a Schema tab to admins — add new fields, set or change their type, and resolve any fields whose type couldn’t be inferred.
Managing every object of a type
Section titled “Managing every object of a type”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" />Defining fields and types
Section titled “Defining fields and types”Fields can be declared two ways, and the server keeps the union of both:
- From code — referencing a field with
<InlineImages value="images" />(oruseInlineContent) 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 pick | Stored as | Resolves 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, select … | scalar | their 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.
Draft and publish
Section titled “Draft and publish”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.
Under the hood
Section titled “Under the hood”The admin surface is just the SDK talking to the same REST API as the public site:
| Endpoint | Purpose |
|---|---|
GET /v1/objects?types=property | Unified read — scalars + collections per object (server-side hydration). |
GET / PATCH /v1/object-fields | Scalar overlay values. |
GET / PATCH /v1/object-collections | Ordered collection items (galleries, lists). |
GET / POST / PATCH /v1/object-types | The object-type schema registry. |
Permissions
Section titled “Permissions”Two tiers, enforced on the server:
- Content — editing an object’s fields and collections requires the
editororadminrole. - 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.