Architecture
This document explains how HeadlessCore is structured, how data flows between WordPress and Next.js, and where each responsibility lives.
High-level design
HeadlessCore follows the decoupled (headless) CMS pattern:
- WordPress = content API + admin UI
- Next.js = public website + performance layer
- WPGraphQL = contract between them
WordPress never renders the public theme to visitors when Enable Headless Mode is on. Instead, template_redirect sends browsers to the Next.js URL.
Request lifecycle (visitor)
Browser → GET https://yoursite.com/blog/my-post
│
▼
Next.js (Vercel/Node)
│
├─ Static HTML from ISR cache? → return immediately
│
└─ Cache miss / stale → fetch WPGraphQL
│
▼
WordPress /graphql
│
▼
JSON response → render React → cache for 60s
Request lifecycle (editor publishes)
Editor clicks "Update" in wp-admin
│
▼
WordPress save_post hook
│
▼
POST https://yoursite.com/api/revalidate
{ secret, path: "/blog/my-slug", tag: "post" }
│
▼
Next.js revalidatePath + revalidateTag
│
▼
Next visitor gets fresh content
WordPress theme layers
| File / folder | Responsibility |
|---|---|
functions.php |
Settings page, core CPTs, GraphQL themeSettings, contact mutation, licensing, revalidation webhook |
inc/extended-cpts.php |
Shop products, team, FAQ, pricing CPTs + GraphQL fields |
inc/demo-importer.php |
One-click sample content |
inc/admin-dashboard.php |
Dashboard health widget |
Next.js layers
| Path | Responsibility |
|---|---|
src/lib/graphql.ts |
All WPGraphQL fetch functions + mock fallback |
src/lib/mockData.ts |
Offline demo data when WP is unreachable |
src/lib/siteMode.ts |
Multipurpose mode config, sitemap routes |
src/lib/blogSearch.ts |
Mock-side blog search/pagination logic |
src/lib/postMapper.ts |
Maps raw GraphQL post nodes to BlogPost type |
src/lib/config.ts |
Environment variable accessors |
src/lib/seo.ts |
Metadata builders, JSON-LD |
src/lib/rateLimit.ts |
In-memory rate limiter for API routes |
src/app/page.tsx |
Homepage router — reads homeLayout from WP |
src/app/[...slug]/page.tsx |
Catch-all for any WordPress page URI |
src/components/BlockRenderer.tsx |
Gutenberg HTML enhancement |
Caching strategy
| Layer | Mechanism | TTL |
|---|---|---|
| Next.js ISR | export const revalidate = 60 |
60 seconds |
| On-demand | WordPress webhook → /api/revalidate |
Instant |
| GraphQL fetch tags | fetch(..., { next: { tags: ["posts"] } }) |
Tag-based invalidation |
| WP object cache | Standard WordPress (optional Redis) | Host-dependent |
Cache tags
| Tag | Invalidated when |
|---|---|
theme-settings |
Any theme option saved |
posts |
Post published/updated |
post-{slug} |
Specific post |
page-{uri} |
Specific page |
services |
Service CPT changed |
testimonials |
Testimonial CPT changed |
headless-products |
Product CPT changed |
team-members |
Team CPT changed |
faqs |
FAQ CPT changed |
pricing-plans |
Pricing CPT changed |
Security boundaries
See the full Security Guide for production hardening and a parallel comparison with WP Engine + Faust.js.
| Concern | Implementation |
|---|---|
| WP admin | Standard WordPress auth; not exposed on Next.js |
| Revalidation secret | Shared secret in WP settings + Next.js env; constant-time compare |
| Preview secret | Query param gate on /api/preview; constant-time compare |
| Draft content | JWT bearer token server-side only |
| Rate limiting | Per-IP on API routes; optional Upstash Redis for multi-instance |
| Contact form | GraphQL mutation + Turnstile CAPTCHA + honeypot + WP-side rate limits |
| GraphQL introspection | Disabled in production (WP_ENVIRONMENT_TYPE=production) |
| Security headers | CSP, HSTS, X-Frame-Options on Next.js; headers on WP CMS |
Mock fallback mode
If NEXT_PUBLIC_WORDPRESS_API_URL is unset or the GraphQL request fails, every function in graphql.ts returns data from mockData.ts. This lets you:
- Develop UI without a running WordPress instance
- Demo the product at conferences
- Run CI builds without a database
A banner is not shown automatically — check the homepage connection status card in multi mode, or the WP dashboard widget.
Multipurpose routing
themeSettings.homeLayout controls what renders at /:
homeLayout → page.tsx switch → <BlogPage /> | <CorporatePage /> | …
Demo routes (/blog, /corporate, etc.) always work regardless of mode, so clients can preview all layouts before choosing one.
Comparison with WP Engine Atlas / Faust.js
See Security — HeadlessCore vs WP Engine for a full parallel comparison (security, features, cost).
| Aspect | HeadlessCore | WP Engine Atlas |
|---|---|---|
| Hosting | Self-hosted | Managed platform |
| Cost | Server only | Platform subscription |
| WordPress | Any host | WP Engine required |
| Frontend | Next.js (you own) | Faust.js / Next.js |
| GraphQL | WPGraphQL (standard) | WPGraphQL + platform tooling |
| Revalidation | Custom webhook | Platform-integrated |
| Vendor lock-in | None | Platform-dependent |
Extension points
- New CPT — Register in
functions.phporinc/extended-cpts.php, add GraphQL fields, add fetcher ingraphql.ts, add mock data - New site mode — Add to
siteMode.ts, createsrc/app/{mode}/page.tsx, add WP admin dropdown option - New theme setting —
register_setting()+register_graphql_field()onThemeSettings - New API route — Add under
src/app/api/