WordPress

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

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

2. 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 { 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')),
    ),
  )

4. Indice del blog

Usa getAllPosts() così l’archivio non si ferma a 50–100 elementi.

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

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

6. Build

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

La 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