Merge branch 'more-metadata' into 'master'

Add more metadata elements

See merge request framasoft/mobilizon!1089
This commit is contained in:
Thomas Citharel 2021-10-22 08:14:31 +00:00
commit 7c27ba4000
10 changed files with 180 additions and 25 deletions

View file

@ -20,14 +20,24 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Actors.Actor do
[ [
Tag.tag(:meta, property: "og:title", content: Actor.display_name_and_username(group)), Tag.tag(:meta, property: "og:title", content: Actor.display_name_and_username(group)),
Tag.tag(:meta, property: "og:url", content: group.url), Tag.tag(:meta,
property: "og:url",
content:
Endpoint
|> Routes.page_url(
:actor,
Actor.preferred_username_and_domain(group)
)
|> URI.decode()
),
Tag.tag(:meta, property: "og:description", content: group.summary), Tag.tag(:meta, property: "og:description", content: group.summary),
Tag.tag(:meta, property: "og:type", content: "profile"), Tag.tag(:meta, property: "og:type", content: "profile"),
Tag.tag(:meta, Tag.tag(:meta,
property: "profile:username", property: "profile:username",
content: Actor.preferred_username_and_domain(group) content: Actor.preferred_username_and_domain(group)
), ),
Tag.tag(:meta, property: "twitter:card", content: "summary") Tag.tag(:meta, property: "twitter:card", content: "summary"),
Tag.tag(:meta, property: "twitter:site", content: "@joinmobilizon")
] ]
|> maybe_add_avatar(group) |> maybe_add_avatar(group)
|> add_group_schema(group) |> add_group_schema(group)
@ -50,8 +60,22 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Actors.Actor do
@spec add_group_schema(list(Tag.t()), Actor.t()) :: list(Tag.t()) @spec add_group_schema(list(Tag.t()), Actor.t()) :: list(Tag.t())
defp add_group_schema(tags, %Actor{} = group) do defp add_group_schema(tags, %Actor{} = group) do
breadcrumbs = %{
"@context" => "https://schema.org",
"@type" => "BreadcrumbList",
"itemListElement" => [
%{
"@type" => "ListItem",
"position" => 1,
"name" => Actor.display_name(group)
}
]
}
tags ++ tags ++
[ [
~s{<script type="application/ld+json">#{Jason.encode!(breadcrumbs)}</script>}
|> HTML.raw(),
~s{<script type="application/ld+json">#{json(group)}</script>} |> HTML.raw() ~s{<script type="application/ld+json">#{json(group)}</script>} |> HTML.raw()
] ]
end end

View file

@ -1,9 +1,12 @@
defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
alias Phoenix.HTML alias Phoenix.HTML
alias Phoenix.HTML.Tag alias Phoenix.HTML.Tag
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.Events.{Event, EventOptions} alias Mobilizon.Events.{Event, EventOptions}
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.JsonLD.ObjectView alias Mobilizon.Web.JsonLD.ObjectView
alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Service.Metadata.Utils, import Mobilizon.Service.Metadata.Utils,
only: [process_description: 2, strip_tags: 1, datetime_to_string: 2, render_address: 1] only: [process_description: 2, strip_tags: 1, datetime_to_string: 2, render_address: 1]
@ -35,9 +38,64 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
] ]
end end
breadcrumbs =
if event.attributed_to do
[
%{
"@context" => "https://schema.org",
"@type" => "BreadcrumbList",
"itemListElement" => [
%{
"@type" => "ListItem",
"position" => 1,
"name" => Actor.display_name(event.attributed_to),
"item" =>
Endpoint
|> Routes.page_url(
:actor,
Actor.preferred_username_and_domain(event.attributed_to)
)
|> URI.decode()
},
%{
"@type" => "ListItem",
"position" => 2,
"name" => event.title
}
]
}
]
else
[]
end
breadcrumbs =
breadcrumbs ++
[
%{
"@context" => "https://schema.org",
"@type" => "BreadcrumbList",
"itemListElement" => [
%{
"@type" => "ListItem",
"position" => 1,
"name" => "Events",
"item" => "#{Endpoint.url()}/search"
},
%{
"@type" => "ListItem",
"position" => 2,
"name" => event.title
}
]
}
]
tags ++ tags ++
[ [
Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"), Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
~s{<script type="application/ld+json">#{Jason.encode!(breadcrumbs)}</script>}
|> HTML.raw(),
~s{<script type="application/ld+json">#{json(event)}</script>} |> HTML.raw() ~s{<script type="application/ld+json">#{json(event)}</script>} |> HTML.raw()
] ]
end end

View file

@ -20,18 +20,20 @@ defmodule Mobilizon.Service.Metadata.Instance do
description = Utils.process_description(Config.instance_description()) description = Utils.process_description(Config.instance_description())
title = "#{Config.instance_name()} - Mobilizon" title = "#{Config.instance_name()} - Mobilizon"
instance_json_ld = """ json_ld = %{
<script type="application/ld+json">{ "@context" => "http://schema.org",
"@context": "http://schema.org", "@type" => "WebSite",
"@type": "WebSite", "name" => "#{title}",
"name": "#{title}", "url" => "#{Endpoint.url()}",
"url": "#{Endpoint.url()}", "potentialAction" => %{
"potentialAction": { "@type" => "SearchAction",
"@type": "SearchAction", "target" => "#{Endpoint.url()}/search?term={search_term}",
"target": "#{Endpoint.url()}/search?term={search_term}", "query-input" => "required name=search_term"
"query-input": "required name=search_term"
} }
}</script> }
instance_json_ld = """
<script type="application/ld+json">#{Jason.encode!(json_ld)}</script>
""" """
[ [

View file

@ -1,9 +1,12 @@
defimpl Mobilizon.Service.Metadata, for: Mobilizon.Posts.Post do defimpl Mobilizon.Service.Metadata, for: Mobilizon.Posts.Post do
alias Phoenix.HTML alias Phoenix.HTML
alias Phoenix.HTML.Tag alias Phoenix.HTML.Tag
alias Mobilizon.Actors.Actor
alias Mobilizon.Medias.{File, Media} alias Mobilizon.Medias.{File, Media}
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.JsonLD.ObjectView alias Mobilizon.Web.JsonLD.ObjectView
alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Service.Metadata.Utils, only: [process_description: 2, strip_tags: 1] import Mobilizon.Service.Metadata.Utils, only: [process_description: 2, strip_tags: 1]
def build_tags(%Post{} = post, locale \\ "en") do def build_tags(%Post{} = post, locale \\ "en") do
@ -21,9 +24,35 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Posts.Post do
] ]
|> maybe_add_post_picture(post) |> maybe_add_post_picture(post)
breadcrumbs = %{
"@context" => "https://schema.org",
"@type" => "BreadcrumbList",
"itemListElement" => [
%{
"@type" => "ListItem",
"position" => 1,
"name" => Actor.display_name(post.attributed_to),
"item" =>
Endpoint
|> Routes.page_url(
:actor,
Actor.preferred_username_and_domain(post.attributed_to)
)
|> URI.decode()
},
%{
"@type" => "ListItem",
"position" => 2,
"name" => post.title
}
]
}
tags ++ tags ++
[ [
Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"), Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
~s{<script type="application/ld+json">#{Jason.encode!(breadcrumbs)}</script>}
|> HTML.raw(),
~s{<script type="application/ld+json">#{json(post)}</script>} |> HTML.raw() ~s{<script type="application/ld+json">#{json(post)}</script>} |> HTML.raw()
] ]
end end

View file

@ -11,13 +11,30 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do
@spec render(String.t(), map()) :: map() @spec render(String.t(), map()) :: map()
def render("group.json", %{group: %Actor{} = group}) do def render("group.json", %{group: %Actor{} = group}) do
%{ res = %{
"@context" => "http://schema.org", "@context" => "http://schema.org",
"@type" => "Organization", "@type" => "Organization",
"url" => group.url, "url" => group.url,
"name" => group.name || group.preferred_username, "name" => group.name || group.preferred_username,
"address" => render_address(group) "address" => render_address(group)
} }
res =
if group.banner do
Map.put(res, "image", group.banner.url)
else
res
end
if group.physical_address do
Map.put(
res,
"address",
render_one(group.physical_address, ObjectView, "address.json", as: :address)
)
else
res
end
end end
def render("event.json", %{event: %Event{} = event}) do def render("event.json", %{event: %Event{} = event}) do
@ -93,12 +110,27 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do
"@context" => "https://schema.org", "@context" => "https://schema.org",
"@type" => "Article", "@type" => "Article",
"name" => post.title, "name" => post.title,
"headline" => post.title,
"author" => %{ "author" => %{
"@type" => "Organization", "@type" => "Organization",
"name" => Actor.display_name(post.attributed_to) "name" => Actor.display_name(post.attributed_to),
"url" =>
Endpoint
|> Routes.page_url(
:actor,
Actor.preferred_username_and_domain(post.attributed_to)
)
|> URI.decode()
}, },
"datePublished" => post.publish_at, "datePublished" => post.publish_at,
"dateModified" => post.updated_at "dateModified" => post.updated_at,
"image" =>
if(post.picture,
do: [
post.picture.file.url
],
else: ["#{Endpoint.url()}/img/mobilizon_default_card.png"]
)
} }
end end

View file

@ -10,7 +10,9 @@ defmodule Mobilizon.Service.Metadata.InstanceTest do
description = Utils.process_description(Config.instance_description()) description = Utils.process_description(Config.instance_description())
assert Instance.build_tags() |> Utils.stringify_tags() == assert Instance.build_tags() |> Utils.stringify_tags() ==
"<title>#{title}</title><meta content=\"#{description}\" name=\"description\"><meta content=\"#{title}\" property=\"og:title\"><meta content=\"#{Endpoint.url()}\" property=\"og:url\"><meta content=\"#{description}\" property=\"og:description\"><meta content=\"website\" property=\"og:type\"><script type=\"application/ld+json\">{\n\"@context\": \"http://schema.org\",\n\"@type\": \"WebSite\",\n\"name\": \"#{title}\",\n\"url\": \"#{Endpoint.url()}\",\n\"potentialAction\": {\n\"@type\": \"SearchAction\",\n\"target\": \"#{Endpoint.url()}/search?term={search_term}\",\n\"query-input\": \"required name=search_term\"\n}\n}</script>\n" """
<title>#{title}</title><meta content="#{description}" name="description"><meta content="#{title}" property="og:title"><meta content="#{Endpoint.url()}" property="og:url"><meta content="#{description}" property="og:description"><meta content="website" property="og:type"><script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","name":"#{title}","potentialAction":{"@type":"SearchAction","query-input":"required name=search_term","target":"#{Endpoint.url()}/search?term={search_term}"},"url":"#{Endpoint.url()}"}</script>
"""
end end
end end
end end

View file

@ -18,7 +18,7 @@ defmodule Mobilizon.Service.MetadataTest do
assert group |> Metadata.build_tags() |> Metadata.Utils.stringify_tags() == assert group |> Metadata.build_tags() |> Metadata.Utils.stringify_tags() ==
String.trim(""" String.trim("""
<meta content="#{group.name} (@#{group.preferred_username}@#{group.domain})" property="og:title"><meta content="#{group.url}" property="og:url"><meta content="The event organizer didn&#39;t add any description." property="og:description"><meta content="profile" property="og:type"><meta content="#{Actor.preferred_username_and_domain(group)}" property="profile:username"><meta content="summary" property="twitter:card"><meta content="#{group.avatar.url}" property="og:image"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","address":null,"name":"#{group.name}","url":"#{group.url}"}</script><link href="#{Routes.feed_url(Endpoint, :actor, Actor.preferred_username_and_domain(group), "atom")}" rel="alternate" title="#{group.name}'s feed" type="application/atom+xml"><link href="#{Routes.feed_url(Endpoint, :actor, Actor.preferred_username_and_domain(group), "ics")}" rel="alternate" title="#{group.name}'s feed" type="text/calendar"><link href=\"#{group.url}\" rel=\"alternate\" type=\"application/activity+json\"><link href=\"#{group.url}\" rel=\"canonical\"><meta content=\"noindex\" name=\"robots\"> <meta content="#{group.name} (@#{group.preferred_username}@#{group.domain})" property="og:title"><meta content="#{URI.decode(Routes.page_url(Endpoint, :actor, Actor.preferred_username_and_domain(group)))}" property="og:url"><meta content="The event organizer didn&#39;t add any description." property="og:description"><meta content="profile" property="og:type"><meta content="#{Actor.preferred_username_and_domain(group)}" property="profile:username"><meta content="summary" property="twitter:card"><meta content="@joinmobilizon" property="twitter:site"><meta content="#{group.avatar.url}" property="og:image"><script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"#{group.name}","position":1}]}</script><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","address":{"@type":"PostalAddress","addressCountry":"My Country","addressLocality":"My Locality","addressRegion":"My Region","postalCode":"My Postal Code","streetAddress":"My Street Address"},"image":"#{group.banner.url}","name":"#{group.name}","url":"#{group.url}"}</script><link href="#{Routes.feed_url(Endpoint, :actor, Actor.preferred_username_and_domain(group), "atom")}" rel="alternate" title="#{group.name}'s feed" type="application/atom+xml"><link href="#{Routes.feed_url(Endpoint, :actor, Actor.preferred_username_and_domain(group), "ics")}" rel="alternate" title="#{group.name}'s feed" type="text/calendar"><link href="#{group.url}" rel="alternate" type="application/activity+json"><link href="#{group.url}" rel="canonical"><meta content="noindex" name="robots">
""") """)
assert group assert group
@ -26,7 +26,7 @@ defmodule Mobilizon.Service.MetadataTest do
|> Metadata.build_tags() |> Metadata.build_tags()
|> Metadata.Utils.stringify_tags() == |> Metadata.Utils.stringify_tags() ==
String.trim(""" String.trim("""
<meta content="#{group.name} (@#{group.preferred_username}@#{group.domain})" property="og:title"><meta content="#{group.url}" property="og:url"><meta content="The event organizer didn&#39;t add any description." property="og:description"><meta content="profile" property="og:type"><meta content="#{Actor.preferred_username_and_domain(group)}" property="profile:username"><meta content="summary" property="twitter:card"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","address":null,"name":"#{group.name}","url":"#{group.url}"}</script><link href="#{Routes.feed_url(Endpoint, :actor, Actor.preferred_username_and_domain(group), "atom")}" rel="alternate" title="#{group.name}'s feed" type="application/atom+xml"><link href="#{Routes.feed_url(Endpoint, :actor, Actor.preferred_username_and_domain(group), "ics")}" rel="alternate" title="#{group.name}'s feed" type="text/calendar"><link href=\"#{group.url}\" rel=\"alternate\" type=\"application/activity+json\"><link href=\"#{group.url}\" rel=\"canonical\"><meta content=\"noindex\" name=\"robots\"> <meta content="#{group.name} (@#{group.preferred_username}@#{group.domain})" property="og:title"><meta content="#{URI.decode(Routes.page_url(Endpoint, :actor, Actor.preferred_username_and_domain(group)))}" property="og:url"><meta content="The event organizer didn&#39;t add any description." property="og:description"><meta content="profile" property="og:type"><meta content="#{Actor.preferred_username_and_domain(group)}" property="profile:username"><meta content="summary" property="twitter:card"><meta content="@joinmobilizon" property="twitter:site"><script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"#{group.name}","position":1}]}</script><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","address":{"@type":"PostalAddress","addressCountry":"My Country","addressLocality":"My Locality","addressRegion":"My Region","postalCode":"My Postal Code","streetAddress":"My Street Address"},"image":"#{group.banner.url}","name":"#{group.name}","url":"#{group.url}"}</script><link href="#{Routes.feed_url(Endpoint, :actor, Actor.preferred_username_and_domain(group), "atom")}" rel="alternate" title="#{group.name}'s feed" type="application/atom+xml"><link href="#{Routes.feed_url(Endpoint, :actor, Actor.preferred_username_and_domain(group), "ics")}" rel="alternate" title="#{group.name}'s feed" type="text/calendar"><link href="#{group.url}" rel="alternate" type="application/activity+json"><link href="#{group.url}" rel="canonical"><meta content="noindex" name="robots">
""") """)
end end
@ -100,7 +100,13 @@ defmodule Mobilizon.Service.MetadataTest do
|> Floki.attribute("content") |> Floki.attribute("content")
|> hd |> hd
assert "event.json" |> ObjectView.render(%{event: event}) |> Jason.encode!() == event_json_ld = "event.json" |> ObjectView.render(%{event: event}) |> Jason.encode!()
breadcrumbs = """
[{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"item\":\"http://mobilizon.test/search\",\"name\":\"Events\",\"position\":1},{\"@type\":\"ListItem\",\"name\":\"#{event.title}\",\"position\":2}]}]
"""
assert String.trim(breadcrumbs) <> event_json_ld ==
document document
|> Floki.find("script[type=\"application/ld+json\"]") |> Floki.find("script[type=\"application/ld+json\"]")
|> Floki.text(js: true) |> Floki.text(js: true)
@ -125,7 +131,7 @@ defmodule Mobilizon.Service.MetadataTest do
|> Metadata.build_tags() |> Metadata.build_tags()
|> Metadata.Utils.stringify_tags() == |> Metadata.Utils.stringify_tags() ==
String.trim(""" String.trim("""
<meta content="#{post.title}" property="og:title"><meta content="#{post.url}" property="og:url"><meta content="#{Metadata.Utils.process_description(post.body)}" property="og:description"><meta content="article" property="og:type"><meta content="summary" property="twitter:card"><link href="#{post.url}" rel="canonical"><meta content="#{post.picture.file.url}" property="og:image"><meta content="summary_large_image" property="twitter:card"><script type="application/ld+json">{"@context":"https://schema.org","@type":"Article","author":{"@type":"Organization","name":"#{post.attributed_to.preferred_username}"},"dateModified":"#{DateTime.to_iso8601(post.updated_at)}","datePublished":"#{DateTime.to_iso8601(post.publish_at)}","name":"My Awesome article"}</script> <meta content="#{post.title}" property="og:title"><meta content="#{post.url}" property="og:url"><meta content="#{Metadata.Utils.process_description(post.body)}" property="og:description"><meta content="article" property="og:type"><meta content="summary" property="twitter:card"><link href="#{post.url}" rel="canonical"><meta content="#{post.picture.file.url}" property="og:image"><meta content="summary_large_image" property="twitter:card"><script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","item":"#{post.attributed_to.url}","name":"#{post.attributed_to.name}","position":1},{"@type":"ListItem","name":"#{post.title}","position":2}]}</script><script type="application/ld+json">{"@context":"https://schema.org","@type":"Article","author":{"@type":"Organization","name":"#{post.attributed_to.name}","url":"#{post.attributed_to.url}"},"dateModified":"#{DateTime.to_iso8601(post.updated_at)}","datePublished":"#{DateTime.to_iso8601(post.publish_at)}","headline":"#{post.title}","image":["#{post.picture.file.url}"],"name":"My Awesome article"}</script>
""") """)
end end
end end

View file

@ -45,6 +45,7 @@ defmodule Mobilizon.Factory do
%Mobilizon.Actors.Actor{ %Mobilizon.Actors.Actor{
preferred_username: preferred_username, preferred_username: preferred_username,
name: sequence("Thomas Named"),
domain: nil, domain: nil,
followers: [], followers: [],
followings: [], followings: [],
@ -81,7 +82,8 @@ defmodule Mobilizon.Factory do
resources_url: Actor.build_url(preferred_username, :resources), resources_url: Actor.build_url(preferred_username, :resources),
inbox_url: Actor.build_url(preferred_username, :inbox), inbox_url: Actor.build_url(preferred_username, :inbox),
outbox_url: Actor.build_url(preferred_username, :outbox), outbox_url: Actor.build_url(preferred_username, :outbox),
user: nil user: nil,
physical_address: build(:address)
} }
) )
end end

