Notes

Inline content editing for Next.js: every option, and what it costs

July 28, 2026 · 9 min read

“Make the copy editable” sounds like one task. In a Next.js App Router app it is at least four decisions: where the text lives, who can change it, when the change becomes visible, and what happens when the store is unreachable.

Here is the honest comparison. No option here is wrong; they are sized for different problems.

1. Roll your own with contentEditable

Slap contentEditable on a div, save on blur. An afternoon of work.

The reason this rarely survives contact with production is not the saving — it is everything around it. contentEditable gives you rich text whether you wanted it or not: paste a styled paragraph from Google Docs and you have just injected a <span style="font-family:Arial"> into your design system. You will need to intercept paste, force plain text, and handle the caret jumping to position zero every time React re-renders the node.

That last one deserves emphasis, because it is the bug everybody hits. If the editable element is a controlled React component, every keystroke re-renders it, and the caret snaps to the start. The fix is to make it uncontrolled while editing — seed the text once via a ref, read it back on blur — which feels wrong until you realise the DOM is the source of truth for exactly as long as the cursor is in it.

Verdict: fine for an internal tool. Budget three times longer than you think for the editing UX, not the persistence.

2. A headless CMS

Sanity, Contentful, Storyblok, Payload. The default recommendation, and often the right one.

What you get: real editorial workflow, versioning, roles, scheduled publishing, localisation, media handling. What you pay: a schema to model and maintain, a fetch on every page, a caching and revalidation strategy, a preview mode, and a monthly bill that scales with seats or API calls.

The hidden cost is coupling. Once your components read from a CMS schema, changing the design means changing the schema, which means a migration, which means coordinating with whoever is mid-edit. For a marketing site with forty strings, the schema is heavier than the site.

Verdict: correct when you have content types, not just content. Blog posts, case studies, products — things with a shape that repeats.

3. Visual page builders

Builder.io, Plasmic, and the visual-editing modes that several headless CMSes now ship. Drag-and-drop on top of your React components.

These are genuinely impressive and genuinely heavy. You register your components with the platform, and non-developers assemble pages from them. The trade is that layout authority moves out of your codebase. If you handed someone a design system precisely so the site would stay consistent, giving them a page builder undoes some of that on purpose.

Verdict: right when the client needs to build layouts. Overkill when they need to fix a typo.

4. i18n dictionaries

next-intl, next-i18next, or a hand-rolled JSON dictionary. Every string becomes t('hero.title'), looked up from a JSON file.

If you already need multiple languages, you have this anyway, and editing English becomes “edit en.json”. Fine.

If you do not need multiple languages, this is a real cost for little gain. Your JSX stops containing readable sentences. Code review gets worse: a diff that changes t('cta.primary') tells the reviewer nothing about what the button now says. And a non-technical editor still has to open a JSON file, where a missing comma takes the whole site down.

Verdict: excellent for localisation. A poor motivation on its own.

5. A sparse override layer

Keep the copy in the components as the default, and store only the strings someone has actually changed.

Concretely, in an App Router app: the layout reads an override map once per request and puts it in context. Each editable string renders through a small component that resolves override ?? children. The children are the real words, in the real JSX.

Four properties fall out of this that are worth naming:

  • The code is the fallback. If the store is down, unreachable, or empty, the page renders the copy in your repo. Not a blank page, not a loading skeleton — the shipped copy. Make the read fail open and this is genuinely resilient.
  • Nothing to model. No schema, no content types, no migration when the design changes. Marking a new string editable is wrapping it.
  • The diff still reads. A developer opening the component sees the sentence, not a key.
  • The store is tiny. A hash of changed strings. Redis, a JSON file, a KV namespace — all fine, and cheap enough to be free at this size.

The costs, stated plainly:

  • Rendering goes dynamic. Reading overrides per request means the page is server-rendered on demand, so you give up fully static output. Mitigate with tag-based revalidation on save if the traffic justifies it.
  • No editorial workflow. No drafts, no approvals, no scheduling. Edits are live the moment you click away, which is the point and also the risk.
  • Drift. Production copy diverges from the repo unless you have a way to pull it back down.

Verdict: the right size for marketing sites, landing pages, and client work where somebody needs to fix words on a live page and nothing more.

Side by side

ApproachSetupOngoing costEditor sees the real page
contentEditable, DIYDaysYou maintain itYes
Headless CMSDays to weeksSubscription + schema upkeepOnly with preview set up
Visual builderDaysSubscriptionYes
i18n dictionaryHoursFreeNo
Override layerHoursNear zeroYes

One thing you cannot skip

Whichever you choose, if text can be written from the browser, the write path needs a server-side authorisation check and the text needs to be stored and rendered as plain text. Hiding the edit UI is not security; the endpoint is what is exposed. And a string that becomes HTML is a cross-site scripting hole with extra steps.

Closing

Most Next.js sites that “need a CMS” need one string changed by one person who is looking right at it.

Copy Edit Mode implements the override-layer approach as a Claude Code skill: Claude reads your app, wires in the ?edit mode, the password gate, and the store, and leaves you with the copy still in your components. $7.99 once.