Commit bcace2de authored by Denis Hovart's avatar Denis Hovart Committed by Jonne Haß

6840 : meta tags update (#6998)

* Adds a new metadata helper and methods to PostPresenter to have metas on post pages.

* Adds tests to post controller to check correctness of metas

* Add methods to PersonPresenter to have metas on profile pages

* Correct meta data helper test

* Update PersonPresenter, add test to PeopleController

* Creates TagPresenter. Display tag metas on tag index page

* Updata meta data helper spec

* Not displaying bio as the description meta on profile page for now. Privacy concerns to be cleared.

* Set meta info as hashes in presenters

* Move original hardcoded metas info to config/defaults.yml

* metas_tags include by default the general metas, update views

* Update code style, clean views

* Renames TagPresenter StreamTagPresenter, updates TagController spec

* Add a default_metas entry to diaspora.yml.example

* Align metas hash in presenters, refactor meta data helper

* Use bio as description meta if user has a public profile

* Rename StreamTagPresenter to TagStreamPresenter
parent 96489e3c
......@@ -69,27 +69,26 @@ class PeopleController < ApplicationController
# renders the persons user profile page
def show
mark_corresponding_notifications_read if user_signed_in?
@person_json = PersonPresenter.new(@person, current_user).as_json
@presenter = PersonPresenter.new(@person, current_user)
respond_to do |format|
format.all do
if user_signed_in?
@contact = current_user.contact_for(@person)
end
gon.preloads[:person] = @person_json
gon.preloads[:person] = @presenter.as_json
gon.preloads[:photos_count] = Photo.visible(current_user, @person).count(:all)
gon.preloads[:contacts_count] = Contact.contact_contacts_for(current_user, @person).count(:all)
respond_with @person, layout: "with_header"
respond_with @presenter, layout: "with_header"
end
format.mobile do
@post_type = :all
person_stream
respond_with @person
respond_with @presenter
end
format.json { render json: @person_json }
format.json { render json: @presenter.as_json }
end
end
......
......@@ -20,13 +20,14 @@ class PhotosController < ApplicationController
@post_type = :photos
@person = Person.find_by_guid(params[:person_id])
authenticate_user! if @person.try(:remote?) && !user_signed_in?
@presenter = PersonPresenter.new(@person, current_user)
if @person
@contact = current_user.contact_for(@person) if user_signed_in?
@posts = Photo.visible(current_user, @person, :all, max_time)
respond_to do |format|
format.all do
gon.preloads[:person] = PersonPresenter.new(@person, current_user).as_json
gon.preloads[:person] = @presenter.as_json
gon.preloads[:photos_count] = Photo.visible(current_user, @person).count(:all)
gon.preloads[:contacts_count] = Contact.contact_contacts_for(current_user, @person).count(:all)
render "people/show", layout: "with_header"
......
......@@ -19,14 +19,15 @@ class PostsController < ApplicationController
def show
post = post_service.find!(params[:id])
post_service.mark_user_notifications(post.id)
presenter = PostPresenter.new(post, current_user)
respond_to do |format|
format.html {
gon.post = PostPresenter.new(post, current_user)
render locals: {post: post}
}
format.html do
gon.post = presenter
render locals: {post: presenter}
end
format.mobile { render locals: {post: post} }
format.xml { render xml: DiasporaFederation::Salmon::XmlPayload.pack(Diaspora::Federation::Entities.post(post)) }
format.json { render json: PostPresenter.new(post, current_user) }
format.json { render json: presenter }
end
end
......
......@@ -37,9 +37,15 @@ class TagsController < ApplicationController
if user_signed_in?
gon.preloads[:tagFollowings] = tags
end
@stream = Stream::Tag.new(current_user, params[:name], :max_time => max_time, :page => params[:page])
stream = Stream::Tag.new(current_user, params[:name], max_time: max_time, page: params[:page])
@stream = TagStreamPresenter.new(stream)
respond_with do |format|
format.json { render :json => @stream.stream_posts.map { |p| LastThreeCommentsDecorator.new(PostPresenter.new(p, current_user)) }}
format.json do
posts = stream.stream_posts.map do |p|
LastThreeCommentsDecorator.new(PostPresenter.new(p, current_user))
end
render json: posts
end
end
end
......
module MetaDataHelper
include ActionView::Helpers::AssetUrlHelper
include ActionView::Helpers::TagHelper
def og_prefix
'og: http://ogp.me/ns# article: http://ogp.me/ns/article# profile: http://ogp.me/ns/profile#'
end
def site_url
AppConfig.environment.url
end
def default_image_url
asset_url "assets/branding/logos/asterisk.png"
end
def default_author_name
AppConfig.settings.pod_name
end
def default_description
AppConfig.settings.default_metas.description
end
def default_title
AppConfig.settings.default_metas.title
end
def general_metas
{
description: {name: "description", content: default_description},
og_description: {property: "description", content: default_description},
og_site_name: {property: "og:site_name", content: default_title},
og_url: {property: "og:url", content: site_url},
og_image: {property: "og:image", content: default_image_url},
og_type: {property: "og:type", content: "website"}
}
end
def metas_tags(attributes_list={}, with_general_metas=true)
attributes_list = general_metas.merge(attributes_list) if with_general_metas
attributes_list.map {|_, attributes| meta_tag attributes }.join("\n").html_safe
end
# recursively calls itself if attribute[:content] is an array
# (metas such as og:image or og:tag can be present multiple times with different values)
def meta_tag(attributes)
return "" if attributes.empty?
return tag(:meta, attributes) unless attributes[:content].respond_to?(:to_ary)
items = attributes.delete(:content)
items.map {|item| meta_tag(attributes.merge(content: item)) }.join("\n")
end
end
module OpenGraphHelper
def og_title(title)
meta_tag_with_property('og:title', title)
end
def og_type(post)
meta_tag_with_property('og:type', 'article')
end
def og_url(url)
meta_tag_with_property('og:url', url)
end
def og_image(post=nil)
tags = []
tags = post.photos.map{|x| meta_tag_with_property('og:image', x.url(:thumb_large))} if post
tags << meta_tag_with_property('og:image', default_image_url) if tags.empty?
tags.join(' ')
end
def og_description(description)
meta_tag_with_property('og:description', description)
end
def og_type(type='website')
meta_tag_with_property('og:type', type)
end
def og_namespace
AppConfig.services.facebook.open_graph_namespace
end
def og_site_name
meta_tag_with_property('og:site_name', AppConfig.settings.pod_name)
end
def og_common_tags
[og_site_name]
end
def og_general_tags
[
*og_common_tags,
og_type,
og_title('diaspora* social network'),
og_image,
og_url(AppConfig.environment.url),
og_description('diaspora* is the online social world where you are in control.')
].join("\n").html_safe
end
def og_page_post_tags(post)
tags = og_common_tags
if post.message
tags.concat [
*tags,
og_type("#{og_namespace}:frame"),
og_title(post_page_title(post, :length => 140)),
og_url(post_url(post)),
og_image(post),
og_description(post.message.plain_text_without_markdown truncate: 1000)
]
end
tags.join("\n").html_safe
end
def og_prefix
"og: http://ogp.me/ns# #{og_namespace}: https://diasporafoundation.org/ns/joindiaspora#"
end
def meta_tag_with_property(name, content)
tag(:meta, :property => name, :content => content)
end
def og_html(cache)
"<a href=\"#{cache.url}\" target=\"_blank\">" +
" <div>" +
......@@ -91,14 +16,4 @@ module OpenGraphHelper
def oembed_image_tag(cache, prefix)
image_tag(cache.data["#{prefix}url"], cache.options_hash(prefix))
end
private
# This method compensates for hosting assets off of s3
def default_image_url
if image_path("branding/logos/asterisk.png").include?("http")
image_path("branding/logos/asterisk.png")
else
"#{root_url.chop}#{image_path('branding/logos/asterisk.png')}"
end
end
end
class BasePresenter
attr_reader :current_user
include Rails.application.routes.url_helpers
class << self
def new(*args)
......@@ -26,4 +27,10 @@ class BasePresenter
nil
end
end
private
def default_url_options
{host: AppConfig.pod_uri.host, port: AppConfig.pod_uri.port}
end
end
......@@ -25,6 +25,21 @@ class PersonPresenter < BasePresenter
base_hash_with_contact.merge(profile: ProfilePresenter.new(profile).for_hovercard)
end
def metas_attributes
{
keywords: {name: "keywords", content: comma_separated_tags},
description: {name: "description", content: description},
og_title: {property: "og:title", content: title},
og_description: {property: "og:title", content: description},
og_url: {property: "og:url", content: url},
og_image: {property: "og:image", content: image_url},
og_type: {property: "og:type", content: "profile"},
og_profile_username: {property: "og:profile:username", content: name},
og_profile_firstname: {property: "og:profile:first_name", content: first_name},
og_profile_lastname: {property: "og:profile:last_name", content: last_name}
}
end
protected
def own_profile?
......@@ -88,4 +103,25 @@ class PersonPresenter < BasePresenter
def is_blocked?
current_user_person_block.present?
end
def title
name
end
def comma_separated_tags
profile.tags.map(&:name).join(", ") if profile.tags
end
def url
url_for(@presentable)
end
def description
public_details? ? bio : ""
end
def image_url
return AppConfig.url_to @presentable.image_url if @presentable.image_url[0] == "/"
@presentable.image_url
end
end
class PostPresenter < BasePresenter
include PostsHelper
include MetaDataHelper
attr_accessor :post
def initialize(post, current_user=nil)
@post = post
@current_user = current_user
def initialize(presentable, current_user=nil)
@post = presentable
super
end
def as_json(_options={})
@post.as_json(only: directly_retrieved_attributes).merge(non_directly_retrieved_attributes)
@post.as_json(only: directly_retrieved_attributes)
.merge(non_directly_retrieved_attributes)
end
def metas_attributes
{
keywords: {name: "keywords", content: comma_separated_tags},
description: {name: "description", content: description},
og_url: {property: "og:url", content: url},
og_title: {property: "og:title", content: title},
og_image: {property: "og:image", content: images},
og_description: {property: "og:description", content: description},
og_article_tag: {property: "og:article:tag", content: tags},
og_article_author: {property: "og:article:author", content: author_name},
og_article_modified: {property: "og:article:modified_time", content: modified_time_iso8601},
og_article_published: {property: "og:article:published_time", content: published_time_iso8601}
}
end
def page_title
post_page_title @post
end
private
......@@ -38,6 +59,10 @@ class PostPresenter < BasePresenter
}
end
def title
@post.message.present? ? @post.message.title : I18n.t("posts.presenter.title", name: @post.author_name)
end
def build_text
if @post.message
@post.message.plain_text_for_json
......@@ -58,10 +83,6 @@ class PostPresenter < BasePresenter
@post.photos.map {|p| p.as_api_response(:backbone) }
end
def title
@post.message.present? ? @post.message.title : I18n.t("posts.presenter.title", name: @post.author_name)
end
def root
if @post.respond_to?(:absolute_root) && @post.absolute_root.present?
PostPresenter.new(@post.absolute_root, current_user).as_json
......@@ -103,4 +124,34 @@ class PostPresenter < BasePresenter
def person
current_user.person
end
def images
photos.any? ? photos.map(&:url) : default_image_url
end
def published_time_iso8601
created_at.to_time.iso8601
end
def modified_time_iso8601
updated_at.to_time.iso8601
end
def tags
tags = @post.is_a?(Reshare) ? @post.root.tags : @post.tags
return tags.map(&:name) if tags
[]
end
def comma_separated_tags
tags.join(", ")
end
def url
post_url @post
end
def description
message.plain_text_without_markdown(truncate: 1000)
end
end
class TagStreamPresenter < BasePresenter
def title
@presentable.display_tag_name
end
def metas_attributes
{
keywords: {name: "keywords", content: tag_name},
description: {name: "description", content: description},
og_url: {property: "og:url", content: url},
og_title: {property: "og:title", content: title},
og_description: {property: "og:description", content: description}
}
end
private
def description
I18n.t("streams.tags.title", tags: tag_name)
end
def url
tag_url tag_name
end
end
- if @post.present?
%link{:rel => 'alternate', :type => "application/json+oembed", :href => "#{oembed_url(:url => post_url(@post))}"}
= og_page_post_tags(@post)
- else
= og_general_tags
......@@ -4,20 +4,17 @@
!!!
%html{lang: I18n.locale.to_s, dir: (rtl?) ? 'rtl' : 'ltr'}
%head{prefix: og_prefix}
%head{prefix: og_prefix}
%title
= page_title yield(:page_title)
%meta{charset: 'utf-8'}/
%meta{"http-equiv" => "Content-Type", :content=>"text/html; charset=utf-8"}/
%meta{name: "description", content: "diaspora*"}/
%meta{name: "author", content: "diaspora* contributors"}/
%meta{name: "viewport", content: "width=device-width, initial-scale=1"}/
= content_for?(:meta_data) ? yield(:meta_data) : metas_tags
%link{rel: 'shortcut icon', href: "#{image_path('favicon.png')}" }
= render 'layouts/open_graph'
= chartbeat_head_block
= include_mixpanel
......
......@@ -4,7 +4,7 @@
!!!
%html{:lang => I18n.locale.to_s, :dir => (rtl?) ? 'rtl' : 'ltr'}
%head{:prefix => og_prefix}
%head
%title
= pod_name
......@@ -30,8 +30,7 @@
/ NOTE(we will enable these once we don't have to rely on back/forward buttons anymore)
/%meta{:name => "apple-mobile-web-app-capable", :content => "yes"}
/%link{:rel => "apple-touch-startup-image", :href => "/images/apple-splash.png"}
= render 'layouts/open_graph'
= yield :meta_data
= chartbeat_head_block
......
......@@ -3,7 +3,10 @@
-# the COPYRIGHT file.
- content_for :page_title do
= @person.name
= @presenter.name
- content_for :meta_data do
= metas_tags @presenter.metas_attributes
.container-fluid#profile_container
.row
......
......@@ -3,7 +3,10 @@
-# the COPYRIGHT file.
- content_for :page_title do
= post_page_title post
= post.page_title
- content_for :meta_data do
= metas_tags post.metas_attributes
- content_for :content do
#container.container-fluid
......@@ -5,6 +5,9 @@
- content_for :page_title do
= @stream.display_tag_name
- content_for :meta_data do
= metas_tags @stream.metas_attributes
.container-fluid#tags_show
.row
.col-md-3.hidden-xs
......
......@@ -144,6 +144,9 @@ defaults:
limit_removals_to_per_day: 100
source_url:
default_color_theme: "original"
default_metas:
title: 'diaspora* social network'
description: 'diaspora* is the online social world where you are in control.'
services:
facebook:
enable: false
......
......@@ -539,6 +539,15 @@ configuration: ## Section
## ("original" for the theme in "app/assets/stylesheets/color_themes/original/", for
## example).
#default_color_theme: "original"
## Default meta tags
## You can change here the default meta tags content included on the pages of your pod.
## Title will be used for the opengraph og:site_name property while description will be used
## for description and og:description.
default_metas:
#title: 'diaspora* social network'
#description: 'diaspora* is the online social world where you are in control.'
## Posting from Diaspora to external services (all are disabled by default).
services: ## Section
......
......@@ -146,6 +146,11 @@ describe PeopleController, :type => :controller do
end
describe '#show' do
before do
@person = FactoryGirl.create(:user).person
@presenter = PersonPresenter.new(@person, @user)
end
it "404s if the id is invalid" do
get :show, :id => 'delicious'
expect(response.code).to eq("404")
......@@ -161,9 +166,15 @@ describe PeopleController, :type => :controller do
expect(response.code).to eq("404")
end
it "returns a person presenter" do
expect(PersonPresenter).to receive(:new).with(@person, @user).and_return(@presenter)
get :show, username: @person.username
expect(assigns(:presenter).to_json).to eq(@presenter.to_json)
end
it 'finds a person via username' do
get :show, username: @user.username
expect(assigns(:person)).to eq(@user.person)
get :show, username: @person.username
expect(assigns(:presenter).to_json).to eq(@presenter.to_json)
end
it "404s if no person is found via diaspora handle" do
......@@ -172,8 +183,8 @@ describe PeopleController, :type => :controller do
end
it 'finds a person via diaspora handle' do
get :show, username: @user.diaspora_handle
expect(assigns(:person)).to eq(@user.person)
get :show, username: @person.diaspora_handle
expect(assigns(:presenter).to_json).to eq(@presenter.to_json)
end
it 'redirects home for closed account' do
......@@ -216,8 +227,8 @@ describe PeopleController, :type => :controller do
end
it "assigns the right person" do
get :show, :id => @user.person.to_param
expect(assigns(:person)).to eq(@user.person)
get :show, id: @person.to_param
expect(assigns(:presenter).id).to eq(@presenter.id)
end
end
......@@ -249,6 +260,27 @@ describe PeopleController, :type => :controller do
get :show, id: @person.to_param
expect(response.body).not_to include(@person.profile.bio)
end
it "includes the correct meta tags" do
presenter = PersonPresenter.new(@person)
methods_properties = {
comma_separated_tags: {html_attribute: "name", name: "keywords"},
url: {html_attribute: "property", name: "og:url"},
title: {html_attribute: "property", name: "og:title"},
image_url: {html_attribute: "property", name: "og:image"},
first_name: {html_attribute: "property", name: "og:profile:first_name"},
last_name: {html_attribute: "property", name: "og:profile:last_name"}
}
get :show, id: @person.to_param
methods_properties.each do |method, property|
value = presenter.send(method)
expect(response.body).to include(
"<meta #{property[:html_attribute]}=\"#{property[:name]}\" content=\"#{value}\" />"
)
end
end
end
context "when the person is a contact of the current user" do
......
......@@ -64,6 +64,7 @@ describe PostsController, type: :controller do
context "user not signed in" do
context "given a public post" do
let(:public) { alice.post(:status_message, text: "hello", public: true) }
let(:public_with_tags) { alice.post(:status_message, text: "#hi #howareyou", public: true) }
it "shows a public post" do
get :show, id: public.id
......@@ -81,6 +82,35 @@ describe PostsController, type: :controller do
expected_xml = DiasporaFederation::Salmon::XmlPayload.pack(Diaspora::Federation::Entities.post(public)).to_xml
expect(response.body).to eq(expected_xml)
end
it "includes the correct uniques meta tags" do
presenter = PostPresenter.new(public)
methods_properties = {
comma_separated_tags: {html_attribute: "name", name: "keywords"},
description: {html_attribute: "name", name: "description"},
url: {html_attribute: "property", name: "og:url"},
title: {html_attribute: "property", name: "og:title"},
published_time_iso8601: {html_attribute: "property", name: "og:article:published_time"},
modified_time_iso8601: {html_attribute: "property", name: "og:article:modified_time"},
author_name: {html_attribute: "property", name: "og:article:author"}
}
get :show, id: public.id, format: :html
methods_properties.each do |method, property|
value = presenter.send(method)
expect(response.body).to include(
"<meta #{property[:html_attribute]}=\"#{property[:name]}\" content=\"#{value}\" />"
)
end
end
it "includes the correct multiple meta tags" do
get :show, id: public_with_tags.id, format: :html
expect(response.body).to include('<meta property="og:article:tag" content="hi" />')
expect(response.body).to include('<meta property="og:article:tag" content="howareyou" />')
end
end
context "given a limited post" do
......
......@@ -120,6 +120,26 @@ describe TagsController, :type => :controller do
expect(JSON.parse(response.body).first["guid"]).to eq(post2.guid)
end
end
it "includes the correct meta tags" do
tag_url = tag_url "yes", host: AppConfig.pod_uri.host, port: AppConfig.pod_uri.port
get :show, name: "yes"
expect(response.body).to include('<meta name="keywords" content="yes" />')
expect(response.body).to include(
%(<meta property="og:url" content="#{tag_url}" />)
)
expect(response.body).to include(
'<meta property="og:title" content="#yes" />'
)
expect(response.body).to include(
%(<meta name="description" content="#{I18n.t('streams.tags.title', tags: 'yes')}" />)
)
expect(response.body).to include(
%(<meta property="og:description" content=\"#{I18n.t('streams.tags.title', tags: 'yes')}" />)
)
end
end
end
......
require "spec_helper"
describe MetaDataHelper, type: :helper do
describe "#meta_tag" do
it "returns an empty string if passed an empty hash" do
expect(meta_tag({})).to eq("")
end
it "returns a meta tag with the passed attributes" do
attributes = {name: "test", content: "foo"}
expect(meta_tag(attributes)).to eq('<meta name="test" content="foo" />')
end
it "returns a list of the same meta type if the value for :content in the passed attribute is an array" do
attributes = {property: "og:tag", content: %w(tag_1 tag_2)}
expect(meta_tag(attributes)).to eq(
%(<meta property="og:tag" content="tag_1" />\n) +
%(<meta property="og:tag" content="tag_2" />)
)
end
end
describe '#metas_tags' do
before do