Skip to content

Міграція з WordPress на Payload CMS: практичний гайд із продакшн-міграції (2026)

Якщо зробити все правильно, міграція з WordPress - це не разовий експорт, а радше невеликий конвеєр, який ви збираєте й запускаєте стільки разів, скільки потрібно. Нижче - продакшн-підхід, яким я переносив мультимовний сайт на Payload 3, PostgreSQL на Neon і Vercel: рішення щодо стека, конвертація HTML у Lexical, на якій тримається будь-яка міграція, і як я зберіг SEO, медіа та посилальну вагу.

Архітектура міграції WordPress на Payload CMS: читання бази WordPress, конвертація HTML у Lexical і сидинг PostgreSQL на Neon
Автор:Опубліковано:Оновлено:Час читання:16 min read

Senior Frontend Architect, 10+ років досвіду побудови production-проєктів на Next.js. Contentful Certified Professional (2024). Спеціалізація: React Server Components, headless eCommerce, інженерія Core Web Vitals.

Нещодавно я переніс мультимовний маркетинговий сайт з WordPress на Payload CMS, і ця робота навчила мене ставитися до міграції як до data-пайплайну, а не як до експорту в один клік. Плагіни рівня "експорт у JSON в один клік" починають тріщати одразу, щойно у вас є переклади WPML, метадані Yoast SEO, перемішана верстка класичного редактора й Gutenberg, і десять років PDF-файлів у wp-content/uploads, на які досі посилаються зовнішні сайти. Далі - продакшн-підхід, який я застосував: багатоетапна скриптова міграція на Payload 3.77 із PostgreSQL на Neon і деплоєм на Vercel. Розберемо рішення щодо стека, конвертацію HTML у Lexical, яка лежить у серці будь-якої міграції WordPress, збереження медіа та SEO, стратегію 301-редиректів і те, як змінилася щоденна робота розробника після переїзду.

Чому Payload, а не черговий плагін WordPress

WordPress прив'язує контент до PHP-теми, набору плагінів і таблиці wp_postmeta - пари ключ-значення, яка з роками перетворюється на болото. Мета переїзду була в тому, щоб взяти модель контенту під контроль версій і віддавати її зі стека, який я повністю контролюю заради продуктивності та SEO. Payload дає саме це: колекції та поля описуються на TypeScript, схема - це код, база - PostgreSQL з міграціями через Drizzle, rich text - Lexical, а в Payload 3 усе це працює всередині Next.js-застосунку. Замість боротьби з темою фронтенд - це React Server Components, якими я володію від початку до кінця. Саме це поєднання - модель контенту у вигляді коду, що працює як Next.js-CMS, - і робить Payload справжньою альтернативою WordPress для контентного сайту, а не просто черговим headless-API.

Стек: Payload 3, PostgreSQL на Neon, Vercel

Цільовий стек був навмисно нудним і сучасним. Усі версії нижче - це те, що реально поїхало в прод, а не абстрактна рекомендація.

  • Payload 3.77 на Next.js 15.4 / React 19: Payload 3 працює *всередині* Next.js-застосунку - адмінка та REST/GraphQL API є роутами Next.js, а не окремим сервером. Одна кодова база, один деплой, один рантайм.
  • PostgreSQL через `@payloadcms/db-postgres` 3.77: реляційний адаптер на базі node-postgres. Контент реляційний і локалізований, тож справжня SQL-база із зовнішніми ключами була правильніша за документне сховище.
  • Neon як керований Postgres: підключення через POSTGRES_URL_NON_POOLING для міграційних скриптів (щоб обійти обмеження PgBouncer на prepared statements), із connectionTimeoutMillis у 30 000 під холодні старти та вікна обслуговування Neon і idleTimeoutMillis у 60 000 для перевикористання простоюючих з'єднань.
  • Vercel Blob для медіа через `@payloadcms/storage-vercel-blob` 3.77: кожне завантаження в колекцію media зберігається в Blob і віддається з edge.
  • Lexical rich text через `@payloadcms/richtext-lexical` 3.77: поле контенту, що замінює post_content з WordPress. Це й є головна причина, чому міграція нетривіальна - їй присвячено весь Етап 2 нижче.

