WordPress
Nesta página
Trata o WordPress como um CMS headless e extrai o site inteiro: pagina por /wp-json/wp/v2/posts, gera um ficheiro HTML por slug e guarda em cache as respostas da API entre compilações.
O que obténs
- Uma página inicial a listar os artigos recentes
/blog— arquivo completo de todos os artigos/blog/[slug]— uma página HTML estática por artigo (aguenta milhares de artigos)fetchWithCache, para que as recompilações reutilizem as respostas do WP em vez de descarregar tudo de novo
Estrutura do projeto
my-site/
sitelo.config.js
src/
lib/
wordpress.js # auxiliares da REST do WP (extração paginada)
index.ht.js # início — lista os artigos mais recentes
blog/
index.ht.js # /blog — arquivo completo
[slug].ht.js # /blog/:slug — todos os artigos
css/
styles.cssexport default {
site: 'https://example.com',
// milhares de páginas? aumenta a concorrência
renderConcurrency: 16,
renderBatchSize: 128,
}1. Aponta para o teu site WordPress
A API REST vem ligada por omissão no WordPress moderno. Confirma em https://o-teu-site.com/wp-json/wp/v2/posts.
Define WP_URL no ambiente (ou escreve-o diretamente enquanto experimentas):
WP_URL=https://your-wordpress-site.com2. Auxiliares partilhados do WordPress
getAllPosts() lê X-WP-TotalPages e percorre todas as páginas (o WordPress limita per_page a 100). Salta _embed enquanto recolhes slugs — só vais buscar os embeds para cada artigo.
import { fetchWithCache } from 'sitelo'
const WP_URL = process.env.WP_URL ?? 'https://your-wordpress-site.com'
const PER_PAGE = 100 // máximo do WP para /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
}
/** Percorre todas as páginas de /posts até extrair o site inteiro. */
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. Página inicial
import { getPosts, postPath } from './lib/wordpress.js'
export async function data() {
const posts = await getPosts({ perPage: 5 })
return { posts }
}
export default ({ data }) => `
<html lang="pt">
<head>
<title>O meu site</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Últimas do blogue</h1>
<ul>
${data.posts
.map(
(post) => `
<li>
<a href="${postPath(post)}">${post.title.rendered}</a>
</li>`,
)
.join('')}
</ul>
<p><a href="/blog">Todos os artigos</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: 'pt' },
head(
title('O meu site'),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
h1('Últimas do blogue'),
ul(
...data.posts.map((post) =>
li(a({ href: postPath(post) }, post.title.rendered)),
),
),
p(a({ href: '/blog' }, 'Todos os artigos')),
),
)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="pt">
<head>
<title>O meu site</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>Últimas do blogue</h1>
<ul>
{data.posts.map((post) => (
<li key={post.slug}>
<a href={postPath(post)}>{post.title.rendered}</a>
</li>
))}
</ul>
<p><a href="/blog">Todos os artigos</a></p>
</body>
</html>
)
}4. Índice do blogue
Usa getAllPosts() para que o arquivo não fique limitado a 50–100 itens.
import { getAllPosts, postPath } from '../lib/wordpress.js'
export async function data() {
// Arquivo completo — pagina por todo o site WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default ({ data }) => `
<html lang="pt">
<head>
<title>Blogue</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Blogue (${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() {
// Arquivo completo — pagina por todo o site WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default ({ data }) =>
html({ lang: 'pt' },
head(
title('Blogue'),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
h1(`Blogue (${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() {
// Arquivo completo — pagina por todo o site WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default function BlogIndex({ data }) {
return (
<html lang="pt">
<head>
<title>Blogue</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>Blogue ({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. Transforma todos os artigos em páginas estáticas
generateStaticParams tem de devolver todos os slugs que queres em dist/. Pagina a API aqui — não chames getPosts({ perPage: 100 }) uma vez e fiques por aí.
import {
getAllPosts,
getPostBySlug,
featuredImage,
} from '../lib/wordpress.js'
export async function generateStaticParams() {
const ripStarted = performance.now()
console.log(
`[wordpress] a extrair artigos… (${process.uptime().toFixed(1)}s desde o arranque)`,
)
// Extrai todos os artigos publicados (milhares não são problema — 100 por pedido)
const posts = await getAllPosts({
embed: false, // apenas slugs; salta _embed por velocidade
onPage: (page, totalPages, count) => {
console.log(`[wordpress] página ${page}/${totalPages} (${count} artigos)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] extraídos ${posts.length} artigos em ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s desde o arranque)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Artigo não encontrado: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
const image = featuredImage(post)
return `
<html lang="pt">
<head>
<title>${post.title.rendered}</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<article>
<p><a href="/blog">← Blogue</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] a extrair artigos… (${process.uptime().toFixed(1)}s desde o arranque)`,
)
// Extrai todos os artigos publicados (milhares não são problema — 100 por pedido)
const posts = await getAllPosts({
embed: false, // apenas slugs; salta _embed por velocidade
onPage: (page, totalPages, count) => {
console.log(`[wordpress] página ${page}/${totalPages} (${count} artigos)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] extraídos ${posts.length} artigos em ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s desde o arranque)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Artigo não encontrado: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
const image = featuredImage(post)
return html({ lang: 'pt' },
head(
title(post.title.rendered),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
article(
p(a({ href: '/blog' }, '← Blogue')),
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] a extrair artigos… (${process.uptime().toFixed(1)}s desde o arranque)`,
)
// Extrai todos os artigos publicados (milhares não são problema — 100 por pedido)
const posts = await getAllPosts({
embed: false, // apenas slugs; salta _embed por velocidade
onPage: (page, totalPages, count) => {
console.log(`[wordpress] página ${page}/${totalPages} (${count} artigos)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] extraídos ${posts.length} artigos em ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s desde o arranque)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Artigo não encontrado: ${params.slug}`)
return { post }
}
export default function BlogPost({ data }) {
const { post } = data
const image = featuredImage(post)
return (
<html lang="pt">
<head>
<title>{post.title.rendered}</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<article>
<p><a href="/blog">← Blogue</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. Compilar
WP_URL=https://your-wordpress-site.com sitelo buildA primeira compilação passa uma vez pelo WordPress e enche a cache de fetch. As seguintes reutilizam as respostas de lista e de detalhe em cache (cache: 'auto' → sistema de ficheiros em produção) até maxAge expirar. Aumenta renderConcurrency em sitelo.config.js se estiveres a renderizar milhares de páginas de artigos.
Notas
HTML vindo do WordPress
title.rendered e content.rendered são strings HTML vindas do WP. Coloca-as no teu template tal como estão (como acima), ou sanitiza-as se não confiares totalmente no CMS.
Conteúdo privado
As rotas REST públicas só expõem artigos publicados. Para rascunhos ou autenticação própria, passa cabeçalhos no segundo argumento de fetchWithCache (o init normal do fetch) e usa um cacheKey estável.
Documentação de carregamento de dados · Documentação de rotas