diff --git a/api/config/spa_urls.py b/api/config/spa_urls.py
index 246338d2dd535186fbd64bfd73c99ed95bd6b5a8..19aefa2ed753bf5375b779ae93ed2c87582fd064 100644
--- a/api/config/spa_urls.py
+++ b/api/config/spa_urls.py
@@ -1,5 +1,6 @@
 from django import urls
 
+from funkwhale_api.audio import spa_views as audio_spa_views
 from funkwhale_api.music import spa_views
 
 
@@ -20,4 +21,9 @@ urlpatterns = [
         spa_views.library_playlist,
         name="library_playlist",
     ),
+    urls.re_path(
+        r"^channels/(?P<uuid>[0-9a-f-]+)/?$",
+        audio_spa_views.channel_detail,
+        name="channel_detail",
+    ),
 ]
diff --git a/api/funkwhale_api/audio/spa_views.py b/api/funkwhale_api/audio/spa_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..34404812d4a9c3252e83cca1b84b6a5485602ed8
--- /dev/null
+++ b/api/funkwhale_api/audio/spa_views.py
@@ -0,0 +1,64 @@
+import urllib.parse
+
+from django.conf import settings
+from django.urls import reverse
+
+from funkwhale_api.common import preferences
+from funkwhale_api.common import utils
+from funkwhale_api.music import spa_views
+
+from . import models
+
+
+def channel_detail(request, uuid):
+    queryset = models.Channel.objects.filter(uuid=uuid).select_related(
+        "artist__attachment_cover", "actor", "library"
+    )
+    try:
+        obj = queryset.get()
+    except models.Channel.DoesNotExist:
+        return []
+    obj_url = utils.join_url(
+        settings.FUNKWHALE_URL,
+        utils.spa_reverse("channel_detail", kwargs={"uuid": obj.uuid}),
+    )
+    metas = [
+        {"tag": "meta", "property": "og:url", "content": obj_url},
+        {"tag": "meta", "property": "og:title", "content": obj.artist.name},
+        {"tag": "meta", "property": "og:type", "content": "profile"},
+    ]
+
+    if obj.artist.attachment_cover:
+        metas.append(
+            {
+                "tag": "meta",
+                "property": "og:image",
+                "content": obj.artist.attachment_cover.download_url_medium_square_crop,
+            }
+        )
+
+    if preferences.get("federation__enabled"):
+        metas.append(
+            {
+                "tag": "link",
+                "rel": "alternate",
+                "type": "application/activity+json",
+                "href": obj.actor.fid,
+            }
+        )
+
+    if obj.library.uploads.all().playable_by(None).exists():
+        metas.append(
+            {
+                "tag": "link",
+                "rel": "alternate",
+                "type": "application/json+oembed",
+                "href": (
+                    utils.join_url(settings.FUNKWHALE_URL, reverse("api:v1:oembed"))
+                    + "?format=json&url={}".format(urllib.parse.quote_plus(obj_url))
+                ),
+            }
+        )
+        # twitter player is also supported in various software
+        metas += spa_views.get_twitter_card_metas(type="channel", id=obj.uuid)
+    return metas
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 281b7d792d0bf825f933d0dad8cf9d80c7d9a074..7b367857ef7418ca9767f6648c68043bd6014bc0 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -614,6 +614,36 @@ class OembedSerializer(serializers.Serializer):
             data["author_url"] = federation_utils.full_url(
                 common_utils.spa_reverse("library_artist", kwargs={"pk": artist.pk})
             )
