Веб-приложения дали разработчикам сильные инструменты и позволили делать то, что раньше было невозможно: виртуальный DOM
, управление состоянием компонента, смена страниц без перезагрузки, вычисления на стороне клиента, offline
-режим, улучшенная политика кеширования через Service Workers
, псевдо-установка приложений - без этого мы уже не можем представить себе современное приложение.
За всем этим фасадом скрывается множество проблемных моментов и сложностей. В частности, одностраничные приложения до сих пор плохо индексируются поисковыми системами, потому что выполнение JS
-кода на них крайне ограничено (а где-то и не выполняется). Для решения этой проблемы есть серверная отрисовка.
Серверная отрисовка - это выполнение всех необходимых функций для отрисовки компонента страницы, перевод результата в html
и выдача пользователю. Таким образом, поисковые боты получают всю разметку страницы в привычном формате.
В NextJS
, например, при сборке проекта готовится информация по всем страницам и если запрашиваемый адрес в этой группе страниц не находится, выдаётся ошибка 404
. Казалось бы, проблема решена. PROFIT????
. Но нет. Помимо статических страниц, зачастую нам нужно пользоваться динамическими шаблонами страниц. Там и возникают проблемы.
Динамический шаблон страницы (или просто динамическая страница) - это общий компонент, который выдаётся по запросу к определённому адрессу, содержимое которого выстраивается на основе приходящих данных. Например, страница товара.
В случаях, когда мы просто пользуемся шаблоном, NextJS
не может знать какие страницы есть, а каких нет - поэтому его стандартная логика не работает, страницы с ошибкой (404
, 500
и пр.) не выводятся. Совершается бесконечная загрузка.
Например, создадим файл pages/[id]/index.js
. Квадратные скобки в названии папки указывают на то, что страница - шаблон. Содержимое скобок (id
) - это параметр контекста, который записывается исходя из запроса. Например, по запросу somesite.com/example
таким параметром будет строка example
(в контексте страницы будет хранится значение id: "example"
).
Содержимое возьмём такое:
import ChildComponent from '/components/ChildComponent';
const DynamicPage = () => (
<div className='container'>
<ChildComponent />
</div>
);
export default DynamicPage;
Допустим, мы делаем какую-то обработку внутри ChildComponent
. Вызываем данные и пр. Но что, если таких данных нет? Вызовет ли NextJS
404
ошибку? Нет, не вызовет, потому что сам из себя он не знает о том, какие страницы подходят в шаблон, а при каких нужно вызывать ошибку.
Например, при вызове страницы somesite.com/wrong-address
, на сервере, к которому мы обращаемся за данными, нет записи с ключом wrong-address
. В этом случае, в ответ на запрос придёт 404
ошибка, но NextJS
об этом не узнает и будет показывать пустой компонент.
Можно это исправить.
import React from 'react';
import ChildComponent from '/components/ChildComponent';
import Error from '/pages/404';
const DynamicPage = () => {
// флаг ошибки
const [is_error, toggle_error] = React.useState(false);
// вызываем данные при монтировании компонента
React.useEffect(() => {
fetch_data();
}, []);
// функция вызова данных
const fetch_data = () => {
// ...
}
return(
<div className='container'>
{
is_error ?
<Error /> : // если флаг включён, то показываем компонент ошибки
<ChildComponent /> // если ошибки не было, то стандартный компонент
}
</div>
);
}
export default DynamicPage;
Мы перенесли вызов данных в родителя, где настроили логику с флагом ошибки is_error
. По комментариям виден механизм работы: если при вызове происходит ошибка, то показываем компонент Error
, который берётся из нашей страницы /pages/404.js
; если ошибки не было, то компонент ChildComponent
.
Вроде бы, проблема решена. Мы заходим на somesite.com/wrong-address
- показывается ошибка. Однако, тут есть некоторые проблемы.
Посмотрите внимательно на код выше. Вам не кажется, что работа настроена однобоко? Не смущает ничего? Правильно: не обрабатываются другие ошибки. Если с сервером будут проблемы и он в ответе передаст, например, 500
ошибку, будет выдан компонент 404
ошибки.
К нашему удобству, NextJS
позволяет использовать компонент стандартной ошибки. Давайте добавим необходимую обработку:
import React from 'react';
import NextError from 'next/error';
import ChildComponent from '/components/ChildComponent';
import Error from '/pages/404';
const DynamicPage = () => {
const [error_code, change_error_code] = React.useState(null);
React.useEffect(() => {
fetch_data();
}, []);
const fetch_data = () => {
// ...
}
// проверяем на состояние ошибки
if (error_code) {
// если ошибка 404, то выводим свой компонент 404 ошибки
if (error_code === 404) {
return(
<div className='container'>
<Error />
</div>
);
// если другая ошибка, выводим компонент `NextError` с обязательной передачей кода ошибки
} else {
return(
<div className='container'>
<NextError statusCode={error_code} />
</div>
);
}
// если ошибки нет, выводим рабочий компонент
} else {
return(
<div className='container'>
<ChildComponent />
</div>
);
}
}
export default DynamicPage;
Мы убрали флаг is_error
в состоянии компонента, так как его функции может выполнять флаг error_code
(если он в положении null
, ошибки нет).
Кажется, что теперь точно всё: все ошибки обрабатываются. Но если мы попробуем запустить индексацию неверного адресса приложения в веб-аналитике поисковых систем (Яндекс.Вебмастер
или Google Search Console
), то увидим, что 404
ошибку они не выдадут. Почему?
Дело в том, что поисковые роботы индексируют страницы не по содержимому, а по коду, который передаёт сервер в ответ на запрос страницы. Роботу одинаково, выдаётся ли компонент ChildComponent
, NextError
или Error
- код страницы приходит 200
, значит страница есть.
Вот несколько примеров, когда эта тонкость работы имеет огромное значение:
- Изменилась работа сервера, из-за чего поменялись идентификаторы сущностей (
id
/slug
); - Каталог товаров активно обновляется (удаляются/добавляются новые товары - с удалением как раз и будут проблемы, поиск будет выдавать старые данные);
- Поменялся принцип построения
url
-строки в приложении (например, вместоid
стали использоватьсяslug
).
Во всех случаях, поиск будет неправильно обрабатывать структуру сайта и хранить неправильные адреса, выдавая их по запросу.
В этом также может помочь настроенный sitemap
, но для поисковых систем это не единственный ориентир - поэтому лучше использовать его как дополнительное улучшение.
Как это исправить? Кажется, просто: возьми, да добавь в код метод, который бы передавал ошибку. Но дело в том, что такого метода просто нет. JS
, который исполняется у пользователя в браузере, не может изнутри страницы передать код статуса страницы. Этим занимается сервер.
В случаях статических страниц NextJS
знает, какие страницы есть, а каких нет - поэтому выдаёт ошибку сам. В случае же динамических страниц, как и писалось выше, он знать этого не может. А передавать состояние страницы из самого компонента нельзя.
И что? Больше нет вариантов? Есть. Опять же, к нашему удобству, NextJS
позволяет пользоваться своей серверной обёрткой из JS
при построении страницы. Метод, который позволяет это сделать, называется getServerSideProps. Этот метод также даёт доступ к контексту, обёртке высшего уровня всего приложения. В этой обёртке и хранится код ошибки, который выдаёт серверная обёртка при запросе за страницей. А по особенностям работы самого JS
мы знаем: имея ссылку на сущность, мы можем её изменять.
Остаётся дело за малым: добавить изменение кода ответа серверной обёртки. Для этого мы добавим сам метод getServerSideProps
в компонент страницы, а также перенесём вызов данных туда (также немного оптимизируем структуру):
import NextError from 'next/error';
import Error from '/pages/404';
import ChildComponent from '/components/ChildComponent';
const DynamicPage = ({
statusCode = 200,
data: null,
}) => {
// если коды позволяют, выводим обычный компонент
if (fetch_res.status >= 200 && fetch_res.status < 300) {
return (
<ChildComponent data={data} /> // теперь в нашей странице нужные данные есть ещё до её открытия
);
// если ошибка 404, то выводим свой компонент ошибки
} else if (statusCode === 404) return <Error />;
// если ошибка другая, выводим компонент `next/error`
else return <NextError statusCode={statusCode} />;
};
// добавили метод серверной обёртки `getServerSideProps`, который исполняется до построения страницы
export async function getServerSideProps(context) {
// получаем параметр `id` сущности из контекста, чтобы потом обратиться к серверу за данными ("example"/"wrong-address" из нашего примера)
const { id } = context.query;
// делаем запрос
const fetch_res = await fetch(/* ...url */);
// немного оптимизируем работу и достанем из запроса данные
const fetch_json = await fetch_res.json();
let statusCode = 200;
// проверяем код ответа у запроса
if (fetch_res.status < 200 || fetch_res.status >= 300) {
// передаём код ошибки в обёртку (для ответа сервера)
context.res.statusCode = fetch_res.status;
// передаём код ошибки в компонент (для вывода компонента)
statusCode = fetch_res.status;
}
return {
props: {
statusCode,
data: fetch_json, // передаём полученные данные в компонент страницы
},
};
}
export default DynamicPage;
Всё. Теперь у нас и для пользователя все ошибки обрабатываются, и поисковые боты получают ошибку при запросе несуществующей страницы. Обработка ошибок настроена верно.
Надеемся, материал вам был полезен и вы узнали кое-что новое.