Всем привет! Меня зовут Николай Каменев, я фронтенд-разработчик в Почтатехе. Мы разрабатываем 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
Открывается страница приложения — значит, все работает, и можно продолжать.
Для генерации изображений возьмем библиотеку 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-репозитории.