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 🎉"