Рішення щодо Postgres-адаптера, яке забезпечило стабільність міграції

У Payload є serverless-адаптер для Neon/Vercel, і я свідомо його не використовував. На глибоко вкладених запитах "сторінка з блоками" - коли сторінка зібрана з десятка типів блоків, у кожного свої зв'язки - serverless-адаптер виявився ненадійним, тоді як звичайний node-postgres через @payloadcms/db-postgres із пулом з'єднань і явними таймаутами залишався бездоганно стабільним саме на таких запитах. Ще одна практична деталь: schema push (push: true) увімкнений лише на localhost для швидкої ітерації; Neon ніколи не отримує schema push - тільки закомічені й відрев'юєні міграції Drizzle. Це розділення робить прод-схему детермінованою.

Чому PostgreSQL і Drizzle, а не MongoDB

Payload підтримує і MongoDB, і PostgreSQL. Я обрав Postgres, бо контент за своєю природою реляційний і локалізований: пости посилаються на категорії, теги та медіа за зовнішніми ключами, а кожне перекладене поле живе в таблиці на локаль на кшталт posts_locales. Зміни схеми їдуть як датовані TypeScript-міграції - отже, історія бази видна в pull request і оборотна. Піти від wp_postmeta WordPress - єдиної EAV-таблиці, де будь-яке налаштування лежить серіалізованим рядком - було самостійною метою, а не побічним ефектом.

Міграція - це пайплайн: Extract, Transform, Localize

Міграція працює у три яруси. Extraction тягне контент із живої бази WordPress, із продакшн-HTML як запасним варіантом, коли база недоступна. Transformation конвертує цей HTML у Lexical, завантажує медіа й створює документи Payload. Localization розносить контент по локалях і заново прив'язує SEO-метадані. Нічого з цього не їде в застосунок: скрипти лежать в окремих каталогах для міграцій і сидів поза бандлом застосунку, кожен ідемпотентний, і будь-який запис закритий за dry-run. Коли всю цю махину можна прогнати десять разів поспіль, нічого не зламавши, перемикання перестає відчуватися ризикованою подією і стає рутиною.

Етап 1 - Extraction: читаємо WordPress на рівні бази

WordPress зберігав усе в MariaDB. Замість REST API - який тихо втрачає стан чернеток і нюанси метаданих - я запитував wp_posts і wp_postmeta напряму. Переклади прийшли з wp_icl_translations WPML: колонка trid групує пост з усіма його перекладами, а language_code дає локаль. Одна деталь виявилася критичною: я кодував post_content у base64 у виводі SQL, щоб переноси рядків і сирий HTML пережили парсинг TSV, а потім писав по одному JSON-файлу на локаль.

  • Ідентичність і структура: post_id, WPML trid і slug, щоб перезібрати переклади та зберегти URL.
  • Контент і дати: base64 post_content, post_date і post_modified для точних міток публікації та оновлення.
  • Посилання на медіа: _thumbnail_id для головного зображення плюс regex-прохід по інлайнових URL wp-content/uploads/... у тілі.
  • SEO і таксономія: повний блок метаданих Yoast плюс slug категорій і тегів для мапінгу зв'язків.

Етап 2 - Найскладніше: конвертація HTML у Lexical

