Генерація зображень 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-зображення для всіх потрібних об'єктів. Також можна використовувати цей код для генерації зображення після створення або оновлення (заголовку) об'єкту.
Результат:
Цей приклад коду / концепції не є фінальним варіантом. Є ще над чим попрацювати.