Headless WordPress Custom Fields Guide — Next.js, Astro, Nuxt
Download Log in

Headless WordPress Custom Fields Guide

Headless WordPress means using WordPress as the CMS (for content editing) while rendering the frontend with a modern JavaScript framework like Next.js, Astro, Nuxt, or SvelteKit. It’s a powerful pattern for sites that need fast, modern frontends but want to keep WordPress’s familiar editor for content teams. The biggest challenge with headless WordPress? Custom fields. This guide covers everything you need to know.


Part 1: What headless WordPress is

The architecture

Traditional WordPress: one stack. WordPress PHP renders everything — frontend pages, admin screens, REST API. Your theme handles both content editing (via the admin) and frontend display (via PHP templates).

Headless WordPress: two stacks. WordPress handles content editing and data storage. A separate JavaScript frontend (running on Vercel, Netlify, Cloudflare Pages, etc.) fetches content from WordPress via REST API or GraphQL and renders pages independently.

Traditional:   [ WordPress ] → Content → Frontend (PHP templates)
                    ↑                          ↑
                    └──── same server ─────────┘

Headless:      [ WordPress ] → Content → [ JavaScript Frontend ]
                  (CMS)       (API)        (Next.js / Astro / etc.)

Why go headless

  • Performance. JavaScript-rendered sites with static generation are dramatically faster than PHP-rendered WordPress on first load and interactive operations.
  • Developer experience. Modern frontend frameworks have better tooling, TypeScript support, component libraries, and deployment pipelines than traditional PHP themes.
  • Scalability. Static-generated sites scale to any traffic without WordPress performance becoming a bottleneck.
  • Freedom from WordPress theme limitations. Your frontend isn’t constrained by WordPress’s template hierarchy, the loop, or PHP idioms.
  • Hire frontend developers. React, Vue, Svelte developers outnumber WordPress theme developers by 10x. Headless WordPress lets you hire from the larger pool.
  • Multi-platform content. One WordPress instance can feed multiple frontends — website, mobile app, kiosk display, email newsletter — via the same API.

Why NOT go headless

  • Added complexity. Two stacks to maintain instead of one. CI/CD pipelines. API authentication. Cache invalidation.
  • Preview breaks. WordPress’s “Preview” button renders in WordPress’s templates, not your headless frontend. Preview workflows need custom solutions.
  • Plugins that generate HTML don’t work. Classic WordPress plugins (contact forms, related posts, Yoast’s meta tags) output HTML assuming PHP rendering. Most don’t work in headless mode.
  • Non-developer content editors need training. The disconnect between what they see in WordPress admin and what users see on the frontend can be confusing.
  • Costs more to develop. Two codebases = more developer time.

For most WordPress sites, traditional WordPress is still the right choice. Headless is the right choice when you need its specific advantages and accept the complexity.


Part 2: Why custom fields are hard in headless

Custom fields are one of the biggest friction points in headless WordPress. Here’s why.

Custom fields are plugin-defined, not core WordPress

WordPress core has standard fields — title, content, excerpt, featured image, author, date. These are well-defined, consistent across all WordPress sites, and fully supported by headless frontend SDKs (wp-rest-api client libraries, WPGraphQL, etc.).

Custom fields are plugin-defined. ACF adds its own fields, stored in its own way, exposed via its own API. Meta Box does the same. Pods, Toolset, CMB2, Carbon Fields — each has its own approach.

Frontend SDKs don’t know about custom fields because they can’t — the definitions live in plugin code, not core WordPress.

Type safety doesn’t exist by default

TypeScript can’t infer custom field types from a WordPress API response. A fetch('/wp-json/wp/v2/posts/123') call returns a generic Post object plus an “ACF” property with whatever fields the plugin added. TypeScript sees this as any or Record<string, unknown>.

The result: data.custom_fields.hero_title compiles fine even if the field is named hero_headline. Typos become runtime errors.

