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
@breadcrumb_dropdown_links[location] ||= []
@breadcrumb_dropdown_links[location] << link
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
......@@ -94,12 +94,19 @@ module GroupsHelper
else
full_title << breadcrumb_list_item(group_title_link(parent, hidable: false))
end
push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent))
end
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 << ' &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
end
......
......@@ -84,18 +84,8 @@ module ProjectsHelper
end
def project_title(project)
namespace_link =
if project.group
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 = build_namespace_breadcrumb_link(project)
project_link = build_project_breadcrumb_link(project)
namespace_link = breadcrumb_list_item(namespace_link) unless project.group
project_link = breadcrumb_list_item project_link
......@@ -787,6 +777,30 @@ module ProjectsHelper
def project_access_token_available?(project)
can?(current_user, :admin_resource_access_tokens, project)
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
ProjectsHelper.prepend_if_ee('EE::ProjectsHelper')
- container = @no_breadcrumb_container ? 'container-fluid' : container_class
- hide_top_links = @hide_top_links || false
- push_to_schema_breadcrumb(@breadcrumb_title, breadcrumb_title_link)
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
.breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border) }
......@@ -17,4 +18,7 @@
= render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
%li
%h2.breadcrumbs-sub-title= link_to @breadcrumb_title, breadcrumb_title_link
%script{ type:'application/ld+json' }
:plain
#{schema_breadcrumb_json}
= 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
end
describe 'group_title' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let_it_be(:group) { create(:group) }
let_it_be(:nested_group) { create(:group, parent: group) }
let_it_be(:deep_nested_group) { create(:group, parent: 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
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)
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
# rubocop:disable Layout/SpaceBeforeComma
......
......@@ -999,4 +999,15 @@ RSpec.describe ProjectsHelper do
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
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