diff --git a/pkg/linking/linking.go b/pkg/linking/linking.go
index 7cf71e16..bd0de3ed 100644
--- a/pkg/linking/linking.go
+++ b/pkg/linking/linking.go
@@ -8,6 +8,7 @@ import (
"fmt"
"log"
"net/url"
+ "strings"
"golang.org/x/net/html"
"stream.place/streamplace/pkg/config"
@@ -56,6 +57,31 @@ var BrandingAssetList = [...]string{
"legalLinks",
}
+// atTags returns meta tags implementing the at-tags proposal
+// (https://tangled.org/chrisshank.com/at-tags), which maps web pages back to
+// atproto identities and records via tags in the page
+//
:
+//
+// - at:canonical — the AT URI of the record this page canonically maps to
+// (pass "" to omit, e.g. for pages that don't represent a record)
+// - at:author — the atproto identity of the page's author, as an
+// at:// URI (pass "" to omit)
+// - at:me — the identity of the overall website, i.e. this node's
+// did:web, when a broadcaster host is configured
+func (l *Linker) atTags(canonicalURI string, authorDID string) []MetaTag {
+ tags := make([]MetaTag, 0, 3)
+ if strings.HasPrefix(canonicalURI, "at://") {
+ tags = append(tags, MetaTag{Type: "name", Key: "at:canonical", Content: canonicalURI})
+ }
+ if authorDID != "" {
+ tags = append(tags, MetaTag{Type: "name", Key: "at:author", Content: "at://" + authorDID})
+ }
+ if l.cli != nil && l.cli.BroadcasterHost != "" {
+ tags = append(tags, MetaTag{Type: "name", Key: "at:me", Content: "at://did:web:" + l.cli.BroadcasterHost})
+ }
+ return tags
+}
+
// fetch branding assets for a given broadcaster DID
func (l *Linker) getBrandingAssets(broadcasterDid string) ([]placestream.BrandingGetBranding_BrandingAsset, error) {
ret := make([]placestream.BrandingGetBranding_BrandingAsset, 0)
@@ -169,6 +195,10 @@ func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *plac
Content: fmt.Sprintf("%s%s", titleStr, brandingTitle),
})
+ // at-tags: this page canonically maps to the livestream record, authored
+ // by the streamer
+ metaTags = append(metaTags, l.atTags(lsv.Uri, lsv.Author.Did)...)
+
return l.GenerateHTML(ctx, &PageConfig{
Title: fmt.Sprintf("%s%s", titleStr, brandingTitle),
Metas: metaTags,
@@ -269,6 +299,10 @@ func (l *Linker) GenerateVideoCard(ctx context.Context, u *url.URL, vv *placestr
MetaTag{Type: "name", Key: "twitter:title", Content: title},
)
+ // at-tags: this page canonically maps to the place.stream.video record,
+ // authored by the streamer
+ metaTags = append(metaTags, l.atTags(vv.Uri, authorDid)...)
+
return l.GenerateHTML(ctx, &PageConfig{
Title: title,
Metas: metaTags,
@@ -343,6 +377,10 @@ func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL, sentryDSN
Content: brandingTitle,
})
+ // at-tags: the site itself is identified by this node's did:web; there's
+ // no single author or canonical record for the front page
+ metaTags = append(metaTags, l.atTags("", "")...)
+
return l.GenerateHTML(ctx, &PageConfig{
Title: brandingTitle,
Metas: metaTags,
diff --git a/pkg/linking/linking_test.go b/pkg/linking/linking_test.go
index 0e8970aa..99edb658 100644
--- a/pkg/linking/linking_test.go
+++ b/pkg/linking/linking_test.go
@@ -13,6 +13,7 @@ import (
"stream.place/streamplace/js/app"
"stream.place/streamplace/pkg/appbsky"
"stream.place/streamplace/pkg/comatproto"
+ "stream.place/streamplace/pkg/config"
"stream.place/streamplace/pkg/placestream"
)
@@ -69,6 +70,14 @@ func TestGenerateLinkCard(t *testing.T) {
require.True(t, strings.Contains(linkStr, "iame.li"))
require.True(t, strings.Contains(linkStr, ls.Title), "should contain the livestream title")
require.True(t, strings.Count(linkStr, "") == 1, "should have exactly one title tag")
+
+ // at-tags (https://tangled.org/chrisshank.com/at-tags)
+ require.Contains(t, linkStr, ``,
+ "should map the page to the canonical livestream record")
+ require.Contains(t, linkStr, ``,
+ "should identify the streamer as the page author")
+ require.NotContains(t, linkStr, `at:me`,
+ "at:me should be omitted when the linker has no CLI/broadcaster host")
}
func TestGenerateVideoCard(t *testing.T) {
@@ -113,4 +122,27 @@ func TestGenerateVideoCard(t *testing.T) {
"https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:2zmxikig2sj7gqaezl5gntae/"+thumbCID+"@jpeg"),
"og:image should be the video thumbnail served via the bsky CDN")
require.True(t, strings.Count(linkStr, "") == 1, "should have exactly one title tag")
+
+ // at-tags (https://tangled.org/chrisshank.com/at-tags)
+ require.Contains(t, linkStr, ``,
+ "should map the page to the canonical place.stream.video record")
+ require.Contains(t, linkStr, ``,
+ "should identify the streamer as the page author")
+}
+
+func TestGenerateDefaultCardAtMe(t *testing.T) {
+ index := IndexHTML(t)
+ linker, err := NewLinker(context.Background(), index, nil, &config.CLI{BroadcasterHost: "stream.place"})
+ require.NoError(t, err)
+ require.NotNil(t, linker)
+
+ u, err := url.Parse("https://stream.place/")
+ require.NoError(t, err)
+ linkCard, err := linker.GenerateDefaultCard(context.Background(), u, "")
+ require.NoError(t, err)
+ linkStr := string(linkCard)
+ require.Contains(t, linkStr, ``,
+ "should identify the node via its did:web")
+ require.NotContains(t, linkStr, "at:canonical", "front page has no canonical record")
+ require.NotContains(t, linkStr, "at:author", "front page has no single author")
}