WordPress
На этой странице
Относитесь к WordPress как к headless-CMS и выкачайте сайт целиком: пройдите постранично по /wp-json/wp/v2/posts, создайте по HTML-файлу на каждый slug и кэшируйте ответы API между сборками.
Что вы получаете
- Главную страницу со списком недавних записей
/blog— полный архив всех записей/blog/[slug]— по одной статической HTML-странице на запись (работает и на тысячах записей)fetchWithCache, чтобы повторные сборки переиспользовали ответы WP вместо повторной загрузки всего
Структура проекта
my-site/
sitelo.config.js
src/
lib/
wordpress.js # помощники REST WP (постраничная выкачка)
index.ht.js # главная — список последних записей
blog/
index.ht.js # /blog — полный архив
[slug].ht.js # /blog/:slug — каждая запись
css/
styles.cssexport default {
site: 'https://example.com',
// тысячи страниц? поднимите параллелизм
renderConcurrency: 16,
renderBatchSize: 128,
}1. Укажите свой сайт WordPress
В современном WordPress REST API включён по умолчанию. Проверьте это по адресу https://ваш-сайт.com/wp-json/wp/v2/posts.
Задайте WP_URL в окружении (или впишите его прямо в код, пока экспериментируете):
WP_URL=https://your-wordpress-site.com2. Общие помощники WordPress
getAllPosts() читает X-WP-TotalPages и обходит все страницы (WordPress ограничивает per_page сотней). Пропускайте _embed при сборе slug — запрашивайте вложения только для отдельных записей.
import { fetchWithCache } from 'sitelo'
const WP_URL = process.env.WP_URL ?? 'https://your-wordpress-site.com'
const PER_PAGE = 100 // максимум WP для /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
}
/** Проходит все страницы /posts, пока сайт не будет выкачан целиком. */
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. Главная страница
import { getPosts, postPath } from './lib/wordpress.js'
export async function data() {
const posts = await getPosts({ perPage: 5 })
return { posts }
}
export default ({ data }) => `
<html lang="ru">
<head>
<title>Мой сайт</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Последнее в блоге</h1>
<ul>
${data.posts
.map(
(post) => `
<li>
<a href="${postPath(post)}">${post.title.rendered}</a>
</li>`,
)
.join('')}
</ul>
<p><a href="/blog">Все записи</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: 'ru' },
head(
title('Мой сайт'),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
h1('Последнее в блоге'),
ul(
...data.posts.map((post) =>
li(a({ href: postPath(post) }, post.title.rendered)),
),
),
p(a({ href: '/blog' }, 'Все записи')),
),
)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="ru">
<head>
<title>Мой сайт</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>Последнее в блоге</h1>
<ul>
{data.posts.map((post) => (
<li key={post.slug}>
<a href={postPath(post)}>{post.title.rendered}</a>
</li>
))}
</ul>
<p><a href="/blog">Все записи</a></p>
</body>
</html>
)
}4. Индекс блога
Используйте getAllPosts(), чтобы архив не обрывался на 50–100 элементах.
import { getAllPosts, postPath } from '../lib/wordpress.js'
export async function data() {
// Полный архив — постранично по всему сайту WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default ({ data }) => `
<html lang="ru">
<head>
<title>Блог</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1>Блог (${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() {
// Полный архив — постранично по всему сайту WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default ({ data }) =>
html({ lang: 'ru' },
head(
title('Блог'),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
h1(`Блог (${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() {
// Полный архив — постранично по всему сайту WP
const posts = await getAllPosts({ embed: false })
return { posts }
}
export default function BlogIndex({ data }) {
return (
<html lang="ru">
<head>
<title>Блог</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>Блог ({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. Превратите все записи в статические страницы
generateStaticParams должен вернуть все slug, которые вы хотите видеть в dist/. Пройдите API постранично прямо здесь — не вызывайте getPosts({ perPage: 100 }) один раз и на этом не останавливайтесь.
import {
getAllPosts,
getPostBySlug,
featuredImage,
} from '../lib/wordpress.js'
export async function generateStaticParams() {
const ripStarted = performance.now()
console.log(
`[wordpress] выкачиваю записи… (${process.uptime().toFixed(1)}s с момента запуска)`,
)
// Выкачивает все опубликованные записи (тысячи — не проблема, по 100 за запрос)
const posts = await getAllPosts({
embed: false, // только slug; пропускаем _embed ради скорости
onPage: (page, totalPages, count) => {
console.log(`[wordpress] страница ${page}/${totalPages} (${count} записей)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] выкачано ${posts.length} записей за ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s с момента запуска)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Запись не найдена: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
const image = featuredImage(post)
return `
<html lang="ru">
<head>
<title>${post.title.rendered}</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<article>
<p><a href="/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] выкачиваю записи… (${process.uptime().toFixed(1)}s с момента запуска)`,
)
// Выкачивает все опубликованные записи (тысячи — не проблема, по 100 за запрос)
const posts = await getAllPosts({
embed: false, // только slug; пропускаем _embed ради скорости
onPage: (page, totalPages, count) => {
console.log(`[wordpress] страница ${page}/${totalPages} (${count} записей)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] выкачано ${posts.length} записей за ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s с момента запуска)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Запись не найдена: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
const image = featuredImage(post)
return html({ lang: 'ru' },
head(
title(post.title.rendered),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
article(
p(a({ href: '/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] выкачиваю записи… (${process.uptime().toFixed(1)}s с момента запуска)`,
)
// Выкачивает все опубликованные записи (тысячи — не проблема, по 100 за запрос)
const posts = await getAllPosts({
embed: false, // только slug; пропускаем _embed ради скорости
onPage: (page, totalPages, count) => {
console.log(`[wordpress] страница ${page}/${totalPages} (${count} записей)`)
},
})
const ripSeconds = ((performance.now() - ripStarted) / 1000).toFixed(1)
console.log(
`[wordpress] выкачано ${posts.length} записей за ${ripSeconds}s` +
` (${process.uptime().toFixed(1)}s с момента запуска)`,
)
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPostBySlug(params.slug)
if (!post) throw new Error(`Запись не найдена: ${params.slug}`)
return { post }
}
export default function BlogPost({ data }) {
const { post } = data
const image = featuredImage(post)
return (
<html lang="ru">
<head>
<title>{post.title.rendered}</title>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<article>
<p><a href="/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. Сборка
WP_URL=https://your-wordpress-site.com sitelo buildПервая сборка один раз проходит через WordPress и наполняет кэш fetch. Последующие переиспользуют закэшированные ответы списков и деталей (cache: 'auto' → файловая система в продакшне), пока не истечёт maxAge. Поднимите renderConcurrency в sitelo.config.js, если рендерите тысячи страниц записей.
Заметки
HTML из WordPress
title.rendered и content.rendered — это строки HTML из WP. Вставляйте их в шаблон как есть (как выше) или очищайте, если не доверяете CMS полностью.
Закрытый контент
Публичные REST-маршруты отдают только опубликованные записи. Для черновиков или своей авторизации передайте заголовки во втором аргументе fetchWithCache (обычный init для fetch) и используйте стабильный cacheKey.
Документация по загрузке данных · Документация по маршрутизации