Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

social cards - part 1 #1

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions Envfile
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ variable :CLOUDINARY_API_SECRET, :String, default: "Optional"
variable :CLOUDINARY_CLOUD_NAME, :String, default: "Optional"
variable :CLOUDINARY_SECURE, :String, default: "Optional"

# HTML/CSS to Image for generating social preview images
variable :HCTI_API_USER_ID, :String, default: "Optional"
variable :HCTI_API_KEY, :String, default: "Optional"

# Dacast for streaming
variable :DACAST_STREAM_CODE, :String, default: "Optional"

Expand Down
53 changes: 46 additions & 7 deletions app/controllers/social_previews_controller.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
class SocialPreviewsController < ApplicationController
# No authorization required for entirely public controller

PNG_CSS = "body { transform: scale(0.3); } .preview-div-wrapper { overflow: unset; margin: 5vw; }".freeze
SHE_CODED_TAGS = %w[shecoded theycoded shecodedally].freeze

def article
@article = Article.find(params[:id])
not_found unless @article.published
if (@article.decorate.cached_tag_list_array & %w[shecoded theycoded shecodedally]).any?
render "shecoded", layout: false
else
render layout: false

template = "article"

template = "shecoded" if (@article.decorate.cached_tag_list_array & SHE_CODED_TAGS).any?

respond_to do |format|
format.html do
render template, layout: false
end
format.png do
html = render_to_string(template, formats: :html, layout: false)
redirect_to HtmlCssToImage.fetch_url(html: html, css: PNG_CSS, google_fonts: "Roboto|Roboto+Condensed"), status: 302
end
end
end

def user
@user = User.find(params[:id]) || not_found
render layout: false

respond_to do |format|
format.html do
render layout: false
end
format.png do
html = render_to_string(formats: :html, layout: false)
redirect_to HtmlCssToImage.fetch_url(html: html, css: PNG_CSS, google_fonts: "Roboto"), status: 302
end
end
end

def organization
@user = Organization.find(params[:id]) || not_found
render "user", layout: false

respond_to do |format|
format.html do
render "user", layout: false
end
format.png do
html = render_to_string("user", formats: :html, layout: false)
redirect_to HtmlCssToImage.fetch_url(html: html, css: PNG_CSS, google_fonts: "Roboto"), status: 302
end
end
end

def tag
@tag = Tag.find(params[:id]) || not_found
render layout: false

respond_to do |format|
format.html do
render layout: false
end
format.png do
html = render_to_string(formats: :html, layout: false)
redirect_to HtmlCssToImage.fetch_url(html: html, css: PNG_CSS, google_fonts: "Roboto"), status: 302
end
end
end
end
27 changes: 27 additions & 0 deletions app/lib/html_css_to_image.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module HtmlCssToImage
AUTH = { username: ApplicationConfig["HCTI_API_USER_ID"],
password: ApplicationConfig["HCTI_API_KEY"] }.freeze

FALLBACK_IMAGE = "https://thepracticaldev.s3.amazonaws.com/i/g355ol6qsrg0j2mhngz9.png".freeze

def self.url(html:, css: nil, google_fonts: nil)
image = HTTParty.post("https://hcti.io/v1/image",
body: { html: html, css: css, google_fonts: google_fonts },
basic_auth: AUTH)

image["url"] || FALLBACK_IMAGE
end

def self.fetch_url(html:, css: nil, google_fonts: nil)
cache_key = "htmlcssimage/#{html}/#{css}/#{google_fonts}"
cached_url = Rails.cache.read(cache_key)

return cached_url if cached_url.present?

image_url = url(html: html, css: css, google_fonts: google_fonts)

Rails.cache.write(cache_key, image_url) unless image_url == FALLBACK_IMAGE

