[deploy] Migrate from Schema.org to JSON-LD (#6956)

* Add JSON-LD in place of schema.org in _video_player.html.erb

* Remove unnecessary tags around JSON-LD to clean up templates

* Fix indenatation and remove extra elements from show.html.erb and _profile_header.html.erb

* Remove empty line from articles/show.html.erb

* Clean up JSON-LD commas across erb templates

* Revert articleBody changes, adjust nesting, and remove extra quote

* Add application/ld+json to allowed_types in .erb-lint.yml

This addition makes it so that the no verify flag is no longer required

* Add additional data for Articles, Users, and Orgs to better SEO

* Add additional data to JSON-LD structures
*Add sameAs data to articles/show.html.erb
*Add sameAs data to users/show.html.erb
*Make adjustments to VideoObject data in _video_player
*Make adjustments to Org in _profile_header
*Add removed erb back to all erb templates

* Refactor code to fix syntax Google syntax errors
* Add missing double quotes to values
* Remove unnecessary meta tags from _video_player.html.erb
* Add additional key-value pairs to data structures
* General code clean up

* Clean up existing data structures
  *Remove useless meta tag and replace with a tag
  *Small refactor to users/show.html.erb data

* Extract user json-ld out of users/show.html.erb
  *Move json-ld logic to stories_controller
  *Use user_json_ld variable in place of entire data structure
  *Set user_json_ld as a before_action in the controller

* Add specs around JSON-LD data in templates
  *Add request spec for users/show.html.erb
  *Add request spec for articles/show.html.erb
  *Add request spec for _video_player.html.erb
  *Add request spec for _profile_header.html.erb
  *WIP: Fix pending specs for all templates

* Remove useless meta tags from users/show and _profile_header.html.erb

* Add additional test coverage to users/show.html.erb
  *Add additional tests to user_show_spec
  *Ensure that all tests pass
  *Small refactor to users/show.html.erb JSON data structure

* Add additional test coverage around JSON-LD data
  *Add more robust tests to template specs
  *Remove unnecessary comments from specs
  *Uncomment pending specs

* Adjust modified date to be more readable in article specs

* Refactor spec to be more readable and update GitHub username test

* Remove meta erb from articles/show.html.erb

* Remove meta data from _video_player.html.erb

* Clean up useless codeand whitespace for final review

* Extract users/show data structure from template into StoriesController
  * Remove JSON-LD object from users/show.html.erb
  * Place JSON-LD for users/show.html.erb in StoriesController
  * Store user_json_ld in set_json_ld method in StoriesController
  * Call user_json_ld variable in script tag in users/show
  * Clean up trailing commas and refactor articles/show.html.erb
  * Remove redundant user/profile_header_spec.rb

* Remove unnecessary code from _profile_header and articles/show
  * Remove Organization from articles/show.html.erb
  * Remove unnecessary comments from _profile_header.html.erb

* Extract user_json_ld into separate methods to fix Code Climate failures

* Rename user_profile_details and user_same_as methods to be more explicit

* Refactored user_json_ld methods to use symbols instead of strings
  * Use symbols to fetch keys from a hash instead of strings
  * Changed user_url in data structure

* Removal of erb tags and general clean up

* Revert schema changes

* Remove unnecessary quotation marks
This commit is contained in:
Julianna Tetreault 2020-04-15 13:03:05 -06:00 committed by GitHub
parent 2c28bcb2b2
commit fd9fdebc0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 274 additions and 40 deletions

View file

@ -12,6 +12,7 @@ linters:
allowed_types:
- 'text/javascript'
- 'text/x-tmpl'
- 'application/ld+json'
allow_blank: true
disallow_inline_scripts: false
Rubocop:

View file

@ -205,6 +205,7 @@ class StoriesController < ApplicationController
return if performed?
set_surrogate_key_header "articles-user-#{@user.id}"
set_json_ld
render template: "users/show"
end
@ -330,4 +331,58 @@ class StoriesController < ApplicationController
def assign_classified_listings
@classified_listings = ClassifiedListing.where(published: true).select(:title, :category, :slug, :bumped_at)
end
def set_json_ld
@user_json_ld = {
"@context": "http://schema.org",
"@type": "Person",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": URL.user(@user)
},
"url": URL.user(@user),
"sameAs": [],
"image": ProfileImage.new(@user).get(width: 320),
"name": @user.name,
"email": "",
"jobTitle": "",
"description": @user.summary.presence || ["404 bio not found"].sample,
"disambiguatingDescription": [],
"worksFor": [
{
"@type": "Organization"
},
],
"alumniOf": ""
}
set_user_profile_json_ld
set_user_same_as_json_ld
end
def set_user_profile_json_ld
@user_json_ld[:disambiguatingDescription].append(@user.mostly_work_with) if @user.mostly_work_with.present?
@user_json_ld[:disambiguatingDescription].append(@user.currently_hacking_on) if @user.currently_hacking_on.present?
@user_json_ld[:disambiguatingDescription].append(@user.currently_learning) if @user.currently_learning.present?
@user_json_ld[:worksFor][0][:employer_name] = @user.employer_name if @user.employer_name.present?
@user_json_ld[:worksFor][0][:employer_url] = @user.employer_url if @user.employer_url.present?
@user_json_ld[:alumniOf] = @user.education if @user.education.present?
@user_json_ld[:email] = @user.email if @user.email_public
@user_json_ld[:jobTitle] = @user.employment_title if @user.employment_title.present?
@user_json_ld[:sameAs].append(@user.twitter_username) if @user.twitter_username.present?
@user_json_ld[:sameAs].append(@user.github_username) if @user.github_username.present?
end
def set_user_same_as_json_ld
@user_json_ld[:sameAs].append(@user.mastodon_url) if @user.mastodon_url.present?
@user_json_ld[:sameAs].append(@user.facebook_url) if @user.facebook_url.present?
@user_json_ld[:sameAs].append(@user.linkedin_url) if @user.linkedin_url.present?
@user_json_ld[:sameAs].append(@user.behance_url) if @user.behance_url.present?
@user_json_ld[:sameAs].append(@user.stackoverflow_url) if @user.stackoverflow_url.present?
@user_json_ld[:sameAs].append(@user.dribbble_url) if @user.dribbble_url.present?
@user_json_ld[:sameAs].append(@user.medium_url) if @user.medium_url.present?
@user_json_ld[:sameAs].append(@user.gitlab_url) if @user.gitlab_url.present?
@user_json_ld[:sameAs].append(@user.instagram_url) if @user.instagram_url.present?
@user_json_ld[:sameAs].append(@user.twitch_username) if @user.twitch_username.present?
@user_json_ld[:sameAs].append(@user.website_url) if @user.website_url.present?
end
end

