astrojs-wp-integration – README
Astro-first integration for WordPress with content loaders, server actions, auth bridge helpers, catalog virtual modules, and rendering components.
This package is built against fluent-wp-client ^3.0.1 and expects your Astro project to use the same client package directly.
It supports Astro ^6.0.0.
Install
Section titled “Install”npm install wp-astrojs-integration fluent-wp-clientIf you use a coding agent, install both the fluent client and Astro integration skills:
npx skills add https://github.com/JUVOJustin/fluent-wp-clientnpx skills add https://github.com/JUVOJustin/astrojs-wp-integrationFeature overview
Section titled “Feature overview”| Feature | What you get in Astro | Main API |
|---|---|---|
| Live content collections | Request-time WordPress data for SSR routes | defineLiveCollection + wordPress*Loader |
| Route caching | Astro cacheHint metadata plus a targeted invalidation action for live routes | Astro.cache.set(), createWpCacheInvalidateAction |
| Catalog integration | Build-time WordPress discovery cached in Astro and reused by catalog-aware helpers | wp-astrojs-integration/integration, virtual:wp-astrojs/catalog |
| Static content collections | Build-time WordPress snapshots for SSG | defineCollection + wordPress*StaticLoader |
| Server actions | Typed create/update/delete actions for posts, pages, users, and abilities | create*Action factories |
| Auth bridge | Login/session helpers for Astro server actions and middleware | createWordPressAuthBridge |
| Discovery + typesafety | Use fluent-wp-client discovery, embed helpers, and Zod conversion utilities with Astro catalog helpers | fluent-wp-client, virtual:wp-astrojs/* |
| Rendering components | Gutenberg-friendly HTML and media rendering in Astro | WPContent, WPImage |
Use fluent-wp-client as the WordPress client and schema source of truth. Use this package for the Astro-specific integration points.
Available entities
Section titled “Available entities”| Entity | Schema from fluent-wp-client/zod | Live loader | Static loader | Notes |
|---|---|---|---|---|
| Posts | postSchema | wordPressPostLoader | wordPressPostStaticLoader | |
| Pages | pageSchema | wordPressPageLoader | wordPressPageStaticLoader | |
| Media | mediaSchema | wordPressMediaLoader | wordPressMediaStaticLoader | |
| Categories | categorySchema | wordPressCategoryLoader | wordPressCategoryStaticLoader | |
| Tags | categorySchema | wordPressTagLoader | wordPressTagStaticLoader | |
| Custom taxonomies | categorySchema | wordPressTermLoader | wordPressTermStaticLoader | Pass custom REST resource |
| Custom Post Types | contentWordPressSchema (extend) | wordPressContentLoader | wordPressContentStaticLoader | Pass custom REST resource |
| Users | WordPressAuthor | wordPressUserLoader | wordPressUserStaticLoader |
Quick start
Section titled “Quick start”1) Configure the catalog integration
Section titled “1) Configure the catalog integration”Add the Astro integration so WordPress discovery metadata is fetched during builds and reused by catalog-aware clients, schemas, collections, and actions.
import { defineConfig } from 'astro/config';import wordpress from 'wp-astrojs-integration/integration';
export default defineConfig({ integrations: [ wordpress({ catalog: { enabled: true, refresh: 'build', required: true, }, }), ],});Set the WordPress URL in .env or in the environment that runs Astro.
WP_CATALOG_URL=https://cms.example.comCatalog setup uses Vite’s supported loadEnv() helper during Astro setup, so .env files and shell or CI variables are available. Authenticated discovery can use WP_CATALOG_AUTH_HEADER or WP_CATALOG_USERNAME plus WP_CATALOG_PASSWORD; username/password credentials use JWT when WordPress exposes the /jwt-auth/v1/token route in the REST index (served at /wp-json/jwt-auth/v1/token) and otherwise use application-password basic auth.
2) Create a catalog-backed client
Section titled “2) Create a catalog-backed client”Create clients from the generated virtual module so discovery-aware calls automatically use the stored catalog instead of re-discovering WordPress.
import { createWordPressClient } from 'virtual:wp-astrojs/catalog';
export const wp = createWordPressClient({ baseUrl: import.meta.env.WP_CATALOG_URL,});3) Define schema-backed collections
Section titled “3) Define schema-backed collections”Use defineWordPressCollection() to create Astro collections with the correct WordPress loader and a catalog-derived Zod schema. When catalog discovery runs with WP_CATALOG_URL, the integration also writes typed Zod artifacts to Astro’s cache using fluent-wp-client schemas --zod-out ... --types-out ... and uses those schemas automatically for stronger collection inference.
import { defineWordPressCollection } from 'virtual:wp-astrojs/collections';import { wp } from './lib/wp';
export const collections = { posts: defineWordPressCollection('posts', { client: wp }), books: defineWordPressCollection('books', { client: wp }), livePosts: defineWordPressCollection('posts', { mode: 'live', client: wp, }),};Generated schemas are also available from virtual:wp-astrojs/generated-schemas when you need to reuse the same real-instance validators directly.
import { wpBookSchema } from 'virtual:wp-astrojs/generated-schemas';
type Book = import('virtual:wp-astrojs/generated-schemas').WPBook;4) Use catalog schemas in actions
Section titled “4) Use catalog schemas in actions”Use withWordPressActionSchemas() to add catalog-derived input and response schemas to action factories. Explicit schema and responseSchema options still win when you pass them.
import { createCreatePostAction } from 'wp-astrojs-integration';import { withWordPressActionSchemas } from 'virtual:wp-astrojs/schemas';import { wp } from '../lib/wp';
export const server = { createBook: createCreatePostAction( wp, withWordPressActionSchemas('books', { resource: 'books' }), ),};5) Render WordPress content in Astro pages
Section titled “5) Render WordPress content in Astro pages”---import { getLiveEntry } from 'astro:content';import WPContent from 'wp-astrojs-integration/components/WPContent.astro';
const { slug } = Astro.params;const { entry: post } = await getLiveEntry('livePosts', { slug });---
<article> <h1 set:html={post.data.title.rendered} /> <WPContent content={post.data.content.rendered} baseUrl={import.meta.env.WP_CATALOG_URL} /></article>Manual Loader Setup
Section titled “Manual Loader Setup”You can still wire loaders and schemas manually when you want full control.
Live collection (SSR)
Section titled “Live collection (SSR)”import { defineLiveCollection } from 'astro:content';import { WordPressClient } from 'fluent-wp-client';import { postSchema } from 'fluent-wp-client/zod';import { wordPressPostLoader } from 'wp-astrojs-integration';
const wp = new WordPressClient({ baseUrl: import.meta.env.PUBLIC_WORDPRESS_BASE_URL,});
const posts = defineLiveCollection({ loader: wordPressPostLoader(wp), schema: postSchema,});
export const collections = { posts };If you need request instrumentation, a cache layer, or a proxy, pass a custom fetch to WordPressClient and then reuse that client with the live loader:
const wp = new WordPressClient({ baseUrl: import.meta.env.PUBLIC_WORDPRESS_BASE_URL, fetch: async (input, init) => { return fetch(input, init); },});
const posts = defineLiveCollection({ loader: wordPressPostLoader(wp), schema: postSchema,});Static collection (SSG)
Section titled “Static collection (SSG)”import { defineCollection } from 'astro:content';import { WordPressClient } from 'fluent-wp-client';import { postSchema } from 'fluent-wp-client/zod';import { wordPressPostStaticLoader } from 'wp-astrojs-integration';
const wp = new WordPressClient({ baseUrl: import.meta.env.PUBLIC_WORDPRESS_BASE_URL,});
const posts = defineCollection({ loader: wordPressPostStaticLoader(wp), schema: postSchema,});
export const collections = { posts };Live loaders return the base resource payload by default. If you need embedded relations like featured media, enable embed on the loader options. If you need a custom request pipeline, set fetch on WordPressClient; the live loaders reuse that client as-is.
Use mapEntry when a site needs to normalize fields before Astro receives loader data. For example, build an ACF choice-label lookup from WordPress discovery metadata and plug that lookup into the mapper:
const acfFields = await wp.content('posts').getSchemaValue< Record<string, { choices?: Array<{ value: string; label: string }> }>>('properties.acf.properties');
const choiceLabels = new Map( Object.entries(acfFields ?? {}) .filter(([, field]) => Array.isArray(field.choices)) .map(([fieldName, field]) => [ fieldName, new Map(field.choices!.map((choice) => [choice.value, choice.label])), ]),);
const posts = defineLiveCollection({ loader: wordPressPostLoader(wp, { mapEntry: (post) => ({ ...post, acf: Object.fromEntries( Object.entries(post.acf ?? {}).map(([fieldName, value]) => [ fieldName, choiceLabels.get(fieldName)?.get(String(value)) ?? value, ]), ), }), }), schema: postSchema,});mapEntry is available on live and static loaders. The callback receives { resource, filter } for live loaders so mappings can vary by resource or request filter. Use mapResponse on create/update actions when successful write responses need the same normalization. See docs/mapping.mdx for a reusable mapper pattern.
Route caching
Section titled “Route caching”Live loaders return Astro-compatible cacheHint values, and createWpCacheInvalidateAction() invalidates changed post, term, or user routes. See docs/caching.mdx for setup details, examples, and Astro reference links.
AI SDK tools
Section titled “AI SDK tools”AI SDK tools live in fluent-wp-client/ai-sdk. This package does not wrap them with live-loader helpers because live loaders are for public/shared content, not per-user request auth.
| Server-side tool group | Factories from fluent-wp-client/ai-sdk | Use for |
|---|---|---|
| Content | getContentTool, getContentCollectionTool, saveContentTool, deleteContentTool | Posts, pages, custom post types, and other post-like resources |
| Terms | getTermTool, getTermCollectionTool, saveTermTool, deleteTermTool | Categories, tags, and custom taxonomies |
| Generic resources | getResourceTool, getResourceCollectionTool, saveResourceTool, deleteResourceTool | Catalog-discovered or plugin REST resources |
| Abilities | getAbilitiesTool, getAbilityTool, executeGetAbilityTool, executeRunAbilityTool, executeDeleteAbilityTool, createAbilityTools | WordPress abilities exposed through the REST API |
| Blocks | getBlocksTool, setBlocksTool | Reading or writing parsed block content |
| Settings | getSettingsTool, updateSettingsTool | WordPress site settings |
| Discovery | describeResourceTool | Resource metadata and schema-aware descriptions |
For public AI reads, use the fluent client tools directly and apply any Astro route cache policy in the endpoint:
import type { APIRoute } from 'astro';import { WordPressClient } from 'fluent-wp-client';import { getContentTool } from 'fluent-wp-client/ai-sdk';
const wp = new WordPressClient({ baseUrl: import.meta.env.WP_URL,});
export const GET: APIRoute = async (context) => { const tool = getContentTool(wp, { contentType: 'posts', });
if (!tool.execute) throw new Error('Content tool is not executable.');
const result = await tool.execute({ slug: 'hello-world' }, { toolCallId: 'read-post', messages: [], });
context.cache.set({ maxAge: 300, swr: 60 });
return Response.json(result);};For public/shared content, fluent AI tools can also fetch through an Astro live collection so the endpoint benefits from live-loader cache hints:
import { getLiveEntry } from 'astro:content';import { getContentTool } from 'fluent-wp-client/ai-sdk';
export const GET: APIRoute = async (context) => { const tool = getContentTool(wp, { contentType: 'posts', fetch: async (input) => { if (!input.id && !input.slug) { throw new Error('Provide either id or slug to read a post.'); }
const lookup = input.id ? { id: input.id } : { slug: input.slug }; const { entry, error, cacheHint } = await getLiveEntry('posts', lookup);
if (error) throw error; if (cacheHint) context.cache.set(cacheHint);
return { item: entry?.data }; }, });
if (!tool.execute) throw new Error('Content tool is not executable.');
const result = await tool.execute({ slug: 'hello-world' }, { toolCallId: 'read-post', messages: [], });
context.cache.set({ maxAge: 300, swr: 60 }); return Response.json(result);};For user-specific AI endpoints, create or read a request-scoped client from middleware/auth bridge state and opt out of route caching:
context.cache.set(false);const wp = context.locals.wp;Do not route personalized AI reads through Astro live loaders. Live loaders do not receive request, cookies, or locals.
Astro actions
Section titled “Astro actions”import { createCreatePostAction, createDeletePostAction, createUpdatePostAction, createCreateUserAction, createDeleteUserAction, createUpdateUserAction,} from 'wp-astrojs-integration';import { WordPressClient } from 'fluent-wp-client';
const wp = new WordPressClient({ baseUrl: import.meta.env.WP_URL, auth: { username: import.meta.env.WP_USERNAME, password: import.meta.env.WP_APP_PASSWORD, },});
export const server = { createPost: createCreatePostAction(wp), updatePost: createUpdatePostAction(wp), deletePost: createDeletePostAction(wp), createUser: createCreateUserAction(wp), updateUser: createUpdateUserAction(wp), deleteUser: createDeleteUserAction(wp),};Action factories accept an optional responseSchema for resource and ability actions when you want Astro-side response validation with a Standard Schema-compatible validator (for example Zod).
For a schema-generation and discovery-based workflow, see docs/typesafe-integration.mdx.
Auth bridge
Section titled “Auth bridge”The auth bridge is the central client-first request-auth layer for Astro middleware and actions.
Helpers like resolveUser() and isAuthenticated() only reflect end-user request auth.
getClient() and getClientConfig() can opt into bridge-level static credentials with { allowStaticAuthFallback: true } when you explicitly want a service-client fallback.
import { defineMiddleware } from 'astro:middleware';import { createCreatePostAction, createUpdatePostAction, createDeletePostAction, createWordPressAuthBridge,} from 'wp-astrojs-integration';
export const wordPressAuthBridge = createWordPressAuthBridge({ baseUrl: import.meta.env.WP_URL, cookieName: 'wp_user_session',});
// Middleware: get the authenticated client for this request.export const onRequest = defineMiddleware(async (context, next) => { const wp = await wordPressAuthBridge.getClient(context);
if (!wp) { return Response.redirect(new URL('/login', context.url), 302); }
const user = await wp.users().me(); context.locals.user = user;
return next();});
// Actions: reuse the same request-scoped client resolver.export const server = { login: wordPressAuthBridge.loginAction, createPost: createCreatePostAction(wordPressAuthBridge.getClient), updatePost: createUpdatePostAction(wordPressAuthBridge.getClient), deletePost: createDeletePostAction(wordPressAuthBridge.getClient),};You can also create one static client directly when you do not need request-scoped auth:
import { WordPressClient } from 'fluent-wp-client';
const wp = new WordPressClient({ baseUrl: import.meta.env.WP_URL, auth: { username: import.meta.env.WP_USERNAME, password: import.meta.env.WP_APP_PASSWORD, },});
export const server = { createPost: createCreatePostAction(wp),};Auth utility exports
Section titled “Auth utility exports”Import auth helpers from fluent-wp-client, including:
createAuthResolverjwtAuthTokenResponseSchemajwtAuthErrorResponseSchemajwtAuthValidationResponseSchema
Use these when building custom login/session flows so you can share the same runtime validation and context-auth patterns as the built-in bridge.
Term actions (categories, tags, custom taxonomies)
Section titled “Term actions (categories, tags, custom taxonomies)”import { createCreateTermAction, createUpdateTermAction, createDeleteTermAction,} from 'wp-astrojs-integration';import { WordPressClient } from 'fluent-wp-client';
const wp = new WordPressClient({ baseUrl: import.meta.env.WP_URL, auth: { username: import.meta.env.WP_USERNAME, password: import.meta.env.WP_APP_PASSWORD, },});
export const server = { createCategory: createCreateTermAction(wp, { resource: 'categories' }), updateTag: createUpdateTermAction(wp, { resource: 'tags' }), deleteGenre: createDeleteTermAction(wp, { resource: 'genres' }),};Extending schemas
Section titled “Extending schemas”import { WordPressClient } from 'fluent-wp-client';import { postSchema } from 'fluent-wp-client/zod';import { wordPressPostLoader } from 'wp-astrojs-integration';import { z } from 'astro/zod';
const wp = new WordPressClient({ baseUrl: import.meta.env.PUBLIC_WORDPRESS_BASE_URL,});
const customPostSchema = postSchema.extend({ acf: z.object({ video_url: z.string().optional(), featured_color: z.string().optional(), }).optional(),});
const posts = defineLiveCollection({ loader: wordPressPostLoader(wp), schema: customPostSchema,});Live vs static loaders
Section titled “Live vs static loaders”| Feature | Live loaders | Static loaders |
|---|---|---|
| Freshness | Request-time | Build-time |
| Best for | SSR and frequently changing public/shared content | SSG and stable content |
| Astro API | defineLiveCollection | defineCollection |
| Content APIs | getLiveEntry, getLiveCollection | getEntry, getCollection |
| Post/page payload shape | Plain serializable objects | Plain serializable objects |
Live loaders accept fluent-wp-client collection filters through getLiveCollection(), including search, include, exclude, taxonomy filters, slug, and custom query params. Static loaders accept the same build-time query shape through options.filter; the filter is passed to listAll() and all matching pages are fetched during the build.
Development and testing
Section titled “Development and testing”npm run wp:startnpm run wp:statusnpm test # Run all test projectsnpm run test:integration # Integration project (actions/loaders/auth)npm run test:build # Static build projectnpm run wp:stopwp-env uses automatic port selection in this repository. If 8888 is unavailable, run npm run wp:status to see the active local WordPress URL and ports.
Other useful commands:
npm run test:watchnpm run wp:cleannpm run wp:statusnpm run buildLocal integration test environment:
.wp-env.jsondefines the local WordPress setup.tests/wp-env/contains mu-plugins and seeded content.tests/setup/global-setup.tsprovisions app password, JWT, cookie+nonce fixtures, and boots a real Astro dev server for theintegrationVitest project.tests/setup/env-loader.tsloads.test-env.jsonvalues for bothintegrationandstatic-buildprojects.tests/fixtures/astro-site/is the shared Astro fixture used by integration action tests (astro dev+/_actions/*), live collection runtime tests (src/live.config.ts+getLiveCollection()/getLiveEntry()), and the build integration test (astro buildwithASTRO_TEST_MODE=build).- Action integration suites call fixture
/_actions/*endpoints via HTTP; tests do not execute package action helpers directly in Vitest workers. tests/integration/contains Astro-facing integration tests for loaders (including live runtime and static build coverage), actions, auth bridge behavior, meta, ACF, and abilities.
- Reading content:
docs/reading-content.mdx - Testing guide:
docs/testing.mdx - Auth bridge:
docs/auth-action-bridge.mdx - Action overview:
docs/actions/index.mdx - Post actions:
docs/actions/posts.mdx - Term actions:
docs/actions/terms.mdx - User actions:
docs/actions/users.mdx - Ability actions:
docs/actions/abilities.mdx
License
Section titled “License”MIT