Prerequisites
At Wannadocs, we're generating Open Graph tags for each article to make it looks as attractive as possible when sharing on social media or messengers.
I didn't want to spend a lot of time on this feature, so I created a 1200x600px image, filled it with the color that a user specified as the primary color in the Knowledge Base theme, and added the title as white text.
It was operating like that for a whole year.
def create_og_image_preview(path, color: '#5168ec', message: ) | |
unless File.exist?(path) | |
rgb = color.match(/#(..)(..)(..)/)[1..3].map{|x| x.hex} | |
im = Vips::Image.black(1200, 630).ifthenelse([0,0,0], rgb) | |
text = Vips::Image.text message, width: im.width, dpi: 400, font: 'sans bold' | |
text = text.gravity :centre, im.width, im.height | |
overlay = (text.new_from_image [255, 255, 255]).copy interpretation: :srgb | |
overlay = overlay.bandjoin text | |
im = im.composite overlay, :over | |
im.write_to_file path | |
end | |
end |
A year later, I began to notice that large companies are moving in this direction. I was excited and wants to know how they do it.
For example:
dev.to
github.com
I did a little investigation. Since the source code of the dev.to is open, I found out that Github and dev.to creating their previews in HTML and send them to a special service that renders previews through Headless Chromium. With the new knowledge, I decided to slightly improve our previews generating to keep it up with the smooth appearance and content.
Since we sell the self-hosted version of Wannadocs, we're not suited to using the third-party service here, so we decided to stay on "libvips" + Ruby.
Let's go!
Installation
First, you need to install all necessary packages on the server and build the "libvips" (Ansible playbook is doing this for me, so I tried to get just shell commands from there):
sudo apt-get install fonts-open-sans devscripts libfftw3-dev libmagickwand-dev libopenexr-dev liborc-0.4-0 gobject-introspection libgsf-1-dev libglib2.0-dev liborc-0.4-dev automake libtool swig gtk-doc-tools libpango1.0-dev -y | |
git clone https://github.com/libvips/libvips.git | |
cd libvips && ./autogen.sh && make && sudo make install | |
ldconfig -v |
Here you can find the official manual on how to build it.
Pay attention to these packages:
fonts-open-sans – don't forget about fonts otherwise you will get that:
And libpango1.0-dev – without this library Vips cannot properly write text.
Anatomy
Our previews are very similar to Github:
- Gray text is a knowledge base name, in this case (Demo)
- Big dark text is an article title
- On the right side knowledge base logo
- At the bottom 20px border is the primary color of the knowledge base
Code
I'm also a big fan of jQuery-style DSL with returning of the “self” object from a method and chaining.
# frozen_string_literal: true | |
require 'vips' | |
module Superdocs | |
module Opengraph | |
class Image | |
attr_reader :elements, :canvas, :borders, :canvas_color | |
def initialize(width, height, color: '#ffffff') | |
@elements = [] | |
@canvas_color = color | |
@canvas = Vips::Image.black(width, height).ifthenelse([0, 0, 0], hex_to_rgb(color)) | |
@borders = {} | |
end | |
def image(source, resize_with_ratio:) | |
im = source ? Vips::Image.new_from_buffer(source, '') : Vips::Image.black(1, 1).ifthenelse([0, 0, 0], hex_to_rgb(canvas_color)) | |
if resize_with_ratio && source | |
width, height = resize_with_ratio | |
ratio = get_ratio(im, width, height, :min) | |
im = im.resize(ratio) | |
end | |
@elements << im | |
self | |
end | |
def text(message, width:, height:, dpi: 300, color: '#2f363d', font: 'Open Sans Bold') | |
im = Vips::Image.text(message, width: width, height: height, dpi: dpi, font: font) | |
im = im.new_from_image(hex_to_rgb(color)).copy(interpretation: :srgb).bandjoin(im) | |
@elements << im | |
self | |
end | |
def border_bottom(height, color: '#000000') | |
im = Vips::Image.black(canvas.width, height).ifthenelse([0, 0, 0], hex_to_rgb(color)) | |
borders[:bottom] = im | |
self | |
end | |
def compose(filename) | |
result = yield(canvas, *elements) | |
@canvas = @canvas.composite( | |
elements + [borders[:bottom]], :over, | |
x: result[:x] + (borders[:bottom] ? [0] : []), | |
y: result[:y] + (borders[:bottom] ? [canvas.height - borders[:bottom].height] : []) | |
) | |
@canvas.write_to_file(filename) | |
end | |
private | |
def hex_to_rgb(input) | |
case input | |
when String | |
input.match(/#(..)(..)(..)/)[1..3].map(&:hex) | |
when Array | |
input | |
else | |
raise ArgumentError, "Unknown input #{input.inspect}" | |
end | |
end | |
# https://github.com/dariocravero/vips-process/blob/master/lib/vips-process/resize.rb#L109 | |
def get_ratio(image, width, height, min_or_max = :min) | |
width_ratio = width.to_f / image.width | |
height_ratio = height.to_f / image.height | |
[width_ratio, height_ratio].send min_or_max | |
end | |
end | |
end | |
end | |
# logo = File.read('logo.png') | |
# Superdocs::Opengraph::Image.new(1200, 600) | |
# .image(logo, resize_with_ratio: [180, 180]) | |
# .text('Demo', width: 900, height: 200, dpi: 200, color: '#959da5') | |
# .text('Custom CSS and Javascript', width: 800, height: 200, dpi: 300, color: '#2f363d') | |
# .border_bottom(20, color: '#4152bc') | |
# .compose(filename) do |canvas, logo, project_name, _note_title| | |
# { | |
# x: [canvas.width - logo.width - 100, 100, 100], | |
# y: [100, 100, project_name.height + 150] | |
# } | |
# end | |
# Github style OG image: https://cdn-images-1.medium.com/max/800/1*fDSZLzAtrs7xOasSULOISg.jpeg |
With DSL I'm trying to reduce the inconvenience of creating image layout with code and make this snippet more portable from project to project.
In Rails, I created a controller that serves up these images like this:
module Frontpage | |
class OpengraphController < ApplicationController | |
def image | |
note = project.notes.find(params[:id]) | |
checksum = Digest::SHA1.hexdigest([note.title, project.primary_color, project.name, project.logo&.file&.id].join) | |
filename = "#{Rails.root}/public/uploads/og-image-#{checksum}.jpg" | |
unless File.exist?(filename) | |
Superdocs::Opengraph::Image.new(1200, 600) | |
.image(project.logo&.file&.read, resize_with_ratio: [180, 180]) | |
.text(project.name, width: 900, height: 200, dpi: 200, color: project_name_color) | |
.text(note.title, width: 800, height: 200, dpi: 300, color: note_title_color) | |
.border_bottom(20, color: project.primary_color) | |
.compose(filename) do |canvas, logo, project_name, _note_title| | |
{ | |
x: [canvas.width - logo.width - 100, 100, 100], | |
y: [100, 100, project_name.height + 150] | |
} | |
end | |
end | |
if stale?(etag: checksum) | |
send_data File.read(path), type: 'image/jpg', disposition: 'inline' | |
end | |
end | |
end | |
end |
As a result, we get Open Graph images for all social networks without unnecessary dependencies on third-party services and very fast generation, unfortunately at the cost of the inconvenience of image layout.
Wannadocs - we make product knowledge bases for your customers, we have:
Cool editor with support for markdown and embedded elements (YouTube, CodePen, Gist, Figma, Airtable etc.)
Analytics by the article: views, likes, dislikes
Built-in full-text search
Templates and customization
SSL certificates in two clicks
Amazing OpenGraph images for your links 😍
Who has read to the end catch the promo code 10% discount: dz74cdq
Top comments (0)