Как мы используем Puppeteer для создания Open Graph изображений с Node.js

—

от автора

В наше время многочисленные сайты создают страницы, которыми пользователи хотели бы делиться в разных социальных сетях или мессенджерах. Благодаря тегам Open Graph ссылки могут иметь красочное превью изображение, которое привлекает еще больше внимания, например, с помощью тега og:image.

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

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

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

Open Graph изображение профиля пользователя

Open Graph изображение профиля пользователя

В нашем проекте первоначальные версии изображений создавались с помощью PHP, и какое-то время этого было достаточно, поскольку изображения были не очень сложными и содержали только картинку пользователя. Однако как только мы добавили имя пользователя, то уже сразу возникли проблемы с позиционированием текста. Возьмем для примера простой скрипт создания изображения на PHP.

Пример создания изображения на PHP
<?php  $width = 400; $height = 200; $image = imagecreatetruecolor($width, $height);  $backgroundColor = imagecolorallocate($image, 255, 255, 255); imagefill($image, 0, 0, $backgroundColor);  $textColor = imagecolorallocate($image, 0, 0, 0);  $fontFile = 'path/to/your/font.ttf';  $language = isset($_GET['lang']) ? $_GET['lang'] : 'ru'; $text = getLocalizedText($language);  $fontSize = 24;  $textbox = imagettfbbox($fontSize, 0, $fontFile, $text); $textX = ($width - ($textbox[2] - $textbox[0])) / 2; $textY = ($height - ($textbox[5] - $textbox[3])) / 2 + ($textbox[5] - $textbox[3]);  imagettftext($image, $fontSize, 0, $textX, $textY, $textColor, $fontFile, $text);  header("Content-Type: image/png"); imagepng($image); imagedestroy($image);  function getLocalizedText($language) {     switch ($language) {         case 'en':             return 'Hello, PHP!';         case 'fr':             return 'Bonjour, PHP!';         default:             return 'Привет, PHP!';     } }

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

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

Решение, о котором я хотел бы рассказать позволяет в разы сократить время разработки и упростить поддержку таких изображений. Более того — оно гибкое и очень легко масштабируется и для других вещей.

Для веб-разработчика HTML — это самый простой и удобный язык разметки. Вместе со стилями CSS вы можете создать гибкий интерфейс, учитывающий расположение любых элементов на странице — изображений, текста, таблиц, списков и т.д.

Давайте посмотрим и на примере ниже. Это простая HTML-страница со множеством стилей CSS, таких как градиент в тексте, тени, ограничение текстовых строк для длинного текста и другое.

