diff --git a/bskyweb/cmd/bskyweb/jsonld.go b/bskyweb/cmd/bskyweb/jsonld.go index 18c7e80a48..8d06b71262 100644 --- a/bskyweb/cmd/bskyweb/jsonld.go +++ b/bskyweb/cmd/bskyweb/jsonld.go @@ -183,7 +183,8 @@ func imageThumbs(images []*appbsky.EmbedImages_ViewImage) []string { // galleryThumbs returns the thumbnail URLs of image items in a gallery // embed, or nil if empty. Items_Elem is a union; non-image variants and // nil entries are skipped so future gallery item types don't break SEO -// extraction. +// extraction. Empty Thumbnail strings are also skipped to avoid emitting +// if the appview ever returns one. func galleryThumbs(items []*appbsky.EmbedGallery_View_Items_Elem) []string { if len(items) == 0 { return nil @@ -193,6 +194,9 @@ func galleryThumbs(items []*appbsky.EmbedGallery_View_Items_Elem) []string { if item == nil || item.EmbedGallery_ViewImage == nil { continue } + if item.EmbedGallery_ViewImage.Thumbnail == "" { + continue + } urls = append(urls, item.EmbedGallery_ViewImage.Thumbnail) } if len(urls) == 0 { diff --git a/bskyweb/cmd/bskyweb/jsonld_test.go b/bskyweb/cmd/bskyweb/jsonld_test.go index 46d2bba022..5c7f1c2bb7 100644 --- a/bskyweb/cmd/bskyweb/jsonld_test.go +++ b/bskyweb/cmd/bskyweb/jsonld_test.go @@ -402,9 +402,10 @@ func TestBuildPostJSONLD_GalleryInRecordWithMedia(t *testing.T) { } } -// Forward-compat: nil items and unknown-variant union elements must be -// skipped, not panic. Unknown variants are dropped silently so older -// deploys keep working when new gallery item types ship. +// Forward-compat: nil items, unknown-variant union elements, and empty +// Thumbnail strings must be skipped, not panic or leak as . Unknown variants are dropped silently +// so older deploys keep working when new gallery item types ship. func TestExtractPostMedia_GallerySkipsUnknownItems(t *testing.T) { thumb := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g@jpeg" pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "gallery") @@ -413,6 +414,7 @@ func TestExtractPostMedia_GallerySkipsUnknownItems(t *testing.T) { Items: []*appbsky.EmbedGallery_View_Items_Elem{ nil, {}, // empty union, no variant set + {EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{Thumbnail: ""}}, // empty Thumbnail {EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{Thumbnail: thumb}}, }, }, @@ -495,6 +497,25 @@ func TestBuildPostJSONLD_HiddenEmbed(t *testing.T) { } } +// Symmetric guard for the gallery extraction path. Functionally redundant +// with the early-return at the top of extractPostMedia, but exists so the +// hide-embed contract is asserted directly against the gallery branch - +// catches anyone who later moves the embedHidden check inside an +// embed-shape branch. +func TestBuildPostJSONLD_HiddenEmbed_Gallery(t *testing.T) { + thumb := "https://cdn.bsky.app/img/g@jpeg" + pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "nsfw", + withGallery(thumb), withSelfLabel("porn")) + out, _ := buildPostJSONLD(pv, nil, "u", hideEmbedLabels, hideReplyLabels) + main := unmarshalLD(t, out)["mainEntity"].(map[string]any) + if _, present := main["image"]; present { + t.Errorf("hidden-embed gallery post should not emit image") + } + if _, present := main["thumbnailUrl"]; present { + t.Errorf("hidden-embed gallery post should not emit thumbnailUrl") + } +} + func TestBuildPostJSONLD_TextEscaping(t *testing.T) { // Includes ", \, newline, , and a unicode char. tricky := "hello \"world\" \\ <\\>\n 🎉"