Commit efd10524 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '36846-fj-add-structured-data-for-projects' into 'master'

Resolve "Provide structured data for projects"

See merge request gitlab-org/gitlab!46858
parents a28219b2 db72091d
...@@ -58,7 +58,7 @@ export default { ...@@ -58,7 +58,7 @@ export default {
</gl-link> </gl-link>
</div> </div>
</div> </div>
<div class="blob-viewer" data-qa-selector="blob_viewer_content"> <div class="blob-viewer" data-qa-selector="blob_viewer_content" itemprop="about">
<gl-loading-icon v-if="loading > 0" size="md" color="dark" class="my-4 mx-auto" /> <gl-loading-icon v-if="loading > 0" size="md" color="dark" class="my-4 mx-auto" />
<div v-else-if="readme" ref="readme" v-html="readme.html"></div> <div v-else-if="readme" ref="readme" v-html="readme.html"></div>
</div> </div>
......
# frozen_string_literal: true
module StatAnchorsHelper
def stat_anchor_attrs(anchor)
{}.tap do |attrs|
attrs[:class] = %w(nav-link gl-display-flex gl-align-items-center) << extra_classes(anchor)
attrs[:itemprop] = anchor.itemprop if anchor.itemprop
end
end
private
def button_attribute(anchor)
"btn-#{anchor.class_modifier || 'missing'}"
end
def extra_classes(anchor)
if anchor.is_link
'stat-link'
else
"btn #{button_attribute(anchor)}"
end
end
end
...@@ -13,7 +13,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -13,7 +13,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
presents :project presents :project
AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon) AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop)
MAX_TOPICS_TO_SHOW = 3 MAX_TOPICS_TO_SHOW = 3
def statistic_icon(icon_name = 'plus-square-o') def statistic_icon(icon_name = 'plus-square-o')
...@@ -277,7 +277,9 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -277,7 +277,9 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false, AnchorData.new(false,
icon + content_tag(:span, license_short_name, class: 'project-stat-value'), icon + content_tag(:span, license_short_name, class: 'project-stat-value'),
license_path, license_path,
'default') 'default',
nil,
'license')
else else
if current_user && can_current_user_push_to_default_branch? if current_user && can_current_user_push_to_default_branch?
AnchorData.new(false, AnchorData.new(false,
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- page_description @project.description_html unless page_description - page_description @project.description_html unless page_description
- header_title project_title(@project) unless header_title - header_title project_title(@project) unless header_title
- nav "project" - nav "project"
- page_itemtype 'http://schema.org/SoftwareSourceCode'
- display_subscription_banner! - display_subscription_banner!
- display_namespace_storage_limit_alert! - display_namespace_storage_limit_alert!
- @left_sidebar = true - @left_sidebar = true
......
...@@ -7,17 +7,17 @@ ...@@ -7,17 +7,17 @@
.row.gl-mb-3 .row.gl-mb-3
.home-panel-title-row.col-md-12.col-lg-6.d-flex .home-panel-title-row.col-md-12.col-lg-6.d-flex
.avatar-container.rect-avatar.s64.home-panel-avatar.gl-flex-shrink-0.gl-w-11.gl-h-11.gl-mr-3.float-none .avatar-container.rect-avatar.s64.home-panel-avatar.gl-flex-shrink-0.gl-w-11.gl-h-11.gl-mr-3.float-none
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64) = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'image')
.d-flex.flex-column.flex-wrap.align-items-baseline .d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline .d-inline-flex.align-items-baseline
%h1.home-panel-title.gl-mt-3.gl-mb-2.gl-font-size-h1.gl-line-height-24.gl-font-weight-bold{ data: { qa_selector: 'project_name_content' } } %h1.home-panel-title.gl-mt-3.gl-mb-2.gl-font-size-h1.gl-line-height-24.gl-font-weight-bold{ data: { qa_selector: 'project_name_content' }, itemprop: 'name' }
= @project.name = @project.name
%span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } %span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, options: { class: 'icon' }) = visibility_level_icon(@project.visibility_level, options: { class: 'icon' })
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project
.home-panel-metadata.d-flex.flex-wrap.text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal .home-panel-metadata.d-flex.flex-wrap.text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal
- if can?(current_user, :read_project, @project) - if can?(current_user, :read_project, @project)
%span.text-secondary %span.text-secondary{ itemprop: 'identifier' }
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id } = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
- if current_user - if current_user
%span.access-request-links.gl-ml-3 %span.access-request-links.gl-ml-3
...@@ -30,10 +30,10 @@ ...@@ -30,10 +30,10 @@
- project_topics_classes = "badge badge-pill badge-secondary gl-mr-2" - project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
- explore_project_topic_path = explore_projects_path(tag: topic) - explore_project_topic_path = explore_projects_path(tag: topic)
- if topic.length > max_project_topic_length - if topic.length > max_project_topic_length
%a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path } %a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
= topic.titleize = topic.titleize
- else - else
%a{ class: project_topics_classes, href: explore_project_topic_path } %a{ class: project_topics_classes, href: explore_project_topic_path, itemprop: 'keywords' }
= topic.titleize = topic.titleize
- if @project.has_extra_topics? - if @project.has_extra_topics?
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
.home-panel-home-desc.mt-1 .home-panel-home-desc.mt-1
- if @project.description.present? - if @project.description.present?
.home-panel-description.text-break .home-panel-description.text-break
.home-panel-description-markdown.read-more-container .home-panel-description-markdown.read-more-container{ itemprop: 'abstract' }
= markdown_field(@project, :description) = markdown_field(@project, :description)
%button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" } %button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
= _("Read more") = _("Read more")
......
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
%ul.nav %ul.nav
- anchors.each do |anchor| - anchors.each do |anchor|
%li.nav-item %li.nav-item
= link_to_if anchor.link, anchor.label, anchor.link, class: anchor.is_link ? 'nav-link stat-link d-flex align-items-center' : "nav-link btn btn-#{anchor.class_modifier || 'missing'} d-flex align-items-center" do = link_to_if(anchor.link, anchor.label, anchor.link, stat_anchor_attrs(anchor)) do
.stat-text.d-flex.align-items-center{ class: ('btn btn-default disabled' if project_buttons) }= anchor.label .stat-text.d-flex.align-items-center{ class: ('btn btn-default disabled' if project_buttons) }= anchor.label
---
title: Add structured data for projects
merge_request: 46858
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Projects > Show > Schema Markup' do
let_it_be(:project) { create(:project, :repository, :public, :with_avatar, description: 'foobar', tag_list: 'tag1, tag2') }
it 'shows SoftwareSourceCode structured markup', :js do
visit project_path(project)
wait_for_all_requests
aggregate_failures do
expect(page).to have_selector('[itemscope][itemtype="http://schema.org/SoftwareSourceCode"]')
expect(page).to have_selector('img[itemprop="image"]')
expect(page).to have_selector('[itemprop="name"]', text: project.name)
expect(page).to have_selector('[itemprop="identifier"]', text: "Project ID: #{project.id}")
expect(page).to have_selector('[itemprop="abstract"]', text: project.description)
expect(page).to have_selector('[itemprop="license"]', text: project.repository.license.name)
expect(find_all('[itemprop="keywords"]').map(&:text)).to match_array(project.tag_list.map(&:capitalize))
expect(page).to have_selector('[itemprop="about"]')
end
end
end
...@@ -29,6 +29,7 @@ exports[`Repository file preview component renders file HTML 1`] = ` ...@@ -29,6 +29,7 @@ exports[`Repository file preview component renders file HTML 1`] = `
<div <div
class="blob-viewer" class="blob-viewer"
data-qa-selector="blob_viewer_content" data-qa-selector="blob_viewer_content"
itemprop="about"
> >
<div> <div>
<div <div
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe StatAnchorsHelper do
let(:anchor_klass) { ProjectPresenter::AnchorData }
describe '#stat_anchor_attrs' do
subject { helper.stat_anchor_attrs(anchor) }
context 'when anchor is a link' do
let(:anchor) { anchor_klass.new(true) }
it 'returns the proper attributes' do
expect(subject[:class]).to include('stat-link')
end
end
context 'when anchor is not a link' do
context 'when class_modifier is set' do
let(:anchor) { anchor_klass.new(false, nil, nil, 'default') }
it 'returns the proper attributes' do
expect(subject[:class]).to include('btn btn-default')
end
end
context 'when class_modifier is not set' do
let(:anchor) { anchor_klass.new(false) }
it 'returns the proper attributes' do
expect(subject[:class]).to include('btn btn-missing')
end
end
end
context 'when itemprop is not set' do
let(:anchor) { anchor_klass.new(false, nil, nil, nil, nil, false) }
it 'returns the itemprop attributes' do
expect(subject[:itemprop]).to be_nil
end
end
context 'when itemprop is set set' do
let(:anchor) { anchor_klass.new(false, nil, nil, nil, nil, true) }
it 'returns the itemprop attributes' do
expect(subject[:itemprop]).to eq true
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