Payload 3 зберігає rich text як Lexical JSON, а не HTML. Десять років WordPress - це суміш розмітки класичного редактора й блокових коментарів Gutenberg. Я написав детермінований HTML-у-Lexical конвертер, а не довірився універсальній бібліотеці, бо мені потрібен був контроль над кожним крайнім випадком за десять років легасі-контенту.

  • Блокові вузли: <p> стає paragraph; <h1>-<h3> стають вузлами heading, притиснутими до діапазону h2-h4, який дозволяє редактор; <li> стає listitem всередині вузла list.
  • Інлайн-форматування як бітова маска: Lexical кодує форматування тексту цілочисельною маскою - <strong>/<b> ставлять формат 1, <em>/<i> - 2, а вкладені жирний-плюс-курсив дають 3.
  • Посилання: <a> стає вузлом link зі своїм href, а target="_blank" мапиться в newTab: true.
  • Сутності та пробіли: декодуємо сутності, якими WordPress засмічує все підряд - &#8217; у прямий апостроф, пару типографських лапок у прямі, &amp; в амперсанд - і повністю викидаємо абзаци з самих пробілів і &nbsp;.

Пропуски так само навмисні, як і перенесення: довільні інлайн-стилі та будь-яка блокова структура за межами заголовків і списків викидаються, тож міграція заодно працює чисткою контенту. Усе справді багатше - callout-блоки, галереї, ембеди - мапиться в окремі блоки Payload на етапах рівня сторінок, а не протягується як сирий HTML.

Етап 3 - Медіа та проблема wp-content/uploads

Задач з медіа було дві. Головні зображення - проста: читаємо кожен файл як буфер, визначаємо MIME-тип за розширенням і робимо payload.create() у колекцію media, яка стоїть на Vercel Blob; повернутий ID документа стає heroImage поста, а дефолтна обкладинка - запасним варіантом, коли зображення не було. Складна - інлайнові медіа, зокрема 100+ PDF у wp-content/uploads/YYYY/MM/..., на які роками посилалися зовнішні сайти й пошуковик. Для них я завантажував кожен файл із продакшену, заново заливав у Vercel Blob під префіксом wp-uploads/ і проганяв SQL REPLACE по JSONB-колонках контенту, переписуючи всі старі URL на місці.

Одне налаштування Payload варте згадки: я виставив disablePayloadAccessControl: true для Blob-сховища, щоб база зберігала повний публічний URL Blob напряму, а не ганяла кожен файл через /api/media/file/.... Менше стрибків на зображення, і URL кешується на edge без звернення до застосунку.

Етап 4 - Не втратьте SEO: міграція метаданих Yoast

Тихий спосіб убити трафік міграцією - викинути SEO-метадані. Yoast зберігає все це в wp_postmeta, тож я написав окремий імпортер, який читав кожен ключ _yoast_wpseo_* і мапив його в групу meta Payload, а не давав йому померти при конвертації HTML.

  • Заголовки й описи: _yoast_wpseo_title і _yoast_wpseo_metadesc мапляться в meta.title і meta.description.
  • Канонікли: _yoast_wpseo_canonical мапиться в meta.canonical з принагідним переписуванням старого local/staging-origin на продакшн-origin.
  • Директиви індексації: _yoast_wpseo_meta-robots-noindex і -nofollow мапляться в meta.noindex і meta.nofollow.
  • Сигнали таргетингу: _yoast_wpseo_focuskw, keywordsynonyms, is_cornerstone і schema_page_type мапляться в окремі поля, щоб редакторський задум зберігся.

Імпортер матчив записи за (collection, slug, locale), за замовчуванням працював у режимі dry-run і писав у Postgres лише за явним прапором --apply. Поля Open Graph і Twitter-карток переїхали тим самим шляхом, тож прев'ю в соцмережах пережили перемикання без змін. Перенесення SEO окремим аудитованим проходом - а не в надії, що воно само поїде всередині контенту - це те, що зберігає вже зароблені позиції, замість того щоб збирати їх наново з нуля.

Етап 5 - 301-редиректи: зберігаємо посилальну вагу

