WordPress Headless con Next.js 15: Architettura Completa per Agenzie [2026]

26 agosto 202616 minGuide

Cosa Significa WordPress Headless (e Perché nel 2026 Ha Senso)

WordPress headless separa il backend dal frontend. Il backend resta WordPress: gestione contenuti, media, utenti, plugin per estendere il data model. Il frontend diventa un’applicazione JavaScript separata, in questo caso Next.js 15, che consuma i dati via API e renderizza pagine statiche o server-side.

Il risultato? Pagine che caricano in sotto 200ms, Core Web Vitals vicino al punteggio massimo, e zero PHP sul percorso critico del visitatore. Nella nostra esperienza con siti client ad alto traffico, l’architettura headless riduce il TTFB del 60-80% rispetto a un setup WordPress tradizionale con Nginx e PHP-FPM ben configurato.

Ma non è tutto oro. Perdi le anteprime native di WordPress, i page builder visivi, e buona parte dei plugin che iniettano HTML nel frontend. Guadagni controllo totale sullo stack frontend, performance costanti, e la capacità di servire lo stesso contenuto a web app, mobile app, e sito pubblico da un’unica API.

Per le agenzie che gestiscono più siti WordPress, headless introduce una variabile interessante: puoi standardizzare il frontend su un unico template Next.js e variare solo il backend WordPress per ogni client. Un setup, dieci clienti, dieci branding diversi.

TL;DR

  • Architettura: WordPress come CMS headless + Next.js 15 App Router come frontend
  • API: REST API nativa o WPGraphQL per query tipizzate
  • Rendering: ISR (Incremental Static Regeneration) per contenuti che cambiano spesso
  • Deploy: Vercel, Railway, o self-hosted con Docker
  • Casi d’uso ideali: siti editoriali, portfolio, marketing site con budget performance strict
  • Casi da evitare: WooCommerce con checkout complesso, siti pieni di shortcode plugin

Quando Headless Ha Senso (e Quando No)

Headless vale la pena quando:

  • Il sito ha bisogno di PageSpeed costantemente sopra 95
  • Il team editoriale è a suo agio in WordPress ma quello tecnico vuole React
  • Devi servire lo stesso contenuto a web app, mobile app, e sito pubblico
  • Vuoi deploy su edge con zero PHP sul percorso utente

Headless diventa doloroso quando:

  • Il sito dipende da WooCommerce (lo stato del carrello è difficile da replicare fuori da PHP)
  • Usi molti plugin che renderizzano shortcode o iniettano HTML
  • Il team non ha esperienza JavaScript
  • Serve anteprima live dei blocchi Gutenberg istantanea

Onestamente, per la maggior parte dei siti aziendali italiani standard, un WordPress tradizionale ben ottimizzato con un tema leggero e cache configurata correttamente raggiunge performance accettabili. Headless è l’arma pesante: la usi quando il traditional stack non basta più.

Preparare WordPress come CMS Headless

Step 1: Verificare la REST API

La REST API è attiva di default in ogni installazione WordPress moderna. Per verificarla, apri nel browser:

https://tuodominio.com/wp-json/wp/v2/posts

Se vedi un array JSON di post, sei a posto. Next.js chiamerà questo endpoint server-side, quindi CORS non è un problema per le Server Components. Lo diventa solo se usi Client Components che fanno fetch dal browser.

Step 2: Configurare CORS (solo per Client Components)

Se hai componenti client che chiamano direttamente l’API WordPress dal browser, devi configurare CORS. Aggiungi questo al functions.php del tuo tema o in un mu-plugin:

add_action( 'rest_api_init', function() {
    remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
    add_filter( 'rest_pre_serve_request', function( $value ) {
        $origin = get_http_origin();
        $allowed = [
            'https://tuosito.vercel.app',
            'http://localhost:3000',
        ];
        if ( in_array( $origin, $allowed, true ) ) {
            header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
            header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );
            header( 'Access-Control-Allow-Credentials: true' );
        }
        return $value;
    } );
} );

Step 3: Installare WPGraphQL (opzionale ma consigliato)

La REST API funziona per query standard su post, page, e tassonomie. WPGraphQL ti dà un endpoint tipizzato dove richiedi solo i campi che ti servono, riducendo il payload e semplificando query annidate complesse. Installa il plugin WPGraphQL dalla directory ufficiale. Una volta attivo, l’endpoint GraphQL è disponibile su:

https://tuodominio.com/graphql

Una query base per recuperare post con titolo, slug, e immagine in evidenza:

query GetPosts {
  posts(first: 10) {
    nodes {
      id
      title
      slug
      date
      excerpt
      featuredImage {
        node {
          sourceUrl
          altText
          mediaDetails {
            width
            height
          }
        }
      }
    }
  }
}

Step 4: Creare una Application Password per le anteprime

Le anteprime bozza richiedono richieste autenticate. Vai su Utenti > Profilo, scorri fino a Password Applicazione, inserisci un nome tipo “Next.js Preview” e clicca Aggiungi. Copia la password subito perché viene mostrata una sola volta.

La userai come credenziale Basic Auth: codifica in Base64 username:password_applicazione e inviala nell’header Authorization per le richieste di anteprima.

Setup del Progetto Next.js 15 App Router

Step 5: Creare il progetto

npx create-next-app@latest mio-headless-wp --typescript --tailwind --app
cd mio-headless-wp

Il flag --app attiva l’App Router. Questa guida usa Next.js 15 con React 19 Server Components come default. Tutte le chiamate fetch avvengono nelle Server Components a meno che non aggiungi esplicitamente "use client" in cima al file.

Step 6: Variabili d’ambiente

Crea un file .env.local nella root del progetto:

NEXT_PUBLIC_WP_URL=https://tuodominio.com
WP_AUTH_USER=tuo-wp-username
WP_AUTH_PASS=tua-application-password
PREVIEW_SECRET=una-stringa-random
REVALIDATE_SECRET=altra-stringa-per-webhook

Mai committare .env.local su Git. Aggiungilo a .gitignore prima del primo push.

Step 7: Client API WordPress

Crea lib/wordpress.ts:

const WP_URL = process.env.NEXT_PUBLIC_WP_URL!;

export async function getAllPosts() {
  const res = await fetch(
    `${WP_URL}/wp-json/wp/v2/posts?_embed&per_page=100`,
    { next: { tags: ['posts'] } }
  );
  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json();
}

export async function getPostBySlug(slug: string) {
  const res = await fetch(
    `${WP_URL}/wp-json/wp/v2/posts?slug=${slug}&_embed`,
    { next: { tags: [`post-${slug}`] } }
  );
  if (!res.ok) throw new Error('Failed to fetch post');
  const data = await res.json();
  return data[0];
}

export async function getAllCategories() {
  const res = await fetch(
    `${WP_URL}/wp-json/wp/v2/categories?per_page=100`,
    { next: { tags: ['categories'] } }
  );
  if (!res.ok) throw new Error('Failed to fetch categories');
  return res.json();
}

La parte importante qui è next: { tags: [...] }. Questi tag permettono a Next.js di invalidare la cache selettivamente quando un contenuto cambia su WordPress, tramite on-demand revalidation.

Routing e Pagine in Next.js 15 App Router

Homepage con lista post

Crea app/page.tsx:

import Link from 'next/link';
import { getAllPosts } from '@/lib/wordpress';

export default async function HomePage() {
  const posts = await getAllPosts();

  return (
    <main className="max-w-4xl mx-auto px-4 py-8">
      <h1>Blog</h1>
      <section>
        {posts.map((post: any) => (
          <article key={post.id}>
            <Link href={`/blog/${post.slug}`}>
              <h2>{post.title.rendered}</h2>
            </Link>
            <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
          </article>
        ))}
      </section>
    </main>
  );
}

Questa è una Server Component. Il fetch avviene sul server, zero JavaScript inviato al client per il rendering della lista.

Pagina singolo articolo

Crea app/blog/[slug]/page.tsx:

import { getAllPosts, getPostBySlug } from '@/lib/wordpress';
import { notFound } from 'next/navigation';

export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post: any) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  if (!post) return notFound();

  return (
    <article className="max-w-3xl mx-auto px-4 py-8">
      <h1>{post.title.rendered}</h1>
      <div
        dangerouslySetInnerHTML={{ __html: post.content.rendered }}
      />
    </article>
  );
}

generateStaticParams pre-genera tutte le pagine dei post a build time. Con ISR, queste pagine vengono rigenerate su intervalli o su webhook, non ad ogni richiesta.

ISR: Incremental Static Regeneration

ISR è il motivo principale per cui Next.js 15 + WordPress headless batte un setup tradizionale. Invece di rigenerare ogni pagina ad ogni richiesta (SSR costoso) o di servire sempre HTML statico (stale), ISR rigenera le pagine in background quando i dati cambiano.

Revalidation basata su tempo

export const revalidate = 3600; // rigenera ogni ora

