Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/feature/update-meta-tags-for-s…
Browse files Browse the repository at this point in the history
…ocial-previews' into temp-decidim-reviews
  • Loading branch information
antopalidi committed Jun 19, 2024
2 parents 9f6b391 + 0b75d67 commit 4b75203
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 111 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<% add_decidim_meta_tags({
title: translated_attribute(current_participatory_space.title),
image_url: current_participatory_space.attached_uploader(:hero_image).path,
description: translated_attribute(current_participatory_space.short_description),
url: assembly_url(current_participatory_space)
}, current_participatory_space) %>
Expand Down
48 changes: 48 additions & 0 deletions decidim-assemblies/spec/system/assemblies_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "spec_helper"
require "decidim/core/test/shared_examples/has_contextual_help"
require "decidim/core/test/shared_examples/meta_image_url_examples"

describe "Assemblies" do
let(:organization) { create(:organization) }
Expand Down Expand Up @@ -36,6 +37,53 @@
switch_to_host(organization.host)
end

describe "testing image hierarchy for meta tags" do
let!(:assembly) { create(:assembly, organization:, description:, hero_image:, banner_image:) }
let(:banner_image) { nil }
let(:hero_image) { nil }
let(:description) { { en: "Description <p><img src=\"#{description_image_path}\"></p>" } }

context "when the assembly has an attachment only" do
let!(:assembly_attachment) { create(:attachment, :with_image, attached_to: assembly, file: assembly_attachment_file) }
let!(:assembly_attachment_file) { Decidim::Dev.test_file("city2.jpeg", "image/jpeg") }
let(:description) { { en: "Description" } }

it_behaves_like "meta image url examples", "city2.jpeg" do
let(:resource) { assembly }
end
end

context "when assembly attachment is not present and the description image is there" do
it_behaves_like "meta image url examples", "description_image.jpg" do
let(:resource) { assembly }
end
end

context "when neither assembly attachment nor description image is present" do
let(:file) { Decidim::Dev.test_file("city2.jpeg", "image/jpeg") }
let!(:assembly_hero_attachment) { create(:attachment, :with_image, attached_to: assembly, file:) }

before do
assembly.update(description: { en: "This is my assembly" })
end

it_behaves_like "meta image url examples", "city2.jpeg" do
let(:resource) { assembly }
end
end

context "when neither assembly attachment nor description image nor hero image is present" do
before do
Decidim::Admin::ContentBlocks::UpdateContentBlock.new(form, content_block, :homepage).call
assembly.update!(description: { en: "This is my assembly" })
end

it_behaves_like "meta image url examples", "default_hero_image.jpg" do
let(:resource) { assembly }
end
end
end

context "when there are no assemblies and directly accessing from URL" do
it_behaves_like "a 404 page" do
let(:target_path) { decidim_assemblies.assemblies_path }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ if respond_to?(:meeting_component) && meeting_component
)
end
%>
<% add_decidim_meta_tags(
{ title: t("conference_program.index.title", scope: "decidim") }, current_participatory_space) %>
<% add_decidim_meta_tags({ title: t("conference_program.index.title", scope: "decidim") }, meeting_component) %>

<%# NOTE: this page does not use a regular layout %>
<main class="container">
Expand Down
103 changes: 0 additions & 103 deletions decidim-core/app/helpers/decidim/meta_image_url_resolver.rb

This file was deleted.

11 changes: 6 additions & 5 deletions decidim-core/app/helpers/decidim/meta_tags_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ def add_decidim_meta_tags(tags, resource = nil)
add_decidim_meta_description(tags[:description])
add_decidim_meta_url(tags[:url])
add_decidim_meta_twitter_handler(tags[:twitter_handler])
add_decidim_meta_image_url(resolve_meta_image_url(tags[:image_url], resource))
image_url = tags[:image_url].presence || resolve_meta_image_url(resource)
add_decidim_meta_image_url(add_base_url_to(image_url)) if image_url.present?
end

# Adds base URL to the given path if it doesn't include a host.
# Adds base URL to the given path if it does not include a host.
#
# @param [String] path - A String containing the path (e.g. "/proposals/1").
#
Expand Down Expand Up @@ -129,12 +130,12 @@ def add_decidim_meta_image_url(image_url)
# This method creates a new instance of MetaImageUrlResolver,
# which handles the logic for determining the most appropriate image URL.
#
# @param [String, nil] image_url - The initial image URL provided in the tags.
# @param [Object, nil] resource - The resource object that may contain the image.
#
# @return [String, nil] - The resolved image URL, or nil if no appropriate image URL is found.
def resolve_meta_image_url(image_url, resource)
MetaImageUrlResolver.new(image_url, resource, self).resolve
def resolve_meta_image_url(resource = nil)
url = MetaImageUrlResolver.new(resource, current_organization).resolve
add_base_url_to(url) if url.present?
end
end
end
175 changes: 175 additions & 0 deletions decidim-core/app/resolvers/decidim/meta_image_url_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# frozen_string_literal: true