View file

@ -1,16 +1,22 @@
<div itemscope itemtype="http://schema.org/VideoObject" class="video-player-header">
<% if meta_tags %>
<meta itemprop="uploadDate" content="<%= article.published_at %>" />
<meta itemprop="name" content="<%= article.title %>" />
<meta itemprop="description" content="<%= article.description %>" />
<meta itemprop="thumbnailUrl" content="<%= cloudinary(article.video_thumbnail_url, 880) %>" />
<meta itemprop="contentUrl" content="<%= article.video_source_url %>" />
<% minutes, seconds = article.video_duration_in_minutes.split(":") %>
<meta itemprop="duration" content="<%= format("PT%<minutes>sM%<seconds>sS", minutes: minutes, seconds: seconds) %>" />
<div id="video-player-source" data-source="<%= article.video_source_url %>"></div>
<% end %>
<div class="video-player-header">
<% minutes, seconds = article.video_duration_in_minutes.split(":") %>
<div id="video-player-source" data-source="<%= article.video_source_url %>"></div>
<script src="//content.jwplatform.com/libraries/b1zWy2iv.js" async></script>
<div id="video-player-<%= article.id %>" class="video-player"></div>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "VideoObject",
"url": "<%= article.video_source_url %>",
"uploadDate": "<%= article.published_at&.rfc3339 %>",
"name": "<%= article.title %>",
"description": "<%= article.description %>",
"thumbnailUrl": "<%= cloudinary(article.video_thumbnail_url, 880) %>",
"contentUrl": "<%= article.video_source_url %>",
"duration": "<%= format("PT%<minutes>sM%<seconds>sS", minutes: minutes, seconds: seconds) %>"
}
</script>
</div>
<script type="text/javascript" async>