Пример HTML кода для страницы профиля
html>     <head>         <style>             html, body {                 margin: 0;                 padding: 0;             }              body {                 background-color: #0093E9;                 background-image: linear-gradient(160deg, #0093E9 0%, #80D0C7 100%);                  font-family: Helvetica, sans-serif;                 padding: 5%;                 text-align: center;             }              header {                 position: relative;             }              header .emoji {                 position: absolute;                 top: -10px;                 left: 0;                 transform: rotate(20deg);                 font-size: 3rem;             }              * {                 box-sizing: border-box;             }              h1 {                 text-transform: uppercase;                 font-size: 3rem;                 background: -webkit-linear-gradient(45deg, #85FFBD 0%, #FFFB7D 100%);                 -webkit-background-clip: text;                 -webkit-text-fill-color: transparent;             }              .wrapper {                 display: flex;                 padding-top: 2rem;             }              .avatar {                 display: flex;                 justify-content: center;                 align-items: center;             }             .avatar img {                 width: 140px;                 height: 140px;                 border-radius: 100px;                 border: 5px solid rgba(255,255,255, 0.5);                 box-shadow: 0 0 10px rgba(0,0,0,0.2);                 object-fit: cover;             }              .content {                 padding: 1rem 2rem;             }              .content .text {                 font-size: 1.5rem;                 display: -webkit-box;                 -webkit-line-clamp: 3; /* number of lines to show */                         line-clamp: 3;                  -webkit-box-orient: vertical;                 overflow: hidden;                 color: rgba(255,255,255, 0.8);             }         </style>     </head>     <body>         <div>             <header>                 <h1>Hello, Javascript</h1>                 <div class="emoji">?</div>             <header>             <div class="wrapper">                 <div class="avatar">                     <img src="https://sun9-57.userapi.com/impg/O3egMIWPZjhcKSThZ2hn7ByaQmET8ySOq5e4ww/O_ngP3qqEd8.jpg?size=1178x1789&quality=95&sign=71fcbf49ffff80fad9f0ef39f598cf69&type=album" alt=""/>                 </div>                 <div class="content">                     <div class="text"><strong>Lorem Ipsum</strong>  is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</div>                 </div>             </div>         </div>     </body> </html>

Теперь все, что нам нужно, так это запустить веб-сервер, который может обслуживать этот HTML-код, и запустить приложение node.js с библиотекой Puppeteer, которая запускает headless версию Google Chrome, а затем вы можете сделать снимок экрана страницы и получить изображение.

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

import puppeteer, { Page } from 'puppeteer-core';  const width = 800; const height = 600; const deviceScaleFactor = 1; const imageType = 'webp'; const imageQuality = 90; const url = '<Путь к странице на веб сервере>';  // Путь к Chrome или Chromium // На macOS путь до браузера будет таким "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" // на Linux Для Google Chrome будет "/usr/bin/google-chrome-stable" const browserPath = '/usr/bin/chromium';    const browser = await puppeteer.launch({     headless: true,     ignoreHTTPSErrors: true,     executablePath: browserPath,     args: ['--no-sandbox', '--disable-setuid-sandbox'], });  const page: Page = await browser.newPage(); await page.setJavaScriptEnabled(false); await page.setViewport({     width,     height,     deviceScaleFactor, });  try {     const result = await page.goto(url, {         waitUntil: 'load',     });      if (result?.status() !== 200) {        page.close();        throw new Error(`Incorrect status page ${result?.status()}`);     } } catch (error) {     page.close();     throw new Error(error as string); }  const data = await page.screenshot({     type: imageType,     quality: imageQuality,     encoding: 'binary', });  page.close();

В результате получается вот такое изображение, которое можно сохранить на диск или загрузить в свое S3 хранилище и предоставить его пользователям.

Изображение созданное с помощью Puppeteer

Изображение созданное с помощью Puppeteer

В нашем случае мы создали небольшое приложение node.js с Puppeteer, которое упаковано в контейнер Docker, и запускаем внутри него HTTP-сервер для управления запросами. HTML код мы генерируем с помощью серверного рендера React, в зависимости от переданных данных в запросе. В итоге приложение позволяет нам создавать изображения различного формата (например, png, jpeg или webp) для любой страницы веб-сайта. Также мы отключаем поддержку JS потому что отрисовкой занимается серверный рендер и экономим время загрузки страницы (нам нужны только картинки). С недавних пор мы полностью перешли на генерацию webp изображений и это позволило нам существенно сократить размер изображений и сделать градиенты (по сравнению с jpeg) более приятными.

В итоге, полная логика сервиса по генерации OG-изображений получилось такой:

  1. Когда пользователь шарит где-то ссылку, то в og:image отдается специальная ссылка (с идентификатором его профиля, необходимым размером и расширением изображения) на прокси-сервер nginx.

  2. Прокси-сервер проверяет, находится ли изображение уже в хранилище S3 или нет.

  3. Если изображение существует, оно отдается напрямую; в противном случае делается запрос к сервису node.js, который генерирует изображение, возвращает его и асинхронно загружает на S3.

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

Само собой такой подход можно использовать и для создания любых других изображений как баннеры или даже, например, PDF файлов с описанием каких-либо товаров. Более того, для Puppeteer есть библиотека puppeteer-screen-recorder благодаря которой можно создавать видео ролики в которой анимация будет создана с помощью CSS. Это тоже может быть полезным потому что, например, iMessage умеет играть небольшие видео которые есть в метатеге og:video. Возможно в будущем мы увидим такую же возможность, например, и в Telegram.

import puppeteer from 'puppeteer'; import { PuppeteerScreenRecorder } from 'puppeteer-screen-recorder';  function delay(time: number) {     return new Promise(function (resolve) {         setTimeout(resolve, time);     }); }  (async () => {     const browser = await puppeteer.launch({ headless: true });     const page = await browser.newPage();      await page.setViewport({         width: 640,         height: 480,     });     await page.goto('http://localhost:10001');      const recorder = new PuppeteerScreenRecorder(page, {         fps: 30,         videoCrf: 20,         videoFrame: {             width: 640,             height: 480,         },     });        await recorder.start('./simple.mp4');      await delay(500);     await recorder.stop();     await page.close();     await browser.close(); })(); 

В итоге получается вот такой ролик

Для удобства вот пример докер-контейнера, который мы используем со всеми необходимыми библиотеками для работы с изображениями.

Пример Docker контейнера
FROM node:18  RUN apt -y update \  && apt -y install \   git \   openssh-server \   gconf-service \   libasound2 \   libatk1.0-0 \   libc6 \   libcairo2 \   libcups2 \   libdbus-1-3 \   libexpat1 \   libfontconfig1 \   libgcc1 \   libgconf-2-4 \   libgdk-pixbuf2.0-0 \   libglib2.0-0 \   libgtk-3-0 \   libnspr4 \   libpango-1.0-0 \   libpangocairo-1.0-0 \   libstdc++6 \   libx11-6 \   libx11-xcb1 \   libxcb1 \   libxcomposite1 \   libxcursor1 \   libxdamage1 \   libxext6 \   libxfixes3 \   libxi6 \   libxrandr2 \   libxrender1 \   libxss1 \   libxtst6 \   ca-certificates \   fonts-liberation \   libappindicator1 \   libnss3 \   lsb-release \   xdg-utils \   wget \   curl \   libnss3-dev \   libgbm-dev \   libu2f-udev \   udev \   libvips \   chromium  # Добавьте нужные вам шрифты COPY ./fonts /root/.fonts RUN fc-cache -fv  # Сборка вашего проекта ...  # Запуск приложения CMD ["node", "app.js"]


ссылка на оригинал статьи https://habr.com/ru/articles/787678/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *