Fix #5360: Use OpenGraph's og:image metadata for posts.

* Add og:image:width, og:image:height, and og:image:type tags.
* Use og:video tags for videos.
* Use 720x720 instead of 150x150 preview images for videos.
* Add duration tag to JSON-LD data for videos.
* Add OpenGraph tags to media assets show page.
* Respect Twitter max image size limits.
* Don't include OpenGraph image tags when someone shares a plain https://danbooru.donmai.us link
  with no tag search. This caused random potentially NSFW images to be shown when someone shared a
  https://danbooru.donmai.us link on social media, which could be cached for long periods of time.
This commit is contained in:
evazion
2022-12-02 00:59:23 -06:00
parent ca0a4af455
commit e11cd288b9
7 changed files with 100 additions and 50 deletions

View File

@@ -0,0 +1,68 @@
# frozen_string_literal: true
# Render Open Graph metatags for an image or video.
#
# @see https://ogp.me/
# @see https://www.opengraph.xyz/
# @see https://developers.facebook.com/docs/sharing/webmasters/
# @see https://developers.facebook.com/tools/debug/
# @see https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started
# @see https://developers.google.com/search/docs/appearance/structured-data
# @see https://search.google.com/test/rich-results
# @see https://validator.schema.org/
class OpenGraphComponent < ApplicationComponent
extend Memoist
attr_reader :media_asset, :current_user
delegate :json_ld_tag, :page_title, :meta_description, to: :helpers
delegate :is_image?, :is_video?, :is_ugoira?, :image_width, :image_height, :file_size, :mime_type, :variant, :has_variant?, to: :media_asset
def initialize(media_asset:, current_user:)
@media_asset = media_asset
@current_user = current_user
end
memoize def video_url
if is_video?
variant("original").file_url
elsif is_ugoira?
variant("sample").file_url
end
end
memoize def image_url
if is_image?
variant("original").file_url
elsif is_video? || is_ugoira?
variant("720x720").file_url
end
end
# https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/summary-card-with-large-image
#
# Images for this Card support an aspect ratio of 2:1 with minimum dimensions of 300x157 or maximum of 4096x4096
# pixels. Images must be less than 5MB in size. JPG, PNG, WEBP and GIF formats are supported. Only the first frame of
# an animated GIF will be used. SVG is not supported.
memoize def twitter_image_url
if is_image? && file_size < 5.megabytes && image_width <= 4096 && image_height <= 4096
variant("original").file_url
elsif has_variant?("720x720")
variant("720x720").file_url
end
end
# https://developers.google.com/search/docs/data-types/video#video-object
def json_ld_video_data
json_ld_tag({
"@context": "https://schema.org",
"@type": "VideoObject",
name: page_title,
description: meta_description,
uploadDate: (media_asset.post || media_asset).created_at.iso8601,
duration: media_asset.duration.seconds.iso8601,
thumbnailUrl: image_url,
contentUrl: video_url,
})
end
end

View File

@@ -0,0 +1,23 @@
<% if policy(media_asset).can_see_image? %>
<% if is_image? %>
<%= tag.meta property: "og:image", content: image_url %>
<%= tag.meta property: "og:image:secure_url", content: image_url %>
<%= tag.meta property: "og:image:width", content: image_width %>
<%= tag.meta property: "og:image:height", content: image_height %>
<%= tag.meta property: "og:image:type", content: mime_type %>
<% elsif is_video? || is_ugoira? %>
<%= tag.meta property: "og:image", content: image_url %>
<%= tag.meta property: "og:video", content: video_url %>
<%= tag.meta property: "og:video:secure_url", content: video_url %>
<%= tag.meta property: "og:video:width", content: image_width %>
<%= tag.meta property: "og:video:height", content: image_height %>
<%= tag.meta property: "og:video:type", content: mime_type %>
<%= json_ld_video_data %>
<% end %>
<% if twitter_image_url.present? %>
<%= tag.meta name: "twitter:card", content: "summary_large_image" %>
<%= tag.meta name: "twitter:image", content: twitter_image_url %>
<% end %>
<% end %>

View File

