next-inline-editor
v0.1.3
Published
Zero-config inline content editor for Next.js sites backed by GitHub. Edit text and images directly on your live pages.
Maintainers
Readme
next-inline-editor
Zero-config inline content editor for Next.js sites. Edit text and images directly on your live pages. Changes commit to GitHub and deploy automatically.
No database. No hosted CMS. No separate editing UI. Your live site is the editor.
Quick start
npm install next-inline-editor
npx next-inline-editor initThe init command will:
- Detect your project structure (App Router, TypeScript or JS)
- Create all API routes under
app/api/admin/ - Create
app/admin/page.tsxandapp/admin/login/page.tsx - Add the required environment variables to
.env.local
Then fill in two values in .env.local and you're done:
ADMIN_PASSWORD=your-secure-password-here
GITHUB_TOKEN=ghp_your-token-here # https://github.com/settings/tokens (Contents: read & write)
GITHUB_REPO=owner/repo-nameVisit /admin/login, enter your password, and start editing.
How it works
- Add
data-edit="field.path"to any text element in your components - Add
data-edit-image="field.path"to any image element - At
/admin, those elements become editable in place — text is clickable, images get a "Change image" button - Saving commits the updated JSON directly to your GitHub repo and triggers your normal deploy pipeline
Adding data-edit attributes
After running init, wire up your components so the editor knows what to edit:
// components/MyHomePage.tsx
export default function MyHomePage({ content }) {
return (
<div>
<h1 data-edit="hero.title">{content.hero.title}</h1>
<p data-edit="hero.subtitle">{content.hero.subtitle}</p>
<div
data-edit-image="hero.backgroundImage"
style={{ backgroundImage: `url(${content.hero.backgroundImage})` }}
/>
<img
data-edit-image="logo.src"
src={content.logo.src}
alt="Logo"
/>
</div>
);
}Then update the generated app/admin/page.tsx to use your component:
import { AdminEditor } from 'next-inline-editor';
import homeContent from '../../content/home.json';
import MyHomePage from '../../components/MyHomePage';
export default function AdminPage() {
return (
<AdminEditor
initialContent={homeContent}
contentFile="content/home.json"
pageLabel="Home"
>
{(content) => <MyHomePage content={content} />}
</AdminEditor>
);
}Multiple pages
Pass a pages array to show a page-switcher dropdown in the editor toolbar:
// app/admin/page.tsx
import { AdminEditor } from 'next-inline-editor';
export default async function AdminPage({ searchParams }) {
const page = (await searchParams).page ?? 'home';
const { content, contentFile, pageLabel } = getPageConfig(page);
return (
<AdminEditor
initialContent={content}
contentFile={contentFile}
pageLabel={pageLabel}
pages={[
{ label: 'Home', href: '/admin' },
{ label: 'About', href: '/admin?page=about' },
{ label: 'Contact', href: '/admin?page=contact' },
]}
>
{(content) => <PageComponent content={content} />}
</AdminEditor>
);
}Content file structure
Your JSON files can be any shape. The editor doesn't care about schema — it just reads and writes whatever you give it.
content/home.json
{
"hero": {
"title": "Welcome",
"subtitle": "We build great things.",
"backgroundImage": "/images/hero.jpg"
},
"about": {
"heading": "About us",
"body": "We are a small team..."
}
}Arrays work too — use numeric indexes in paths:
<div data-edit-image="slides.0.image" />
<p data-edit="slides.0.caption">{content.slides[0].caption}</p>Image uploads
When an editor clicks "Change image", the file is:
- Validated (JPEG, PNG, WebP, or GIF; max 10MB)
- Committed to
public/uploads/in your GitHub repo - The path is saved into your content JSON as
/uploads/filename.jpg
The image is immediately previewed before the deploy completes.
To change the upload directory, replace the generated upload route:
// app/api/admin/upload/route.ts
import { handleUpload } from 'next-inline-editor/api/upload';
export async function POST(request: Request) {
return handleUpload(request, {
uploadDir: 'public/media',
publicPrefix: '/media',
});
}Deploy pipeline
This package commits directly to your GitHub repo. For automatic deploys on commit, connect your repo to:
- Vercel — auto-deploys on every push, no config needed
- Netlify — enable "continuous deployment" in site settings
- Cloudflare Pages — connect repo and set build command
After saving in the editor, the live site updates in ~1 minute.
Environment variables
| Variable | Required | Description |
|---|---|---|
| ADMIN_PASSWORD | Yes | Password for the login page |
| ADMIN_SESSION_SECRET | Yes | Secret for signing session tokens — auto-generated by init |
| GITHUB_TOKEN | Yes | GitHub personal access token with Contents read & write scope |
| GITHUB_REPO | Yes | owner/repo format |
| GITHUB_BRANCH | No | Branch to commit to (default: main) |
Security notes
- Session cookie is
httpOnly,secure(in production), andsameSite: lax - Password and token comparisons use timing-safe equality to prevent timing attacks
- The save route validates file paths against your explicit
ALLOWED_FILESwhitelist — arbitrary file writes are not possible - The
/adminpage has no server-side auth guard — the API routes are always protected, but add Next.js middleware if you want to block unauthenticated page access entirely
TypeScript
The AdminEditor children prop passes a loosely typed content object. Cast it to your own type for full safety:
import type { HomeContent } from '../types/content';
<AdminEditor initialContent={homeContent} ...>
{(content) => <MyHomePage content={content as unknown as HomeContent} />}
</AdminEditor>