WordPress
Sur cette page
Traitez WordPress comme un CMS headless et aspirez le site entier : paginez /wp-json/wp/v2/posts, générez un fichier HTML par slug, et mettez les réponses de l’API en cache entre les builds.
Ce que vous obtenez
- Une page d’accueil listant les articles récents
/blog— archive complète de tous les articles/blog/[slug]— une page HTML statique par article (tient à des milliers d’articles)fetchWithCache, pour que les rebuilds réutilisent les réponses WP au lieu de tout retélécharger
Structure du projet
my-site/
sitelo.config.js
src/
lib/
wordpress.js # utilitaires REST WP (aspiration paginée)
index.ht.js # accueil — liste les derniers articles
blog/
index.ht.js # /blog — archive complète
[slug].ht.js # /blog/:slug — tous les articles
css/
styles.cssexport default {
site: 'https://example.com',
// des milliers de pages ? augmentez la concurrence
renderConcurrency: 16,
renderBatchSize: 128,
}1. Pointer vers votre site WordPress
L’API REST est active par défaut sur les WordPress modernes. Vérifiez-le sur https://votre-site.com/wp-json/wp/v2/posts.
Définissez WP_URL dans l’environnement (ou codez-le en dur le temps d’expérimenter) :
WP_URL=https://your-wordpress-site.com2. Utilitaires WordPress partagés
getAllPosts() lit X-WP-TotalPages et parcourt toutes les pages (WordPress plafonne per_page à 100). Sautez _embed pendant la collecte des slugs — ne récupérez les embeds que pour chaque article.
import { fetchWithCache } from 'sitelo'
const WP_URL = process.env.WP_URL ?? 'https://your-wordpress-site.com'
const PER_PAGE = 100 // maximum WP pour /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
}
/** Parcourt toutes les pages de /posts jusqu’à aspirer le site entier. */
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. Page d’accueil
import { getPosts, postPath } from './lib/wordpress.js'
export async function data() {
const posts = await getPosts({ perPage: 5 })
return { posts }
}
export default ({ data }) => `
<html lang="fr">
<head>
<title>Mon site</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Derniers articles du blog</h1>
<ul>
${data.posts
.map(
(post) => `
<li>
<a href="${postPath(post)}">${post.title.rendered}</a>
</li>`,
)
.join('')}
</ul>
<p><a href="/blog">Tous les articles</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: 'fr' },
head(
title('Mon site'),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
h1('Derniers articles du blog'),
ul(
...data.posts.map((post) =>
li(a({ href: postPath(post) }, post.title.rendered)),
),
),
p(a({ href: '/blog' }, 'Tous les articles')),
),
)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="fr">
<head>
<title>Mon site</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>Derniers articles du 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">Tous les articles</a></p>
</body>
</html>
)
}4. Index du blog
Utilisez getAllPosts() pour que l’archive ne soit pas plafonnée à 50–100 éléments.
import { getAllPosts, postPath } from '../lib/wordpress.js'
export async function data() {
// Archive complète — pagine à travers tout le site WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default ({ data }) => `
<html lang="fr">
<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() {
// Archive complète — pagine à travers tout le site WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default ({ data }) =>
html({ lang: 'fr' },
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() {
// Archive complète — pagine à travers tout le site WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default function BlogIndex({ data }) {
return (
<html lang="fr">
<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. Transformer chaque article en page statique
generateStaticParams doit renvoyer tous les slugs que vous voulez dans dist/. Paginez l’API ici — n’appelez pas getPosts({ perPage: 100 }) une seule fois pour vous arrêter là.
import {
getAllPosts,
getPostBySlug,
featuredImage,
} from '../lib/wordpress.js'
export async function generateStaticParams() {
const ripStarted = performance.now()
console.log(
`[wordpress] aspiration des articles… (${process.uptime().toFixed(1)}s depuis le démarrage)`,
)
// Aspire chaque article publié (des milliers, sans souci — 100 par requête)
const posts = await getAllPosts({
embed: false, // slugs seulement ; on saute _embed pour la vitesse
onPage: (page, totalPages, count) => {
console.log(`[wordpress] page ${page}/${totalPages} (${count} articles)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] aspirés ${posts.length} articles en ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s depuis le démarrage)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Article introuvable: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
const image = featuredImage(post)
return `
<html lang="fr">
<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] aspiration des articles… (${process.uptime().toFixed(1)}s depuis le démarrage)`,
)
// Aspire chaque article publié (des milliers, sans souci — 100 par requête)
const posts = await getAllPosts({
embed: false, // slugs seulement ; on saute _embed pour la vitesse
onPage: (page, totalPages, count) => {
console.log(`[wordpress] page ${page}/${totalPages} (${count} articles)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] aspirés ${posts.length} articles en ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s depuis le démarrage)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Article introuvable: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
const image = featuredImage(post)
return html({ lang: 'fr' },
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] aspiration des articles… (${process.uptime().toFixed(1)}s depuis le démarrage)`,
)
// Aspire chaque article publié (des milliers, sans souci — 100 par requête)
const posts = await getAllPosts({
embed: false, // slugs seulement ; on saute _embed pour la vitesse
onPage: (page, totalPages, count) => {
console.log(`[wordpress] page ${page}/${totalPages} (${count} articles)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] aspirés ${posts.length} articles en ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s depuis le démarrage)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Article introuvable: ${params.slug}`)
return { post }
}
export default function BlogPost({ data }) {
const { post } = data
const image = featuredImage(post)
return (
<html lang="fr">
<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 buildLe premier build passe une fois par WordPress et remplit le cache de fetch. Les suivants réutilisent les réponses de liste et de détail mises en cache (cache: 'auto' → système de fichiers en production) jusqu’à expiration de maxAge. Augmentez renderConcurrency dans sitelo.config.js si vous rendez des milliers de pages d’articles.
Remarques
Le HTML venu de WordPress
title.rendered et content.rendered sont des chaînes HTML fournies par WP. Insérez-les telles quelles dans votre gabarit (comme ci-dessus), ou nettoyez-les si vous ne faites pas entièrement confiance au CMS.
Contenu privé
Les routes REST publiques n’exposent que les articles publiés. Pour les brouillons ou une authentification personnalisée, passez des en-têtes dans le deuxième argument de fetchWithCache (l’init fetch standard) et utilisez un cacheKey stable.
Docs du chargement de données · Docs du routage