---
title: "Content API · Leiho docs"
description: "Load your content in one request, with types."
url: https://leiho.dev/docs/content-api
lang: en
updated: 2026-11-04
---
Guides

# Content API

Load your content in one request, with types.

Leiho also serves your content to the front end, so WordPress is the only source of truth and the front-end repository holds code, not content.

```ts
// astro.config / build script — Node 18+
import { loadLeihoContent, indexLeihoContent } from 'leiho/content';
import type { LeihoExport, Page } from './src/leiho'; // npx leiho types

const { data } = await loadLeihoContent( { url: 'https://cms.example.com' } );
const content = indexLeihoContent( data as LeihoExport );
const home = content.byPath( '/' ) as Page;
home.fields.hero_title; // typed
```

| Endpoint | Returns |
| --- | --- |
| `GET /wp-json/leiho/v1/export` | Every published item of the Leiho content types, in one response. Cached until something changes, with an `ETag` (an unchanged build gets a `304`). |
| `GET /wp-json/leiho/v1/content?type=&path=&slug=` | A filtered, paginated list. |
| `GET /wp-json/leiho/v1/content/{id}` | One item (drafts for logged-in editors). |
| `GET /wp-json/leiho/v1/types` | TypeScript declarations of exactly that JSON, generated from your ACF field groups. |

Each item carries `id`, `type`, `slug`, `path` (the front-end path from your URL settings), `title`, dates, `excerpt`, rendered `content`, `featured_image`, `terms` and `fields`. Fields are keyed by name and already shaped for a front end: an image is `{ id, url, alt, width, height, sizes }`, a relation is `{ id, type, slug, title, path }`, a WYSIWYG is rendered HTML, a flexible content row has a `_layout`.

- **One request per build** instead of one per page: no more "Error establishing a database connection" on shared hosting.
- **Snapshot fallback**: every successful load is written to `.leiho/content.json`. If WordPress doesn't answer, the build carries on with the last known content and says so (`fallback: 'error'` to fail instead).
- **Types**: `npx leiho types --url https://cms.example.com` writes `src/leiho.d.ts`. Regenerate it when you change fields.
- **Access**: public by default (published content only, which is public anyway). Switch to _Restricted_ in the settings and set `LEIHO_TOKEN` in the build environment.
- **Caching**: Leiho plugs into the caches the site already has (Settings → _Content API_ → _Cache_, on by default).
- With a persistent object cache (Redis Object Cache, Memcached, LiteSpeed, W3 Total Cache…), the export, single items, path lookups and the types live in the WordPress object cache (group `leiho`). Invalidation is O(1): every change switches to a new cache generation instead of deleting keys. Without one, Leiho keeps its own copy of the export in a non-autoloaded option and writes nothing else to the database.
- Every endpoint sends an `ETag` and `Cache-Control: public, max-age=0, must-revalidate`, and answers `304` to a matching `If-None-Match`. In restricted mode, for logged-in users and for drafts, responses are `private` (and LiteSpeed is told not to cache them).
- When served content changes, Leiho purges the content API from the page caches it finds, once per request: LiteSpeed (by tag and URL, Hostinger included), WP Rocket and W3 Total Cache and WP Super Cache (by URL when they can), SiteGround, WP Fastest Cache, Cloudflare, Breeze, Nginx Helper. Hook `leiho_purge_caches` to purge anything else (a CDN, a reverse proxy); `leiho_purge_urls` changes the URLs.
- _Clear the content cache_ in the settings does all of this on demand.

Updated November 4, 2026[Something unclear on this page? Tell us →](https://github.com/pixelersagency/leiho/issues/new?title=Docs%3A%20Content%20API)
