---
title: "Leiho for Astro: visual editing on WordPress"
description: "Static or server output, Leiho fits both: one request to the content API per build, and a script that only loads inside the editor."
url: https://leiho.dev/frameworks/astro
lang: en
updated: 2026-11-04
---
Astro · Static or server output

# Visual editing for Astro, on WordPress.

Static or server output, Leiho fits both: one request to the content API per build, and a script that only loads inside the editor.

1. 01
   ## Allow the editor to display your site
   Set the header where the site is hosted.
   ```
   /*
     Content-Security-Policy: frame-ancestors 'self' https://cms.example.com
   ```
   ```
   {
     "headers": [
       {
         "source": "/(.*)",
         "headers": [
           { "key": "Content-Security-Policy", "value": "frame-ancestors 'self' https://cms.example.com" }
         ]
       }
     ]
   }
   ```
   ```
   import { defineMiddleware } from 'astro:middleware';
   
   export const onRequest = defineMiddleware(async (_context, next) => {
     const response = await next();
     response.headers.set('Content-Security-Policy', "frame-ancestors 'self' https://cms.example.com");
     return response;
   });
   ```
   Applies to pages rendered on request. Prerendered pages take their headers from the host.
2. 02
   ## Add the bridge
   Install the package, then add this script to your base layout. Astro bundles it, and the bridge only downloads inside the editor.
   ```Terminal
   npm install leiho
   ```
   ```astro
   <head>
     <!-- … -->
     <script>
       if (window.self !== window.top) {
         import('leiho').then(({ initLeihoBridge }) => {
           initLeihoBridge({ wpOrigin: 'https://cms.example.com' });
         });
       }
     </script>
   </head>
   ```
3. 03
   ## Read the content
   Load the content in getStaticPaths: one request per build.
   ```astro
   ---
   import { loadLeihoContent, indexLeihoContent } from 'leiho/content';
   import Base from '../layouts/Base.astro';
   
   export async function getStaticPaths() {
     const { data } = await loadLeihoContent({ url: 'https://cms.example.com/wp-json/leiho/v1/export' });
     return indexLeihoContent(data).items.map((page) => ({
       params: { path: page.path.replace(/^\/|\/$/g, '') || undefined },
       props: { page },
     }));
   }
   
   const { page } = Astro.props;
   ---
   <Base title={page.title}>
     <main data-wp-post={page.id}>
       <h1 data-wp-field="_title">{page.title}</h1>
     </main>
   </Base>
   ```
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.
   ```astro
   ---
   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 {...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

- [**Next.js**](https://leiho.dev/frameworks/nextjs)
- [**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)