View file

@ -31,6 +31,40 @@
</script>
<% end %>
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Article",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "<%= article_url(@article) %>"
},
"url": "<%= article_url(@article) %>",
"image": "<%= article_social_image_url(@article) %>",
"publisher": {
"@context": "http://schema.org",
"@type": "Organization",
"name": "<%= community_qualified_name %>",
"logo": {
"@context": "http://schema.org",
"@type": "ImageObject",
"url": "<%= cloudinary(SiteConfig.logo_png, 192, "png") %>",
"width": "192",
"height": "192"
}
},
"headline": "<%= @article.title %>",
"author": {
"@context": "http://schema.org",
"@type": "Person",
"url": "<%= user_url(@user) %>",
"name": "<%= @user.name %>"
},
"datePublished": "<%= @article.published_timestamp %>",
"dateModified": "<%= @article.edited_at&.iso8601 || @article.published_timestamp %>"
}
</script>
<% if @article.processed_html.include? "runkit-element" %>
<%= javascript_include_tag "https://embed.runkit.com" %>
<% end %>
@ -85,17 +119,7 @@
<% if @article.video_state == "PROGRESSING" %>
<h1 style="text-align:center;">⏳ Video Transcoding In Progress ⏳</h1>
<% end %>
<article itemscope itemtype="http://schema.org/Article" itemprop="mainEntity">
<meta itemprop="url" content="<%= article_url(@article) %>">
<meta itemprop="image" content="<%= article_social_image_url(@article) %>">
<div itemprop="publisher" itemscope itemtype="https://schema.org/Organization">
<div itemprop="logo" itemscope itemtype="https://schema.org/ImageObject">
<meta itemprop="url" content="<%= cloudinary(SiteConfig.logo_png, 192, "png") %>">
<meta itemprop="width" content="192">
<meta itemprop="height" content="192">
</div>
<meta itemprop="name" content="<%= community_qualified_name %>">
</div>
<article>
<section>
<% if !@article.published %>
<a href="<%= @article.path %>/edit" class="unpublished">
@ -120,15 +144,14 @@
</div>
</a>
<% end %>
<h1 class="<%= @article.title_length_classification %>" itemprop="name headline">
<h1 class="<%= @article.title_length_classification %>">
<%= @article.title %>
</h1>
<h3>
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="url" content="<%= user_url(@user) %>">
<span>
<a href="/<%= @user.username %>" class="author">
<img class="profile-pic" src="<%= ProfileImage.new(@user).get(width: 50) %>" alt="<%= @user.username %> profile image" />
<span itemprop="name"><%= @user.name %></span>
<span><%= @user.name %></span>
</a>
</span>
<% if @user.twitter_username.present? %>
@ -138,7 +161,7 @@
<a href="http://github.com/<%= @user.github_username %>"><%= image_tag_or_inline_svg_tag "github", width: 18, height: 18 %></a>
<% end %>
<% if @article.published_timestamp.present? %>
<time itemprop="datePublished" datetime="<%= @article.published_timestamp %>"><%= @article.readable_publish_date %></time>
<time datetime="<%= @article.published_timestamp %>"><%= @article.readable_publish_date %></time>
<% end %>
<% if @second_user.present? %>
<em>with <b><a href="<%= @second_user.path %>"><%= @second_user.name %></a></b></em>
@ -147,7 +170,7 @@
<em> and <b><a href="<%= @third_user.path %>"><%= @third_user.name %></a></b></em>
<% end %>
<% if should_show_updated_on?(@article) %>
<span><em>Updated on <time itemprop="dateModified" datetime="<%= @article.edited_at&.utc&.iso8601 %>"><%= @article.edited_at&.strftime("%b %d, %Y") %></time></em></span>
<span><em>Updated on <time datetime="<%= @article.edited_at&.utc&.iso8601 %>"><%= @article.edited_at&.strftime("%b %d, %Y") %></time></em></span>
<% elsif should_show_crossposted_on?(@article) %>
<span>
<em>
@ -179,7 +202,7 @@
articles: @collection_articles,
position: :top %>
<% end %>
<div class="body" data-article-id="<%= @article.id %>" id="article-body" itemprop="articleBody">
<div class="body" data-article-id="<%= @article.id %>" id="article-body">
<%= @article.processed_html.html_safe %>
</div>
<% if @article.body_markdown && @article.body_markdown.size > 900 && @collection %>

