Разработка быстрых и современных сайтов на базе Next.js, с использованием GraphQL & WordPress
Введение
Next.js — современный фреймворк на базе React.js, который значительно набирает обороты среди разработчиков и предоставляет инструменты для разных видов рендеринга страниц.
WordPress — популярная headless CMS, применяемая для различных проектов — от простых блогов до сложных приложений.
У нас был доступ к админке живого сайта на WordPress, шило в коде и непреодолимое желание поэксперементировать с Next.js.
Решение основано на статье (и шаблоне) Vercel Using Headless WordPress with Next.js and Vercel.
Получилось достаточно быстрое приложение с примерно небольшими трудозатратами.
TL DR
MVP подход
У нас была основная цель — эксперимент со стеком Next.js-GraphQL-WordPress для получения практики и лучшего понимания возможностей.
Потому все остальное решено было нагло стырить скопировать для экономии времени:
дизайн блога взяли у kod.ru (блог про Телеграм, ТОН и проекты Павла Дурова)
дизайн ленда взяли у ton.org (блокчейн с теми же корнями)
контент взяли у wpcraft.ru — потому что был доступ в админку
В итоге получился некий гибрид этих 3х проектов, который позволил нам с одной стороны получить эксперимент максимально близкий к реальности, с другой стороны обойтись без лишних затрат на дизайн и контент.
Бэкенд: WordPress + GraphQL
WordPress, возможно, самый простой способ сборки бэкенда, но в нашем случае мы взяли готовый работающий сайт, в котором уже был какой-то контент. И сразу перешли к настройке GraphQL.
Настройка GraphQL на базе WordPress
Процесс настройки займёт условных 5 минут:
зайти в админку, в раздел плагинов
найти плагин WPGraphQL — установили
указать GraphQL Endpoint, остальные настройки — по умолчанию
Фронтенд: Next.js + TypeScript
Создаём проект
Официальной документацией для Next.js приложения рекомендуется использовать create-next-app.
В наших примерах используем TypeScript и папку pages. Аналогично будет работать и с эксперементальной директорией app в т.ч. на чистом JavaScript.
npx create-next-app@latest --typescript
Выбираем нужные опции, дожидаемся установки всех зависимостей и получаем базовую структуру приложения.
Переходим в папку проекта (если ещё не) и в терминале выполняем npm run dev
. Если нигде не промазали, должен запуститься сервер на порту 3000 (по умолчанию):
ready - started server on 0.0.0.0:3000, url:
event - compiled client and server successfully in 469 ms (170 modules)
Переходим по указанному адресу, убеждаемся что всё работает.
Теперь у нас есть простое Next.js приложение.
Формируем GraphQL запрос
Плагин WPGraphQL в CMS WordPress предоставляет IDE для формирования и тестирования запросов.
Открываем wp-admin и находим GraphQL.
Query Composer — это графический редактор запросов, он содержит древовидную стурктуру всех доступных полей CMS. Тут мы можем выбрать необходимые поля, задать условия выборки, сортировку и получить готовый запрос.
Например, мы хотим получить 5 последних добавленных постов категории «development»:
нажимаем Query Composer, находим в дереве posts, раскрываем
ставим галочку first и указываем значение 5
раскрываем where, выбираем categoryName и указываем «development»
далее раскрываем nodes и выбираем нужные поля: slug, title, excerpt, date
URL картинки лежит чуть глубже — featureImage/node/sourceUrl
Далее запускаем выполнение запроса кнопкой Execute Query (или Ctrl+Enter) и в правой секции видим результат запроса.
Query Composer в админке WordPress
Таким образом можно формировать все необходимые запросы, проверять какие данные они возвращают, исследовать структуру данных и т.д.
Если не указать параметр first, то WPGraphQL вернёт 10 постов.
Максимальное возвращаемое количество постов — 100, даже если указать first: 1000.
А если нужно получить больше 100 постов? Автор плагина WPGraphQL говорит, что большие запросы могут привести к проблемам с производительностью клиента и сервера и предлагает использовать пагинацию.
Добавляем интерфейс
Теперь мы знаем структуру возвращаемых данных и можем описать интерфейсы:
// types.ts
export interface IPostPreview {
slug: string;
title: string;
excertp: string;
featuredImage: {
node: {
sourceUrl: string;
}
}
date: string;
}
export interface IPost extends IPostPreview {
content: string;
}
Получаем данные из CMS WordPress
Всё готово для получения данных на стороне клиента.
Добавим функцию getPosts
, использующую метод fetch
:
// wp-api.ts
export async function getPosts() {
// определяем Content-Type для JSON
const headers = { 'Content-Type': 'application/json' };
// формируем GraphQL запрос
const query = `
query FavoriteBlogs {
posts {
nodes {
slug
title
excerpt
date
featuredImage {
node {
sourceUrl
}
}
}
}
}
`;
// Первым аргументом метода fetch указываем GraphQL ендпоинт,
// который мы определили в настройках CMS.
// Второй аргумент - объект запроса.
const res = await fetch('', {
headers,
method: 'POST',
body: JSON.stringify({
query,
}),
});
// получаем JSON из объекта Promise
const json = await res.json();
// возвращаем посты
return json.data?.posts.nodes;
}
Заголовки и обработка ответа будут нужны во всех запросах, поэтому имеет смысл вынести этот код в функцию-обёртку fetchData
, которая будет принимать текст запроса и возвращать данные:
// wp-api.ts
async function fetchData(query: string) {
const headers = { 'Content-Type': 'application/json' };
const res = await fetch('<https://wpcraft.ru/graphql>', {
headers,
method: 'POST',
body: JSON.stringify({
query,
}),
});
const json = await res.json();
return json.data;
}
export async function getPosts() {
const data = await fetchData(`
query getPosts{
posts {
nodes {
slug
title
excerpt
date
featuredImage {
node {
sourceUrl
}
}
}
}
}
`);
return data.posts.nodes as IPostPreview[];
}
И т.к. мы не указали параметр first
, WPGraphQL вернёт нам 10 постов.
Структура и маршрутизация
Мы хотим сделать страницу, на которой будет список постов.
При клике на пост должна открываться страница с контентом поста.
Каждая страница с постом будет иметь свой уникальный URL, который будет формироваться динамически используя slug.
Сейчас в директории pages у нас есть файл index.tsx — это главная страница, которая открывается по адресу http://localhost:3000/. Роут — /
.
В директорию pages добавляем файл [slug].tsx — тут мы будем отрисовывать каждый отдельный пост. Роут — /some-meaningful-post-title
Если на главной странице мы хотим разместить, допустим, лендинг, а список постов отображать на другом роуте, скажем, через префикс /blog
, мы можем создать внутри pages директорию blog, в неё добавить index.tsx для списока постов (роут — /blog
) и [slug].tsx для каждого поста (роут — /blog/uniq-post-slug
).
Вот тут подробней про роутинг в Next.js.
Варианты генерации страниц в Next.js
Next.js поддерживает разные способы генерации страниц, рассмотрим SSR и SSG.
ISG отличается от SSG парой параметров внутри тех же самых функций, поэтому в данной статье ISG рассматривать не будем.
// [slug].tsx
import styles from './slug.module.scss';
SSR (генерация на стороне сервера)
Страница со списком постов
Получаем список постов с помощью
getServerSideProps
, передаём через пропсы в компонент страницы Home:// pages/index.ts // опишем явно какие пропсы ожидаем в Home interface IHomeProps { posts: IPostPreview[]; } export default function Home({ posts }: IHomeProps) { return (
{posts.map((post) => ( // используем Link из 'next/link' {post.title} ))} ); } // тип GetServerSideProps экспортируем из 'next' export const getServerSideProps: GetServerSideProps = async () => { // тип IPostPreview[] переменной posts можно не указывать, // т.к. мы явно указали в getPost какого типа данные мы возвращаем const posts: IPostPreview[] = await getPosts(); return { props: { posts, }, }; }Теперь на страницу выводится кликабельный список заголовков постов, клик на пост открывает страницу [slug].tsx, URL меняется на
http://localhost:3000/[slug]
.Страница контента поста
Добавим функцию
getPostBySlug
, с её помощью мы будем получать пост с контентом для отображения на странице поста.// wp-api.ts export async function getPostBySlug(slug: string) { const data = await fetchData(` query getPostBySlug { post(id: "${slug}", idType: SLUG) { title content excerpt slug featuredImage { node { sourceUrl } } date } `}); return data.post as IPost; }
Теперь всё готово для получения поста и генерации страницы:
// [slug].ts // опишем явно какие пропсы ожидаем в Post interface IPostProps { post: IPost; } export default function Post({ post }: IPostProps) { return ( <> {post && (
На данном этапе при клике на заголовок статьи главной страницы мы должны увидеть статью целиком. Но выглядеть контент будет плохо, т.к. никаких стилей мы ему не задали.
Стилизуем контент:
// slug.module.scss .content { p, ul, ol, blockquote { margin: 1.5rem 0; } a { cursor: pointer; text-decoration: underline; } ul, ol { padding: 0 0 0 1rem; } ul { list-style-type: disc; } ol { list-style-type: decimal; } pre { white-space: pre; overflow-x: auto; padding: 1rem; font-size: 1.25rem; line-height: 1.25; border: 1px solid rgb(156 163 175); background-color: rgb(243 244 246); } code { font-size: 0.875rem; line-height: 1.25rem; } figcaption { text-align: center; font-size: 0.875rem; line-height: 1.25rem; } ...
// [slug].tsx ... ...
Мы пока оставляем за скобками вопрос о пробросе стилей сформированных в WordPress с помощью Gutenberg, т.к. автор ещё сам не разобрался как это сделать