diff --git a/decidim-core/app/helpers/decidim/breadcrumb_helper.rb b/decidim-core/app/helpers/decidim/breadcrumb_helper.rb index 7d96bfb3652e..de367fb7341d 100644 --- a/decidim-core/app/helpers/decidim/breadcrumb_helper.rb +++ b/decidim-core/app/helpers/decidim/breadcrumb_helper.rb @@ -56,5 +56,11 @@ def active_breadcrumb_item(target_menu) active: active_item.active? } end + + def render_schema_org_breadcrumb_list(breadcrumb_items) + exporter_options = { breadcrumb_items:, base_url: request.base_url, organization_name: current_organization_name } + exported_breadcrumb_list = Decidim::Exporters::JSON.new([exporter_options], Decidim::SchemaOrgBreadcrumbListSerializer).export.read + JSON.pretty_generate(JSON.parse(exported_breadcrumb_list).first) + end end end diff --git a/decidim-core/app/serializers/decidim/schema_org_breadcrumb_list_serializer.rb b/decidim-core/app/serializers/decidim/schema_org_breadcrumb_list_serializer.rb new file mode 100644 index 000000000000..3ffa411042f7 --- /dev/null +++ b/decidim-core/app/serializers/decidim/schema_org_breadcrumb_list_serializer.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "uri" + +module Decidim + class SchemaOrgBreadcrumbListSerializer < Decidim::Exporters::Serializer + include Decidim::SanitizeHelper + + # Public: Initializes the serializer with a list of breadcrumb items. + def initialize(options) + @breadcrumb_items = options[:breadcrumb_items] + @base_url = options[:base_url] + @organization_name = options[:organization_name] + end + + # Serializes a breadcrumb items list for the Schema.org BreadcrumbList type + # + # @see https://schema.org/BreadcrumbList + # @see https://developers.google.com/search/docs/appearance/structured-data/breadcrumb?hl=en + def serialize + return {} if breadcrumb_items.none? { |item| item.has_key?(:url) } + + { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + name: "#{organization_name} breadcrumb", + itemListElement: breadcrumb_items_serialized + } + end + + private + + attr_reader :breadcrumb_items, :base_url, :organization_name + + def breadcrumb_items_serialized + all_items = [] + + breadcrumb_items.each_with_index do |item, index| + next if item.empty? + + all_items << { + "@type": "ListItem", + position: index + 1, + name: decidim_sanitize_translated(item[:label]), + item: URI.join(base_url, item[:url]).to_s + } + end + + all_items + end + end +end diff --git a/decidim-core/app/views/layouts/decidim/_schema_org_breadcrumb_list.html.erb b/decidim-core/app/views/layouts/decidim/_schema_org_breadcrumb_list.html.erb new file mode 100644 index 000000000000..f43804a9c099 --- /dev/null +++ b/decidim-core/app/views/layouts/decidim/_schema_org_breadcrumb_list.html.erb @@ -0,0 +1,3 @@ + diff --git a/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_items.html.erb b/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_items.html.erb index d6cfd341871a..9c293f1f2831 100644 --- a/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_items.html.erb +++ b/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_items.html.erb @@ -30,3 +30,5 @@ <% end %> <% end %> <% end %> + +<%= render partial: "layouts/decidim/schema_org_breadcrumb_list", locals: { breadcrumb_items: } %> diff --git a/decidim-core/spec/helpers/decidim/breadcrumb_helper_spec.rb b/decidim-core/spec/helpers/decidim/breadcrumb_helper_spec.rb new file mode 100644 index 000000000000..8b5636a47e37 --- /dev/null +++ b/decidim-core/spec/helpers/decidim/breadcrumb_helper_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe BreadcrumbHelper do + describe "#render_schema_org_breadcrumb_list" do + subject { helper.render_schema_org_breadcrumb_list(breadcrumb_items) } + + let(:breadcrumb_items) do + [ + { + label: "Processes", + url: "/processes", + active: true + }, + { + label: { ca: "Hola mon", es: "Hola mundo", en: "Hello world" }, + url: "/processes/hello-world", + dropdown_cell: "decidim/participatory_processes/process_dropdown_metadata", + resource: participatory_process + } + + ] + end + + let(:participatory_process) { create(:participatory_process) } + + before do + allow(helper).to receive(:current_organization).and_return(participatory_process.organization) + end + + it "renders a schema.org event" do + keys = JSON.parse(subject).keys + expect(keys).to include("@context") + expect(keys).to include("@type") + expect(keys).to include("itemListElement") + end + end + end +end diff --git a/decidim-core/spec/serializers/decidim/schema_org_breadcrumb_list_serializer_spec.rb b/decidim-core/spec/serializers/decidim/schema_org_breadcrumb_list_serializer_spec.rb new file mode 100644 index 000000000000..43d998ea5979 --- /dev/null +++ b/decidim-core/spec/serializers/decidim/schema_org_breadcrumb_list_serializer_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe SchemaOrgBreadcrumbListSerializer do + subject do + described_class.new({ breadcrumb_items:, base_url:, organization_name: }) + end + + let(:breadcrumb_items) do + [ + { + label: "Processes", + url: "/processes", + active: true + }, + { + label: { ca: "Hola mon", es: "Hola mundo", en: "Hello world" }, + url: "/processes/hello-world", + dropdown_cell: "decidim/participatory_processes/process_dropdown_metadata", + resource: participatory_process + } + ] + end + + let(:base_url) { "https://example.org" } + let(:participatory_process) { create(:participatory_process) } + let(:organization_name) { "ACME Corp" } + + describe "#serialize" do + let(:serialized) { subject.serialize } + + it "serializes the @context" do + expect(serialized[:@context]).to eq("https://schema.org") + end + + it "serializes the @type" do + expect(serialized[:@type]).to eq("BreadcrumbList") + end + + it "serializes the name" do + expect(serialized[:name]).to eq("ACME Corp breadcrumb") + end + + it "serializes the breadcrumb items" do + expected_items_elements = [ + { "@type": "ListItem", position: 1, name: "Processes", item: "https://example.org/processes" }, + { "@type": "ListItem", position: 2, name: "Hello world", item: "https://example.org/processes/hello-world" } + ] + expect(serialized[:itemListElement]).to eq(expected_items_elements) + end + + context "when there are empty items" do + let(:breadcrumb_items) do + [ + { + label: "Processes", + url: "/processes", + active: true + }, + { + label: { ca: "Hola mon", es: "Hola mundo", en: "Hello world" }, + url: "/processes/hello-world", + dropdown_cell: "decidim/participatory_processes/process_dropdown_metadata", + resource: participatory_process + }, + {} + ] + end + + it "ignores them" do + expected_items_elements = [ + { "@type": "ListItem", position: 1, name: "Processes", item: "https://example.org/processes" }, + { "@type": "ListItem", position: 2, name: "Hello world", item: "https://example.org/processes/hello-world" } + ] + expect(serialized[:itemListElement]).to eq(expected_items_elements) + end + end + + context "when there are only items without URLs" do + let(:breadcrumb_items) do + [ + { + label: "Profile", + active: true + } + ] + end + + it "returns an empty JSON" do + expect(serialized).to eq({}) + end + end + end + end +end