Опыт использования WordPress с Nextjs

Йо! Сейчас (осень 2021) я перехожу на новый сайт express.xakplant.ru. Делаю это по нескольким причинам. Основная в том, что на js мне проще и быстрее что-то сделать чем на php, да и php стал для меня не интересен.

Почему next

Мне нужен был серверный рендеринг, чтобы не просесть по seo. Я рассматривал два варианта express и next. Выбрал более простой вариант — next. В отличие от express он не нужен при написании серверного кода и из коробки работает с реактом. Так как это личный проект, а время ключевой фактор, можно меньше часов на реализацию.

Проблемы работы wordpress c next

Конечно же все проблемы связаны с получением контента с сайта и отображением его. Контент нужно получить и ещё вставить в рендер реакта.

Получение данных из wordpress в next

Для получения есть два распространённых пути: wp-rest и WP GraphQL. Сначала я хотел использовать rest, но в нём чёрт ногу сломит, нужно разбираться с документацией. В ответах куча ненужных данных, а ещё хотелось поковырять GraphQL.

Плагины WP GraphQL

Для GraphQL я использую два плагина WP GraphQL и WPGraphQL Offset Pagination. Первый добавляет возможность работы с graphql, а второй позволяет делать нормальную пагинацию, почему-то из коробки этого нет.

Получение поста по uri

Для чего получать посты по uri? Для того, чтобы сохранить ЧПУ и чтобы можно было в будущем настроить простейший редирект на новый сайт.

У себя в проекте я решил использовать axios (только потому, что для меня это повседневный, знакомый инструмент) для запросов к wp graphql. Сделать в папочке с конфигом инстанс аксиоса и только добавил там УРЛ graphql.

import axios from "axios";

export const request = axios.create({
  baseURL: "АДРЕС GRAPHQL",
});

Сам же запрос выглядит так:

import { request } from "config/request";
// Самописный тип данных
import { WPPostByUri } from "dto";

export const getPostByUri = (uri: string): Promise<WPPostByUri> => {
  return request
    .post("/", {
      // ТИП данных ID!
      query: `query MyQuery($uri: ID!) {
          post(id: $uri, idType: URI) {
            id
            databaseId
            title
            uri
            excerpt(format: RENDERED)
            content(format: RENDERED)
            date
            tags {
              nodes {
                name
              }
            }
            featuredImage {
              node {
                uri
                sourceUrl
                altText
              }
            }
            author {
              node {
                avatar {
                  url
                }
                name
              }
            }
          }
        }`,
      variables: {
        // URI поста
        uri,
      },
    })
    .then(({ data }) => data);
};

URI у меня строится по шаблону [year]/[month]/[slug]. Конечно и структура папок в next такая же. В файле [year] > [month] > [slug].tsx получилась функция getServerSideProps такая:

