Next.js Setup
Frontend lives in nextjs-starter-kit/ — Next.js 16, React 19, Tailwind CSS 4, App Router.
Environment variables
Copy .env.example to .env.local:
# Required for live WordPress data
NEXT_PUBLIC_WORDPRESS_API_URL=http://localhost/headleespro/graphql
NEXT_PUBLIC_WORDPRESS_URL=http://localhost/headleespro
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Server-only secrets (must match WordPress admin)
REVALIDATION_SECRET=
PREVIEW_SECRET=
# JWT for draft preview (server-only)
WORDPRESS_AUTH_TOKEN=
| Variable | Scope | Description |
|---|---|---|
NEXT_PUBLIC_WORDPRESS_API_URL |
Public | GraphQL endpoint |
NEXT_PUBLIC_WORDPRESS_URL |
Public | WP base URL for images and admin links |
NEXT_PUBLIC_SITE_URL |
Public | Frontend URL for SEO canonicals |
REVALIDATION_SECRET |
Server | Shared with WP for cache busting |
PREVIEW_SECRET |
Server | Shared with WP for preview URLs |
WORDPRESS_AUTH_TOKEN |
Server | JWT for fetching draft content |
MULTI_TENANT_ENABLED |
Server | Set to 1 to enable multi-tenant mode (see Multi-Tenant) |
I18N_ENABLED |
Server | Set to 1 to enable locale-prefixed routing (see Internationalization) |
If NEXT_PUBLIC_WORDPRESS_API_URL is unset or unreachable, all data functions fall back to src/lib/mockData.ts.
Multi-tenant (optional)
Off by default. When MULTI_TENANT_ENABLED=1, middleware.ts resolves the request Host header against tenants.config.json and sets an x-headlesscore-tenant header. getTenantConfig() in src/lib/tenant.ts returns per-tenant WordPress URLs and secrets; fetchAPI() uses them as the single choke point for all data fetching.
See Multi-Tenant for the full schema and known limitations.
Internationalization (optional)
Off by default. When I18N_ENABLED=1, middleware.ts parses a leading locale segment (/fr/blog), sets x-headlesscore-locale, and rewrites to the unprefixed path. getLocale() in src/lib/locale.ts reads that header; fetchAPI() sends X-HeadlessCore-Language to WordPress. UI chrome strings load from messages/{locale}.json via next-intl.
See Internationalization.
Scripts
npm run dev # Development server on :3000
npm run build # Production build
npm run start # Serve production build
npm run lint # ESLint
npm test # Vitest unit tests (41 tests)
npm run test:watch # Watch mode
From repo root: npm test runs tests in nextjs-starter-kit/.
Project structure
src/
├── app/
│ ├── page.tsx # Homepage router (respects homeLayout)
│ ├── layout.tsx # Global shell, nav, footer, GA
│ ├── sitemap.ts # Mode-aware sitemap
│ ├── blog/
│ │ ├── page.tsx # Blog listing + search + pagination
│ │ └── [slug]/page.tsx # Single post
│ ├── corporate/page.tsx # Agency layout
│ ├── portfolio/page.tsx # Freelancer layout
│ ├── business/page.tsx # Local business layout
│ ├── landing/page.tsx # Campaign landing page
│ ├── shop/page.tsx # Product catalog
│ ├── [...slug]/page.tsx # Dynamic WP pages + BlockRenderer
│ └── api/
│ ├── revalidate/route.ts
│ ├── preview/route.ts
│ └── exit-preview/route.ts
├── lib/
│ ├── graphql.ts # All WPGraphQL fetchers
│ ├── mockData.ts # Offline demo data
│ ├── siteMode.ts # 8-mode config + sitemap routes
│ ├── blogSearch.ts # Client-side search/filter/pagination
│ ├── postMapper.ts # GraphQL → BlogPost mapper
│ ├── rateLimit.ts # API rate limiter
│ ├── tenant.ts # Multi-tenant config resolver
│ ├── locale.ts # i18n locale resolver (server)
│ ├── localePaths.ts # Pure locale URL helpers (middleware-safe)
│ ├── config.ts # Site URL helpers
│ └── seo.ts # Metadata + JSON-LD
└── components/
├── BlockRenderer.tsx # Gutenberg block → React
├── TeamSection.tsx # Corporate team grid
├── FaqSection.tsx # Accordion FAQ
├── PricingSection.tsx # Pricing table
├── FeaturedCarousel.tsx # Blog featured posts
├── ThemeToggle.tsx
├── SocialIcons.tsx
└── SafeImage.tsx # next/image with fallback
Data fetching pattern
All CMS data goes through src/lib/graphql.ts:
// Theme & settings
const settings = await getThemeSettings();
const profile = await getSiteProfile();
const menu = await getNavigationMenu();
// Blog
const posts = await getBlogPosts();
const paginated = await getBlogPostsPaginated({ page: 1, perPage: 9 });
const categories = await getBlogCategories();
// Corporate
const services = await getCorporateServices();
const caseStudies = await getCorporatePortfolio();
const team = await getTeamMembers();
const faqs = await getFaqItems();
const plans = await getPricingPlans();
// Portfolio
const projects = await getPortfolioProjects();
const skills = await getPortfolioSkills();
const timeline = await getTimelineItems();
// Business
const testimonials = await getTestimonials();
// Shop
const products = await getHeadlessProducts(); // CPT first
const wooProducts = await getShopProducts(); // WooCommerce fallback
// Dynamic pages
const content = await getContentByUri("about");
Functions use Next.js fetch with cache tags for on-demand revalidation:
fetchAPI(query, variables, { tags: ["posts"] });
Blog search & pagination
src/app/blog/page.tsx accepts URL search params:
| Param | Example | Description |
|---|---|---|
q |
?q=headless |
Full-text search |
category |
?category=tech |
Category slug filter |
page |
?page=2 |
Page number |
Server fetches from WordPress via getBlogPostsPaginated(). Client-side filtering uses blogSearch.ts with mock data fallback.
ISR & revalidation
- Default
revalidate = 60on layout pages - WordPress webhooks call
/api/revalidatewith path + tag revalidateTag(tag, "max")(Next.js 16 API)- Rate limited: 30 requests/minute per IP
See API Routes.
Preview mode
/api/preview enables draftMode() and redirects to the correct route. Layout and pages check draftMode().isEnabled to fetch draft content with JWT auth.
Yellow preview banner appears at top. Exit Preview links to /api/exit-preview.
BlockRenderer (Gutenberg)
src/components/BlockRenderer.tsx enhances common core block HTML with Tailwind classes for wp_page mode and dynamic pages. Pro blocks (CTA Banner, Pricing Table, FAQ Accordion, Testimonial Slider, Team Grid, Stats Counter) are fully styled server-side in WordPress and pass through unchanged.
Images
next.config.ts allows remote images from your WordPress host via images.remotePatterns. Uses next/image with fallbacks via SafeImage.
Testing
npm test
| Test file | Coverage |
|---|---|
blogSearch.test.ts |
Search, filter, pagination |
siteMode.test.ts |
Mode resolution, routes, nav |
rateLimit.test.ts |
Rate limiter allow/block |
tenant.test.ts |
Host → tenant ID resolution |
locale.test.ts |
Locale path parsing and prefixing |
seo.test.ts |
Metadata and JSON-LD builders |
BlockRenderer.test.ts |
Gutenberg HTML enhancement |
See Testing and CI in Deployment.
Customising a layout
- Pick a mode in WordPress or edit the page under
src/app/{mode}/page.tsx. - Add GraphQL fields in
functions.phpandgraphql.ts. - Extend mock data in
mockData.tsfor offline development. - Add components under
src/components/.
Scaffold new project
From monorepo root:
npm run create my-site
# or
npx create-headlesscore my-site
See CLI Scaffold.