SSR, Nuxt и SEO // отладка заголовков
Это вообще что? «Скелеты, спорющие из-за селёдки» Джеймс Энсор .
TL;DR: скрипт, который загружает страницы сайта, сопоставляет title с примером дважды через паузу и пишет совпадают они или нет.
Берёте скрипт, устанавливаете зависимости, запускаете, смотрите --help, кладёте рядом с ним файлик urls.json, в нём перечисляете url тестируемых страниц и какие title у них должны быть и вот у вас есть скрипт автоматического тестирования title. Формат json (можно глянуть в нижней части этого поста), ключи и прочее, понятно из текста скрипта.
Чтобы понять что чинить, надо знать что сломалось
Давным-давно, примерно в прошлую пятницу, я в запаре столкнулся с ситуацией, знакомой многим: правишь локально, а обновляешь браузер, нацеленный на ту же самую страницу, но открытую на тестовом сервере, стейдже, или, вообще, в продакшне…
Конечно, все, кроме меня, пишут код сразу на чистовик, без ошибок, прямыми руками, выспавшись и после предварительного поста. Однако, я не таков, очевидно поэтому или в наказание за грехи, ко мне приходят глюки после десятого эспрессо, когда ломит шею, а в молчащий чат вот-вот ядовитой иголкой из духового ружья вылетит вопрос «Когда уже?» Ясное дело, учитывая эти обстоятельства, отладке никак не способствует ситуация, в которой баг проявляет себя на доли секунды, как раз пока усталые веки, смежаясь, блокируют доступ фотонов к зрительному нерву.
То в прошлую пятницу, а вот в актуальном временном отрезке динамически устанавливаемые элементы брали из хранилища Pinia правильные данные, корректно отображали их в тестовом окружении, но в продакшне статически сгенерированный сайт (SSR), навлекал на себя гнев богов из самых недр Яндекса, при том, что визуально с заголовками всё было окей.
Гипотеза — содержимое элемента title меняется быстрее, чем мои глаза это успевают зафиксировать. Как проверить?
Приостановить мгновение
Наши глаза — хрупкий, но не очень точный инструмент, что уж говорить о таком эфемерном понятии как внимание: кому-то видно разницу между 100- и 60-герцевым монитором, а кто-то не видит что свет моргнул во всей квартире. Компьютер же, напротив — переплетение высокочастотных повторяющихся процессов. Следовательно, почему бы не поручить бездушной машине проверку чего-то, что может меняться во времени. Разумеется, в JavaScript нет уверенности, что какой-то процесс будет занимать конкретное количество миллисекунд, но нам это и не нужно, нам нужно один раз сформулировать правила, по которым мы сможем много раз проверять волнующую нас часть проекта.
Как мы это делаем?
Если хочем делать руками и проверять глазами, то идём читать как ставить EventBreakpoints в Chrome.
Если хотим автоматизации, то разматываем своего толстого питона, берём playwright и делаем так, чтобы раз в какой-то период он ходил по указанным в списке url-адресам, сравнивал названия страниц с указанными у него в настройках, ждал какое-то время и сравнивал снова. До кучи просим его делать скриншот, раз уж мы всё равно до этого добрались. Потом пишем результаты в консоль или в файл. Можете взять мои слова и отнести их в искусственный интеллект, а можете взять готовый файл.
Устанавливаем зависимости: playwright, asyncio… вот весь requirements.txt:
argparse==1.4.0
asyncio==4.0.0
datetime==6.0
greenlet==3.3.0
pathlib==1.0.1
playwright==1.57.0
pyee==13.0.0
pytz==2025.2
typing-extensions==4.15.0
zope-interface==8.1.1
Запускаем, смотрим --help, указываем что надо, запускаем снова и вот у нас есть скрипт автоматического тестирования title. Это нужно для того, чтобы убедиться, что при рендеринге сайта, написанного с использованием SSR, title, срабатывающий при изначальном рендеринге, совпадает с тайтлом, который устанавливается в процессе hydration, а затем, что он не затирается при навигации на эту же страницу. То есть, скрипт проверяет «моргает» тайтл или нет. Формат json вот такой:
[
{
"url": "http://localhost:3000/",
"title": "Главная страница"
},
{
"url": "http://localhost:3000/about/",
"title": "О нас"
}
]
И теперь не надо в Devtools на вкладке Sources втыкать в Event Listener Breakpoint на событие DOMCharacterDataModified или DOMSubtreeModified внутри