image_url
end
end
15 changes: 8 additions & 7 deletions app/views/social_previews/article.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<% accent_color = HexComparer.new([user_colors(@article.user)[:bg], user_colors(@article.user)[:text]]).biggest %>
<% color = HexComparer.new([user_colors(@article.user)[:bg], user_colors(@article.user)[:text]]).brightness(1.4) %>
<% dark_color = HexComparer.new([user_colors(@article.user)[:bg], user_colors(@article.user)[:text]]).brightness(0.7) %>
<% not_so_rand = Random.new(@article.id) # Using ID as seed ensures we get the same "random" numbers on each page load, Improves caching %>
<style>
body {
background: white;
Expand All @@ -22,22 +23,22 @@
.circle1 {
height: 40vw;
width: 40vw;
right: -<%= rand(2..15) %>vw;
bottom: -<%= rand(2..15) %>vw;
right: -<%= not_so_rand.rand(2..15) %>vw;
bottom: -<%= not_so_rand.rand(2..15) %>vw;
}

.circle2 {
height: 30vw;
width: 30vw;
left: <%= rand(2..16) %>vw;
bottom: 0vw;
left: <%= not_so_rand.rand(2..16) %>vw;
bottom: -<%= not_so_rand.rand(2..15) %>vw;
}

.circle3 {
height: 24vw;
width: 24vw;
left: -8vw;
top: -<%= rand(2..6) %>vw;
top: -<%= not_so_rand.rand(2..6) %>vw;
}

.preview-div {
Expand All @@ -54,7 +55,7 @@
.preview-info-header {
color: black;
margin: 2vw auto 0vw;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, "Roboto", monospace;
font-size: 3.5vw;
width: 92%;
}
Expand All @@ -70,7 +71,7 @@
color: <%= dark_color %>;
width: 92%;
margin: 1vw auto;
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", "Roboto", sans-serif;
}

.preview-user {
Expand Down
37 changes: 22 additions & 15 deletions app/views/social_previews/shecoded.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,40 @@
background-repeat: repeat;
<% end %>
}
.content {
.preview-div-wrapper {
background: white;
position: absolute;
top: 5vw;
bottom: 5vw;
left: 5vw;
right: 5vw;
width: 80vw;
height: 35vw;
margin: 2.5vw auto 2.5vw;
padding: 4vw;
}
.avatar {
margin-left: auto;
margin-right: auto;
margin-bottom: 2.5vw;
width: 12vw;
}
img {
border-radius: 200vw;
border: 0.6vw solid #70589b;
margin: 3.5vw auto 2.5vw;
display: block;
height: 10vw;
border-radius: 12vw;
border: .75vw solid #70589b;
height: 12vw;
width: 12vw;
}
.title {
font-size: <%= 7 - @article.title.size / 40 %>vw;
margin: auto;
width: 80%;
color: #70589b;
text-align: center;
font-family: "HelveticaNeue-CondensedBold", "HelveticaNeueBoldCondensed", "HelveticaNeue-Bold-Condensed", "Helvetica Neue Bold Condensed", "HelveticaNeueBold", "HelveticaNeue-Bold", "Helvetica Neue Bold", "HelveticaNeue", "Helvetica Neue", 'TeXGyreHerosCnBold', "Helvetica", "Tahoma", "Geneva", "Arial Narrow", "Arial", sans-serif;
font-family: "HelveticaNeue-CondensedBold", "HelveticaNeueBoldCondensed", "HelveticaNeue-Bold-Condensed", "Helvetica Neue Bold Condensed", "HelveticaNeueBold", "HelveticaNeue-Bold", "Helvetica Neue Bold", "HelveticaNeue", "Helvetica Neue", 'TeXGyreHerosCnBold', "Helvetica", "Tahoma", "Geneva", "Arial Narrow", "Arial", "Roboto Condensed", sans-serif;
}
</style>
<div class="content">
<img src="<%= ProfileImage.new(@article.user).get(320) %>" />

<div class="preview-div-wrapper">
<div class="avatar">
<img src="<%= ProfileImage.new(@article.user).get(320) %>" />
</div>
<div class="title">
<%= @article.title %>
<strong><%= @article.title %></strong>
</div>
</div>
13 changes: 7 additions & 6 deletions app/views/social_previews/tag.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<% accent_color = HexComparer.new([@tag.bg_color_hex, @tag.text_color_hex]).biggest %>
<% color = HexComparer.new([@tag.bg_color_hex, @tag.text_color_hex]).brightness(1.4) %>
<% dark_color = HexComparer.new([@tag.bg_color_hex, @tag.text_color_hex]).brightness(0.7) %>
<% not_so_rand = Random.new(@tag.id) # Using ID as seed ensures we get the same "random" numbers on each page load, Improves caching %>
<style>
body {
background: white;
Expand All @@ -22,22 +23,22 @@
.circle1 {
height: 40vw;
width: 40vw;
right: -<%= rand(2..15) %>vw;
bottom: -<%= rand(2..15) %>vw;
right: -<%= not_so_rand.rand(2..15) %>vw;
bottom: -<%= not_so_rand.rand(2..15) %>vw;
}

.circle2 {
height: 30vw;
width: 30vw;
left: <%= rand(2..16) %>vw;
bottom: 0vw;
left: <%= not_so_rand.rand(2..16) %>vw;
bottom: -<%= not_so_rand.rand(2..15) %>vw;
}

.circle3 {
height: 24vw;
width: 24vw;
left: -8vw;
top: -<%= rand(2..6) %>vw;
top: -<%= not_so_rand.rand(2..6) %>vw;
}

.preview-div {
Expand All @@ -55,7 +56,7 @@
color: <%= dark_color %>;
width: 92%;
margin: 10vw auto 0vw;
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", "Roboto", sans-serif;
font-size: 12vw;
}

Expand Down
13 changes: 7 additions & 6 deletions app/views/social_previews/user.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<% accent_color = HexComparer.new([user_colors(@user)[:bg], user_colors(@user)[:text]]).biggest %>
<% color = HexComparer.new([user_colors(@user)[:bg], user_colors(@user)[:text]]).brightness(1.4) %>
<% dark_color = HexComparer.new([user_colors(@user)[:bg], user_colors(@user)[:text]]).brightness(0.7) %>
<% not_so_rand = Random.new(@user.id) # Using ID as seed ensures we get the same "random" numbers on each page load, Improves caching %>
<style>
body {
background: white;
Expand All @@ -22,22 +23,22 @@
.circle1 {
height: 40vw;
width: 40vw;
right: -<%= rand(2..15) %>vw;
bottom: -<%= rand(2..15) %>vw;
right: -<%= not_so_rand.rand(2..15) %>vw;
bottom: -<%= not_so_rand.rand(2..15) %>vw;
}

.circle2 {
height: 30vw;
width: 30vw;
left: <%= rand(2..16) %>vw;
bottom: 0vw;
left: <%= not_so_rand.rand(2..16) %>vw;
bottom: -<%= not_so_rand.rand(2..15) %>vw;
}

.circle3 {
height: 24vw;
width: 24vw;
left: -8vw;
top: -<%= rand(2..6) %>vw;
top: -<%= not_so_rand.rand(2..6) %>vw;
}

.preview-div {
Expand All @@ -55,7 +56,7 @@
color: <%= dark_color %>;
width: 92%;
margin: 10vw auto 1vw;
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", "Roboto", sans-serif;
font-size: 8vw;
}

Expand Down
50 changes: 50 additions & 0 deletions spec/lib/html_css_to_image_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require "rails_helper"

RSpec.describe HtmlCssToImage do
describe ".url" do
it "returns the url to the created image" do
stub_request(:post, /hcti.io/).
to_return(status: 200,
body: '{ "url": "https://hcti.io/v1/image/6c52de9d-4d37-4008-80f8-67155589e1a1" }',
headers: { "Content-Type" => "application/json" })

expect(described_class.url(html: "test")).to eq("https://hcti.io/v1/image/6c52de9d-4d37-4008-80f8-67155589e1a1")
end

it "returns fallback image if the request fails" do
stub_request(:post, /hcti.io/).
to_return(status: 429,
body: '{ "error": "Plan limit exceeded" }',
headers: { "Content-Type" => "application/json" })

expect(described_class.url(html: "test")).to eq described_class::FALLBACK_IMAGE
end
end

describe ".fetch_url" do
before do
allow(Rails.cache).to receive(:write)
allow(Rails.cache).to receive(:read)
end

it "caches the image url when successful" do
stub_request(:post, /hcti.io/).
to_return(status: 200,
body: '{ "url": "https://hcti.io/v1/image/6c52de9d-4d37-4008-80f8-67155589e1a1" }',
headers: { "Content-Type" => "application/json" })

expect(described_class.fetch_url(html: "test")).to eq("https://hcti.io/v1/image/6c52de9d-4d37-4008-80f8-67155589e1a1")
expect(Rails.cache).to have_received(:write).once
end

it "does not cache errors" do
stub_request(:post, /hcti.io/).
to_return(status: 429,
body: '{ "error": "Plan limit exceeded" }',
headers: { "Content-Type" => "application/json" })

expect(described_class.fetch_url(html: "test")).to eq described_class::FALLBACK_IMAGE
expect(Rails.cache).not_to have_received(:write)
end
end
end