WPSSR

WPSSR

Предисловие

В начале данной статьи я хотел бы сделать небольшое лирическое отступление и рассказать о том, как я вообще пришёл к тому подходу, о котором буду рассказывать. А то, что это оказалось закономерным итогом нескольких лет я понял, готовя к публикации данную статью.

Определённым началом можно назвать лето 2020 года — помните, был тогда COVID-19 и мы всей планетой целый год дружно сидели по домам. А вот летом 20 был небольшой перерыв, в который мы работали в офисе. И вот, отойдя компанией за кофе, мне и мой коллега Василий и говорит:

— Слушай, а что у тебя сайт не на нексте?

— А что?

— Ну так красивей же было бы, с плавными переходами и без перезагрузок…

За точность разговора за давностью лет не ручаюсь, но смысл примерно был такой. Я тогда парировал, то это надо будет ещё как-то придумать, чтобы статьи редактировать, да ещё и ноду поднимать… Но, откровенно говоря, мысль меня тогда задела.

Потом у меня был ряд статей, опубликованных здесь, и выступления на митапах, и небольшая лекция онлайн. Всё их объединяла одна тема — WordPress и его REST API.

Рождение технологии

В прошлом году, когда Telegram мне выписал последнее китайское предупреждение, я понял, что конечно всякие платформы — это хорошо и удобно, но свой блог лучше.

Тогда же решил и обновить немного свой блог в плане визуала и технологий, т.к. предыдущая версия была сделана ещё в 2017 году и в целом уже морально устарела.

Захотелось чего-то нового. Тогда-то я и вспомнил тот разговор про использование Next’а и все плюшки, которые нам даёт использование JS-фреймворков. Но переежать полностью на новые рельсы не очень хотелось, так как и времени это бы заняло достаточно, да и административная часть в WordPress очень и очень нравится, хотелось бы внутреннее содержание сохранить.

Да, первое что приходит в голову в таком случае — просто написать пользовательскую часть на Next.js, вывести через API для него данные, а админскую часть WordPress’а куда-нибудь спрятать. Это достаточно распространённый подход, даже есть специальная библиотека для Next’а — Faust.js.

Однако, всё это в любом случае требует наличия как минимум двух отдельных сервисов — клиенского с Node/Next, и админского с php/WordPress. Возможно, для каких-то крупных проектов решение вполне себе годное, но для персонального блога…

В итоге получилось несколько условий, которым должен был соответствовать новый сайт:

  1. Работать под управлением WordPress
  2. Не использовать дополнительные отдельные сервисы, т.е. работать по принципу всё-в-одном
  3. Создавать пользовательский интерфейс с помощью js-фреймворков
  4. Поддерживать микроразметки для того, чтобы поисковые системы, социальные сети и мессенджеры могли корректно распознавать содержимое

Решение оказалось, лежало практически на поверхности.

Основной принцип WPSSR

Суть технологии типичная для всех SSR-технологий с той лишь разницей, что здесь данные для отображения готовятся на стороне php. И да, это пожалуй одна из самых проблемных частей данного подхода, т.к. для разработки нужен fullstack-разработчик на php/js.

Важная ремарка: очень много разработчиков на WordPress как раз представляют собой таких специалистов, поэтому проблема не столь глобальна

Данных подход стоит на 3 обязательном и 1 опциональном принципах:

  1. SSD (Server-Side Data) — данные, формируемые на стороне сервера, которые затем передаются в JS для последующей обработки
  2. Backend for Frontend (BFF) — паттерн, который нам указывает, что мы разрабатываем backend под конкретный клиент и все передаваемые данные должны быть подогнаны под него
  3. ShadowMarkup — разметка для поисковых систем, социальных сетей и прочих краулеров
  4. Managed object cache — необязательный аспект, который, однако, позволяет добиваться максимальной скорости работы

Теперь каждый из принципов давайте разберём отдельно.

Server-Side Data

Один из самых фундаментальных вопросов — как передать данные в JS с сервера? Обычно используется принцип гидратации, при котором на сервере генерируется статический html, который потом в браузере обрабатывается фреймворком и в случае успеха добавляет обработчики.

