Как мы подружили WordPress, Gutenberg и Vue/Nuxt

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

Как мы подружили WordPress, Gutenberg и Vue/Nuxt

Привет! Я Серёжа Шилов, фаундер IT-аутсорс компании We Wizards. Моя команда занимается web&mobile разработкой для e-comm, лучше всего делаем e-commerce решения от складов до аналитики.

Все кейсы можно посмотреть на сайте.

Как мы подружили WordPress, Gutenberg и Vue/Nuxt

Кто клиент?

Катерина Перез — это блогер и эксперт в области ювелирных украшений, известная своими обзорами и рекомендациями по выбору и стилю украшений.

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

Что мы вообще делали?

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

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

Нам поручили именно разработку сайта.

Мы выбрали WordPress — во-первых, потому что это исторически блоговая платформа, а во-вторых, потому что там используется один из лучших в мире визуальных редакторов контента — Gutenberg.

В рамках этого проекта нам нужно было сделать платформу для блогов

Задача заключалась в том, чтобы дать контент-менеджерам возможность работать с редактором Gutenberg от WordPress, но в современном формате — через REST API.

Gutenberg — это современный визуальный редактор контента от WordPress. Он сам по себе написан на React, и блоки для него можно создавать либо с помощью React UI (специальной библиотеки от WordPress), либо на синтаксисе JSX. Однако такой подход предполагает монолитное решение — то есть фронтенд пришлось бы разрабатывать внутри WordPress. А мы хотели разделить API и клиентскую часть.

Поэтому выбрали следующий стек: WordPress в headless-режиме с Gutenberg и ACF — в роли CMS и API, а фронтенд — на Vue 3 с Nuxt. Ну а дальше — как пойдёт :)

Как мы подружили WordPress, Gutenberg и Vue/Nuxt

А почему Gutenberg — революционный и лучший редактор контента и по сегодняшний день, мы писали в этой статье https://wewizards.ru/news/kak-my-sozdaem-admin-paneli-o-kotoryh-mechtajut-vse-vladelcy-sajtov/"

Почему всё оказалось не так просто?

Сначала мы наткнулись на один неприятный момент: ACF (поля для блоков в WordPress) по умолчанию возвращает данные в API в плоском, неструктурированном виде. Причём одинаковые поля могут приходить в разных форматах, что добавляет путаницы.

В монолитной архитектуре Gutenberg строит всё из блоков — внутри блоков могут быть другие блоки и поля, но мы ожидали увидеть просто массив блоков с полями в них. А на деле получаем плоский JSON, и приходится разбираться с этим.

Чтобы это исправить, мы написали собственные фильтры и адаптер, который на бэке приводил все данные к нормальному формату.

Да, пришлось даже залезть в код ACF и разобраться, как он вообще формирует структуру. Но теперь у нас есть JSON, понятный фронту.

{ acf_fields: { title: "Онлайн курсы <br />и живые мероприятия", desc: "На сегодняшний день мы затрагиваем самые различные направления, которые позволят Вам совершенствоваться в каждом аспекте, познавать окружающий мир и себя в этом пространстве. <br />Практикуя и работая над собой, вы сможете быстро и легко перенестись на новый качественный уровень жизни.<br />", menu: [ { title: "Финансы", link: "#" }, { title: "Питание", link: "#" }, { title: "Отношения", link: "#" }, { title: "Энергия", link: "#" } ], show_menu_bg: true, bg_img: false, bottom_big_space: true, } }

Как выглядел фронт

На фронте мы выстроили такую архитектуру:

  • Все блоки приходят массивом в JSON.
  • У каждого блока есть slug — его уникальный идентификатор.
  • Компоненты блоков лежат в отдельной папке и подключаются по этому слагу.
  • Всё это динамически рендерится на странице через v-for и функцию findBlock().

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

const componentMap: Record<string, Component> = {}; Object.entries(import.meta.glob('~/components/block/*.vue')).forEach(([path, importFn]) => { componentMap[path.split('/').pop()?.replace('.vue', '') || ''] = defineAsyncComponent( importFn as () => Promise<Component>, ); }); export const getComponent = (name: string): Component => { const founded = componentMap[name]; if (!founded) console.warn(`Block "${name}" not found`); return founded; };

Данные приходили как попало, без валидации