View file

@ -7,23 +7,38 @@
.user-profile-follow-button {
border: 1px solid <%= user_colors(@user)[:text] %> !important;
}
<% end %>
</style>
<div class="user-profile-header" style="<%= user_colors_style(@user) %>" itemscope itemtype="http://schema.org/Person" itemprop="mainEntity">
<meta itemprop="url" content="https://dev.to/<%= @user.username %>">
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Organization",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "<%= user_url(@user) %>"
},
"url": "<%= user_url(@user) %>",
"image": "<%= ProfileImage.new(@user).get(width: 320) %>",
"name": "<%= sanitize @user.name %>",
"description": "<%= sanitize(@user.summary.presence || "404 bio not found") %>"
}
</script>
<div class="user-profile-header" style="<%= user_colors_style(@user) %>">
<a href="<%= user_url(@user) %>"></a>
<div class="user-profile-header-container">
<div class="profile-pic-wrapper">
<img class="profile-pic" src="<%= ProfileImage.new(@user).get(width: 320) %>" itemprop="image" alt="<%= @user.username %> profile" style="border-color:<%= user_colors(@user)[:bg] %>;background:<%= user_colors(@user)[:bg] %>" />
<img class="profile-pic" src="<%= ProfileImage.new(@user).get(width: 320) %>" alt="<%= @user.username %> profile" style="border-color:<%= user_colors(@user)[:bg] %>;background:<%= user_colors(@user)[:bg] %>" />
</div>
<div class="profile-details">
<h1 style="color:<%= HexComparer.new([user_colors(@user)[:bg], user_colors(@user)[:text]]).brightness(0.78) %>">
<span itemprop="name"><%= sanitize @user.name %></span>
<span><%= sanitize @user.name %></span>
<span class="user-profile-follow-button-wrapper">
<button id="user-follow-butt" class="cta follow-action-button user-profile-follow-button" style="color:<%= user_colors(@user)[:text] %>;background-color:<%= user_colors(@user)[:bg] %>" data-info='{"id":<%= @user.id %>,"className":"<%= @user.class.name %>"}'>&nbsp;</button>
</span>
</h1>
<p class="profile-description" itemprop="description">
<p class="profile-description">
<%= sanitize(@user.summary.presence || "404 bio not found") %>
</p>
<style>

View file

@ -24,15 +24,20 @@
}
<% end %>
</style>
<div class="user-profile-header" style="<%= user_colors_style(@user) %>" itemscope itemtype="http://schema.org/Person" itemprop="mainEntity">
<meta itemprop="url" content="https://dev.to/<%= @user.username %>">
<script type="application/ld+json">
<%= @user_json_ld.to_json.html_safe %>
</script>
<div class="user-profile-header" style="<%= user_colors_style(@user) %>">
<a href="https://dev.to/<%= user_url(@user) %>"></a>
<div class="user-profile-header-container">
<div class="profile-pic-wrapper">
<img class="profile-pic" src="<%= ProfileImage.new(@user).get(width: 320) %>" itemprop="image" alt="<%= @user.username %> profile" style="border-color:<%= user_colors(@user)[:bg] %>;background:<%= user_colors(@user)[:bg] %>" />
<img class="profile-pic" src="<%= ProfileImage.new(@user).get(width: 320) %>" alt="<%= @user.username %> profile" style="border-color:<%= user_colors(@user)[:bg] %>;background:<%= user_colors(@user)[:bg] %>" />
</div>
<div class="profile-details">
<h1>
<span itemprop="name"><%= @user.name %><%= render "shared/pro_checkmark", style: nil, width: nil %></span>
<span><%= @user.name %><%= render "shared/pro_checkmark", style: nil, width: nil %></span>
<span class="user-profile-follow-button-wrapper">
<button id="user-follow-butt" class="cta follow-action-button user-profile-follow-button" style="color:<%= user_colors(@user)[:text] %>;background-color:<%= user_colors(@user)[:bg] %>" data-info='{"id":<%= @user.id %>,"className":"<%= @user.class.name %>"}'>&nbsp;</button>
</span>
@ -44,7 +49,7 @@
</span>
<% end %>
</h1>
<p class="profile-description" itemprop="description">
<p class="profile-description">
<%= @user.summary.presence || ["404 bio not found"].sample %>
</p>
<style>

