WordPress

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

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.css
export 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.com

2. Auxiliares partilhados do WordPress

getAllPosts()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 { 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')),
    ),
  )

4. Índice do blogue

Usa getAllPosts() para que o arquivo não fique limitado a 50–100 itens.

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)),
          ),
        ),
      ),
    ),
  )

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 {
  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),
      ),
    ),
  )
}

6. Compilar

WP_URL=https://your-wordpress-site.com sitelo build

A 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