Йо! Сейчас (осень 2021) я перехожу на новый сайт express.xakplant.ru. Делаю это по нескольким причинам. Основная в том, что на js мне проще и быстрее что-то сделать чем на php, да и php стал для меня не интересен.
Мне нужен был серверный рендеринг, чтобы не просесть по seo. Я рассматривал два варианта express и next. Выбрал более простой вариант — next. В отличие от express он не нужен при написании серверного кода и из коробки работает с реактом. Так как это личный проект, а время ключевой фактор, можно меньше часов на реализацию.
Конечно же все проблемы связаны с получением контента с сайта и отображением его. Контент нужно получить и ещё вставить в рендер реакта.
Для получения есть два распространённых пути: wp-rest и WP GraphQL. Сначала я хотел использовать rest, но в нём чёрт ногу сломит, нужно разбираться с документацией. В ответах куча ненужных данных, а ещё хотелось поковырять GraphQL.
Для GraphQL я использую два плагина WP GraphQL и WPGraphQL Offset Pagination. Первый добавляет возможность работы с graphql, а второй позволяет делать нормальную пагинацию, почему-то из коробки этого нет.
Для чего получать посты по 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 на английском языке, то это не имеет значения.
Я получил выше контент поста в 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)
Я уже упомянул, что из коробки нет пагинации, поэтому нужен плагин 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»
Для этой задачи есть 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 как компонент и хранить картинки рядом с компонентом. Плюс стандартный механизм работы с картинками в 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 на новом и старом сайтах с одними и теми же комментариями.
Для этого было важно, чтобы страницы постов на обоих сайтах были одинаковые.
Как же внедрять? Устанавливаем 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", // У меня русский, у вас может быть другое }} /> }
Надеюсь тебе понравилась статья. Напиши, что для тебя было самым важным и что ты искал изначально. Не забудь подписаться на мой канал в ютюбе и на группу в ВК. Так я пойму, что мой труд был проделан не зря.