Всем привет! Меня зовут Николай Каменев, я фронтенд-разработчик в Почтатехе. Мы разрабатываем UI для порталов и приложений Почты России.

Я хочу поделиться коротким гайдом, как автоматически рендерить og:image-изображения для превью сайтов. 

Дальше будет небольшая вводная для тех, кто не знаком со сборкой превью по технологии Open Graph. Если вы и так все знаете, переходите сразу в следующий раздел :)

О технологии Open Graph

Open Graph позволяет сформировать превью сайта, которое будет отображаться при публикации ссылки на каком-либо ресурсе. Его используют почти все соцсети, мессенджеры и интернет-сервисы. 

По стандарту, превью собирается из элементов, помеченных определенными тегами. Они добавляются в тег head. С отображением картинки в превью (ее помечают тегом og:image) бывают проблемы. У каждой площадки свои требования к размеру превью, поэтому исходная картинка может некорректно кадрироваться под нужное окно. Чтобы этого избежать, приходится вручную все правильно настраивать. Моя инструкция поможет сделать это автоматически.

Пример превью в разметке Open Graph

Настраиваем автоматический рендеринг

Я буду генерировать og:image с помощью Next.js. Вы можете использовать любой другой фреймворк, главное, стоит помнить, что изображение формируется на сервере.

0. Инициализируем проект

Создаем приложение командой:

yarn create next-app --typescript

и запускаем его:

yarn dev
Стартовая страница после генерации Next.js-приложения
Стартовая страница после генерации Next.js-приложения

Открывается страница приложения — значит, все работает, и можно продолжать.

Для генерации изображений возьмем библиотеку Puppeteer для Node.js. Она дает возможность использовать Headless Chrome Node.js API. С ее помощью мы можем запустить страницу в браузере Chrome на беке, сделать скриншот и отправить клиенту как og:image.

Меньше слов, больше дела. Устанавливаем Puppeteer:

yarn add puppeteer

1. Пишем функцию — обработчик запроса

Переходим в папку /api, создаем файл og-image.ts и пишем функцию OGImage, которая принимает request и отдает response (подробнее про Next.js API — в документации).

// pages/api/og-image.ts

import {NextApiRequest, NextApiResponse} from "next";

export default async function OGImage(req: NextApiRequest, res: NextApiResponse) {
   try {
      
   } catch (e) {
      
   }
}

2. Формируем интерфейс запроса данных для картинки 

Теперь нужно решить, каким образом будем передавать в запросе данные для картинки. Я выбрал метод query. Он позволяет использовать простой запрос GET с параметрами. Пример запроса: https://example.com/api/og-image?title=”Test”

Определяем, что мы ожидаем от клиента title и description, создаем файл types.ts в папке _lib и описываем интерфейс request.

// pages/api/_lib/types.ts

export interface ParsedRequest {
   title?: string;
   desc?: string;
}

3. Пишем функцию для получения параметров запроса

Создаем parser.ts в папке _lib. Функция будет обрабатывать запрос и возвращать заголовок и описание.

// pages/api/_lib/parser.ts

import {NextApiRequest} from "next";
import {ParsedRequest} from "./types";

export function parseRequest(req: NextApiRequest) {
   const { title, desc } = req.query as ParsedRequest;

   return {
       title,
       desc,
   };
}

4. Делаем шаблон разметки картинки

Параметры получили, теперь самое время заняться оформлением картинки. Для упрощения стилизации я буду использовать библиотеку готовых компонентов MUI.

Создаем React-компонент (можно использовать чистый html):

// pages/api/_lib/template.tsx

export const OGImage: FC<ParsedRequest> = ({title, desc}) => {
   return (
       <Box height='100%' width='100%' bgcolor='#1937ff' display='flex'>
                   <Stack m='auto'>
                       <Typography variant='h1' color='white'>
                           {title}
                       </Typography>
                       <Typography variant='h2' color='white'>
                           {desc}
                       </Typography>
                   </Stack>
       </Box>
   )
}

и функцию для рендеринга React-компонента в html-код:

// pages/api/_lib/template.tsx

export function getHtml(parsedReq: ParsedRequest) {
   const { title, desc } = parsedReq;

   return renderToString(<OGImage title={title} desc={desc} />);
}

Осталось отрендерить эту страницу и сделать скриншот. Снова переходим в папку _lib и создаем файл render.ts. В нем сделаем функцию для генерации браузерной страницы и сохранения скриншота. Функция принимает html-код, который мы получили выше.

// pages/api/_lib/render.ts

import core from 'puppeteer';

export async function getScreenshot(html: string) {
   const browser = await core.launch();
   const page = await browser.newPage();
  
   await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 2 });
   await page.setContent(html);
   return await page.screenshot({ type: 'png' });
}

Ширину и высоту я указал оптимальную для разных платформ, deviceScaleFactor позволяет увеличить изображение в два раза для устройств с высоким разрешением экрана. Тип скриншота можно выбрать любой (png, jpeg, webp).

5. Собираем превью

// pages/api/og-image.ts

import {NextApiRequest, NextApiResponse} from "next";
import {parseRequest} from "./_lib/parser";
import {getHtml} from "./_lib/template";
import {getScreenshot} from "./_lib/render";

export default async function OGImage(req: NextApiRequest, res: NextApiResponse) {
   try {
       const parsedReq = parseRequest(req);
       const html = getHtml(parsedReq);
       const file = await getScreenshot(html);
       res.statusCode = 200;
       res.setHeader('Content-Type', `image/png`);
       res.setHeader('Cache-Control', `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`);
       res.end(file);
   } catch (e) {
       res.statusCode = 500;
       res.setHeader('Content-Type', 'text/html');
       res.end('<h1>Internal Error</h1><p>Sorry, there was a problem</p>');
       console.error(e);
   }
}

Переходим по запросу: http://localhost:3000/api/og-image?title=Заголовок&desc=Тестируем%20динамическое%20превью

Вуаля! У нас появляется картинка.

Превью, которое мы получили, используя шаблон из четвертого шага
Превью, которое мы получили, используя шаблон из четвертого шага

6. Добавляем тег в head

И последний шаг:

<meta property="og:image" content="http://localhost:3000/api/og-image?title=Заголовок&desc=Тестируем%20динамическое%20превью">

Заключение

Мы получили быстрое и простое решение для динамического рендеринга превью. Это базовый функционал, в котором используются только текстовые переменные. К нему можно добавить и картинки — достаточно сделать новый шаблон. 

Полный пример проекта, который мы собрали в этой статье, вы можете найти в GitHub-репозитории.

Комментарии (2)


  1. Andrew_br
    13.09.2022 13:43
    -1

    Интересная статья!


  1. Lidiya_Kiryachenko
    15.09.2022 18:04
    +1

    Полезненько :)