diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex
index 48801b588..f232fbb0e 100644
--- a/lib/pleroma/web/metadata.ex
+++ b/lib/pleroma/web/metadata.ex
@@ -64,7 +64,11 @@ def activity_nsfw?(_) do
defp activated_providers do
unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do
- [Pleroma.Web.Metadata.Providers.Feed | Pleroma.Config.get([__MODULE__, :providers], [])]
+ [
+ Pleroma.Web.Metadata.Providers.Feed,
+ Pleroma.Web.Metadata.Providers.ApUrl
+ | Pleroma.Config.get([__MODULE__, :providers], [])
+ ]
else
[]
end
diff --git a/lib/pleroma/web/metadata/providers/ap_url.ex b/lib/pleroma/web/metadata/providers/ap_url.ex
new file mode 100644
index 000000000..cff581713
--- /dev/null
+++ b/lib/pleroma/web/metadata/providers/ap_url.ex
@@ -0,0 +1,35 @@
+# Akkoma: Magically expressive social media
+# Copyright © 2025 Akkoma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.ApUrl do
+ alias Pleroma.Web.Metadata.Providers.Provider
+
+ @behaviour Provider
+
+ defp alt_link(uri, type) do
+ {
+ :link,
+ [rel: "alternate", href: uri, type: type],
+ []
+ }
+ end
+
+ defp ap_alt_links(uri) do
+ [
+ alt_link(uri, "application/activity+json"),
+ alt_link(uri, "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
+ ]
+ end
+
+ @impl Provider
+ def build_tags(%{object: %{data: %{"id" => ap_id}}}) when is_binary(ap_id) do
+ ap_alt_links(ap_id)
+ end
+
+ def build_tags(%{user: %{ap_id: ap_id}}) when is_binary(ap_id) do
+ ap_alt_links(ap_id)
+ end
+
+ def build_tags(_), do: []
+end
diff --git a/test/pleroma/web/metadata/providers/ap_url_test.exs b/test/pleroma/web/metadata/providers/ap_url_test.exs
new file mode 100644
index 000000000..b01c58fff
--- /dev/null
+++ b/test/pleroma/web/metadata/providers/ap_url_test.exs
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.ApUrlTest do
+ use Pleroma.DataCase, async: true
+ import Pleroma.Factory
+ alias Pleroma.Web.Metadata.Providers.ApUrl
+
+ @ap_type_compliant "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
+ @ap_type_mastodon "application/activity+json"
+
+ test "it preferentially renders a link to a post" do
+ user = insert(:user)
+ note = insert(:note, user: user)
+
+ assert ApUrl.build_tags(%{object: note, user: user}) == [
+ {:link, [rel: "alternate", href: note.data["id"], type: @ap_type_mastodon], []},
+ {:link, [rel: "alternate", href: note.data["id"], type: @ap_type_compliant], []}
+ ]
+ end
+
+ test "it renders a link to a user" do
+ user = insert(:user)
+
+ assert ApUrl.build_tags(%{user: user}) == [
+ {:link, [rel: "alternate", href: user.ap_id, type: @ap_type_mastodon], []},
+ {:link, [rel: "alternate", href: user.ap_id, type: @ap_type_compliant], []}
+ ]
+ end
+end
diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs
index 79d4d6261..4d128af73 100644
--- a/test/pleroma/web/static_fe/static_fe_controller_test.exs
+++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs
@@ -321,6 +321,16 @@ defp meta_find_twitter(document, name) do
Floki.find(document, "head>meta[name=\"twitter:" <> name <> "\"]")
end
+ defp meta_find_alt_links(document) do
+ Floki.find(document, "head>link[rel=\"alternate\"]")
+ |> Enum.map(fn {_, attr, _} ->
+ {
+ :proplists.get_value("type", attr),
+ :proplists.get_value("href", attr)
+ }
+ end)
+ end
+
# Detailed metadata tests are already done for each builder individually, so just
# one check per type of content should suffice to ensure we're calling the providers correctly
describe "metadata tags for" do
@@ -350,6 +360,8 @@ test "user profile", %{conn: conn, user: user, user_avatar_url: user_avatar_url}
[{"meta", tw_desc, _}] = meta_find_twitter(document, "description")
[{"meta", tw_img, _}] = meta_find_twitter(document, "image")
+ alt_links = meta_find_alt_links(document)
+
assert meta_content(og_type) == "article"
assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user)
assert meta_content(og_url) == user.ap_id
@@ -362,6 +374,8 @@ test "user profile", %{conn: conn, user: user, user_avatar_url: user_avatar_url}
assert meta_content(tw_title) == meta_content(og_title)
assert meta_content(tw_desc) == meta_content(og_desc)
assert meta_content(tw_img) == meta_content(og_img)
+
+ assert Enum.any?(alt_links, fn e -> e == {"application/activity+json", user.ap_id} end)
end
test "text-only post", %{conn: conn, user: user, user_avatar_url: user_avatar_url} do
@@ -386,6 +400,8 @@ test "text-only post", %{conn: conn, user: user, user_avatar_url: user_avatar_ur
[{"meta", tw_desc, _}] = meta_find_twitter(document, "description")
[{"meta", tw_img, _}] = meta_find_twitter(document, "image")
+ alt_links = meta_find_alt_links(document)
+
assert meta_content(og_type) == "article"
assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user)
assert meta_content(og_url) == activity.data["id"]
@@ -398,6 +414,10 @@ test "text-only post", %{conn: conn, user: user, user_avatar_url: user_avatar_ur
assert meta_content(tw_title) == meta_content(og_title)
assert meta_content(tw_desc) == meta_content(og_desc)
assert meta_content(tw_img) == meta_content(og_img)
+
+ assert Enum.any?(alt_links, fn e ->
+ e == {"application/activity+json", activity.object.data["id"]}
+ end)
end
test "post with attachments", %{conn: conn, user: user} do