Пермалінки WordPress рідко збігаються з роутингом нового фреймворку, і кожному проіндексованому URL чи зовні-залінкованому PDF потрібен 301, інакше ви повертаєте позиції пошуковику. Я зібрав маніфест редиректів - включно зі 100+ старими шляхами завантажень - і налаштував постійні редиректи так, щоб старі URL wp-content/uploads/... вели на їхні нові місця /media/... на Blob. Це найнепомітніший етап - і той, який я вважаю обов'язковим: саме він стоїть між тихим перемиканням і просіданням трафіку, що зринає на другому тижні.

Етап 6 - Локалізація на 30+ ринків

Сайт охоплював близько 30 локалей. Групи перекладів trid із WPML чисто лягли на локалізацію Payload, яка тримає перекладені поля в таблицях на локаль. Я спершу засидив мастер-локаль, потім проганяв скрипти застосування локалей, які копіювали структуру в кожну нову локаль через SQL і накладали зверху локаль-специфічні оверайди. Після кожної зміни схеми payload generate:types перегенеровував TypeScript-типи, тож адмінка й кодова база не розходилися - гарантія, якої WordPress просто не дає.

Ідемпотентність, dry-run і чому код міграції не в застосунку

Скрипти міграції ніколи не їхали в бандл застосунку - вони лежать в окремих каталогах для міграцій і сидів поза рантаймом. Три властивості зробили їх безпечними для прогону по живій базі. Вони ідемпотентні: повторний запуск не створює дублів, бо будь-який запис спершу матчиться за slug і локаллю. Вони dry-run за замовчуванням: кожен руйнівний крок вимагає явного --apply. І вони аудитовані: кожен крок логує, що він сматчив і що змінив би, тож dry-run - це читабельний diff до того, як щось торкнеться Postgres.

Як виглядає робота розробника після переїзду

Виграш - у тому, що модель контенту стала кодом. Колекції на кшталт Posts і Pages - це TypeScript-файли, тож додати поле - це коміт і міграція, а не встановлення плагіна. payload generate:types дає наскрізну типобезпеку від бази через API Payload у React-компоненти. Локально Postgres крутиться в Docker із увімкненим schema push для швидкої ітерації, а Neon отримує лише закомічені міграції. Редактори отримують чистий редактор Lexical і блоковий конструктор сторінок замість стіни з шорткодів. Немає бігової доріжки оновлення плагінів, не треба гнатися за версіями PHP і немає functions.php, що тихо накопичує бізнес-логіку.

Що реально стало кращим

  • Наскрізна типобезпека: ті самі типи контенту йдуть із Postgres через API Payload у Server Components Next.js, тож перейменоване поле - це помилка компіляції, а не сюрприз у проді.
  • Немає розростання плагінів: поведінка, яка була п'ятьма плагінами WordPress, тепер - пара сотень рядків відрев'юєного TypeScript.
  • Реляційна цілісність: категорії, теги, медіа й локалі - це зовнішні ключі в Postgres, а не серіалізовані рядки в wp_postmeta.
  • Рендеринг, який я контролюю: оскільки фронтенд - це Next.js App Router на Vercel, Core Web Vitals і SEO стали інженерними рішеннями, а не обмеженнями теми.
  • Один рантайм: Payload 3 живе всередині Next.js-застосунку, тож CMS, API та публічний сайт збираються й деплояться разом.

SEO після переїзду: краще, ніж на WordPress, і без рахунку за плагіни

Деталь, яка хвилювала клієнта найбільше: пошукові показники пережили перемикання й потім зросли - без жодного платного SEO-плагіна. На WordPress SEO-стек був регулярним рахунком: Yoast Premium для метаданих і редиректів, плагін кешування для Core Web Vitals, оптимізатор зображень - у кожного свій цикл оновлень і своє гальмо для адмінки. На Payload ті самі можливості - частина застосунку. Група meta дає кожному документу title, description, canonical і robots-директиви як типізовані поля; редиректи живуть у відстежуваному маніфесті; а Core Web Vitals - це результат рендерингу Next.js, який я контролюю, а не плагін в оренду. Чистіший серверний HTML, швидкі сторінки й метадані, перевірені на рівні типів, дають краулеру менше приводів вагатися - і позиції це відобразили. Очевидний наступний крок - генерація полів meta.title і meta.description автоматично, AI-проходом по контенту - тема для окремої статті.

