Commit d22a8575 authored by fjsanpedro's avatar fjsanpedro

Add structured markup for users

This commit introduces the the Schema.org structured
markup in the user's page.
parent 6cab9c41
...@@ -150,6 +150,14 @@ module PageLayoutHelper ...@@ -150,6 +150,14 @@ module PageLayoutHelper
css_class.join(' ') css_class.join(' ')
end end
def page_itemtype(itemtype = nil)
if itemtype
@page_itemtype = { itemscope: true, itemtype: itemtype }
else
@page_itemtype || {}
end
end
private private
def generic_canonical_url def generic_canonical_url
......
...@@ -91,18 +91,18 @@ module UsersHelper ...@@ -91,18 +91,18 @@ module UsersHelper
end end
end end
def work_information(user) def work_information(user, with_schema_markup: false)
return unless user return unless user
organization = user.organization organization = user.organization
job_title = user.job_title job_title = user.job_title
if organization.present? && job_title.present? if organization.present? && job_title.present?
s_('Profile|%{job_title} at %{organization}') % { job_title: job_title, organization: organization } render_job_title_and_organization(job_title, organization, with_schema_markup: with_schema_markup)
elsif job_title.present? elsif job_title.present?
job_title render_job_title(job_title, with_schema_markup: with_schema_markup)
elsif organization.present? elsif organization.present?
organization render_organization(organization, with_schema_markup: with_schema_markup)
end end
end end
...@@ -151,6 +151,35 @@ module UsersHelper ...@@ -151,6 +151,35 @@ module UsersHelper
items items
end end
def render_job_title(job_title, with_schema_markup: false)
if with_schema_markup
content_tag :span, itemprop: 'jobTitle' do
job_title
end
else
job_title
end
end
def render_organization(organization, with_schema_markup: false)
if with_schema_markup
content_tag :span, itemprop: 'worksFor' do
organization
end
else
organization
end
end
def render_job_title_and_organization(job_title, organization, with_schema_markup: false)
if with_schema_markup
job_title = '<span itemprop="jobTitle">'.html_safe + job_title + "</span>".html_safe
organization = '<span itemprop="worksFor">'.html_safe + organization + "</span>".html_safe
end
html_escape(s_('Profile|%{job_title} at %{organization}')) % { job_title: job_title, organization: organization }
end
end end
UsersHelper.prepend_if_ee('EE::UsersHelper') UsersHelper.prepend_if_ee('EE::UsersHelper')
...@@ -20,6 +20,6 @@ ...@@ -20,6 +20,6 @@
- unless @hide_breadcrumbs - unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs" = render "layouts/nav/breadcrumbs"
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" } %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" } .content{ id: "content-body", **page_itemtype }
= render "layouts/flash", extra_flash_class: 'limit-container-width' = render "layouts/flash", extra_flash_class: 'limit-container-width'
= yield = yield
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name - page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name
- page_description @user.bio_html - page_description @user.bio_html
- header_title @user.name, user_path(@user) - header_title @user.name, user_path(@user)
- page_itemtype 'http://schema.org/Person'
- link_classes = "flex-grow-1 mx-1 " - link_classes = "flex-grow-1 mx-1 "
= content_for :meta_tags do = content_for :meta_tags do
...@@ -35,7 +36,7 @@ ...@@ -35,7 +36,7 @@
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] } .profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
.avatar-holder .avatar-holder
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: '' = image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: '', itemprop: 'image'
- if @user.blocked? - if @user.blocked?
.user-info .user-info
...@@ -44,7 +45,7 @@ ...@@ -44,7 +45,7 @@
= render "users/profile_basic_info" = render "users/profile_basic_info"
- else - else
.user-info .user-info
.cover-title .cover-title{ itemprop: 'name' }
= @user.name = @user.name
- if @user.status - if @user.status
...@@ -54,15 +55,15 @@ ...@@ -54,15 +55,15 @@
= render "users/profile_basic_info" = render "users/profile_basic_info"
.cover-desc.cgray.mb-1.mb-sm-2 .cover-desc.cgray.mb-1.mb-sm-2
- unless @user.location.blank? - unless @user.location.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mb-1.mb-sm-0 .profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mb-1.mb-sm-0{ itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' }
= sprite_icon('location', css_class: 'vertical-align-sub fgray') = sprite_icon('location', css_class: 'vertical-align-sub fgray')
%span.vertical-align-middle %span.vertical-align-middle{ itemprop: 'addressLocality' }
= @user.location = @user.location
- unless work_information(@user).blank? - unless work_information(@user).blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline .profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline
= sprite_icon('work', css_class: 'vertical-align-middle fgray') = sprite_icon('work', css_class: 'vertical-align-middle fgray')
%span.vertical-align-middle %span.vertical-align-middle
= work_information(@user) = work_information(@user, with_schema_markup: true)
.cover-desc.cgray.mb-1.mb-sm-2 .cover-desc.cgray.mb-1.mb-sm-2
- unless @user.skype.blank? - unless @user.skype.blank?
.profile-link-holder.middle-dot-divider .profile-link-holder.middle-dot-divider
...@@ -80,10 +81,10 @@ ...@@ -80,10 +81,10 @@
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0 .profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
- if Feature.enabled?(:security_auto_fix) && @user.bot? - if Feature.enabled?(:security_auto_fix) && @user.bot?
= sprite_icon('question', css_class: 'gl-text-blue-600') = sprite_icon('question', css_class: 'gl-text-blue-600')
= link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow' = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url'
- unless @user.public_email.blank? - unless @user.public_email.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0 .profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
= link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link' = link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link', itemprop: 'email'
- if @user.bio.present? - if @user.bio.present?
.cover-desc.cgray .cover-desc.cgray
.profile-user-bio .profile-user-bio
......
---
title: Add structured markup for users
merge_request: 46553
author:
type: added
...@@ -5,11 +5,13 @@ require 'spec_helper' ...@@ -5,11 +5,13 @@ require 'spec_helper'
RSpec.describe 'User page' do RSpec.describe 'User page' do
include ExternalAuthorizationServiceHelpers include ExternalAuthorizationServiceHelpers
let(:user) { create(:user, bio: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } let_it_be(:user) { create(:user, bio: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') }
subject { visit(user_path(user)) }
context 'with public profile' do context 'with public profile' do
it 'shows all the tabs' do it 'shows all the tabs' do
visit(user_path(user)) subject
page.within '.nav-links' do page.within '.nav-links' do
expect(page).to have_link('Overview') expect(page).to have_link('Overview')
...@@ -22,14 +24,12 @@ RSpec.describe 'User page' do ...@@ -22,14 +24,12 @@ RSpec.describe 'User page' do
end end
it 'does not show private profile message' do it 'does not show private profile message' do
visit(user_path(user)) subject
expect(page).not_to have_content("This user has a private profile") expect(page).not_to have_content("This user has a private profile")
end end
context 'work information' do context 'work information' do
subject { visit(user_path(user)) }
it 'shows job title and organization details' do it 'shows job title and organization details' do
user.update(organization: 'GitLab - work info test', job_title: 'Frontend Engineer') user.update(organization: 'GitLab - work info test', job_title: 'Frontend Engineer')
...@@ -57,24 +57,24 @@ RSpec.describe 'User page' do ...@@ -57,24 +57,24 @@ RSpec.describe 'User page' do
end end
context 'with private profile' do context 'with private profile' do
let(:user) { create(:user, private_profile: true) } let_it_be(:user) { create(:user, private_profile: true) }
it 'shows no tab' do it 'shows no tab' do
visit(user_path(user)) subject
expect(page).to have_css("div.profile-header") expect(page).to have_css("div.profile-header")
expect(page).not_to have_css("ul.nav-links") expect(page).not_to have_css("ul.nav-links")
end end
it 'shows private profile message' do it 'shows private profile message' do
visit(user_path(user)) subject
expect(page).to have_content("This user has a private profile") expect(page).to have_content("This user has a private profile")
end end
it 'shows own tabs' do it 'shows own tabs' do
sign_in(user) sign_in(user)
visit(user_path(user)) subject
page.within '.nav-links' do page.within '.nav-links' do
expect(page).to have_link('Overview') expect(page).to have_link('Overview')
...@@ -88,36 +88,36 @@ RSpec.describe 'User page' do ...@@ -88,36 +88,36 @@ RSpec.describe 'User page' do
end end
context 'with blocked profile' do context 'with blocked profile' do
let(:user) { create(:user, state: :blocked) } let_it_be(:user) { create(:user, state: :blocked) }
it 'shows no tab' do it 'shows no tab' do
visit(user_path(user)) subject
expect(page).to have_css("div.profile-header") expect(page).to have_css("div.profile-header")
expect(page).not_to have_css("ul.nav-links") expect(page).not_to have_css("ul.nav-links")
end end
it 'shows blocked message' do it 'shows blocked message' do
visit(user_path(user)) subject
expect(page).to have_content("This user is blocked") expect(page).to have_content("This user is blocked")
end end
it 'shows user name as blocked' do it 'shows user name as blocked' do
visit(user_path(user)) subject
expect(page).to have_css(".cover-title", text: 'Blocked user') expect(page).to have_css(".cover-title", text: 'Blocked user')
end end
it 'shows no additional fields' do it 'shows no additional fields' do
visit(user_path(user)) subject
expect(page).not_to have_css(".profile-user-bio") expect(page).not_to have_css(".profile-user-bio")
expect(page).not_to have_css(".profile-link-holder") expect(page).not_to have_css(".profile-link-holder")
end end
it 'shows username' do it 'shows username' do
visit(user_path(user)) subject
expect(page).to have_content("@#{user.username}") expect(page).to have_content("@#{user.username}")
end end
...@@ -126,7 +126,7 @@ RSpec.describe 'User page' do ...@@ -126,7 +126,7 @@ RSpec.describe 'User page' do
it 'shows the status if there was one' do it 'shows the status if there was one' do
create(:user_status, user: user, message: "Working hard!") create(:user_status, user: user, message: "Working hard!")
visit(user_path(user)) subject
expect(page).to have_content("Working hard!") expect(page).to have_content("Working hard!")
end end
...@@ -135,7 +135,7 @@ RSpec.describe 'User page' do ...@@ -135,7 +135,7 @@ RSpec.describe 'User page' do
it 'shows the sign in link' do it 'shows the sign in link' do
stub_application_setting(signup_enabled: false) stub_application_setting(signup_enabled: false)
visit(user_path(user)) subject
page.within '.navbar-nav' do page.within '.navbar-nav' do
expect(page).to have_link('Sign in') expect(page).to have_link('Sign in')
...@@ -147,7 +147,7 @@ RSpec.describe 'User page' do ...@@ -147,7 +147,7 @@ RSpec.describe 'User page' do
it 'shows the sign in and register link' do it 'shows the sign in and register link' do
stub_application_setting(signup_enabled: true) stub_application_setting(signup_enabled: true)
visit(user_path(user)) subject
page.within '.navbar-nav' do page.within '.navbar-nav' do
expect(page).to have_link('Sign in / Register') expect(page).to have_link('Sign in / Register')
...@@ -157,7 +157,7 @@ RSpec.describe 'User page' do ...@@ -157,7 +157,7 @@ RSpec.describe 'User page' do
context 'most recent activity' do context 'most recent activity' do
it 'shows the most recent activity' do it 'shows the most recent activity' do
visit(user_path(user)) subject
expect(page).to have_content('Most Recent Activity') expect(page).to have_content('Most Recent Activity')
end end
...@@ -168,7 +168,7 @@ RSpec.describe 'User page' do ...@@ -168,7 +168,7 @@ RSpec.describe 'User page' do
end end
it 'hides the most recent activity' do it 'hides the most recent activity' do
visit(user_path(user)) subject
expect(page).not_to have_content('Most Recent Activity') expect(page).not_to have_content('Most Recent Activity')
end end
...@@ -177,14 +177,14 @@ RSpec.describe 'User page' do ...@@ -177,14 +177,14 @@ RSpec.describe 'User page' do
context 'page description' do context 'page description' do
before do before do
visit(user_path(user)) subject
end end
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
end end
context 'with a bot user' do context 'with a bot user' do
let(:user) { create(:user, user_type: :security_bot) } let_it_be(:user) { create(:user, user_type: :security_bot) }
describe 'feature flag enabled' do describe 'feature flag enabled' do
before do before do
...@@ -192,7 +192,7 @@ RSpec.describe 'User page' do ...@@ -192,7 +192,7 @@ RSpec.describe 'User page' do
end end
it 'only shows Overview and Activity tabs' do it 'only shows Overview and Activity tabs' do
visit(user_path(user)) subject
page.within '.nav-links' do page.within '.nav-links' do
expect(page).to have_link('Overview') expect(page).to have_link('Overview')
...@@ -211,7 +211,7 @@ RSpec.describe 'User page' do ...@@ -211,7 +211,7 @@ RSpec.describe 'User page' do
end end
it 'only shows Overview and Activity tabs' do it 'only shows Overview and Activity tabs' do
visit(user_path(user)) subject
page.within '.nav-links' do page.within '.nav-links' do
expect(page).to have_link('Overview') expect(page).to have_link('Overview')
...@@ -224,4 +224,24 @@ RSpec.describe 'User page' do ...@@ -224,4 +224,24 @@ RSpec.describe 'User page' do
end end
end end
end end
context 'structured markup' do
let_it_be(:user) { create(:user, website_url: 'https://gitlab.com', organization: 'GitLab', job_title: 'Frontend Engineer', email: 'public@example.com', public_email: 'public@example.com', location: 'Country', created_at: Time.now, updated_at: Time.now) }
it 'shows Person structured markup' do
subject
aggregate_failures do
expect(page).to have_selector('[itemscope][itemtype="http://schema.org/Person"]')
expect(page).to have_selector('img[itemprop="image"]')
expect(page).to have_selector('[itemprop="name"]')
expect(page).to have_selector('[itemprop="address"][itemscope][itemtype="https://schema.org/PostalAddress"]')
expect(page).to have_selector('[itemprop="addressLocality"]')
expect(page).to have_selector('[itemprop="url"]')
expect(page).to have_selector('[itemprop="email"]')
expect(page).to have_selector('span[itemprop="jobTitle"]')
expect(page).to have_selector('span[itemprop="worksFor"]')
end
end
end
end end
...@@ -208,4 +208,27 @@ RSpec.describe PageLayoutHelper do ...@@ -208,4 +208,27 @@ RSpec.describe PageLayoutHelper do
end end
end end
end end
describe '#page_itemtype' do
subject { helper.page_itemtype(itemtype) }
context 'when itemtype is passed' do
let(:itemtype) { 'http://schema.org/Person' }
it 'stores and returns the itemtype value' do
attrs = { itemscope: true, itemtype: itemtype }
expect(subject).to eq attrs
expect(helper.page_itemtype(nil)).to eq attrs
end
end
context 'when no itemtype is provided' do
let(:itemtype) { nil }
it 'returns an empty hash' do
expect(subject).to eq({})
end
end
end
end end
...@@ -204,40 +204,72 @@ RSpec.describe UsersHelper do ...@@ -204,40 +204,72 @@ RSpec.describe UsersHelper do
end end
describe '#work_information' do describe '#work_information' do
subject { helper.work_information(user) } let(:with_schema_markup) { false }
context 'when both job_title and organization are present' do subject { helper.work_information(user, with_schema_markup: with_schema_markup) }
let(:user) { build(:user, organization: 'GitLab', job_title: 'Frontend Engineer') }
it 'returns job title concatenated with organization' do context 'when neither organization nor job_title are present' do
is_expected.to eq('Frontend Engineer at GitLab') it { is_expected.to be_nil }
end
end end
context 'when only organization is present' do context 'when user parameter is nil' do
let(:user) { build(:user, organization: 'GitLab') } let(:user) { nil }
it "returns organization" do it { is_expected.to be_nil }
is_expected.to eq('GitLab')
end
end end
context 'when only job_title is present' do context 'without schema markup' do
let(:user) { build(:user, job_title: 'Frontend Engineer') } context 'when both job_title and organization are present' do
let(:user) { build(:user, organization: 'GitLab', job_title: 'Frontend Engineer') }
it 'returns job title' do it 'returns job title concatenated with organization' do
is_expected.to eq('Frontend Engineer') is_expected.to eq('Frontend Engineer at GitLab')
end
end end
end
context 'when neither organization nor job_title are present' do context 'when only organization is present' do
it { is_expected.to be_nil } let(:user) { build(:user, organization: 'GitLab') }
it "returns organization" do
is_expected.to eq('GitLab')
end
end
context 'when only job_title is present' do
let(:user) { build(:user, job_title: 'Frontend Engineer') }
it 'returns job title' do
is_expected.to eq('Frontend Engineer')
end
end
end end
context 'when user parameter is nil' do context 'with schema markup' do
let(:user) { nil } let(:with_schema_markup) { true }
it { is_expected.to be_nil } context 'when both job_title and organization are present' do
let(:user) { build(:user, organization: 'GitLab', job_title: 'Frontend Engineer') }
it 'returns job title concatenated with organization' do
is_expected.to eq('<span itemprop="jobTitle">Frontend Engineer</span> at <span itemprop="worksFor">GitLab</span>')
end
end
context 'when only organization is present' do
let(:user) { build(:user, organization: 'GitLab') }
it "returns organization" do
is_expected.to eq('<span itemprop="worksFor">GitLab</span>')
end
end
context 'when only job_title is present' do
let(:user) { build(:user, job_title: 'Frontend Engineer') }
it 'returns job title' do
is_expected.to eq('<span itemprop="jobTitle">Frontend Engineer</span>')
end
end
end end
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