Commit 9b6d8dbb authored by Francisco Javier López's avatar Francisco Javier López Committed by Paul Slaughter

Add schema markup to breadcrumb

This commit add schema markup to breadcrumbs. It
adds it usings json+ld instead of microdata due to
the complexity building the breadcrumbs.
parent 1f043d22
...@@ -32,4 +32,46 @@ module BreadcrumbsHelper ...@@ -32,4 +32,46 @@ module BreadcrumbsHelper
@breadcrumb_dropdown_links[location] ||= [] @breadcrumb_dropdown_links[location] ||= []
@breadcrumb_dropdown_links[location] << link @breadcrumb_dropdown_links[location] << link
end end
def push_to_schema_breadcrumb(text, link)
list_item = schema_list_item(text, link, schema_breadcrumb_list.size + 1)
schema_breadcrumb_list.push(list_item)
end
def schema_breadcrumb_json
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
'itemListElement': build_item_list_elements
}.to_json
end
private
def schema_breadcrumb_list
@schema_breadcrumb_list ||= []
end
def build_item_list_elements
return @schema_breadcrumb_list unless @breadcrumbs_extra_links&.any?
last_element = schema_breadcrumb_list.pop
@breadcrumbs_extra_links.each do |el|
push_to_schema_breadcrumb(el[:text], el[:link])
end
last_element['position'] = schema_breadcrumb_list.last['position'] + 1
schema_breadcrumb_list.push(last_element)
end
def schema_list_item(text, link, position)
{
'@type' => 'ListItem',
'position' => position,
'name' => text,
'item' => link
}
end
end end
...@@ -94,12 +94,19 @@ module GroupsHelper ...@@ -94,12 +94,19 @@ module GroupsHelper
else else
full_title << breadcrumb_list_item(group_title_link(parent, hidable: false)) full_title << breadcrumb_list_item(group_title_link(parent, hidable: false))
end end
push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent))
end end
full_title << render("layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups")) full_title << render("layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups"))
full_title << breadcrumb_list_item(group_title_link(group)) full_title << breadcrumb_list_item(group_title_link(group))
full_title << ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name push_to_schema_breadcrumb(simple_sanitize(group.name), group_path(group))
if name
full_title << ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text')
push_to_schema_breadcrumb(simple_sanitize(name), url)
end
full_title.join.html_safe full_title.join.html_safe
end end
......
...@@ -84,18 +84,8 @@ module ProjectsHelper ...@@ -84,18 +84,8 @@ module ProjectsHelper
end end
def project_title(project) def project_title(project)
namespace_link = namespace_link = build_namespace_breadcrumb_link(project)
if project.group project_link = build_project_breadcrumb_link(project)
group_title(project.group, nil, nil)
else
owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner))
end
project_link = link_to project_path(project) do
icon = project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15) if project.avatar_url && !Rails.env.test?
[icon, content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text")].join.html_safe
end
namespace_link = breadcrumb_list_item(namespace_link) unless project.group namespace_link = breadcrumb_list_item(namespace_link) unless project.group
project_link = breadcrumb_list_item project_link project_link = breadcrumb_list_item project_link
...@@ -787,6 +777,30 @@ module ProjectsHelper ...@@ -787,6 +777,30 @@ module ProjectsHelper
def project_access_token_available?(project) def project_access_token_available?(project)
can?(current_user, :admin_resource_access_tokens, project) can?(current_user, :admin_resource_access_tokens, project)
end end
def build_project_breadcrumb_link(project)
project_name = simple_sanitize(project.name)
push_to_schema_breadcrumb(project_name, project_path(project))
link_to project_path(project) do
icon = project_icon(project, alt: project_name, class: 'avatar-tile', width: 15, height: 15) if project.avatar_url && !Rails.env.test?
[icon, content_tag("span", project_name, class: "breadcrumb-item-text js-breadcrumb-item-text")].join.html_safe
end
end
def build_namespace_breadcrumb_link(project)
if project.group
group_title(project.group, nil, nil)
else
owner = project.namespace.owner
name = simple_sanitize(owner.name)
url = user_path(owner)
push_to_schema_breadcrumb(name, url)
link_to(name, url)
end
end
end end
ProjectsHelper.prepend_if_ee('EE::ProjectsHelper') ProjectsHelper.prepend_if_ee('EE::ProjectsHelper')
- container = @no_breadcrumb_container ? 'container-fluid' : container_class - container = @no_breadcrumb_container ? 'container-fluid' : container_class
- hide_top_links = @hide_top_links || false - hide_top_links = @hide_top_links || false
- push_to_schema_breadcrumb(@breadcrumb_title, breadcrumb_title_link)
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] } %nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
.breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border) } .breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border) }
...@@ -17,4 +18,7 @@ ...@@ -17,4 +18,7 @@
= render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after = render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
%li %li
%h2.breadcrumbs-sub-title= link_to @breadcrumb_title, breadcrumb_title_link %h2.breadcrumbs-sub-title= link_to @breadcrumb_title, breadcrumb_title_link
%script{ type:'application/ld+json' }
:plain
#{schema_breadcrumb_json}
= yield :header_content = yield :header_content
---
title: Add SEO schema markup to breadcrumbs
merge_request: 46991
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Breadcrumbs schema markup', :aggregate_failures do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, namespace: user.namespace) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:subgroup) { create(:group, :public, parent: group) }
let_it_be(:group_project) { create(:project, :public, namespace: subgroup) }
it 'generates the breadcrumb schema for user projects' do
visit project_url(project)
item_list = get_schema_content
expect(item_list.size).to eq 3
expect(item_list[0]['name']).to eq project.namespace.name
expect(item_list[0]['item']).to eq user_path(project.owner)
expect(item_list[1]['name']).to eq project.name
expect(item_list[1]['item']).to eq project_path(project)
expect(item_list[2]['name']).to eq 'Details'
expect(item_list[2]['item']).to eq project_path(project)
end
it 'generates the breadcrumb schema for group projects' do
visit project_url(group_project)
item_list = get_schema_content
expect(item_list.size).to eq 4
expect(item_list[0]['name']).to eq group.name
expect(item_list[0]['item']).to eq group_path(group)
expect(item_list[1]['name']).to eq subgroup.name
expect(item_list[1]['item']).to eq group_path(subgroup)
expect(item_list[2]['name']).to eq group_project.name
expect(item_list[2]['item']).to eq project_path(group_project)
expect(item_list[3]['name']).to eq 'Details'
expect(item_list[3]['item']).to eq project_path(group_project)
end
it 'generates the breadcrumb schema for group' do
visit group_url(subgroup)
item_list = get_schema_content
expect(item_list.size).to eq 3
expect(item_list[0]['name']).to eq group.name
expect(item_list[0]['item']).to eq group_path(group)
expect(item_list[1]['name']).to eq subgroup.name
expect(item_list[1]['item']).to eq group_path(subgroup)
expect(item_list[2]['name']).to eq 'Details'
expect(item_list[2]['item']).to eq group_path(subgroup)
end
it 'generates the breadcrumb schema for issues' do
visit project_issues_url(project)
item_list = get_schema_content
expect(item_list.size).to eq 3
expect(item_list[0]['name']).to eq project.namespace.name
expect(item_list[0]['item']).to eq user_path(project.owner)
expect(item_list[1]['name']).to eq project.name
expect(item_list[1]['item']).to eq project_path(project)
expect(item_list[2]['name']).to eq 'Issues'
expect(item_list[2]['item']).to eq project_issues_path(project)
end
it 'generates the breadcrumb schema for specific issue' do
visit project_issue_url(project, issue)
item_list = get_schema_content
expect(item_list.size).to eq 4
expect(item_list[0]['name']).to eq project.namespace.name
expect(item_list[0]['item']).to eq user_path(project.owner)
expect(item_list[1]['name']).to eq project.name
expect(item_list[1]['item']).to eq project_path(project)
expect(item_list[2]['name']).to eq 'Issues'
expect(item_list[2]['item']).to eq project_issues_path(project)
expect(item_list[3]['name']).to eq issue.to_reference
expect(item_list[3]['item']).to eq project_issue_path(project, issue)
end
def get_schema_content
content = find('script[type="application/ld+json"]', visible: false).text(:all)
expect(content).not_to be_nil
Gitlab::Json.parse(content)['itemListElement']
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BreadcrumbsHelper do
describe '#push_to_schema_breadcrumb' do
it 'enqueue element name, link and position' do
element = %w(element1 link1)
helper.push_to_schema_breadcrumb(element[0], element[1])
list = helper.instance_variable_get(:@schema_breadcrumb_list)
aggregate_failures do
expect(list[0]['name']).to eq element[0]
expect(list[0]['item']).to eq element[1]
expect(list[0]['position']).to eq(1)
end
end
end
describe '#schema_breadcrumb_json' do
let(:elements) do
[
%w(element1 link1),
%w(element2 link2)
]
end
subject { helper.schema_breadcrumb_json }
it 'returns the breadcrumb schema in json format' do
enqueue_breadcrumb_elements
expected_result = {
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => [
{
'@type' => 'ListItem',
'position' => 1,
'name' => elements[0][0],
'item' => elements[0][1]
},
{
'@type' => 'ListItem',
'position' => 2,
'name' => elements[1][0],
'item' => elements[1][1]
}
]
}.to_json
expect(subject).to eq expected_result
end
context 'when extra breadcrumb element is added' do
let(:extra_elements) do
[
%w(extra_element1 extra_link1),
%w(extra_element2 extra_link2)
]
end
it 'include the extra elements before the last element' do
enqueue_breadcrumb_elements
extra_elements.each do |el|
add_to_breadcrumbs(el[0], el[1])
end
expected_result = {
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => [
{
'@type' => 'ListItem',
'position' => 1,
'name' => elements[0][0],
'item' => elements[0][1]
},
{
'@type' => 'ListItem',
'position' => 2,
'name' => extra_elements[0][0],
'item' => extra_elements[0][1]
},
{
'@type' => 'ListItem',
'position' => 3,
'name' => extra_elements[1][0],
'item' => extra_elements[1][1]
},
{
'@type' => 'ListItem',
'position' => 4,
'name' => elements[1][0],
'item' => elements[1][1]
}
]
}.to_json
expect(subject).to eq expected_result
end
end
def enqueue_breadcrumb_elements
elements.each do |el|
helper.push_to_schema_breadcrumb(el[0], el[1])
end
end
end
end
...@@ -87,15 +87,26 @@ RSpec.describe GroupsHelper do ...@@ -87,15 +87,26 @@ RSpec.describe GroupsHelper do
end end
describe 'group_title' do describe 'group_title' do
let(:group) { create(:group) } let_it_be(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) } let_it_be(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) } let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
subject { helper.group_title(very_deep_nested_group) }
it 'outputs the groups in the correct order' do it 'outputs the groups in the correct order' do
expect(helper.group_title(very_deep_nested_group)) expect(subject)
.to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m) .to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
end end
it 'enqueues the elements in the breadcrumb schema list' do
expect(helper).to receive(:push_to_schema_breadcrumb).with(group.name, group_path(group))
expect(helper).to receive(:push_to_schema_breadcrumb).with(nested_group.name, group_path(nested_group))
expect(helper).to receive(:push_to_schema_breadcrumb).with(deep_nested_group.name, group_path(deep_nested_group))
expect(helper).to receive(:push_to_schema_breadcrumb).with(very_deep_nested_group.name, group_path(very_deep_nested_group))
subject
end
end end
# rubocop:disable Layout/SpaceBeforeComma # rubocop:disable Layout/SpaceBeforeComma
......
...@@ -999,4 +999,15 @@ RSpec.describe ProjectsHelper do ...@@ -999,4 +999,15 @@ RSpec.describe ProjectsHelper do
end end
end end
end end
describe '#project_title' do
subject { helper.project_title(project) }
it 'enqueues the elements in the breadcrumb schema list' do
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.namespace.name, user_path(project.owner))
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.name, project_path(project))
subject
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment