Next.js · App Router
Visual editing for Next.js, on WordPress.
Keep the App Router, your components and your hosting. Leiho adds a client component to your layout and one spread per editable element.
- 01
Allow the editor to display your site
Copy this into next.config.ts.
next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { async headers() { return [ { source: '/:path*', headers: [ { key: 'Content-Security-Policy', value: "frame-ancestors 'self' https://cms.example.com" }, ], }, ]; }, }; export default nextConfig; - 02
Add the bridge
Install the package, then add the component to app/layout.tsx. It’s already a client component.
Terminal npm install leihoapp/layout.tsx import { LeihoBridge } from 'leiho/react'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <LeihoBridge wpOrigin="https://cms.example.com" /> </body> </html> ); } - 03
Read the content
Load the content in lib/leiho.ts, then read each page by its path.
lib/leiho.ts import { cache } from 'react'; import { loadLeihoContent, indexLeihoContent } from 'leiho/content'; // One request per render, shared by every component. export const getContent = cache(async () => { const { data } = await loadLeihoContent({ url: 'https://cms.example.com/wp-json/leiho/v1/export' }); return indexLeihoContent(data); });app/[[...slug]]/page.tsx import { notFound } from 'next/navigation'; import { getContent } from '@/lib/leiho'; export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) { const { slug = [] } = await params; const page = (await getContent()).byPath('/' + slug.join('/')); if (!page) notFound(); return ( <main data-wp-post={page.id}> <h1 data-wp-field="_title">{page.title}</h1> </main> ); }Serverless host with a read-only disk? Pass snapshot: false to loadLeihoContent.
- 04
Mark what editors can change
Spread field() on each element editors should change. Always render the element, even when its value is empty.
JSX import { field, entry } from 'leiho'; // field() writes data-wp-post + data-wp-field, entry() names a section. // Flexible content rows: block(page, 'sections', i, 'Label'). <section {...entry(page, 'Hero')}> <h1 {...field(page, 'hero_title')}>{page.fields.hero_title}</h1> <img {...field(page, 'hero_image')} src={page.fields.hero_image?.url} alt={page.fields.hero_image?.alt} /> {page.fields.cards?.map((card, i) => ( <article key={i} {...field(page, `cards.${i}.title`)}>{card.title}</article> ))} </section>
Replace https://cms.example.com with your WordPress address. In wp-admin, Leiho → Get started shows these steps with your address filled in, checks each one, and can give the whole setup to your coding agent as one prompt.
Edit the real site.
Free and open source. Try it on a real WordPress, in your browser.