С PHP всё сложнее, т.к. он понятия не имеет, как правильно генерировать такой html-файл, да и в целом такой подход видится больше как тупиковый.

Конечно, есть всегда достаточно простой вариант — просто по передаваемому идентификатору (хоть по адресной строке, хоть по параметрам в разметке) запрашивать через REST API данные и отображать. Метод 100% рабочий, но с точки зрения UX вызывает вопросы — шаг с загрузкой данных хотелось бы пропустить. Мы же уже загрузили страницу, не так ли?

Если вы хотя бы раз задумывались над тем, как различные данные с сервера передаёт тот же Next, то вы обязательно увидели бы в теле страницы отдельный тег script с идентификатором __NEXT_DATA__.

Поэтому, чтобы не изобретать велосипед внутри велосипеда, можно поступить ровно так же, добавив при генерации html-файла данные в JSON-формате, которые при инициалиации js-приложение читает и на его основе создаёт нужное нам состояние. Только зададим этому контейнеру передачи данных идентификатор __wpssr.

Для примера приведу его здесь в очень сокращённом виде:

<script id="__wpssr">{    "page": "Post",    "payload": {        "slug": "wpssr",        "format": "standard",        "title": "WPSSR",        "date": {            "published": "2025-11-10 20:42:50",            "modified": "2025-11-10 20:42:50",        }    },    "global": {}</script>

Наш передаваемый объект состоит из 2 обязательных признаков:

  1. page — это шаблон страницы, который необходимо использовать для вывода данных; в зависимости от сложности проекта их может быть сколько угодно, в моём случае имеются как минимум HomePost и Search. Отметьте, что названия типа страницы пишется с большой буквы — в идеале, оно должно совпадать с названием компонента, который отвечает за её отрисовку
  2. payload — это передаваемые данные для страницы, которые ожидает указанный в первом параметре шаблон.
  3. global — параметр, в котором для страницы передаются глобальные данные для сайта; например, в моём блоге на всех страницах есть блок с автором (где моя аватарка и ссылки на другие ресурсы) — вот они передаются там

Для передачи данных из json в приложение используется ряд специальных функций, у которых есть одна корневая:

export const getWPSSR = (): TWPSSR<any> | null => {  const element = document.getElementById('__wpssr');  if (element?.textContent) {    try {      return JSON.parse(element.textContent);    } catch (e) {      return null;    }  }  return null;};

Принцип её работы достаточно прост — при инициализации компонент, отвечающий за отрисовку страницы, запрашивает данные из ssd-контейнера и если в нём содержится та страница, которая открыта в данный момент, то забираем данные из неё. Иначе запрашиваем через REST.

Теперь давайте разберём более подробно процесс запроса данных на примере главной страницы.

У нас есть компонент HomePage, который при инициализации получает изначальный стейт, который заполняется чере специальную функцию getHomeData:

export const getHomeData = (): TPostsList => {  const currentData = getWPSSRHomePage();  if (currentData && currentData.page === 'Home') {    return currentData.payload;  }  return initialPostsList;};

Внутри себя она вызывает ещё одну функцию getWPSSRHomePage, которая является по сути расширением ранее упомянутой getWPSSR:

export const getWPSSRHomePage = (): TWPSSR<TPostsList> => {  const data = getWPSSR();    if (data !== null && data.page === 'Home') {    return data;  }  return {        page: 'Home',        payload: initialPostsList,        meta: null    };};

Здесь я несколько раз перестраховываюсь и если что не так, то просто возвращаю initialPostsList, который представляет собой объект с пустым списком — намёк приложению, чтобы оно сделало запрос в API.

Backend for Frontend

Если мы взглянем на TPostsList, то нас там встретит вот такой объект:

export type TPostsList = PaginationDTO<PostCardDTO[]>;

Сейчас не будем вдаваться в подробности всех типов и интерфейсов, отметим только, что у нас появились названия с постфиксом DTO. Он здесь неспроста — это явное указание на то, что объект данного типа используется и на frontend’е, и на backend’е.

Для примера не будем уходить далеко — вот у нас есть PaginationDTO:

export interface PaginationDTO<T> {  total: number;  page: number;  lastPage: number;  items: T;}

И вот он же, но уже в PHP:

class PaginationDTO{    public function __construct(        public int $total,        public int $page,        public int $lastPage,        public array $items = [],    ){}    public function getItems(): array    {        return $this->items;    }}

Но не только универсальностью типов данных едины. Поскольку для frontend-приложения стандартный объект WP_Post подходит слабо, то вместо него приходит модернизированная версия:

export interface PostFullDTO {  slug: string;  format: PostFormat;  title: string;  description: string;  content: PostContentDTO[];  category: PostCategoryDTO;  keywords: string[];  thumbnail: ThumbnailDTO;  author: AuthorDTO;  date: PostDateDTO;  lastPosts: PostCardDTO[];  similar: PostCardDTO[];}

Да, здесь нет никаких стандартных ID и прочих атрибутов, только то, что нужно для js единым объектом.

Кстати, в данном объекте есть ещё один интересный параметр. Если у нас description — это по сути wp_posts.post_excerpt, то вот content уже не просто wp_posts.post_content. Это было не обязательно, но тут я заморочился и сделал так, чтобы блоки Gutenberg-редактора трансформировались в отдельные объекты — таким образом мы не занимаемся вставкой html, а можемт очень гибко управлять каждым элементом записи.

ShadowMarkup

Итак, мы передали на клиент данные нужного ему формата, в этом плане всё отлично. Но, если мы говорим про разработку сайтов, которые должны парсится в интернете, то нам необходимо создать микроразметку для сайта, чтобы различные роботы и краулеры смогли его проиндексировать.

Конечно, тут возникает дилемма — мы же вроде бы сделали уже один раз вёрстку на js, не переделывать же это вторично? Естественно, что не нужно. Но какая-то минимальная разметка нам в любом случае нужна.

И здесь нам на помощь сразу же приходит сам WordPress — при создании записей в редакторе они уже имеют нормальную разметку. Останется всего лишь добавить минимум.

Вот так будет выглядеть главная страница:

$markup =    '<header itemscope itemtype="https://schema.org/WebSite">' .        '<h1 itemprop="name" >'. $title .'</h1>' .        '<p itemprop="description" >'. $description .'</p>' .    '</header>' .    postsList('Последние публикации', $posts->items);

Причём заметьте, что на главной странице используется SSR-компонент для списка постов, поэтому он выводится отдельной функцией (чтобы не повторяться):

/** * @param string $title * @param PostCardDTO[]|array $posts * @return string */function postsList(    string $title,    array $posts): string{    return        '<main aria-labelledby="recent-posts" aria-label="'. $title .'">' .            '<h2 id="recent-posts">'. $title .'</h2>' .            '<ol>' .                implode('', array_map(function (PostCardDTO $cardDTO) {                    return                        '<li itemscope itemtype="https://schema.org/BlogPosting" itemprop="blogPost">' .                            postCard($cardDTO) .                        '</li>';                }, $posts)) .            '</ol>' .        '</main>';}

И в итоге получившуюся строку $markup мы передаём в специальную функцию, которая генерирует итоговый html со всеми микроразметками, а также ssd-контейнером.

Таким образом, когда на наш сайт попадёт любой краулер, то он не заблудится.

Также распознаются и ссылки на внешние ресурсы:

Managed object cache

Выше мы несколько раз рассматривали нестандартные объекты данных, которые могут быть как достаточно простыми, так и сложносоставными т.е. для одного объекта нужно выполнить несколько дополнительных запросов.

И если для сайтов с небольшим количеством посетителей это некритично, то вот при нагрузке это уже может иметь проблемы.

И здесь вступает последняя, необязательная ступень подхода — Managed object cache.

Принцип на самом деле максимально простой — мы создаём кэшированные объекты данных. Все методы получений данных оформлены в отдельные action-классы, которые имеют специальную проверку на наличие кэшированного результата:

public function execute(WP_Post $post): PostFullDTO{  $cached = (new WPSSRCache())->get('post', $post);  if($cached) {    return $cached;  }  // Тут дальнейшая логика}

При этом мы можем использовать как подключение к Redis (если таковой есть), так и просто сохранять в виде json-файлов. Независимо от этого мы получаем достаточно высокую скорость, при этом убираем ненужные запросы в базу, если у нас уже есть готовые данные.

При этом мы можем сохранять не только отдельные записи, но и даже списки записей для главной страницы, для страниц категорий и прочих таксономий.

Отдельно хочу заметить, что здесь мы генирируем именно что объекты данных, а не результирующий html — это необходимо для того, чтобы при запросе данных через REST мы получили тот же кэшированный объект.

Плюсы и минусы данного подхода

Плюсы и минусы данного подхода

Как и всего, у данного подхода есть преимущества и недостатки. Начнём, пожалуй, с последних и накинем несколько, которые видны сразу:

  1. Требования к разработчику: как и упоминал ранее, для того, чтобы пользоваться данной концепцией, необходимо быть fullstack-разработчиком.
  2. Отсутствие готового фреймворка: WPSSR — это не библиотека, а концепт, для реализации которого придётся реализовывать инфраструктуру руками: сборку JS, внедрение контейнера, маршрутизацию и т.д.
  3. Нестандартная реализация: плавно вытекающий из предыдущего пункта параметр, поскольку нет ни готовых паттернов, ни законченной формализованной документации, и работа с таким кодом в принципе может вызывать сложность.
  4. SEO и ShadowMarkup: требуют ручной работы, а следовательно, либо знаний у разработчика, либо наличие SEO-специалиста.

Теперь выделим и плюсы, которые есть:

  1. All-in-One: пожалуй, одно из главных преимуществ, т.к. вся система работает в рамках одного приложения, что существенно упрощает его развёртывание и управление, что особенно актуально для небольших проектов
  2. Не ограничены никаким js-фреймворком: кроме того, что мы используем WordPress, более ограничений с точки зрения использования js-фреймворков мы не имеем. Можно использовать вообще любой.
  3. Возможности WordPress: у нас уже имеется готовая, достаточно сильно проработанная система управления контентом, а кроме этого — буквально тысячи плагинов, расширяющих функционал.
  4. SEO- и share-friendly: использование микроразметки и статического HTML позволяет поисковым системам и соцсетям корректно парсить страницы.

Есть ещё несколько, возможно, спорных моментов, которые я всё же отнёс бы плюсам данного подхода:

  1. Скорость разработки интерфейсов: за многие года WordPress оброс просто тонной решений относительно того, как можно собирать пользовательский интерфейс. В конце концов можно вспомнить и текущий FSE-подход, когда тема — это просто набор блоков, который мы можем прямо в редакторе собирать. Но если мы говорим про кастомные решения, то использовать готовые UI-библиотеки всё равно гораздо проще, да и зачастую быстрее.
  2. Раздельная разработка: мы можем разделить процесс между frontend и backend разработчиками, что может существенно ускорить процесс.
  3. Авторизация: она уже есть на стороне WordPress, достаточно надёжная и в целом очень очевидная. Если нужно для авторизованных пользователей выдавать какой-то дополнительный контент — достаточно проверить его на серверной стороне, а далее уже заполнять ssd-контейнер нужными данными. В целом, даже сам механизм авторизации на js можно не делать, т.к. условный объект пользователя придёт сразу при инициализации, а если авторизация закончится, то первый rest-запрос вернёт 401 ошибку.

Здесь я привёл только те плюсы и минусы, которые вижу лично, а на деле их может быть гораздо больше.

Заключение

Наконец-то смог закрыть свой гештальт и рассказать о своём подходе 😎

Хотя идея и появилась достаточно давно, но вот опубличить результаты своих исследований смог только сейчас.

WPSSR — это гибридный подход между классическим WordPress и headless-архитектурой, ориентированный на небольшие и средние проекты.

Он выигрывает в простоте инфраструктуры, удобстве управления и SEO, но требует более глубоких технических навыков и ручной настройки.

На данном подходе уже написаны несколько сайтов, в том числе — этот блог, который вы сейчас читаете.

Самое главное, что у меня получилось сделать то, что хотелось — свобода фронтенда, мощь WordPress. И на самом деле мне настолько оно доставило, что пожалуй, большую часть сайтов, которые буду делать в дальнейшем, буду делать именно так.