@@ -14,19 +14,6 @@ module SeoHelper
"#{Danbooru.config.canonical_app_name} is the original anime image booru. Search millions of anime pictures categorized by thousands of tags." "#{Danbooru.config.canonical_app_name} is the original anime image booru. Search millions of anime pictures categorized by thousands of tags."
end end
# https://developers.google.com/search/docs/data-types/video#video-object
def json_ld_video_data(post)
json_ld_tag({
"@context": "https://schema.org",
"@type": "VideoObject",
name: page_title,
description: meta_description,
uploadDate: post.created_at.iso8601,
thumbnailUrl: post.media_asset.variant("360x360").file_url,
contentUrl: post.file_url,
})
end
def json_ld_website_data def json_ld_website_data
urls = [ urls = [
Danbooru.config.twitter_url, Danbooru.config.twitter_url,

View File

@@ -167,18 +167,6 @@ class Post < ApplicationRecord
media_asset.variant(:preview).file_url media_asset.variant(:preview).file_url
end end
def open_graph_image_url
if is_image?
if has_large?
large_file_url
else
file_url
end
else
preview_file_url
end
end
def file_url_for(user) def file_url_for(user)
if user.default_image_size == "large" && image_width > Danbooru.config.large_image_width if user.default_image_size == "large" && image_width > Danbooru.config.large_image_width
tagged_large_file_url tagged_large_file_url
@@ -209,10 +197,6 @@ class Post < ApplicationRecord
end end
concerning :ImageMethods do concerning :ImageMethods do
def twitter_card_supported?
image_width.to_i >= 280 && image_height.to_i >= 150
end
def has_large? def has_large?
return false if has_tag?("animated_gif") || has_tag?("animated_png") return false if has_tag?("animated_gif") || has_tag?("animated_png")
return true if is_ugoira? return true if is_ugoira?

View File

@@ -193,4 +193,8 @@
</div> </div>
</div> </div>
<% content_for(:html_header) do %>
<%= render OpenGraphComponent.new(media_asset: @media_asset, current_user: CurrentUser.user) %>
<% end %>
<%= render "uploads/secondary_links" %> <%= render "uploads/secondary_links" %>

View File

@@ -237,6 +237,10 @@
<% page_title(@post_set.page_title) %> <% page_title(@post_set.page_title) %>
<% meta_description @post_set.meta_description %> <% meta_description @post_set.meta_description %>
<% atom_feed_tag "Posts: #{@post_set.page_title}", posts_url(tags: @post_set.tag_string, format: :atom) %> <% atom_feed_tag "Posts: #{@post_set.page_title}", posts_url(tags: @post_set.tag_string, format: :atom) %>
<% if @post_set.best_post.present? %>
<%= render OpenGraphComponent.new(media_asset: @post_set.best_post.media_asset, current_user: CurrentUser.user) %>
<% end %>
<% end %> <% end %>
<% if params[:tags].blank? && @post_set.current_page == 1 %> <% if params[:tags].blank? && @post_set.current_page == 1 %>
@@ -250,10 +254,4 @@
<% if @post_set.has_explicit? %> <% if @post_set.has_explicit? %>
<meta name="rating" content="adult"> <meta name="rating" content="adult">
<% end %> <% end %>
<% if @post_set.best_post.present? %>
<%= tag.meta property: "og:image", content: @post_set.best_post.open_graph_image_url %>
<%= tag.meta name: "twitter:image", content: @post_set.best_post.open_graph_image_url %>
<%= tag.meta name: "twitter:card", content: "summary_large_image" %>
<% end %>
<% end %> <% end %>

View File

@@ -393,21 +393,7 @@
<meta name="post-id" content="<%= @post.id %>"> <meta name="post-id" content="<%= @post.id %>">
<meta name="post-has-embedded-notes" content="<%= @post.has_embedded_notes? %>"> <meta name="post-has-embedded-notes" content="<%= @post.has_embedded_notes? %>">
<% if policy(@post).visible? %> <%= render OpenGraphComponent.new(media_asset: @post.media_asset, current_user: CurrentUser.user) %>
<%= tag.meta property: "og:image", content: @post.open_graph_image_url %>
<% if @post.is_video? %>
<%= json_ld_video_data(@post) %>
<% end %>
<% end %>
<% if @post.twitter_card_supported? %>
<meta name="twitter:card" content="summary_large_image">
<% if policy(@post).visible? %>
<%= tag.meta name: "twitter:image", content: @post.open_graph_image_url %>
<% end %>
<% end %>
<% if @post.rating == "e" %> <% if @post.rating == "e" %>
<meta name="rating" content="adult"> <meta name="rating" content="adult">