Schema drift across environments

A developer adds a new field group on their local WordPress. They push code to staging. The staging WordPress doesn’t have that field group yet. The frontend code that expects the new field breaks.

Without schema synchronization, headless WordPress becomes a debugging nightmare.

REST API limitations

WordPress’s REST API exposes custom fields inconsistently:

  • Some plugins expose them under meta (default WP core behavior)
  • Some expose them under acf (ACF’s integration)
  • Some expose them under a custom namespace
  • Image fields return only IDs, not full image objects
  • Relational fields return IDs, requiring additional API calls

For a Next.js component to render a Hero block, it might need 3 separate API calls: one for the post, one for the featured image, one for the related post referenced in a relationship field. This is slow and complex.

GraphQL solves some problems but needs setup

GraphQL (via WPGraphQL) is a better fit for headless because it lets you query deeply nested data in a single request. But WPGraphQL doesn’t know about custom fields — you need a separate “WPGraphQL for ACF” plugin to bridge the gap, and it’s maintained independently, so it lags behind ACF updates.


Part 3: How Field Forge solves headless WordPress custom fields

Field Forge was designed with headless WordPress in mind. It addresses every pain point listed above.

Custom table storage for fast API responses

Instead of wp_postmeta, Field Forge stores field values in a dedicated indexed table. API responses that pull custom fields are dramatically faster — 3–10x faster on large sites. For headless workflows where the frontend fetches content from WordPress on every build (or every request for SSR), this matters.

Custom table storage feature →

TypeScript type generation

Field Forge auto-generates .d.ts TypeScript definitions for every field group. Download the file, commit it to your frontend repo, get full type safety:

import type { HeroSectionFields } from '@/types/fieldforge';

function Hero({ data }: { data: HeroSectionFields }) {
  return <h1>{data.hero_title}</h1>;  // TypeScript autocomplete + error checking
}

Types update automatically when you edit field groups. Re-run the TypeScript export to sync.

TypeScript generation feature →

Native WPGraphQL integration

When Field Forge and WPGraphQL are both active, Field Forge auto-registers GraphQL types for every field group on activation. Zero configuration. Custom fields become queryable immediately:

query GetPage {
  page(id: "home", idType: URI) {
    title
    fieldforge {
      heroSection {
        title
        subtitle
        backgroundImage { sourceUrl }
      }
    }
  }
}

No separate WPGraphQL for ACF plugin needed.

GraphQL generation feature →

REST API with inline custom fields

Field Forge exposes custom field values on core REST endpoints under a fieldforge property:

GET /wp-json/wp/v2/posts/123

{
  "id": 123,
  "title": { "rendered": "Our Services" },
  "fieldforge": {
    "service_list": [
      { "name": "Web Design", "description": "..." },
      { "name": "SEO", "description": "..." }
    ]
  }
}

One API call returns the post + all custom fields. No N+1 problem.

Schema synchronization via Local JSON

Field Forge’s Local JSON Sync saves field groups as JSON files in your theme directory. Commit to git. Deploy across environments. Dev, staging, and production always have the same schema.


Part 4: Next.js + Field Forge setup

Here’s a complete setup guide for Next.js with Field Forge.

Install WordPress with Field Forge

  1. Install WordPress (traditional or managed host)
  2. Install Field Forge (free version on WordPress.org is fine for testing)
  3. Create field groups in Field Forge for your content types

Install Next.js frontend

npx create-next-app@latest my-headless-site --typescript --app
cd my-headless-site

Create a WordPress API client

Create lib/wordpress.ts:

const WP_API_URL = process.env.WP_API_URL || 'https://wp.example.com/wp-json';

export async function fetchPost(slug: string) {
  const res = await fetch(`${WP_API_URL}/wp/v2/posts?slug=${slug}`);
  const posts = await res.json();
  return posts[0];
}

