From dd84051ce411d7f6106f0f8201c25e881e4c33c7 Mon Sep 17 00:00:00 2001 From: Mike Coutermarsh Date: Fri, 22 Mar 2019 13:28:02 -0700 Subject: [PATCH] Social Card upgrade - Part 1 (#2090) * Add HtmlCssToImage for generating images This will be used by social_previews to convert the templates into images. Setup a fallback image primarily for local dev, so that this won't cause errors if authentication is missing. Caching is setup to use contents of the html/css. So it with autobust/generate a new image whenever the markup changes. * Update Article social preview template to not be random + Being random breaks caching (since we generate cachekey by a SHA of html content) + Use Roboto for when generating via HTML/CSS to Image * Update Tag social preview template to not be random + Make rand deterministic so it doesn't break caching * Update user social preview template for html/csstoimage * Update shecoded template to work with both url2png and htmlcsstoimage * Add ability to respond to .png for social previews controller When adding .png to the url, these routes will now generate an image and redirect to it. This leaves the existing pages as they were, so we're backwards compatible with all the existing links out in the world. * Fix merge conflicts * Remove some duplication * Improve spec descriptions/read better * clean up setting template with a ternary --- Envfile | 4 ++ app/controllers/social_previews_controller.rb | 51 ++++++++++++++++--- app/lib/html_css_to_image.rb | 27 ++++++++++ app/views/social_previews/article.html.erb | 15 +++--- app/views/social_previews/shecoded.html.erb | 37 ++++++++------ app/views/social_previews/tag.html.erb | 13 ++--- app/views/social_previews/user.html.erb | 13 ++--- spec/lib/html_css_to_image_spec.rb | 50 ++++++++++++++++++ spec/requests/social_previews_spec.rb | 45 +++++++++++++++- 9 files changed, 212 insertions(+), 43 deletions(-) create mode 100644 app/lib/html_css_to_image.rb create mode 100644 spec/lib/html_css_to_image_spec.rb diff --git a/Envfile b/Envfile index 4880742c6..ec5bd9c80 100644 --- a/Envfile +++ b/Envfile @@ -84,6 +84,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" diff --git a/app/controllers/social_previews_controller.rb b/app/controllers/social_previews_controller.rb index 9efa7289b..a10c626a3 100644 --- a/app/controllers/social_previews_controller.rb +++ b/app/controllers/social_previews_controller.rb @@ -1,28 +1,65 @@ 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.decorate.cached_tag_list_array & SHE_CODED_TAGS).any? ? "shecoded" : "article" + + 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 diff --git a/app/lib/html_css_to_image.rb b/app/lib/html_css_to_image.rb new file mode 100644 index 000000000..30bec0472 --- /dev/null +++ b/app/lib/html_css_to_image.rb @@ -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 diff --git a/app/views/social_previews/article.html.erb b/app/views/social_previews/article.html.erb index 79f13c083..0eaad485c 100644 --- a/app/views/social_previews/article.html.erb +++ b/app/views/social_previews/article.html.erb @@ -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 %> -
- + +
+
+ +
- <%= @article.title %> + <%= @article.title %>
diff --git a/app/views/social_previews/tag.html.erb b/app/views/social_previews/tag.html.erb index 3520190f2..edaab4310 100644 --- a/app/views/social_previews/tag.html.erb +++ b/app/views/social_previews/tag.html.erb @@ -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 %>