View file

@ -0,0 +1,40 @@
require "rails_helper"
RSpec.describe "ArticlesShow", type: :request do
let_it_be(:user) { create(:user) }
let_it_be(:organization) { create(:organization) }
let_it_be(:organization_article) { create(:article, organization_id: organization.id) }
let_it_be(:article, reload: true) { create(:article, user: user, published: true) }
describe "GET /:slug (articles)" do
before do
get article.path
end
it "returns a 200 status when navigating to the article's page" do
expect(response).to have_http_status(:ok)
end
it "renders the proper title" do
expect(response.body).to include CGI.escapeHTML(article.title)
end
it "renders the proper published at date" do
expect(response.body).to include CGI.escapeHTML(article.readable_publish_date)
end
it "renders the proper modified at date" do
article.update(edited_at: Time.zone.now)
expect(response.body).to include CGI.escapeHTML(article.edited_at.strftime("%b %d, %Y"))
end
it "renders the proper author" do
expect(response.body).to include CGI.escapeHTML(article.cached_user_username)
end
it "renders the proper organization for an article when one is present" do
get organization.path
expect(response.body).to include CGI.escapeHTML(organization_article.title)
end
end
end

View file

@ -0,0 +1,37 @@
require "rails_helper"
RSpec.describe "VideoPlayerShow", type: :request do
let_it_be(:user) { create(:user) }
let_it_be(:video_article) { create(:article, user: user) }
describe "GET /:slug (video articles)" do
before do
video_article.update_columns(video: "video", video_source_url: "video", title: "A Video")
get video_article.path
end
it "returns a 200 status when navigating to the video article's page" do
expect(response).to have_http_status(:ok)
end
it "renders the proper title" do
expect(response.body).to include CGI.escapeHTML(video_article.title)
end
it "renders the proper description" do
expect(response.body).to include CGI.escapeHTML(video_article.description)
end
it "renders the proper video url" do
expect(response.body).to include CGI.escapeHTML(video_article.video_source_url)
end
it "renders the proper published at date" do
expect(response.body).to include CGI.escapeHTML(video_article.readable_publish_date)
end
it "renders the proper author" do
expect(response.body).to include CGI.escapeHTML(video_article.cached_user_username)
end
end
end

View file

@ -0,0 +1,52 @@
require "rails_helper"
RSpec.describe "UserShow", type: :request do
let_it_be(:user) { create(:user, email_public: true, currently_hacking_on: "JSON-LD", education: "DEV University") }
describe "GET /:slug (user)" do
before do
user.update_columns(employment_title: "SEO", employer_name: "DEV", linkedin_url: "www.linkedin.com/company/example/")
get user.path
end
it "returns a 200 status when navigating to the user's page" do
expect(response).to have_http_status(:ok)
end
it "renders the proper username for a user" do
expect(response.body).to include CGI.escapeHTML(user.username)
end
it "renders the proper bio for a user" do
expect(response.body).to include CGI.escapeHTML(user.summary)
end
it "renders the proper education for a user" do
expect(response.body).to include CGI.escapeHTML(user.education)
end
it "renders the proper currently hacking on info for a user" do
expect(response.body).to include CGI.escapeHTML(user.currently_hacking_on)
end
it "renders the proper job title for a user" do
expect(response.body).to include CGI.escapeHTML(user.employment_title)
end
it "renders the proper employer name for a user" do
expect(response.body).to include CGI.escapeHTML(user.employer_name)
end
it "renders the proper linkedin url for a user" do
expect(response.body).to include CGI.escapeHTML(user.linkedin_url)
end
it "renders the proper additional username for a user when one is present" do
expect(response.body).to include CGI.escapeHTML(user.github_username)
end
it "renders the proper email for a user when one is public and present" do
expect(response.body).to include CGI.escapeHTML(user.email)
end
end
end