Одно и то же поле могло быть строкой, объектом или вообще массивом. А так как на стороне WordPress контент-менеджер может в любой момент что-то поменять, не заполнить поле или внезапно переименовать его, всё могло пойти по наклонной.

На старте мы просто прокидывали огромные props и всё оборачивали в optional chaining, чтобы хотя бы не падало. Но это была временная мера — чтобы просто работало.

const props = defineProps<{ data: { acf_fields: { s_image_settings?: string; s_image_txt_settings?: string; s_icon: boolean; image_with_button_block?: { old_url_img: string | OldImg; img: IImage; btn_url?: string; btn_txt?: string; desc: string; s_txt?: string; }; image_with_txt?: { include_stars?: boolean; txt_1?: string; txt_2?: string; desc?: string; old_url_img: string | OldImg; img?: IImage; }; half_image_with_button_block?: { img_1: { liked: boolean; old_url_img: string | OldImg; id: number; url: string; width: number; height: number; alt: string; }; img_2: { liked: boolean; old_url_img: string | OldImg; id: number; url: string; width: number; height: number; alt: string; }; desc_1: string; desc_2: string; btn_txt_1: string; btn_txt_2: string; btn_url: string; btn_url_2: string; }; }; }; }>();

Как мы сделали "редактор в редакторе"

Gutenberg — штука красивая. И в идеале она должена показывать превью контента в реалтайме — то есть пока редактируешь данные, сразу видишь, как будет выглядеть итог. Но в headless-режиме всё рушится: фронта внутри WordPress нет, это два разных мира, два инстанса.

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

Мы решили так:

  • Бэкенд генерит временный токен;
  • По нему можно открыть превью на фронте;
  • Открывается страница на сайте закрытая под хэш ключик
const { private_token, pid } = route.query; const pageKey = `article-${slug}`; const updatePostContent = async (): Promise<void> => { if (useUserStore().user.info.club_member && import.meta.client) { const reqData: { slug: string; private_token?: string; pid?: string; } = { slug: slug, }; if (private_token && pid) { reqData.private_token = String(private_token); reqData.pid = String(pid); } const res = await api.blogController.getPost(reqData as PostDataRequest); postData.value = { data: res.data } as PostDataResponse; } };

Да, это не идеально. Но работает.

Пишите в комментах, если у вас есть идеи, как сделать лучше.

SSR, SSG и боль с модалками

По дизайну все статьи (Кор механика сайта) открываются в модальном виде, отдельной страницы статьи по урлу не открыть, поэтому…

Мы хотели использовать SSG (статическую генерацию) для скорости. Но:

  • контент часто меняется (думали чтобы Wordpress триггерил по API сервер с фронтом, но NUXT Не умеет генерить отдельные страницы по ID или SLUG);
  • Cloudflare не всегда видит эти изменения (первое время мы вручную управляли CF, сейчас там уже апишка после деплоя обнлять кеш, но все равно см.п.1);
  • при открытии статьи в модалке — слетает гидрация, ну эт потому что NUXT. Кстати, пишите в комменты если вы знаете решение

В итоге остались на SSR. Зато теперь всё работает стабильно. Ну, почти всегда :)

Мини-Sentry на коленке и мониторинг статуса фронта

Настоящий Sentry — круто, но не всегда вписывается в бюджет MVP. Мы сделали свой мини-логгер:

  • Ошибки на фронте ловятся плагином и отправляются в API;
  • Бэк сохраняет их в лог и отправляет в Telegram-бота.

Так мы узнали, какие страницы роняют сайт. Написали автотест, который просто проходится по всем статьям и проверяет статус-коды.

Бот в ТГ 
Бот в ТГ 
Бот в ТГ
Бот в ТГ

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

Что мы сейчас имеем? Почти все преимущества Gutenberg для редакторов контента, классный бекенд с редисами и прочими плюхами, классный фронт.

Ранее я не видел в интернете хорошего коммерческого решения решения связки Guteberg + Vue/Nuxt, а теперь оно есть!

На этом всё!

Спасибо всем, кто дочитал до конца) Оставляю ссылку на наш сайт:

И на тг-канал, там мы делимся буднями, рассказываем о проектах и ищем людей в команду:

А если нужны руки для разработки – пишите лично мне в телеграм @olivoin или на почту hello@wewizards.ru

1
Начать дискуссию