export async function getServerSideProps(
  ctx: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<PostPageProps>> {
  const { params } = ctx;

  const { year, mount, day, slug } = params!;

  const uri = `/${year}/${mount}/${day}/${slug}`;

  try {
    const responce = await getPostByUri(uri);

    return {
      props: { responce, uri: `https://xakplant.ru${uri}` },
    };
  } catch (e) {
    return { notFound: true };
  }
}

Здесь uri получается путём склейки параметров. Почему не query > resolvedUrl? У меня slug на русском, в resolvedUrl уже лежит преобразованный url для адресной строки, а wp-graphql ожидает от меня slug такой, который лежит в базе. Если slug на английском языке, то это не имеет значения.

Рендеринг WordPress HTML в react + next

Я получил выше контент поста в content(format: RENDERED). Как же его вывести? В самом начале отмёл dangerouslySetInnerHTML и выбрал html-react-parser. Выглядит это так:

Устанавливаем:

npm i html-react-parser

Используем:

import parce from "html-react-parser";

export const Post = (props) => {
const { content } = props
....

return (<div>{parce(content)}</div>)
}

Тоже самое и с отрывком (excerpt)

WP GraphQL и пагинация

Я уже упомянул, что из коробки нет пагинации, поэтому нужен плагин WPGraphQL Offset Pagination. Запрос выгляди так:

// Запрос списка постов
export const getPostList = (
  offset: number,
  size: number
): Promise<WPPostQuery> => {
  return request
    .post("/", {
      query: `query MyQuery($offset: Int, $size: Int) {
        posts(where: {offsetPagination: {offset: $offset, size: $size}}) {
          pageInfo {
            offsetPagination {
              total
              hasMore
              hasPrevious
            }
            endCursor
            hasNextPage
            hasPreviousPage
            startCursor
          }
          edges {
            node {
              id
              title
              slug
              uri
              postId
              excerpt(format: RENDERED)
              featuredImage {
                node {
                  uri
                  sourceUrl
                  altText
                }
              }
              date
              author {
                node {
                  avatar {
                    url
                  }
                  name
                }
              }
            }
          }
        }
      }`,
      variables: {
        offset,
        size,
      },
    })
    .then(({ data }) => data);
};

Вызов у меня выглядит так:

export async function getServerSideProps(
  ctx: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<{ data: PrevPageData }>> {
  let page = 0;
  if (ctx) {
    const { p } = ctx.query;
    page = p ? Number(p) - 1 : page;
  }
  // WP_PAGE_SIZE моя локальная константа
  const offset = page * WP_PAGE_SIZE;

  try {
    const result = await getPostList(offset, WP_PAGE_SIZE);

    return {
      props: { data },
    };
  } catch (e) {
    return { notFound: true };
  }
}

Поиск

Код поиска для wp graphql

/** Поиск */
export const getSearchPost = (search: string): Promise<WPPostQuery> => {
  return request
    .post("/", {
      query: `query MyQuery ($search: String){
        posts(where: {search: $search}) {
          pageInfo {
            offsetPagination {
              total
              hasMore
              hasPrevious
            }
            endCursor
            hasNextPage
            hasPreviousPage
            startCursor
          }
          edges {
            node {
              id
              title
              slug
              uri
              postId
              excerpt(format: RENDERED)
              featuredImage {
                node {
                  uri
                  sourceUrl
                  altText
                }
              }
            }
          }
        }
      }`,
      variables: {
        search,
      },
    })
    .then(({ data }) => data);
};

Пагинация

Тут я столкнулся с тем как мне рисовать элементы, а точнее сколько на экране. Я решил просто использовать пример кода https://jasonwatmore.com/post/2018/08/07/javascript-pure-pagination-logic-in-vanilla-js-typescript. Там просто описан код для получения конфигурации компонента постраничной навигации.

Карта сайта

Создание карты сайта происходит из статических и динамических страниц. Так как у меня только динамические посты, мне достаточно запросить список постов. Всё остальное у меня есть.

Запрос списка постов для карты сайта:

// Запрос списка постов
export const getPostListSitemap = (offset: number): Promise<WPPostQuery> => {
  return request
    .post("/", {
      query: `query MyQuery($offset: Int, $size: Int) {
        posts(where: {offsetPagination: {offset: $offset, size: $size}}) {
          edges {
            node {
              slug
              uri
              date
            }
          }
        }
      }`,
      variables: {
        offset,
        size: 1000,
      },
    })
    .then(({ data }) => data);
};

Код создания карты сайта:

import fs from "fs";
import { XAKPL_PRODUCTION } from "config/constants";
import { getPostListSitemap } from "api";
import type { GetServerSidePropsContext } from "next";

const Sitemap = () => {};

export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
  try {
    const { res } = ctx;

    const baseUrl = {
      development: "http://localhost:3000",
      production: XAKPL_PRODUCTION,
      test: "http://localhost:5000",
    }[process.env.NODE_ENV];

    const dimanicPages = await getPostListSitemap(0);

    const {
      data: { posts },
    } = dimanicPages;

    const staticPages = fs
      .readdirSync("pages")
      .filter((staticPage) => {
        return ![
          "_app.tsx",
          "_document.tsx",
          "_error.tsx",
          "sitemap.xml.ts",
        ].includes(staticPage);
      })
      .filter((staticPage) => {
        return !staticPage.includes("[");
      })
      .map((staticPagePath) => {
        return `${baseUrl}/${staticPagePath
          .replace(".tsx", "")
          .replace("index", "")}`;
      });

    const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    ${staticPages
      .map((url) => {
        return `
            <url>
              <loc>${url}</loc>
              <lastmod>${new Date().toISOString()}</lastmod>
              <changefreq>monthly</changefreq>
              <priority>1.0</priority>
            </url>
          `;
      })
      .join("")}
      ${posts.edges
        .map(({ node }) => {
          const { uri, date } = node;
          const parseDate = new Date(date).toISOString();
          return `
        <url>
          <loc>${baseUrl}${uri}</loc>
          <lastmod>${parseDate}</lastmod>
          <changefreq>monthly</changefreq>
          <priority>1.0</priority>
        </url>
      `;
        })
        .join("")}
    </urlset>
  `;

    res.setHeader("Content-Type", "text/xml");
    res.write(sitemap);
    res.end();

    return {
      props: {},
    };
  } catch (e) {
    return { notFound: true };
  }
};

export default Sitemap;

Объяснение есть здесь https://cheatcode.co/tutorials/how-to-generate-a-dynamic-sitemap-with-next-js

Индикация загрузки в nextjs

Про это я писал в статье «Собственный прелоадер для NextJs»

Внедрение yandex метрики

Для этой задачи есть https://github.com/futpib/next-ym, а процесс очень простой. В файле _app.jsx/tsx импортируем HOC и передаём в него id счётчика и компонент

import Router from "next/router";
import withYM from "next-ym";


function MyApp(appProps: AppProps) {
 ...
}

export default withYM("ID счётчика", Router)(MyApp);

Добавление типов

Многих модулей нет для typescript. Вопрос добавления точно встанет. Нужно создать в корне файл additional.d.ts и в него их добавить. У меня он выглядит так:

declare module "*.png" {
  const value: string;
  export default value;
}

declare module "*.jpg" {
  const value: string;
  export default value;
}

declare module "*.jpeg" {
  const value: string;
  export default value;
}

declare module "react-vk" {
  const value: any;
  export const Group: any;
  export default value;
}

declare module "*.svg" {
  const value: React.ReactComponentElement;
  export default value;
}

declare module "next-ym" {
  export const withYM: any;
  export default withYM;
}

В файле tsconfig.json в include нужно добавить additional.d.ts

SVG и картинки в nextjs

Мне привычно использовать svg как компонент и хранить картинки рядом с компонентом. Плюс стандартный механизм работы с картинками в next (лично для меня) больше проблем создаёт чем решает. Далее «перемотка» и готовый next.config.js для 11й версии с webpack 5. Если хотите подробностей, придётся погуглить).

const withImages = require("next-images");

module.exports = withImages({
  images: {
    disableStaticImages: true,
  },
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      issuer: /\.(js|ts)x?$/,
      use: ["@svgr/webpack"],
    });

    config.module.rules.map((rule) => {
      if (rule.test !== undefined && rule.test.source.includes("|svg|")) {
        rule.test = new RegExp(rule.test.source.replace("|svg|", "|"));
      }
    });

    return config;
  },
});

Ну и после этого я могу делать так:

import LogoIcon from "./assets/logo_xpl.svg";

export const Example = () => <LogoIcon/>

Disqus react + next + WordPress

Моя задача была следующая: нужно обеспечить работу disqus на новом и старом сайтах с одними и теми же комментариями.

Для этого было важно, чтобы страницы постов на обоих сайтах были одинаковые.

Как же внедрять? Устанавливаем disqus-react

npm i --save disqus-react

Идём в админку disqus. Открываем настройки и во вкладке advanced дописываем в «Trusted Domains» дополнительный домен, на котором будут видны комментарии так же как на основном.

Переходим во вкладку general и копируем Shortname. Идем в наш код и в компоненте вызываем компонент дискуса.

import { DiscussionEmbed } from "disqus-react";

export const Post = (props) => {
....
<DiscussionEmbed
  shortname="SHORT_NAME СЮДА"
  config={{
    url: props.uri, // Сформировал ранее в разделе "Получение поста по uri"
    identifier: props.databaseId.toString(), // Получил из поста 
    title: props.title, // Получил из поста 
    language: "ru_Ru", // У меня русский, у вас может быть другое
  }}
/>

}

P.S.

Надеюсь тебе понравилась статья. Напиши, что для тебя было самым важным и что ты искал изначально. Не забудь подписаться на мой канал в ютюбе и на группу в ВК. Так я пойму, что мой труд был проделан не зря.