export async function fetchPage(slug: string) {
  const res = await fetch(`${WP_API_URL}/wp/v2/pages?slug=${slug}`);
  const pages = await res.json();
  return pages[0];
}

Import Field Forge TypeScript types

Download .d.ts file from Field Forge admin: Field Forge → Tools → TypeScript Export → Download.

Save to types/fieldforge.d.ts:

export interface WPImage {
  id: number;
  url: string;
  alt: string;
  sizes: {
    thumbnail: string;
    medium: string;
    large: string;
    full: string;
  };
}

export interface HeroSectionFields {
  title: string;
  subtitle: string;
  background_image: WPImage;
  cta_button: {
    text: string;
    url: string;
  };
}

export interface PageWithFields {
  id: number;
  title: { rendered: string };
  fieldforge: {
    hero_section?: HeroSectionFields;
  };
}

Create a page component

app/[slug]/page.tsx:

import { fetchPage } from '@/lib/wordpress';
import type { PageWithFields, HeroSectionFields } from '@/types/fieldforge';

export default async function Page({ params }: { params: { slug: string } }) {
  const page: PageWithFields = await fetchPage(params.slug);

  return (
    <main>
      <h1>{page.title.rendered}</h1>

      {page.fieldforge.hero_section && (
        <Hero data={page.fieldforge.hero_section} />
      )}
    </main>
  );
}

function Hero({ data }: { data: HeroSectionFields }) {
  return (
    <section className="hero">
      <img
        src={data.background_image.url}
        alt={data.background_image.alt}
      />
      <h2>{data.title}</h2>
      <p>{data.subtitle}</p>
      <a href={data.cta_button.url}>{data.cta_button.text}</a>
    </section>
  );
}

That’s a minimal Next.js + Field Forge setup. TypeScript autocomplete works. Refactoring field names is safe. Building fails if you typo a property.

Static generation with generateStaticParams

For static site generation (fastest option), fetch all pages at build time:

export async function generateStaticParams() {
  const res = await fetch(`${WP_API_URL}/wp/v2/pages`);
  const pages = await res.json();

  return pages.map((page: any) => ({
    slug: page.slug,
  }));
}

Next.js will pre-render every page as static HTML at build time.


Part 5: Astro + Field Forge setup

Astro is excellent for content sites with headless WordPress.

Create Astro project

npm create astro@latest my-site
cd my-site

Fetch WordPress content

Create src/lib/wordpress.ts:

const WP_API_URL = import.meta.env.WP_API_URL;

export async function getPages() {
  const res = await fetch(`${WP_API_URL}/wp/v2/pages`);
  return res.json();
}

export async function getPageBySlug(slug: string) {
  const res = await fetch(`${WP_API_URL}/wp/v2/pages?slug=${slug}`);
  const [page] = await res.json();
  return page;
}

Page with Field Forge types

src/pages/[slug].astro:

---
import { getPages, getPageBySlug } from '@/lib/wordpress';
import type { PageWithFields } from '@/types/fieldforge';
import Hero from '@/components/Hero.astro';

export async function getStaticPaths() {
  const pages = await getPages();
  return pages.map((page: any) => ({
    params: { slug: page.slug },
    props: { page },
  }));
}

const { page } = Astro.props as { page: PageWithFields };
---

<html>
  <body>
    <h1 set:html={page.title.rendered} />

    {page.fieldforge.hero_section && (
      <Hero data={page.fieldforge.hero_section} />
    )}
  </body>
</html>

Astro’s static generation works well with Field Forge’s REST API — one build, all content fetched, static HTML output.


Part 6: Nuxt + Field Forge setup

For Vue developers, Nuxt is the go-to WordPress headless framework.

Create Nuxt project

npx nuxi@latest init my-site
cd my-site

Nuxt server route

server/api/page/[slug].get.ts:

export default defineEventHandler(async (event) => {
  const { slug } = getRouterParams(event);
  const config = useRuntimeConfig();

  const response = await $fetch(
    `${config.wpApiUrl}/wp/v2/pages?slug=${slug}`
  );

  return response[0];
});

