Переезд с Webflow на Next.js по одной странице: Payload CMS, next-intl и проверки в CI

Португальскую версию нашего сайта мы собирали два месяца. Переключаешь локаль в дизайнере Webflow, правишь текст в блоке, публикуешь, идёшь к следующему. Иногда после публикации на проде оставался старый текст, и блок приходилось проходить заново.
Последние месяцы я переношу этот сайт на Next.js 15 и Payload CMS. Переезд на финишной прямой: большая часть страниц уже работает на Next, остальные идут по тому же конвейеру. Ниже про то, как он устроен, и про грабли, на которые я наступил по дороге. Компанию не называю, это мой текущий работодатель, поэтому код в статье обезличен.
Немного контекста. Я фулстек-разработчик, в вебе семь лет. Сайт веду один, макеты приходят из Figma от дизайнеров. К началу переезда в Webflow было больше 800 компонентов, около сотни статических страниц, несколько CMS-коллекций (блог, новости, упоминания в прессе, changelog) и пять языков: английский, русский, испанский, португальский и французский. Трафик 100–150 тысяч пользователей в месяц.
Из чего складывался счёт
Webflow для такого сайта был хорошим выбором два года назад. CMS встроенная, хостинг, CDN и SSL работают из коробки, а лендинг в визуальном редакторе собирается в разы быстрее, чем руками. Проблемы начинаются с ростом: локализация вручную, редактор тормозит на сотнях компонентов, нет ни ревью кода, ни тестов.
Деньги тоже сыграли роль. Весной 2026 года Webflow обновил тарифы, и при нашем трафике главной статьёй стал bandwidth:
Статья | В месяц |
|---|---|
Site plan: хостинг одного сайта и 50 ГБ трафика | $39 |
Трафик сверх 50 ГБ, ступень +450 ГБ | $260 |
Локализация на пять языков | $140 |
Seats: full и limited ($648 в год) | $54 |
Workspace Core ($228 в год) | $19 |
Второй сайт ($168 в год) | $14 |
Итого | $526 |

За трафик мы платили почти в семь раз больше, чем за сам тариф. Суммы за seats, локализацию и второй сайт взяты из наших счетов, остальное есть на странице тарифов Webflow.
Почему не переписать всё разом
Сотня страниц на пяти языках плюс CMS — это несколько месяцев работы одного человека. Переписать всё и переключить домен в один день значило бы на эти месяцы заморозить контент, а сайт всё это время живёт: выходят новые лендинги и вебинары.
Поэтому переезд постраничный, по схеме strangler fig. Новое приложение растёт рядом со старым и забирает у него страницы по одной. В середине августа на Next работали три страницы, к началу октября 57.
Next.js задеплоен на Webflow Cloud. Это хостинг Webflow для приложений на Next.js, Astro и других фреймворках. Next там собирается через OpenNext под Cloudflare Workers. Приложение смонтировано на тот же домен под /app (basePath: '/app' в next.config.ts). Корень домена и страницы, которые ещё не переехали, отдаёт Webflow. Когда страница переезжает, в Webflow появляется 301 со старого адреса на новый.