WordPress - хороше місце для старту й важке місце для масштабування кастомного, мультимовного, критичного до продуктивності сайту. Переїзд на Payload не був експортом в один клік; це був пайплайн, який я міг прогнати, проаудитувати й перезапустити, поки дані не вийшли чистими. Своє відпрацювали найнепомітніші частини - конвертер HTML у Lexical, імпортер метаданих Yoast і маніфест редиректів. Зробіть ці три правильно - і переїзду ніхто не помітить: ні ваші читачі, ні пошуковики.

Чи достатньо плагіна експорту WordPress для справжньої міграції?

Ні, для чогось складнішого за хобі-блог. Плагіни експорту втрачають зв'язки перекладів WPML, метадані Yoast і різницю між класичним і Gutenberg-контентом, і нічого не роблять з інлайновими URL wp-content/uploads, від яких залежать зовнішні сайти. Скриптовий багатоетапний пайплайн - ось що виживає при зустрічі з десятирічним інстансом.

Чому PostgreSQL замість MongoDB для Payload?

Бо контент реляційний і локалізований. Зовнішні ключі до категорій, тегів і медіа плюс таблиці на локаль і відстежувані міграції Drizzle дають цілісність і оборотну історію схеми, якої немає в документного сховища. Postgres на Neon до того ж природно поєднується з деплоєм на Vercel.

Як HTML із WordPress зберігається після переїзду?

Як Lexical JSON, а не HTML. Детермінований HTML-у-Lexical конвертер мапить абзаци, заголовки, списки, інлайн-форматування й посилання у вузли Lexical, декодує HTML-сутності й викидає порожні абзаци - тоді як інлайн-стилі та екзотична блокова структура навмисно відкидаються в рамках чистки.

Чи працюватимуть старі URL і залінковані PDF?

Так, якщо ставитися до редиректів як до повноцінного етапу. Кожен старий URL - включно зі 100+ PDF із wp-content/uploads - отримує постійний 301 на нове місце на Blob, тож зовнішні посилання й позиції в пошуку переносяться, а не ламаються.

Джерела

Плануєте схожий переїзд - WordPress, Drupal чи саморобну CMS на Payload і Next.js - і хочете свіжий погляд на план міграції? Напишіть мені.

Схожі статті

Міграція на Next.js App Router з Pages Router: повний практичний гайд (2026)

Практичний гайд з міграції продакшн-застосунків Next.js з Pages Router на App Router. Вартісна модель гідратації RSC, механіка модульного графа webpack і директиви 'use client', внутрішній устрій Data Cache, інкрементальна стратегія, розбір типових помилок і фреймворк прийняття рішень.

Next.jsArchitectureMigration
Читати статтю

Архітектура веб-продуктивності: системний аналіз 12 інженерних принципів (видання 2026)

Стрибок Lighthouse на 25 пунктів - з 72 до 99 на десктопі - рідко складається зі сотні мікроправок; він береться з перенесення двох-трьох ассетів з основного потоку. Глибокий технічний розбір веб-продуктивності 2026 року: фізика мереж, конвеєри зображень, моделі виконання JavaScript, Critical Rendering Path, edge-обчислення, Core Web Vitals та продакшн RUM - з реальним продакшн-кейсом та конкретними прикладами реалізації.

EngineeringArchitectureCore Web Vitals
Читати статтю

React 19: useOptimistic, use(), Server Actions - механізм і архітектура (2026)

Детальний розбір п'яти нових примітивів React 19: useOptimistic, use(), Server Actions, useActionState, useFormStatus. Action-based mutation архітектура, що замінює Redux/Zustand для серверних мутацій.

React 19ArchitectureNext.js
Читати статтю