---
title: "Leiho for Next.js: visual editing on WordPress"
description: "Keep the App Router, your components and your hosting. Leiho adds a client component to your layout and one spread per editable element."
url: https://leiho.dev/frameworks/nextjs
lang: en
updated: 2026-11-04
---
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.

1. 01
   ## Allow the editor to display your site
   Copy this into next.config.ts.
   ```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;
   ```
2. 02
   ## Add the bridge
   Install the package, then add the component to app/layout.tsx. It’s already a client component.
   ```Terminal
   npm install leiho
   ```
   ```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>
     );
   }
   ```
3. 03
   ## Read the content
   Load the content in lib/leiho.ts, then read each page by its path.
   ```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);
   });
   ```
   ```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.
4. 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.

Other frameworks

- [**Astro**](https://leiho.dev/frameworks/astro)
- [**Nuxt**](https://leiho.dev/frameworks/nuxt)
- [**SvelteKit**](https://leiho.dev/frameworks/sveltekit)
- [**React**](https://leiho.dev/frameworks/react)
- [**HTML, PHP and others**](https://leiho.dev/frameworks/html)

## Edit the real site.

Free and open source. Try it on a real WordPress, in your browser.

- [Try it in your browser](https://leiho.dev/try)
- [View on GitHub](https://github.com/pixelersagency/leiho)