+        elif match.url_name == "channel_detail":
+            from funkwhale_api.audio.models import Channel
+
+            qs = Channel.objects.filter(uuid=match.kwargs["uuid"]).select_related(
+                "artist__attachment_cover"
+            )
+            try:
+                channel = qs.get()
+            except models.Artist.DoesNotExist:
+                raise serializers.ValidationError(
+                    "No channel matching id {}".format(match.kwargs["uuid"])
+                )
+            embed_type = "channel"
+            embed_id = channel.uuid
+
+            if channel.artist.attachment_cover:
+                data[
+                    "thumbnail_url"
+                ] = channel.artist.attachment_cover.download_url_medium_square_crop
+                data["thumbnail_width"] = 200
+                data["thumbnail_height"] = 200
+            data["title"] = channel.artist.name
+            data["description"] = channel.artist.name
+            data["author_name"] = channel.artist.name
+            data["height"] = 400
+            data["author_url"] = federation_utils.full_url(
+                common_utils.spa_reverse(
+                    "channel_detail", kwargs={"uuid": channel.uuid}
+                )
+            )
         elif match.url_name == "library_playlist":
             qs = playlists_models.Playlist.objects.filter(
                 pk=int(match.kwargs["pk"]), privacy_level="everyone"
diff --git a/api/tests/audio/test_spa_views.py b/api/tests/audio/test_spa_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..bae96e711668b421e356a18f474f48f946da6c25
--- /dev/null
+++ b/api/tests/audio/test_spa_views.py
@@ -0,0 +1,96 @@
+import urllib.parse
+
+from django.urls import reverse
+
+from funkwhale_api.common import utils
+from funkwhale_api.federation import utils as federation_utils
+from funkwhale_api.music import serializers
+
+
+def test_library_artist(spa_html, no_api_auth, client, factories, settings):
+    channel = factories["audio.Channel"]()
+    factories["music.Upload"](playable=True, library=channel.library)
+    url = "/channels/{}".format(channel.uuid)
+
+    response = client.get(url)
+
+    expected_metas = [
+        {
+            "tag": "meta",
+            "property": "og:url",
+            "content": utils.join_url(settings.FUNKWHALE_URL, url),
+        },
+        {"tag": "meta", "property": "og:title", "content": channel.artist.name},
+        {"tag": "meta", "property": "og:type", "content": "profile"},
+        {
+            "tag": "meta",
+            "property": "og:image",
+            "content": channel.artist.attachment_cover.download_url_medium_square_crop,
+        },
+        {
+            "tag": "link",
+            "rel": "alternate",
+            "type": "application/activity+json",
+            "href": channel.actor.fid,
+        },
+        {
+            "tag": "link",
+            "rel": "alternate",
+            "type": "application/json+oembed",
+            "href": (
+                utils.join_url(settings.FUNKWHALE_URL, reverse("api:v1:oembed"))
+                + "?format=json&url={}".format(
+                    urllib.parse.quote_plus(utils.join_url(settings.FUNKWHALE_URL, url))
+                )
+            ),
+        },
+        {"tag": "meta", "property": "twitter:card", "content": "player"},
+        {
+            "tag": "meta",
+            "property": "twitter:player",
+            "content": serializers.get_embed_url("channel", id=channel.uuid),
+        },
+        {"tag": "meta", "property": "twitter:player:width", "content": "600"},
+        {"tag": "meta", "property": "twitter:player:height", "content": "400"},
+    ]
+
+    metas = utils.parse_meta(response.content.decode())
+
+    # we only test our custom metas, not the default ones
+    assert metas[: len(expected_metas)] == expected_metas
+
+
+def test_oembed_channel(factories, no_api_auth, api_client, settings):
+    settings.FUNKWHALE_URL = "http://test"
+    settings.FUNKWHALE_EMBED_URL = "http://embed"
+    channel = factories["audio.Channel"]()
+    artist = channel.artist
+    url = reverse("api:v1:oembed")
+    obj_url = "https://test.com/channels/{}".format(channel.uuid)
+    iframe_src = "http://embed?type=channel&id={}".format(channel.uuid)
+    expected = {
+        "version": "1.0",
+        "type": "rich",
+        "provider_name": settings.APP_NAME,
+        "provider_url": settings.FUNKWHALE_URL,
+        "height": 400,
+        "width": 600,
+        "title": artist.name,
+        "description": artist.name,
+        "thumbnail_url": federation_utils.full_url(
+            artist.attachment_cover.file.crop["200x200"].url
+        ),
+        "thumbnail_height": 200,
+        "thumbnail_width": 200,
+        "html": '<iframe width="600" height="400" scrolling="no" frameborder="no" src="{}"></iframe>'.format(
+            iframe_src
+        ),
+        "author_name": artist.name,
+        "author_url": federation_utils.full_url(
+            utils.spa_reverse("channel_detail", kwargs={"uuid": channel.uuid})
+        ),
+    }
+
+    response = api_client.get(url, {"url": obj_url, "format": "json"})
+
+    assert response.data == expected
diff --git a/front/src/EmbedFrame.vue b/front/src/EmbedFrame.vue
index f44f1ba4d2d07654ac5857f75eaf69cf70c5cbea..8c684d9225344d5877af09165f0665660427d438 100644
--- a/front/src/EmbedFrame.vue
+++ b/front/src/EmbedFrame.vue
@@ -139,7 +139,7 @@ export default {
   data () {
     return {
       time,
-      supportedTypes: ['track', 'album', 'artist', 'playlist'],
+      supportedTypes: ['track', 'album', 'artist', 'playlist', 'channel'],
       baseUrl: '',
       error: null,
       type: null,
@@ -230,7 +230,10 @@ export default {
         this.fetchTrack(id)
       }
       if (type === 'album') {
-        this.fetchTracks({album: id, playable: true, ordering: ",disc_number,position"})
+        this.fetchTracks({album: id, playable: true, ordering: "disc_number,position"})
+      }
+      if (type === 'channel') {
+        this.fetchTracks({channel: id, playable: true, include_channels: 'true', ordering: "-creation_date"})
       }
       if (type === 'artist') {
         this.fetchTracks({artist: id, playable: true, ordering: "-release_date,disc_number,position"})