Aggiungi questa riga in cima a qualsiasi pagina e Next.js la rigenererà in background ogni ora, servendo nel frattempo la versione cache.

On-demand revalidation via webhook

Questo è il metodo migliore per agenzie. Quando un contenuto viene aggiornato su WordPress, un webhook notifica Next.js di rigenerare solo quella pagina. Zero attesa, zero risorse sprecate.

Crea app/api/revalidate/route.ts:

import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const body = await req.json();
  const secret = req.headers.get('x-revalidate-secret');

  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  if (body.slug) {
    revalidateTag(`post-${body.slug}`);
  }
  revalidateTag('posts');

  return NextResponse.json({ revalidated: true });
}

Su WordPress, aggiungi un webhook nel functions.php o via plugin quando un post viene salvato:

add_action( 'save_post_post', function( $post_id ) {
    if ( wp_is_post_revision( $post_id ) ) return;
    
    $post = get_post( $post_id );
    
    wp_remote_post( 'https://tuosito.vercel.app/api/revalidate', [
        'headers' => [
            'Content-Type' => 'application/json',
            'x-revalidate-secret' => 'tua-secret-qui',
        ],
        'body' => json_encode([
            'slug' => $post->post_name,
        ]),
        'timeout' => 10,
    ] );
} );

Così, ogni volta che un editor pubblica o aggiorna un post, WordPress chiama Next.js che rigenera solo la pagina interessata. Il visitatore vede sempre contenuto fresco, ma con performance da sito statico.

Gestione Immagini con next/image

Le immagini sono il problema più comune nei setup headless. WordPress serve immagini dal suo dominio, Next.js vuole ottimizzarle tramite next/image. Devi configurare i domini remoti.

In next.config.ts:

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'tuodominio.com',
      },
    ],
  },
};

export default nextConfig;

Poi usa next/image normalmente:

import Image from 'next/image';

// Dentro la componente post
{post.featured_image_url && (
  <Image
    src={post.featured_image_url}
    alt={post.title.rendered}
    width={1200}
    height={630}
    priority
  />
)}

Next.js ottimizza automaticamente: WebP, resize responsive, lazy loading. Zero plugin WordPress necessari.

Anteprima Bozza (Draft Preview)

Una delle funzionalità che perdi andando headless è l’anteprima nativa di WordPress. La ricostruisci con un endpoint dedicato.

Crea app/api/preview/route.ts:

import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const secret = searchParams.get('secret');
  const slug = searchParams.get('slug');

  if (secret !== process.env.PREVIEW_SECRET || !slug) {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
  }

  const res = NextResponse.redirect(
    `${process.env.NEXT_PUBLIC_WP_URL.replace(/\/$/, '')}/blog/${slug}?preview=true`
  );
  res.cookies.set('preview', 'true', {
    httpOnly: true,
    sameSite: 'strict',
  });
  return res;
}

Nella pagina del post, leggi il cookie e fai una chiamata autenticata se l’anteprima è attiva:

import { cookies } from 'next/headers';

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const isPreview = cookies().get('preview')?.value === 'true';
  
  const headers: HeadersInit = {};
  if (isPreview) {
    const auth = Buffer.from(
      `${process.env.WP_AUTH_USER}:${process.env.WP_AUTH_PASS}`
    ).toString('base64');
    headers.Authorization = `Basic ${auth}`;
  }

  const res = await fetch(
    `${process.env.NEXT_PUBLIC_WP_URL}/wp-json/wp/v2/posts?slug=${params.slug}&_embed&status=draft,publish`,
    { headers, cache: isPreview ? 'no-store' : 'force-cache' }
  );
  // ... rendering
}

Su WordPress, configura il link di anteprima nel functions.php:

add_filter( 'preview_post_link', function( $link, $post ) {
    return sprintf(
        'https://tuosito.vercel.app/api/preview?secret=%s&slug=%s',
        esc_html( 'tua-preview-secret' ),
        $post->post_name
    );
}, 10, 2 );

REST API vs WPGraphQL: Quale Scegliere

d>Manuale (TypeScript types da generare)

Criterio REST API WPGraphQL
Setup Zero, nativa in WordPress Plugin da installare
Payload size Più grande (campi fissi) Minimizzato (solo campi richiesti)
Query complesse Multiple chiamate necessarie Single query annidata
Caching HTTP cache standard POST, cache più complessa
Tipizzazione Schema introspezionabile, codegen nativo
Ackenze Documentazione ampia, esempi ovunque Documentazione buona, community minore

