N
HeadlessCoreHeadless Core
Back to Home

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 = 60 on layout pages
  • WordPress webhooks call /api/revalidate with 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

  1. Pick a mode in WordPress or edit the page under src/app/{mode}/page.tsx.
  2. Add GraphQL fields in functions.php and graphql.ts.
  3. Extend mock data in mockData.ts for offline development.
  4. 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.