module Decidim
# Resolves the most appropriate image URL for meta tags from a given resource within an organization.
# Searches in the following order: attachment images, images in resource description, participatory space images, and default content block images.
# Ensures the selected image is resized to 1200x630 pixels.
class MetaImageUrlResolver
include TranslatableAttributes

def initialize(resource, current_organization)
@resource = resource
@current_organization = current_organization
end

# Resolves the image URL to be used for meta tags.
#
# @return [String, nil] - The resolved image URL or nil if no image is found.
def resolve
blob = determine_image_blob.presence
resize_image_blob(blob) if blob.present?
end

private

# Determines the image blob to be used for meta tags by following a hierarchy.
#
# @return [ActiveStorage::Blob, nil] - The image blob or nil if no image is found.
def determine_image_blob
methods_with_args = [
(has_attached_fields? ? { method: :blob_from_image_fields } : { method: :blob_from_attachment_image }),
{ method: :blob_from_description, args: [@resource] },
{ method: :blob_from_participatory_space },
{ method: :blob_from_content_blocks }
]

methods_with_args << { method: :blob_from_attachment_image } if has_attached_fields?

methods_with_args.each do |hash|
next if hash[:method].nil?

method = hash[:method]
args = hash[:args] || []
blob = send(method, *args)
return blob if blob.present?
end
nil
end

# Checks if the resource has attached fields like hero_image or banner_image.
#
# @return [Boolean] - True if the resource has attached fields, false otherwise.
def has_attached_fields?
@resource.respond_to?(:hero_image) || @resource.respond_to?(:banner_image)
end

# Fetches the image blob from the resource's attached image fields.
#
# @return [ActiveStorage::Blob, nil] - The image blob or nil if not found.
def blob_from_image_fields
path = get_attached_uploader_path(@resource, [:hero_image, :banner_image])
return unless path

blob_key = extract_blob_key_from_path(path)
find_blob_by_key(blob_key)
end

# Fetches the default homepage image blob.
#
# @return [ActiveStorage::Blob, nil] - The image blob or nil if not found.
def blob_from_content_blocks
content_block = Decidim::ContentBlock.find_by(
organization: @current_organization,
manifest_name: :hero,
scope_name: :homepage
)

return unless content_block

attachment = content_block.attachments.first
return unless attachment&.file&.attached?

attachment.file.blob
end

# Fetches the first attachment image blob.
#
# @return [ActiveStorage::Blob, nil] - The image blob or nil if not found.
def blob_from_attachment_image
attachment = find_image_attachment(@resource)
return unless attachment&.file&.attached?

attachment.file.blob
end

# Finds the first image attachment for a given entity.
#
# @param [Object] entity - The entity to search for attachments.
#
# @return [Decidim::Attachment, nil] - The first image attachment or nil if not found.
def find_image_attachment(entity)
entity.try(:attachments)&.find { |a| a.content_type.in?(%w(image/jpeg image/png)) }
end

# Extracts the first image blob from the resource description.
#
# @return [ActiveStorage::Blob, nil] - The image blob or nil if not found.
def blob_from_description(resource)
html_fields = [resource.try(:body), resource.try(:description), resource.try(:short_description)]

html_fields.each do |html|
next unless html

html = translated_attribute(html).strip if html.is_a?(Hash)
next if html.blank?

image_element = Nokogiri::HTML(html).css("img").first
next unless image_element

image_url = image_element["src"]
next if image_url.blank?

blob_key = extract_blob_key_from_path(image_url)
blob = find_blob_by_key(blob_key)
return blob if blob.present?
end

nil
end

# Fetches the image blob from the participatory space.
#
# @return [ActiveStorage::Blob, nil] - The image blob or nil if not found.
def blob_from_participatory_space
participatory_space = @resource.try(:participatory_space)
return unless participatory_space

blob = participatory_space.try(:hero_image).try(:blob) || participatory_space.try(:banner_image).try(:blob)
blob.presence || blob_from_description(participatory_space)
end

# Resizes the image blob to the specified dimensions (1200x630) if it is larger.
#
# @param [ActiveStorage::Blob] blob - The image blob to be resized.
#
# @return [String] - The URL of the resized image.
def resize_image_blob(blob)
return unless blob

resized_variant = blob.variant(resize_to_limit: [1200, 630]).processed
Rails.application.routes.url_helpers.rails_representation_url(resized_variant, only_path: true)
end

# Retrieves the path for the attached uploader of the specified image types.
#
# @param [Object] resource - The resource to search for attached uploaders.
# @param [Array<Symbol>] image_types - The types of images to search for.
#
# @return [String, nil] - The path of the attached uploader or nil if not found.
def get_attached_uploader_path(resource, image_types)
image_types.each do |image_type|
path = resource.try(:attached_uploader, image_type)&.path
return path if path
end
nil
end

def extract_blob_key_from_path(path)
path.split("/").second_to_last
end

def find_blob_by_key(blob_key)
ActiveStorage::Blob.find_signed(blob_key) if blob_key.present?
end
end
end
Loading

0 comments on commit 4b75203

Please sign in to comment.