Nella nostra esperienza, REST API è sufficiente per il 90% dei siti. WPGraphQL brilla quando hai data model complessi con relazioni profonde (Custom Post Types con campi ACF correlati). Per un blog standard o un sito aziendale, REST con _embed è più semplice da mantenere.

Autenticazione: JWT, OAuth e Application Passwords

Per le chiamate pubbliche (lettura contenuti published), non serve autenticazione. Per le anteprime e le chiamate autenticate, hai tre opzioni:

Application Passwords (consigliato per agenzie)

Native in WordPress dalla 5.6. Crei una password per applicazione nel profilo utente e la usi come Basic Auth. Semplice, funziona, zero configurazione extra. Per un’agenzia che gestisce 20 siti, questo è il metodo più rapido.

const auth = Buffer.from(
  `${process.env.WP_AUTH_USER}:${process.env.WP_AUTH_PASS}`
).toString('base64');

fetch(`${WP_URL}/wp-json/wp/v2/posts?status=draft`, {
  headers: { Authorization: `Basic ${auth}` }
});

JWT Auth

Richiede il plugin JWT Authentication for WP REST API. Utile quando devi autenticare utenti front-end (login area clienti). Il token JWT scade e va rinnovato, il che aggiunge complessità.

OAuth 2.0

Eccessivo per la maggior parte dei casi d’uso agency. Lo consideriamo solo per integrazioni enterprise con flussi OAuth multi-tenant.

Per approfondire l’autenticazione REST API, abbiamo scritto una guida dedicata sugli endpoint personalizzati WordPress.

Deploy in Produzione

Opzione 1: Vercel (più semplice)

Vercel è il creatore di Next.js. Il deploy è diretto dal repo Git, preview deployment per ogni branch, edge network globale. Il piano gratuito basta per progetti piccoli, quello Pro ($20/mese) per siti client reali.

npm install -g vercel
vercel --prod

Opzione 2: Self-hosted con Docker

Per i client che richiedono data residency in Italia o non vogliono lock-in Vercel, Docker è la strada. Un docker-compose.yml base:

version: '3.8'
services:
  nextjs:
    build: .
    ports:
      - '3000:3000'
    environment:
      - NEXT_PUBLIC_WP_URL=https://wp.cliente.it
      - WP_AUTH_USER=${WP_AUTH_USER}
      - WP_AUTH_PASS=${WP_AUTH_PASS}
      - PREVIEW_SECRET=${PREVIEW_SECRET}
      - REVALIDATE_SECRET=${REVALIDATE_SECRET}
    restart: unless-stopped
  
  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./certs:/etc/nginx/certs
    depends_on:
      - nextjs
    restart: unless-stopped

Il Dockerfile per Next.js 15 standalone:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Abilita l’output standalone in next.config.ts:

const nextConfig: NextConfig = {
  output: 'standalone',
  // ... resto config
};

Questo setup gira su qualsiasi VPS con Docker. Costo: 5-10€/mese per un VPS Hetzner o Contabo, contro i $20+ di Vercel Pro. Per un’agenzia che gestisce molti siti, la differenza si somma in fretta.

Opzione 3: Railway

Railway è un mezzo termine. Deploy dal repo, ma più economico di Vercel e con più controllo sulla region di deploy. Utile per siti client che devono girare su server EU.

SEO: Cosa Cambia con Headless

La SEO in headless richiede attenzione specifica. Next.js 15 risolve molti problemi che nei framework precedenti erano manuali:

Metadata dinamico

Next.js 15 ha il Metadata API. In ogni pagina, esporta un oggetto metadata o una funzione generateMetadata:

import { Metadata } from 'next';
import { getPostBySlug } from '@/lib/wordpress';

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPostBySlug(params.slug);
  
  return {
    title: post.title.rendered,
    description: post.yoast_head_json?.description || post.excerpt.rendered.replace(/<[^>]*>/g, ''),
    openGraph: {
      title: post.title.rendered,
      type: 'article',
      images: post.featured_image_url ? [{ url: post.featured_image_url }] : [],
    },
  };
}

Sitemap

Next.js genera sitemap XML nativamente con app/sitemap.ts:

import { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/wordpress';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();
  const postUrls = posts.map((post: any) => ({
    url: `https://tuosito.com/blog/${post.slug}`,
    lastModified: new Date(post.modified),
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }));

  return [
    { url: 'https://tuosito.com', lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
    { url: 'https://tuosito.com/blog', lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 },
    ...postUrls,
  ];
}

robots.txt

// app/robots.ts
import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/', disallow: '/api/' },
    sitemap: 'https://tuosito.com/sitemap.xml',
  };
}

