Разработка быстрых и современных сайтов на базе 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

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

480f078f95335ee43ce229e60f3ab8e0.png

Если на главной странице мы хотим разместить, допустим, лендинг, а список постов отображать на другом роуте, скажем, через префикс /blog, мы можем создать внутри pages директорию blog, в неё добавить index.tsx для списока постов (роут — /blog) и [slug].tsx для каждого поста (роут — /blog/uniq-post-slug).

6ee7a2bd72dd1511df5e5fe18e7352e1.png

Вот тут подробней про роутинг в Next.js.

Варианты генерации страниц в Next.js

Next.js поддерживает разные способы генерации страниц, рассмотрим SSR и SSG.

ISG отличается от SSG парой параметров внутри тех же самых функций, поэтому в данной статье ISG рассматривать не будем.

// [slug].tsx

import styles from './slug.module.scss';
  1. 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 && (
            

    {post.title}

    )} ); } export const getServerSideProps: GetServerSideProps = async (context) => { // params может быть undefined, slug может быть string | string[] | undefined // поэтому укажем явно какой тип мы передаём в slug // это немного "костыль", но пока так const slug = context.params?.slug as string; // тип для переменной post не указываем, т.к. в getPostBySlug указали // какого типа данные возвращаем const post = await getPostBySlug(slug); return { props: { 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, т.к. автор ещё сам не разобрался как это сделать

    © Habrahabr.ru