Vue page component

pages/[slug].vue:

<template>
  <main v-if="page">
    <h1 v-html="page.title.rendered"></h1>

    <Hero v-if="page.fieldforge.hero_section" :data="page.fieldforge.hero_section" />
  </main>
</template>

<script setup lang="ts">
import type { PageWithFields } from '@/types/fieldforge';

const route = useRoute();
const { data: page } = await useFetch<PageWithFields>(`/api/page/${route.params.slug}`);
</script>

Part 7: SvelteKit + Field Forge

For Svelte fans:

// src/routes/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import type { PageWithFields } from '$lib/types/fieldforge';

export const load: PageServerLoad = async ({ params, fetch }) => {
  const res = await fetch(`${WP_API_URL}/wp/v2/pages?slug=${params.slug}`);
  const pages = await res.json();
  const page: PageWithFields = pages[0];

  return { page };
};
<!-- src/routes/[slug]/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
  import Hero from '$lib/components/Hero.svelte';

  export let data: PageData;
</script>

<h1>{@html data.page.title.rendered}</h1>

{#if data.page.fieldforge.hero_section}
  <Hero data={data.page.fieldforge.hero_section} />
{/if}

Part 8: GraphQL approach (WPGraphQL + Field Forge)

For sites needing deep nested queries, GraphQL is more efficient than REST:

Install WPGraphQL

  1. WordPress admin → Plugins → Add New → search “WPGraphQL”
  2. Install and activate
  3. Field Forge auto-registers GraphQL types on WPGraphQL activation

Query in Next.js

async function fetchHomepage() {
  const query = `
    query GetHomepage {
      page(id: "home", idType: URI) {
        title
        fieldforge {
          heroSection {
            title
            subtitle
            backgroundImage {
              sourceUrl
              altText
            }
            ctaButton {
              text
              url
            }
          }
        }
      }
    }
  `;

  const res = await fetch(`${WP_API_URL}/graphql`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query }),
  });

  const { data } = await res.json();
  return data.page;
}

One request fetches the entire page including custom fields. No N+1 problem.


Frequently asked questions

Should I use REST or GraphQL with headless WordPress?

Depends on the frontend and complexity. REST is simpler to set up. GraphQL is more efficient for deeply nested queries and reduces over-fetching. For small sites, REST. For sites with complex content structures, GraphQL.

How do I handle previews in headless WordPress?

Preview is one of the hardest parts of headless. Options: Next.js preview mode (fetches draft content with a secret token), Gatsby’s preview plugin, or custom WordPress → frontend webhook triggers.

How often should I rebuild my static site?

Depends on content update frequency. For daily-updated content, nightly builds. For less frequent updates, webhook-triggered builds (WordPress publishes a post → webhook triggers a build).

Can I edit content through the headless frontend?

Yes, via the WordPress REST API or WPGraphQL mutations. But for editorial workflows, most teams edit in WordPress admin and let the frontend read.

How do I handle forms in headless WordPress?

Options: 1) Form Forge posts to WordPress REST API, 2) Third-party form service (Formspree, Netlify Forms), 3) Direct API integration with form services. For most cases, Form Forge + REST API is the simplest.

What about media uploads?

Headless frontends typically still use WordPress’s media library for storage. The frontend fetches image URLs from API responses. For user-uploaded content, POST to WordPress’s media REST endpoint.


Ready to build headless WordPress with Field Forge?

Get Field Forge — from $35/year →

Free version includes 24 field types and the visual builder. Paid plans unlock TypeScript auto-generation, GraphQL auto-registration, REST API fields, and the rest of the headless tooling.

Forge AI Assistant Online

Hi! I'm the Field Forge AI assistant. Ask me anything about the plugin — setup, features, troubleshooting, or development.

Just now
Powered by Forge AI · Browse docs