Из-за этого в коде три вида ссылок, и путать их нельзя. На страницу, которая уже на Next, ведёт <Link>: Next сам добавит basePath и префикс локали. На страницу, которая ещё на Webflow, ведёт обычный <a>, и префикс локали мы клеим сами. Остальное внешние ссылки.
export type Route =
| { kind: 'internal'; path: string } // уже на Next: <Link>
| { kind: 'webflow'; path: string } // тот же домен, ещё Webflow: <a>
| { kind: 'external'; href: string; newTab?: true };
// Для плоского <a>: в отличие от <Link>, он не клеит ни basePath, ни локаль
export function internalHref(path: string, locale: string): string {
const prefix = locale === routing.defaultLocale ? '' : `/${locale}`;
return `/app${prefix}${path === '/' ? '' : path}`;
}
export function webflowHref(path: string, locale: string): string {
return locale === routing.defaultLocale ? path : `/${locale}${path}`;
}
Переезд страницы на прод в коде выглядит как смена kind: 'webflow' на 'internal' в одной строке. До этого новая страница живёт на Next под noindex, а оригинал всё ещё отвечает на Webflow. Потом снимаем noindex, добавляем страницу в sitemap, заливаем редиректы и переводим страницу Webflow в Draft.
Редиректы в Webflow
У 301 в Webflow несколько особенностей, которые я выяснил на практике.
Bulk import в настройках проекта работает на замену. Если залить CSV из десяти строк, остальные правила сайта пропадут. Поэтому в репозитории лежит только полный файл: экспортировали текущий список, дописали, залили обратно. Сейчас в нём 327 правил, рядом лежит исходная выгрузка на 80 правил как точка отката.
Локаль в Webflow часть пути, так что одна страница на пяти языках даёт пять правил. Wildcard-ов мы избегаем, цепочек 301 → 301 не допускаем. Webflow не даст повесить редирект поверх опубликованной страницы: её нужно перевести в Draft. Переносить такую страницу в папку /archive/ плохая идея, она попадёт в sitemap дублем.
Все страницы сайта собраны в один реестр, migration.registry.ts: у каждой статус (live, in-progress, planned) и старые адреса. На реестре держатся две проверки. Unit-тест сверяет его с роутами в src/app. Удалённый тест проходит по каждой live-странице и для каждой локали проверяет, что старый адрес отдаёт 301 на новый в той же локали, а новый отвечает 200. Удалённые тесты в CI нет, и это сделано намеренно: они проверяют состояние сайта, а не код. Их запускают руками до выкатки и после публикации в Webflow.
Что пришлось обойти на Webflow Cloud
Первая неприятность нашлась на реальных устройствах. Иконки в меню появлялись с задержкой, картинки дропдаунов дорисовывались на глазах, сильнее всего в iOS Safari. На localhost этого не видно. curl показал, что всё под /app приходит так:
cache-control: private, no-cache
cf-cache-status: BYPASS
set-cookie: _cfuvid=…
Платформа вешает куку _cfuvid на каждый ответ, а ответ с Set-Cookie кэши обязаны считать приватным. Это касается даже _next/static/* с хэшем в имени, которые Next помечает immutable. Переопределить заголовки из приложения нельзя: ни через headers() в next.config, ни файлом _headers.
Обходили на своей стороне. SVG-иконки и логотипы теперь лежат спрайтом прямо в DOM, иконка ссылается на <use href="#name"> и сеть ей не нужна. Картинки меню грузятся через <link rel="preload">. Растр в public/ только AVIF или WebP и не тяжелее 250 КБ. Это проверяет тест, иначе забытый PNG оплачивается трафиком на каждой загрузке: пять картинок меню после конвертации весили 78 КБ вместо мегабайта.
С картинками есть ещё нюанс: sharp на Workers не работает, поэтому next/image тут отдаёт файл как есть. В проекте нет ни одного next/image. Вместо него свой компонент Picture: нативный <picture> с <source media> для мобильной и десктопной картинки, а в AVIF их заранее конвертирует скрипт на sharp.
С кэшем страниц тоже не всё гладко. Сентябрьский аудит показал, что OpenNext без настроенного инкрементального кэша отдавал x-nextjs-cache: MISS на каждый запрос: TTFB 0,5–0,8 с, и 33 статических роута рендерились заново. R2-кэш в open-next.config.ts пока закомментирован, и насколько ISR в OpenNext на Workers готов к продакшну, я до конца не проверял.
После отключения Webflow фронт уедет на обычный Node-хостинг. Поэтому код пишем переносимым: в страницах нет fs и других Node-only API, всё платформенное спрятано за тонким адаптером. Отдельный документ перечисляет обходы, которые после переезда нужно откатить.
800 компонентов и дизайн-система
Компоненты собирали заново по дизайн-системе в Figma, экспорт HTML из Webflow не брали. Первые коммиты тянули из Webflow токены через DevLink, но их быстро заменили SCSS-переменными, написанными руками по Figma.
Код разложен на три слоя, и для каждого компонента есть правило, куда его класть. Если у него своя логика, состояние или данные, это features/ (квиз, поиск, запись на звонок, блог). Если он собран из двух и больше именованных компонентов в цельную секцию, это blocks/. Всё остальное идёт в ds/, это одиночные самодостаточные компоненты. Срабатывает первое «да» сверху вниз.
Сейчас в ds/ 114 компонентов, в blocks/ 89, подсистем в features/ 12. Вместо 800 с лишним компонентов Webflow вышло около двухсот. Большая часть разницы приходится на варианты: в Figma их 944 в 132 наборах, а в React это пропсы одного компонента.
Интерактивные компоненты стоят на headless-примитивах Ark UI: поведение, состояния и доступность берём у Ark, стили свои. Стили — SCSS и CSS Modules. Отступы, радиусы и цвета только из токенов, и это проверяет не ревьюер, а stylelint:
// Пропускаем 0 и хайрлайн 1px: это прижимы к пикселю, а не шкала
const RAW_LENGTH = /(?<![\w.$#])-?(?!(?:0|1)px\b)(?!0rem\b)\d*\.?\d+(?:px|rem)\b/;
// Прямой toRem(24) в обход шкалы — то же нарушение
const RAW_TOREM = /toRem\(\s*-?\d/;
const tokenRules = (props) =>
Object.fromEntries(props.map((prop) => [prop, [RAW_LENGTH, RAW_TOREM]]));
export default {
customSyntax: 'postcss-scss',
rules: {
'declaration-property-value-disallowed-list': {
...tokenRules(SPACING_PROPS),
...tokenRules(RADIUS_PROPS),
},
'color-no-hex': true,
},
};
У каждого компонента есть сторис в Storybook 10, и сторис здесь тоже тест. Всего 255 файлов и 779 сторис. Каждая рендерится в браузере через @storybook/addon-vitest и прогоняется через axe с a11y.test: 'error', 574 из них ещё и проигрывают сценарий в play. Визуальные регрессии ловит Chromatic.
Правила React, которые держат страницы лёгкими
Главное правило: компонент серверный, пока не доказано обратное. Статичная секция без 'use client' не добавляет на клиент ни байта JS. 'use client' стоит в 135 файлах из 530. Где нужна интерактивность, клиентская оболочка принимает серверные children: так устроены шапка и анимации появления.
Переводы рендерятся на сервере через getTranslations, а клиентскому компоненту передаются только нужные ему строки. Аудит в сентябре нашёл нарушение: NextIntlClientProvider получал весь словарь, это около 145 КБ в RSC-payload каждой страницы.
Картинку под вьюпорт выбирает браузер через <picture>. User-Agent на сервере для этого не читаем. Вызов headers() делает роут динамическим, а HTML, который зависит от UA, ломает кэш на CDN. Серверный isMobile остаётся только для поведения, которое не выразить в CSS.
memo() в проекте не используется ни разу, а useMemo и useCallback стоят примерно в 60 местах. Когда большая часть дерева серверная, на клиенте почти нечему перерисовываться. Лишние рендеры ищем react-scan: npm run dev:scan подключает его только в dev. Анимации идут через LazyMotion с domAnimation и m.*, тяжёлые диалоги с видео и записью на звонок подгружаются через next/dynamic. Бандл смотрим @next/bundle-analyzer, и он собирает в отдельный distDir, чтобы не снести .next у запущенного dev-сервера.
Пару правил пришлось оформить в ESLint, потому что на них уже наступили. Тип ReactNode | ReactNode[] запрещён селектором в no-restricted-syntax: t.rich тоже отдаёт массив, и компонент, который делит массив на абзацы, однажды поставил <br> отдельным пустым <p> в первый экран страницы. Внутренний <a href="/…"> запрещён, потому что перезагружает страницу. Видеоплеер подключается только через одну обёртку: там у iframe правильный title, а куки выключены до согласия.
i18n на next-intl
Пять локалей, английская по умолчанию и без префикса:
export const routing = defineRouting({
locales: ['en', 'ru', 'es', 'pt', 'fr'],
defaultLocale: 'en',
localePrefix: 'as-needed',
// Без автоопределения по Accept-Language и cookie: /app всегда отдаёт en,
// локаль переключается явно. Иначе русский браузер всегда улетал бы на /ru
localeDetection: false,
});
В middleware есть тонкость с basePath: матчер считается относительно него, и '/' в матчере обязателен. Без него middleware не срабатывает на корне /app, и дефолтная локаль без префикса отдаёт 404.
Тексты статических страниц лежат в TS-объектах, по файлу на секцию: src/messages/<locale>/<section>.ts. Источник типов — английское дерево. Индекс остальных локалей устроен как { ...en, ...переведённые секции }: непереведённая секция показывает английский текст, а тест паритета ключей следит, чтобы переведённая не потеряла ни одного ключа.
Предложение с акцентом или ссылкой внутри хранится одной строкой с тегами и рендерится через t.rich. Делить предложение на несколько ключей нельзя, в разных языках разный порядок слов. Неразрывные пробелы руками не ставим: Typograf один раз проходит по всему дереву сообщений локали, и результат кэшируется на воркер. Это важно, потому что Figma MCP показывает неразрывный пробел как обычный. В русском макете одной страницы было 156 неразрывных пробелов, а до сообщений доехало 4. Typograf восстанавливает около 88% таких мест, остальное ставим вручную.
Страница, которой нет в какой-то локали, в этой локали отдаёт настоящий 404 через notFound(). hreflang и sitemap перечисляют только те локали, где страница есть. Редиректы, sitemap и hreflang должны совпадать.
Самое интересное в локализации связано с тем, откуда берётся текст. Правило в проекте такое: текст локали берётся только из макета этой локали в Figma и никогда не переводится с английского. Исключение одно: французский мы всегда переводим автоматически, макетов на нём нет. Для остальных языков автоперевод допустим только с явного разрешения, и это разрешение записано прямо в тесте. Почему так, видно в разделе про грабли.
Payload CMS
CMS — отдельное приложение: Payload 3 внутри Next.js, собранное в Docker (output: 'standalone'). Данные в Postgres, медиа в S3-совместимом хранилище. Схема меняется только миграциями (push: false), в контейнере они применяются при старте. Фронт читает CMS по REST.
Локализация в Payload включена на те же пять языков с fallback: true, но фронт всегда просит fallback-locale=none:
export function buildPostsUrl(collection: string, q: PostsQuery): string {
const params = new URLSearchParams();
params.set('locale', q.locale);
// без этого Payload молча отдаёт en-текст там, где локали нет
if (!q.fallback) params.set('fallback-locale', 'none');
params.set('depth', String(q.depth ?? 1));
// …limit, page, sort, where, select
return `/api/${collection}?${params.toString()}`;
}
Иначе русская страница новости, которой на русском нет, спокойно покажет английский текст под русским URL.
Коллекции: новости, changelog, блог с таксономиями, упоминания в прессе, баннеры, вебинары, люди, медиа и пользователи. Тело поста — локализованный Lexical richText с блоками внутри текста:
// картинки в теле — только блоком media: у картинки один путь рендера
const EXCLUDED_FEATURES = new Set(['upload', 'relationship']);
export function postBody(blocks: Block[]): RichTextField {
return {
name: 'body',
type: 'richText',
required: true,
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures.filter((f) => !EXCLUDED_FEATURES.has(f.key) && f.key !== 'link'),
urlLinkFeature(),
BlocksFeature({ blocks }),
],
}),
};
}
Блоков семь: цитата, выделенный абзац, CTA-баннер внутри статьи, карточка человека, медиа (одна картинка или слайдер), видео и FAQ. Ссылки в тексте только по URL, без связей с коллекциями.
Одна мина Payload нашлась при проектировании схемы. Поле со списком доступных локалей нельзя назвать locales. Дочерняя таблица select-hasMany получила бы имя <collection>_locales, а это имя Payload резервирует под таблицу локализации коллекции, и билдер запросов на нём падает. Поэтому поле называется availableLocales.
Посты с черновиками и live preview на двух брейкпоинтах, 375 и 1440. Когда редактор публикует или снимает пост, afterChange дёргает ревалидацию на фронте:
export function shouldRevalidate(doc: PostDoc, previousDoc?: PostDoc): boolean {
return doc._status === 'published' || previousDoc?._status === 'published';
}
export function revalidatePost(collection: string): CollectionAfterChangeHook {
return async ({ doc, previousDoc }) => {
if (shouldRevalidate(doc, previousDoc)) await callRevalidate(revalidateBody(collection, doc));
return doc;
};
}
Сохранение черновика ревалидацию не трогает. callRevalidate шлёт POST с секретом в заголовке на роут фронта, тот вызывает revalidatePath для каждой локали поста. Ошибка только логируется: публикация в CMS не должна падать из-за того, что фронт недоступен.
Поиск по новостям и блогу сделан на Postgres. Плагин @payloadcms/plugin-search собирает индекс, а миграция добавляет к нему сгенерированную колонку tsvector с весами (заголовок A, лид B, текст C), конфигурацией словаря под локаль и GIN-индексом. Если по запросу ничего не нашлось, эндпоинт пробует исправить опечатку по расстоянию Дамерау — Левенштейна против словаря раздела и возвращает corrected. Ещё одна тонкость: при синхронизации документ перечитывается с fallbackLocale: false, иначе английский текст попадает в русский индекс.
Как переносили контент из Webflow CMS
Небольшую коллекцию галерей хватило выгрузить в CSV одной кнопкой из админки Webflow, она уехала в статический контент. Для блога, новостей и changelog нужны были все пять локалей и записи из связанных коллекций-модулей. Их забирали через Webflow Data API v2 (/v2/collections/{id}/items/live с cmsLocaleId и пагинацией по 100), часть снимков сняли через MCP-сервер Webflow. Снимки лежат в JSON, импорт работает с ними, а не с живым API.
У Webflow есть особенность: он держит вариант записи в каждой локали, даже если её никто не переводил. Непереведённый вариант — это копия английского, причём часто устаревшая. Импорт считает локаль настоящей, только если вариант опубликован и его название отличается от английского:
export function availableLocales(variants: WebflowItem[]): WebflowLocale[] {
const en = variants.find((v) => v.locale === 'en');
return LOCALE_ORDER.filter((locale) => {
const v = variants.find((x) => x.locale === locale);
if (!v || !isLive(v)) return false;
if (locale === 'en' || !en) return true;
return v.fieldData.name !== en.fieldData.name;
});
}
Rich text Webflow отдаёт HTML, Payload хранит Lexical JSON. Конвертация идёт в три шага.
Сначала HTML чистится в JSDOM. <figure> с картинкой или iframe превращается в абзац-маркер @@marker:N@@, а данные картинки или видео уходят в отдельный массив. В маркеры превращаются и плейсхолдеры вида {{_quote…}}, через которые в Webflow вставлялись цитаты, выделения и баннеры из отдельных коллекций. Заодно h6 становится h4, а пустые абзацы с одним неразрывным пробелом выбрасываются.
Потом очищенный HTML превращается в Lexical штатным convertHTMLToLexical из @payloadcms/richtext-lexical. Маркеры доходят до Lexical обычными абзацами, и третий шаг меняет их на блоки:
const MARKER = /^@@marker:(\d+)@@$/;
export function replaceMarkers(
state: EditorState,
resolve: (index: number) => LexNode | null,
): EditorState {
const children = state.root.children.flatMap((node) => {
const index = markerIndex(node); // абзац из одного текста по MARKER
if (index === null) return [node];
const block = resolve(index);
return block ? [block] : [];
});
return { ...state, root: { ...state.root, children } };
}
Если после этого остался неразобранный плейсхолдер или маркер оказался внутри списка, импорт падает до записи в базу. Картинки скачиваются с CDN Webflow и загружаются в медиа Payload. Дубли отсекаются по ID файла Webflow, а у картинок из тела статьи, где ID нет в данных, его достают из 24-символьного hex-префикса имени файла. Записи обновляются по webflowId, поэтому импорт можно перезапускать. С DRY_RUN=1 он ничего не пишет и печатает отчёт в JSON.
Самые странные находки в данных:
кириллические буквы в ключах плейсхолдеров: их набирали в русской раскладке, и одна русская «а» молча ломала вставку блока;
кириллическая «С» в английских подписях;
поле месяца без года у части выпусков changelog;
тестовые записи
test1,test-2иempty-reserved;русское слово в английском названии одного из форматов блога.
У каждой такой обработки в коде есть комментарий, почему она там. Встроенные посты из соцсетей и таблицы импорт не переносит молча, а выносит в отчёт для ручной проверки.
Проверки вместо ручной регрессии
На каждый PR в CI параллельно идут три job. verify гоняет stylelint, tsc --noEmit, unit-тесты (их больше 700) и ESLint. ESLint вешает аннотации прямо на строки диффа. secrets прогоняет gitleaks по коммитам PR. runtime-gates поднимает next dev и обходит все роуты из src/app и sitemap: axe, горизонтальный скролл на 375 px и пустые абзацы. Pre-commit гоняет пять быстрых тестов и gitleaks по staged-файлам. Storybook-тесты в CI не попали, на workerd они флейкают, поэтому идут локально и через Chromatic.
Правило в проекте простое: если договорённость может проверить машина, она переезжает из документации в гейт. Почти каждый гейт появился после конкретного случая.
Что случилось | Что ловит теперь |
|---|---|
Русскую вкладку на странице тарифов собрали по английскому макету и перевели. «Создать инвойс» вместо «Создать счёт», 18 расхождений, дизайнер нашёл их через месяц | Тест происхождения текста: в каждом файле локали тег |
На одной локали страница отдала 500: обычный | Тест: ключ с тегом хотя бы в одной локали можно читать только через |
Страница ехала вбок на 481 px при ширине 375: 35 абсолютных | Runtime-гейт меряет горизонтальный скролл и называет виновника: |
Агент взял ширину формы 496 из архивной ноды Figma вместо 544, и формы разъехались на всех страницах | Тест: ширина карточки формы задаётся только в трёх модулях |
Редиректы завели только на английский, 8 адресов отдавали 404 | Удалённый тест редиректов по всем локалям |
В русском меню Webflow ссылка вела на английскую страницу Next | Удалённый тест локали ссылок в меню |
В | Тест формата и веса растра |
Экспорт из Figma MCP запекал серую подложку | Детектор подложки в скрипте экспорта и тест на него |
В | Тест суффикса в метатегах |
Отдельная история с Lighthouse. Пресет unlighthouse меряет без замедления CPU (cpuSlowdownMultiplier: 1), а мобильный PageSpeed замедляет в четыре раза. На странице тарифов это дало 87 баллов вместо 61. Параллельные прогоны давали ещё и 52. Теперь замер идёт с замедлением в 4 раза и строго по одному. Про производительность будет отдельная статья. Пока одна цифра: из 660 мс Total Blocking Time нашему коду принадлежат 34, остальное съедают сторонние скрипты аналитики, чата и пикселей.
Где тут AI-агент
Код я пишу вместе с Claude Code, и в репозитории под это заведена инфраструктура. В CLAUDE.md лежит короткая карта проекта, а правила вынесены в .claude/rules/ и подгружаются по путям: стили только при работе с *.scss, правило «страница отдельно от контента» только для page.tsx. Под повторяющиеся задачи написано десять skills. Обязательный из них — чеклист перед любой страницей или блоком. В нём один блокирующий пункт: если нет макета локали, агент останавливается и спрашивает.
Есть скилл выгрузки картинок из Figma с правилом «выгружаем контейнер, а не рисунок». Есть скилл метатегов: title и description он пишет по стоп-листу и глоссарию, а текущую мету читает в последнюю очередь, чтобы не подстраиваться под неё. Ещё есть скиллы разбора комментариев дизайнера из Figma, проверки доступности, еженедельного аудита и статуса переезда.
Страница собирается так. Геометрию макета даёт Figma MCP, а тексты с неразрывными пробелами и скрытые слои приходят через REST. Потом пишется код, прогоняются npm run verify и runtime-гейт. После этого идёт сверка с макетом числами: Playwright снимает getBoundingClientRect на 1440 и 375, отдельный скрипт проверяет переносы строк. Совпадение с макетом до пикселя гейты не проверяют, это делают замер, Storybook и Chromatic.
Агент работает быстро и ошибается правдоподобно. «Создать инвойс» выглядит таким же нормальным текстом, как «Создать счёт», а ширина 496 такой же нормальной цифрой, как 544. По-моему, главный навык в такой разработке — заранее знать, где агент ошибётся, и поставить там проверку. Поэтому в проекте есть ещё и мод для Claude Code. Он не даёт агенту запустить полный runtime-аудит без списка роутов (однажды это повесило мой Mac), собрать next build рядом с работающим next dev (общая папка .next, dev начинает отдавать 500) и убить Storybook.
Что получилось

Новая страница в Webflow занимала 2–3 дня, теперь в среднем 3–6 часов. Перевести сайт на новый язык в Webflow стоило двух месяцев ручной работы. Теперь это строка в routing, локаль в Payload и файлы сообщений, которые до перевода показывают английский. По моей оценке, на весь сайт уйдёт около дня, и узким местом остаётся сам текст. Регрессию после изменений проверяет машина.
Webflow стоил $526 в месяц. После его отключения инфраструктура, по нашему расчёту, будет стоить $20–40 в месяц при том же трафике.
Реестр страниц появился только в августе, через полтора месяца после старта. До него счётчик прогресса считали руками, и он долго показывал 1 из 150 при трёх живых страницах. Заводить реестр стоит в первый же день, вместе с удалённым тестом редиректов.
Если вы тоже переезжали с Webflow или другого конструктора с локалями в пути, расскажите в комментариях, как вы решали редиректы. Особенно интересно, как вы разбирались с непереведёнными вариантами записей в CMS.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.