How to generate OG links preview like Dev.to (for free) with Rails 7
Tags: rails, ruby
Published: 2022-04-28 23:41:55

Hey hey heyyyyyy
Quick article on how to have beauty link previews, just like Dev.to does

You're gonna get something like this:

![Final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1mxuv3c26w4duy8l634y.png)

Yup, was being literal with the "*like dev.to*" 😛
I've build something similar for my new project [workby.io](https://workby.io), here's how i've build it...

---

Almost every guide that i found was using [htmlcsstoimage.com](https://htmlcsstoimage.com), in fact, dev.to uses it. It's pretty popular, and efficient, BUT, i don't want to pay $14 bucks for each 1k images 😂🐁

So, i'm going to use [Grover](https://github.com/Studiosity/grover): A Ruby gem to transform HTML into PDFs, PNGs or JPEGs using Google Puppeteer and Chromium.

If you want to build this with, let's say, Node.js, you can also follow me. Puppeteer is popular and easy to use.

**Long story short:** We're going to build a HTML view and take an screenshot of it.

You can create a blank Rails project, or just add additional functionality to your current app.

### Setup
Install Grover gem with
`bundle add grover`

And the dependency of Grover, *Puppeteer*, with:
`yarn add puppeteer`

Then create an empty controller where you can respond with different link preview templates:
`rails g controller og-imager dev_to`

In the `dev_to` action, let's call Grover and let him do the magic
`app/controllers/og_imager_controller.rb`
```ruby
class OgImagerController < ApplicationController
  # GET /og_imager/dev_to
  # @param {string} title
  # @param {string} avatar
  # @param {string} username
  # @param {string} timestamp
  # @param {array} logos
  # @returns image/png
  def dev_to
    # Get params and set to variable
    # TODO: Retrieve your object instead :p
    @title = params[:title]
    @avatar = params[:avatar]
    @username = params[:username]
    @timestamp = params[:timestamp]
    @logos = params[:logos]

    # Grover.new accepts a URL or inline HTML and optional parameters for Puppeteer
    grover = Grover.new(
      render_to_string
    )

    # Get a screenshot
    png = grover.to_png

    # Render image
    send_data(png, type: 'image/png', disposition: 'inline')
  end
end
```

Here's our view, just simple HTML/CSS
`app/views/og_imager/dev_to.html.erb`
```html
<div class="container">
  <div class="card">
    <h1 class="title">
      <%= @title %>
    </h1>
    <div class="details">
      <div class="user">
        <img src="<%= @avatar %>" class="avatar">
        <p class="username">
          <%= @username %> - <%= @timestamp %>
        </p>
      </div>
      <div class="logos">
        <% @logos.each do |logo| %>
          <img src="<%= logo %>" class="logo">
        <% end %>
      </div>
    </div>
  </div>
</div>

<style>
  @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap');

  *{
    font-family: 'Roboto', sans-serif;
    color: #cd685f;
  }

  .container{
    width: 1128px;
    height: 600px;
    display: flex;
  }

  .card{
    display: flex;
    justify-content: space-between;
    flex-direction: column;
    margin: 2.8rem 4rem 4rem 2.8rem;
    border: 3px solid #c56b61;
    border-radius: 25px 25px 0px 0px;
    box-shadow: 7px 10px 0px 1px #c56b61;
    padding: 3.8rem 1.8rem 1.8rem 1.8rem;
  }

  .title{
    margin: 0px;
    font-weight: 500;
    font-size: 5rem;
    line-height: 80px;
  }

  .details{
    display: flex;
    justify-content: space-between;
  }

  .user{
    display: flex;
    align-items: center;
  }

  .avatar{
    border: 3px solid #c56b61;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    margin-right: 10px;
  }

  .username{
    margin: 0;
    font-size: 2.5rem;
  }

  .logos{
    display: flex;
    justify-content: space-between;
  }

  .logo{
    width: 60px;
    height: 60px;
    margin-left: 20px;
    transform: rotate(5deg);
  }
</style>
```

And finally, create an initializer to set our Browser viewport (final image size).

`initializers/grover.rb`
```ruby
Grover.configure do |config|
  config.options = {
    viewport: {
      width: 1128,
      height: 600
    },
  }
end
```

If you have created a blank project, and want to use this as a micro-service, here's a guide on how to deploy this thing to Heroku.

1. Create the app on your Heroku account (T'm assuming you already have the cli installed): `heroku create your_app_name`
2. Warn Heroku of our JS library (Puppeteer): `heroku buildpacks:add heroku/nodejs --index=1`
3. And also help him to setup Puppeteer dependencies 😛: `heroku buildpacks:add jontewks/puppeteer --index=2`
4. Tell Grover to run Puppeteer in the "no-sandbox": `heroku config:set GROVER_NO_SANDBOX=true`

To make use of this image on the link preview, you will need [The Open Graph protocol](https://ogp.me/). Just simple meta tags that browsers will look for.

Meta tags are always on <head> so,
`app/views/layouts/application.html.erb`
```html
<%= yield(:head) %>
```

And then ask ERB to fill previous block with
```html
<%= content_for :head do %>
  <meta property="og:title" content="<%= @article.title %>">
  <meta property="og:image" content="<%= article_link_preview(@article) %>">
  <meta property="og:image:type" content="image/png" />
  <meta property="og:image:width" content="1128">
  <meta property="og:image:height" content="600">
  <meta name="twitter:card" content="summary_large_image" />
<% end %>
```
at your details view. Like: `/articles/:id`

The `twitter:card` thing it's because they are more fancy on link previews, you can read more about that [here](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup)

The previous `article_link_preview` method it's a simple helper that generates the URL that we are looking for.

`app/helpers/articles_helper.rb`
```ruby
def article_link_preview article
  uri = URI.parse('https://your_app_name.herokuapp.com/ogimage')
  uri.query = URI.encode_www_form(
    title: article.title,
    'images[]': article.tags.collect(&:image),
    timestamp: article.created_at.to_formatted_s(:short),
    ...
  )
  uri.to_s
end
```

You will get something like this:
`https://your_app_name.herokuapp.com/og_imager/dev_to?title=Real%20Time%20Notification%20System%20with%20Hotwire,%20in%20Rails%207&avatar=https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/369241/a0111bcb-a046-4398-98cd-f4cf0e2f8d0d.png&username=Matias%20Carpintini&timestamp=13%20April&logos[]=https://img.icons8.com/office/80/000000/ruby-gemstone.png&logos[]=https://practicaldev-herokuapp-com.freetls.fastly.net/assets/devlogo-pwa-512.png`

Here's the [repo](https://github.com/matias-carpintini/og-imager) of the whole thing.