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.
- 01
Allow the editor to display your site
Set the header where the site is hosted.
public/_headers · Netlify · Cloudflare Pages /* Content-Security-Policy: frame-ancestors 'self' https://cms.example.comvercel.json · Vercel { "headers": [ { "source": "/(.*)", "headers": [ { "key": "Content-Security-Policy", "value": "frame-ancestors 'self' https://cms.example.com" } ] } ] }src/middleware.ts · Node server 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.
- 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 leihosrc/layouts/Base.astro <head> <!-- … --> <script> if (window.self !== window.top) { import('leiho').then(({ initLeihoBridge }) => { initLeihoBridge({ wpOrigin: 'https://cms.example.com' }); }); } </script> </head> - 03
Read the content
Load the content in getStaticPaths: one request per build.
src/pages/[...path].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> - 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.
Edit the real site.
Free and open source. Try it on a real WordPress, in your browser.