View file

@ -216,7 +216,7 @@ defmodule Mix.Tasks.Mobilizon.UsersTest do
actor2 = insert(:actor, user: user) actor2 = insert(:actor, user: user)
output = output =
"Informations for the user #{@email}:\n - account status: Activated on #{confirmed_at} (UTC)\n - Role: #{role}\n Identities (2):\n - @#{actor1.preferred_username} / \n - @#{actor2.preferred_username} / \n\n\n" "Informations for the user #{@email}:\n - account status: Activated on #{confirmed_at} (UTC)\n - Role: #{role}\n Identities (2):\n - @#{actor1.preferred_username} / #{actor1.name}\n - @#{actor2.preferred_username} / #{actor2.name}\n\n\n"
Show.run([@email]) Show.run([@email])
assert_received {:mix_shell, :info, [output_received]} assert_received {:mix_shell, :info, [output_received]}

View file

@ -30,7 +30,7 @@ defmodule Mobilizon.Web.FeedControllerTest do
{:ok, feed} = ElixirFeedParser.parse(conn.resp_body) {:ok, feed} = ElixirFeedParser.parse(conn.resp_body)
assert feed.title == assert feed.title ==
actor.preferred_username <> "'s public events feed on #{Config.instance_name()}" actor.name <> "'s public events feed on #{Config.instance_name()}"
[entry1, entry2] = entries = feed.entries [entry1, entry2] = entries = feed.entries
@ -270,7 +270,7 @@ defmodule Mobilizon.Web.FeedControllerTest do
{:ok, feed} = ElixirFeedParser.parse(conn.resp_body) {:ok, feed} = ElixirFeedParser.parse(conn.resp_body)
assert feed.title == assert feed.title ==
"#{actor1.preferred_username}'s private events feed on #{Config.instance_name()}" "#{actor1.name}'s private events feed on #{Config.instance_name()}"
[entry] = feed.entries [entry] = feed.entries
assert entry.title == event1.title assert entry.title == event1.title