Per la gestione dei robots.txt e AI bot, headless ti dà controllo totale su quale bot può accedere a quale route.

GEO: Ottimizzazione per Motori AI

L’architettura headless ha un vantaggio non ovvio per la Generative Engine Optimization. I motori AI (ChatGPT, Perplexity, Gemini) prediligono contenuti strutturati, semanticamente chiari, e velocemente accessibili. Next.js 15 server-rendera HTML completo, che i crawler AI possono leggere senza eseguire JavaScript.

Punti chiave per GEO in headless:

  • Contenuto sempre nel HTML server-renderato (zero client-side rendering per contenuti pubblici)
  • Schema JSON-LD iniettato lato server, non via JavaScript client-side
  • Tabelle e liste strutturate che i motori AI possono estrarre direttamente
  • TTFB basso: i crawler AI hanno timeout aggressivi, server-side rendering su edge vince
  • llms.txt servito dalla root, con indicizzazione dei contenuti disponibili via API

Costi Reali per un’Agenzia

Voce Setup Traditional Setup Headless
Hosting WordPress 20-50€/mese 10-20€/mese (solo backend)
Hosting Frontend — 0-20€/mese (Vercel free/Pro)
Sviluppo tema 40-80 ore 80-160 ore (Next.js + API)
Manutenzione mensile 4-8 ore 2-6 ore (meno plugin, meno conflitti)
Aggiornamenti core/plugin Rischio alto (plugin tema) Rischio basso (backend isolato)

Il costo di sviluppo iniziale è circa il doppio. Ma la manutenzione mensile è più bassa perché il backend WordPress ha molti meno plugin (niente cache, niente SEO plugin, niente page builder, niente form plugin nel backend). I backup sono più semplici perché riguarda solo il database e gli upload, non il tema.

FAQ: WordPress Headless con Next.js

Quanto costa passare a headless per un sito client esistente?

Dipende dalla complessità del sito. Per un sito aziendale con 10-15 pagine e un blog, il rebuild richiede 80-120 ore di sviluppo. Per un e-commerce WooCommerce, raddoppia o triplica. Considera che il backend WordPress va pulito: rimozione plugin inutili, disattivazione del tema frontend, configurazione CORS e API.

Si possono usare i plugin WordPress nel frontend headless?

No. I plugin che renderizzano HTML nel frontend (form plugin, page builder, slider) non funzionano in headless. I plugin che estendono il data model (ACF, Custom Post Type UI, WPGraphQL) continuano a funzionare perché agiscono sul backend e sui dati esposti via API.

Headless è più sicuro di WordPress tradizionale?

Sì, in parte. La superficie di attacco del frontend è zero (zero PHP esposto al web). Il backend WordPress va protetto con le stesse pratiche di hardening, ma puoi limitare l’accesso pubblico alla cartella /wp-admin via IP allowlist o basic auth Nginx, dato che i visitatori non toccano mai il backend.

Come si gestiscono i form di contatto in headless?

I form vanno gestiti lato Next.js. Puoi usare un endpoint API Next.js che invia i dati al backend WordPress via REST API (creando un Custom Post Type “lead” o usando un plugin come Contact Form 7 con REST API), oppure usare servizi esterni come Formspree, Resend, o un’API custom. In AgencyPilot abbiamo integrato la gestione lead direttamente nel dashboard, bypassando WordPress per i form.

WooCommerce funziona in headless?

Parzialmente. WooCommerce espone una REST API per prodotti, ordini, e clienti. Ma il checkout (gestione carrello, calcolo spedizione, gateway di pagamento) è complesso da replicare fuori da PHP. Esistono soluzioni come WooCommerce Blocks e il Store API, ma per store con logiche di pricing complesse, headless è ancora doloroso. Per e-commerce semplici (catalogo + checkout PayPal/Stripe), si fa. Per shop complessi, resta tradizionale.

Next.js 15 richiede React 19?

Next.js 15 supporta React 19 come default. Le Server Components, il Metadata API, e il sistema di cache basato su tag funzionano meglio con React 19. Puoi usare React 18 con il flag --react-18, ma per nuovi progetti conviene partire con React 19.

Gestisci i siti WordPress dei tuoi clienti?

AgencyPilot ti dà report AI, uptime monitoring, backup e portale clienti in un’unica dashboard. Gratis per 3 siti.

Prova gratis
Leggi anche
Tutti gli articoli
Tutti gli articoli