WordPress
In questa pagina
Tratta WordPress come un CMS headless e scarica tutto il sito: percorri a pagine /wp-json/wp/v2/posts, genera un file HTML per slug e metti in cache le risposte dell’API fra una build e l’altra.
Cosa ottieni
- Una home page che elenca gli articoli recenti
/blog— archivio completo di tutti gli articoli/blog/[slug]— una pagina HTML statica per articolo (regge migliaia di articoli)fetchWithCachecosì le ricostruzioni riusano le risposte di WP invece di riscaricare tutto
Struttura del progetto
my-site/
sitelo.config.js
src/
lib/
wordpress.js # helper REST di WP (scarico paginato)
index.ht.js # home — elenca gli ultimi articoli
blog/
index.ht.js # /blog — archivio completo
[slug].ht.js # /blog/:slug — ogni articolo
css/
styles.cssexport default {
site: 'https://example.com',
// migliaia di pagine? alza la concorrenza
renderConcurrency: 16,
renderBatchSize: 128,
}1. Punta al tuo sito WordPress
Nelle versioni moderne di WordPress la REST API è attiva per impostazione predefinita. Verificalo su https://your-site.com/wp-json/wp/v2/posts.
Imposta WP_URL nell’ambiente (oppure scrivilo nel codice mentre sperimenti):
WP_URL=https://your-wordpress-site.com2. Helper condivisi per WordPress
getAllPosts() legge X-WP-TotalPages e percorre ogni pagina (WordPress limita per_page a 100). Salta _embed mentre raccogli gli slug — scarica gli embed solo per i singoli articoli.
import { fetchWithCache } from 'sitelo'
const WP_URL = process.env.WP_URL ?? 'https://your-wordpress-site.com'
const PER_PAGE = 100 // massimo di WP per /wp/v2/posts
async function wpFetch(path, query = {}) {
const url = new URL(`/wp-json/wp/v2${path}`, WP_URL)
for (const [key, value] of Object.entries(query)) {
if (value != null) url.searchParams.set(key, String(value))
}
const res = await fetchWithCache(url, undefined, {
maxAge: 3600,
cache: 'auto',
})
if (!res.ok) {
throw new Error(`WordPress ${res.status}: ${url}`)
}
return {
data: await res.json(),
totalPages: Number(res.headers.get('X-WP-TotalPages') ?? 1),
total: Number(res.headers.get('X-WP-Total') ?? 0),
}
}
export async function getPosts({ page = 1, perPage = 20, embed = true } = {}) {
const { data } = await wpFetch('/posts', {
page,
per_page: perPage,
_embed: embed ? '1' : undefined,
})
return data
}
/** Percorri ogni pagina di /posts finché il sito non è scaricato del tutto. */
export async function getAllPosts({
perPage = PER_PAGE,
embed = false,
onPage,
} = {}) {
const first = await wpFetch('/posts', {
page: 1,
per_page: perPage,
_embed: embed ? '1' : undefined,
})
const posts = [...first.data]
onPage?.(1, first.totalPages, posts.length)
for (let page = 2; page <= first.totalPages; page += 1) {
const next = await wpFetch('/posts', {
page,
per_page: perPage,
_embed: embed ? '1' : undefined,
})
posts.push(...next.data)
onPage?.(page, first.totalPages, posts.length)
}
return posts
}
export async function getPostBySlug(slug) {
const { data } = await wpFetch('/posts', {
slug,
_embed: '1',
})
return data[0] ?? null
}
export function postPath(post) {
return `/blog/${post.slug}`
}
export function featuredImage(post) {
return post._embedded?.['wp:featuredmedia']?.[0]?.source_url
}3. Home page
import { getPosts, postPath } from './lib/wordpress.js'
export async function data() {
const posts = await getPosts({ perPage: 5 })
return { posts }
}
export default ({ data }) => `
<html lang="it">
<head>
<title>Il mio sito</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Ultimi dal blog</h1>
<ul>
${data.posts
.map(
(post) => `
<li>
<a href="${postPath(post)}">${post.title.rendered}</a>
</li>`,
)
.join('')}
</ul>
<p><a href="/blog">Tutti gli articoli</a></p>
</body>
</html>
`import { html, head, title, link, body, h1, ul, li, a, p } from 'javascript-to-html'
import { getPosts, postPath } from './lib/wordpress.js'
export async function data() {
const posts = await getPosts({ perPage: 5 })
return { posts }
}
export default ({ data }) =>
html({ lang: 'it' },
head(
title('Il mio sito'),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
h1('Ultimi dal blog'),
ul(
...data.posts.map((post) =>
li(a({ href: postPath(post) }, post.title.rendered)),
),
),
p(a({ href: '/blog' }, 'Tutti gli articoli')),
),
)import { getPosts, postPath } from './lib/wordpress.js'
export async function data() {
const posts = await getPosts({ perPage: 5 })
return { posts }
}
export default function Home({ data }) {
return (
<html lang="it">
<head>
<title>Il mio sito</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>Ultimi dal blog</h1>
<ul>
{data.posts.map((post) => (
<li key={post.slug}>
<a href={postPath(post)}>{post.title.rendered}</a>
</li>
))}
</ul>
<p><a href="/blog">Tutti gli articoli</a></p>
</body>
</html>
)
}4. Indice del blog
Usa getAllPosts() così l’archivio non si ferma a 50–100 elementi.
import { getAllPosts, postPath } from '../lib/wordpress.js'
export async function data() {
// Archivio completo — pagina attraverso tutto il sito WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default ({ data }) => `
<html lang="it">
<head>
<title>Blog</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Blog (${data.posts.length})</h1>
<ul>
${data.posts
.map(
(post) => `
<li>
<a href="${postPath(post)}">${post.title.rendered}</a>
<time>${post.date.slice(0, 10)}</time>
</li>`,
)
.join('')}
</ul>
</body>
</html>
`import { html, head, title, link, body, h1, ul, li, a, time } from 'javascript-to-html'
import { getAllPosts, postPath } from '../lib/wordpress.js'
export async function data() {
// Archivio completo — pagina attraverso tutto il sito WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default ({ data }) =>
html({ lang: 'it' },
head(
title('Blog'),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
h1(`Blog (${data.posts.length})`),
ul(
...data.posts.map((post) =>
li(
a({ href: postPath(post) }, post.title.rendered),
time(post.date.slice(0, 10)),
),
),
),
),
)import { getAllPosts, postPath } from '../lib/wordpress.js'
export async function data() {
// Archivio completo — pagina attraverso tutto il sito WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default function BlogIndex({ data }) {
return (
<html lang="it">
<head>
<title>Blog</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>Blog ({data.posts.length})</h1>
<ul>
{data.posts.map((post) => (
<li key={post.slug}>
<a href={postPath(post)}>{post.title.rendered}</a>
<time>{post.date.slice(0, 10)}</time>
</li>
))}
</ul>
</body>
</html>
)
}5. Scarica ogni articolo in pagine statiche
generateStaticParams deve restituire ogni slug che vuoi in dist/. Qui paginare l’API è obbligatorio — non chiamare getPosts({ perPage: 100 }) una volta sola e fermarti.
import {
getAllPosts,
getPostBySlug,
featuredImage,
} from '../lib/wordpress.js'
export async function generateStaticParams() {
const ripStarted = performance.now()
console.log(
`[wordpress] scarico articoli… (${process.uptime().toFixed(1)}s dall’inizio)`,
)
// Scarica ogni articolo pubblicato (migliaia vanno bene — 100 per richiesta)
const posts = await getAllPosts({
embed: false, // solo slug; salta _embed per velocità
onPage: (page, totalPages, count) => {
console.log(`[wordpress] pagina ${page}/${totalPages} (${count} articoli)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] scaricati ${posts.length} articoli in ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s dall’inizio)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Articolo non trovato: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
const image = featuredImage(post)
return `
<html lang="it">
<head>
<title>${post.title.rendered}</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<article>
<p><a href="/blog">← Blog</a></p>
<h1>${post.title.rendered}</h1>
<time>${post.date.slice(0, 10)}</time>
${image ? `<img src="${image}" alt="">` : ''}
<div class="content">
${post.content.rendered}
</div>
</article>
</body>
</html>
`
}import {
html, head, title, link, body, article, p, a, h1, time, img, div,
} from 'javascript-to-html'
import {
getAllPosts,
getPostBySlug,
featuredImage,
} from '../lib/wordpress.js'
export async function generateStaticParams() {
const ripStarted = performance.now()
console.log(
`[wordpress] scarico articoli… (${process.uptime().toFixed(1)}s dall’inizio)`,
)
// Scarica ogni articolo pubblicato (migliaia vanno bene — 100 per richiesta)
const posts = await getAllPosts({
embed: false, // solo slug; salta _embed per velocità
onPage: (page, totalPages, count) => {
console.log(`[wordpress] pagina ${page}/${totalPages} (${count} articoli)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] scaricati ${posts.length} articoli in ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s dall’inizio)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Articolo non trovato: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
const image = featuredImage(post)
return html({ lang: 'it' },
head(
title(post.title.rendered),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
article(
p(a({ href: '/blog' }, '← Blog')),
h1(post.title.rendered),
time(post.date.slice(0, 10)),
image ? img({ src: image, alt: '' }) : '',
div({ class: 'content' }, post.content.rendered),
),
),
)
}import {
getAllPosts,
getPostBySlug,
featuredImage,
} from '../lib/wordpress.js'
export async function generateStaticParams() {
const ripStarted = performance.now()
console.log(
`[wordpress] scarico articoli… (${process.uptime().toFixed(1)}s dall’inizio)`,
)
// Scarica ogni articolo pubblicato (migliaia vanno bene — 100 per richiesta)
const posts = await getAllPosts({
embed: false, // solo slug; salta _embed per velocità
onPage: (page, totalPages, count) => {
console.log(`[wordpress] pagina ${page}/${totalPages} (${count} articoli)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] scaricati ${posts.length} articoli in ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s dall’inizio)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Articolo non trovato: ${params.slug}`)
return { post }
}
export default function BlogPost({ data }) {
const { post } = data
const image = featuredImage(post)
return (
<html lang="it">
<head>
<title>{post.title.rendered}</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<article>
<p><a href="/blog">← Blog</a></p>
<h1>{post.title.rendered}</h1>
<time>{post.date.slice(0, 10)}</time>
{image ? <img src={image} alt="" /> : null}
<div
className="content"
dangerouslySetInnerHTML={{ __html: post.content.rendered }}
/>
</article>
</body>
</html>
)
}6. Build
WP_URL=https://your-wordpress-site.com sitelo buildLa prima build passa da WordPress una volta e riempie la cache delle fetch. Le build successive riusano le risposte di elenco e dettaglio in cache (cache: 'auto' → filesystem in produzione) finché maxAge non scade. Alza renderConcurrency in sitelo.config.js se stai renderizzando migliaia di pagine di articoli.
Note
HTML da WordPress
title.rendered e content.rendered sono stringhe HTML che arrivano da WP. Inseriscile nel tuo template così come sono (come qui sopra), oppure sanificale se non ti fidi del tutto del CMS.
Contenuti privati
Le rotte REST pubbliche espongono solo gli articoli pubblicati. Per le bozze o per un’autenticazione personalizzata, passa gli header nel secondo argomento di fetchWithCache (il normale init di fetch) e usa una cacheKey stabile.
Documentazione sul caricamento dati · Documentazione sul routing