Генерація зображень Open Graph в Rails за допомогою SVG-шаблонів

В якості шаблону для генерації JPG зображень я обрав саме SVG через простоту та швидкість роботи з цим форматом. Наприклад HTML в якості шаблону дає більше можливостей, але більш складний у реалізації та підтримці.
Задача - генерація JPG картинки для OpenGraph розмітки (це картинка/прев'юшка для деяких соц. мереж). Rails застосунок знаходиться на Heroku. Кількість RAM не дуже велика, тож задачу генерації картинки треба кудись винести. Налаштовувати та платити за sidekiq для пет-проєкту немає сенсу. Гарним варіантом для мене виявився Heroku Scheduler. Цей стандартний (безкоштовний) addon буде запускати нашу рейк-таску за розкладом (раз на добу). Ну а сама таска буде знаходити пости, які ще не мають OG картинки, та будуть її генерувати та приатачувати (використовується ActiveStorage, зображення буде завантажене на aws) до моделі.
Перше що треба зробити - це зробити SVG шаблон. Спочатку граємось з SVG. OG картинка має бути 1200px x 630px.
Ось приблизно так буде виглядати SVG шаблон:
<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <!-- Фон -->
  <rect width="1200" height="630" fill="black"/>
  <rect x="56" y="56" width="1088" height="518" rx="13" stroke="white" fill="black" stroke-width="13"/>
  
  <!-- Заголовок -->
  <text x="160" y="150" fill="white" font-size="50px" font-weight="bold" line-height="1.2" margin="0">
    Article Title
  </text>

  <!-- Автор -->
  <text x="160" y="510" fill="white" font-size="30px">
    Author Name
  </text>

  <!-- URL веб-сайту -->
  <text x="860" y="510" fill="white" font-size="30px">
    site url
  </text>
</svg>
Цей шаблон не має ніяких змінних. Змінні будуть інтерпретуватись у .erb шаблоні. Закинемо цей SVG-шаблон до нашого rails-застосунка та додамо розширення .erb, наприклад lib/templates/og_image_template.svg.erb.
Я використовую бібліотеку mini_magick. Тож вже маю в Gemfile:
gem "mini_magick"
Далі сам сам код lib/cover_image_generator.rb.
require 'mini_magick'
require 'erb'

module CoverImageGenerator
  class Generator
    def generate_and_attach(entry, entry_title)
      begin
        # Динамічно заповнити SVG-шаблон
        @entry_title = entry_title.truncate(160)
        @blog_name = entry.blog.slug
        svg_template = File.read(File.expand_path('./templates/cover_image_template.svg.erb', __dir__))
        svg_content = ERB.new(svg_template).result(binding)
        
        # Створити зображення MiniMagick з вмісту SVG
        cover_image = MiniMagick::Image.read(svg_content)

        # Конвертувати в JPG
        cover_image.format('jpg')
        temp_file_path = entry.slug + '.jpg'
        cover_image.write(temp_file_path)

        # Прикріпити зображення до запису
        entry.cover_image.attach(io: File.open(temp_file_path), filename: "#{entry.slug}.jpg", content_type: 'image/jpeg')

        # Очистити тимчасовий файл
        File.delete(temp_file_path)
      rescue => e
        # Тут скоріш за все будуть проблеми з текстом, який має специфічні символи, які не можуть будуть конвертовані у JPG.
        # Поки що такі тайтли я просто ігнорую - треба знайти рішення для цієї проблеми.
        # Наразі, якщо модель немає доданого OG-зображення, я показую дефолтну картинку.
        puts "Помилка на: #{@entry_title}, ID: #{entry.id}"
        puts e
        puts "=========================================="
      end
      puts "Зображення прикріплено до: #{@entry_title}, ID: #{entry.id}"
      puts "=========================================="
    end
  end
end
AWS вже налаштован й все що треба зробити - це додати cover_image до нашої моделі (наприклад Topic)
# app/models/topic.rb
has_one_attached :cover_image
Наш SVG шаблон має рендерити наші змінні. Також, враховуючі особливості стилей SVG-файлу нам треба розбити задовгі тайтли на кілька рядків.
<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <!-- Фон -->
  <rect width="1200" height="630" fill="black"/>
  <rect x="56" y="56" width="1088" height="518" rx="13" stroke="white" fill="black" stroke-width="13"/>
  
  <!-- Багаторядковий заголовок -->
  <text x="160" y="110" fill="white" font-size="50px" font-weight="bold" line-height="1.2" margin="0">
    <% lines = @entry_title.scan(/\S.{0,34}\S(?=\s|\z)/) %>
    <% lines.each_with_index do |line, index| %>
      <tspan x="160" dy="<%= index == 0 ? '1.5em' : '1.2em' %>"><%= line.strip %></tspan>
    <% end %>
  </text>

  <!-- Назва блогу -->
  <text x="160" y="510" fill="white" font-size="30px">
    <%= @blog_name %>
  </text>

  <!-- URL веб-сайту -->
  <text x="860" y="510" fill="white" font-size="30px">
    tseivo.com
  </text>
</svg>
І в кінці для генерації картинки:
topic = Topic.last
CoverImageGenerator::Generator.new.generate_and_attach(topic, topic.name)
Цей код треба додати в рейк таску, яка пройду та згенерує OG-зображення для всіх потрібних об'єктів. Також можна використовувати цей код для генерації зображення після створення або оновлення (заголовку) об'єкту.
Результат:
Приклад створеного JPEG з SVG шаблону
Приклад створеного JPEG з SVG шаблону
Цей приклад коду / концепції не є фінальним варіантом. Є ще над чим попрацювати.

🔗 Цитувати допис: "Генерація зображень Open Graph в Rails за допомогою SVG-шаблонів"

Якщо ви хочете процитувати цей допис у своїй роботі, статті, блозі, використовуйте наведену нижче інформацію.

Розгорнути деталі


🙌 Підтримати блог @memecode

Ви можете поширити цей допис у соцмережах, чим допоможете платформі цейво розвиватись (* ^ ω ^)

📝 Більше публікацій:
Дисклеймер

Інформація на сайті tseivo.com є суб'єктивною та відображає особисті погляди та досвід авторів та авторок блогів.

Використовуйте цей ресурс як одне з декількох джерел інформації під час своїх досліджень та прийняття рішень. Завжди застосовуйте критичне мислення. Людина сама несе відповідальність за свої рішення та дії.