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.

    public/_headers · Netlify · Cloudflare Pages
    /*
      Content-Security-Policy: frame-ancestors 'self' https://cms.example.com
    vercel.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.

  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
    src/layouts/Base.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.

    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>
  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.

Edit the real site.

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