fix discussionForumPosting.comment field type
This commit is contained in:
@@ -9,15 +9,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// schema.org structured-data types emitted on post and profile pages.
|
// schema.org structured-data types emitted on post and profile pages.
|
||||||
//
|
// Building these as Go structs (rather than inline JSON in templates) gives
|
||||||
// These mirror the shapes recommended by Google Search for
|
// us correct JSON escaping via encoding/json and a single place to test the
|
||||||
// DiscussionForumPosting (post pages) and ProfilePage (profile pages). The
|
// schema. Optional fields use omitempty so empty values don't reach Google's
|
||||||
// goal of building these in Go (rather than writing JSON inline in Pongo2
|
// validator as null/empty strings.
|
||||||
// templates) is to get correct JSON escaping for free via encoding/json, and
|
|
||||||
// to keep the schema in one place that is easy to unit-test.
|
|
||||||
//
|
|
||||||
// All fields use omitempty aggressively so optional fields don't emit empty
|
|
||||||
// strings or null values that would otherwise confuse Google's validator.
|
|
||||||
|
|
||||||
const schemaOrgContext = "https://schema.org"
|
const schemaOrgContext = "https://schema.org"
|
||||||
|
|
||||||
@@ -45,27 +40,35 @@ type sharedContent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type discussionForumPosting struct {
|
type discussionForumPosting struct {
|
||||||
Context string `json:"@context,omitempty"`
|
Context string `json:"@context,omitempty"`
|
||||||
Type string `json:"@type"`
|
Type string `json:"@type"`
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
Identifier string `json:"identifier,omitempty"`
|
Identifier string `json:"identifier,omitempty"`
|
||||||
Author *personOrOrg `json:"author,omitempty"`
|
Author *personOrOrg `json:"author,omitempty"`
|
||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
Image []string `json:"image,omitempty"`
|
Image []string `json:"image,omitempty"`
|
||||||
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
||||||
DatePublished string `json:"datePublished,omitempty"`
|
DatePublished string `json:"datePublished,omitempty"`
|
||||||
InteractionStat []interactionStat `json:"interactionStatistic,omitempty"`
|
InteractionStat []interactionStat `json:"interactionStatistic,omitempty"`
|
||||||
CommentCount *int64 `json:"commentCount,omitempty"`
|
CommentCount *int64 `json:"commentCount,omitempty"`
|
||||||
Comment []discussionForumPosting `json:"comment,omitempty"`
|
Comment []comment `json:"comment,omitempty"`
|
||||||
IsBasedOn string `json:"isBasedOn,omitempty"`
|
IsBasedOn string `json:"isBasedOn,omitempty"`
|
||||||
SharedContent *sharedContent `json:"sharedContent,omitempty"`
|
SharedContent *sharedContent `json:"sharedContent,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replies reuse discussionForumPosting via buildReplyNode, which produces a
|
// comment is the schema.org Comment shape used inside
|
||||||
// "shallow" form: it includes author, text, datePublished, url, identifier,
|
// DiscussionForumPosting.comment[]. The comment property does not accept
|
||||||
// and media (image, thumbnailUrl) but does NOT recurse into nested
|
// DiscussionForumPosting, so replies map to Comment.
|
||||||
// comment[], isBasedOn, or sharedContent. This bounds the size of the
|
type comment struct {
|
||||||
// emitted JSON-LD on posts with deep reply trees.
|
Type string `json:"@type"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Identifier string `json:"identifier,omitempty"`
|
||||||
|
Author *personOrOrg `json:"author,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Image []string `json:"image,omitempty"`
|
||||||
|
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
||||||
|
DatePublished string `json:"datePublished,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type webPage struct {
|
type webPage struct {
|
||||||
Context string `json:"@context"`
|
Context string `json:"@context"`
|
||||||
@@ -346,7 +349,7 @@ func buildPostNode(pv *appbsky.FeedDefs_PostView, replies []*appbsky.FeedDefs_Th
|
|||||||
}
|
}
|
||||||
reply := buildReplyNode(r.FeedDefs_ThreadViewPost.Post, hideLabels)
|
reply := buildReplyNode(r.FeedDefs_ThreadViewPost.Post, hideLabels)
|
||||||
if reply.Type == "" {
|
if reply.Type == "" {
|
||||||
// nil-Author guard tripped; skip rather than emit a malformed entry.
|
// nil-Author guard tripped; skip.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
node.Comment = append(node.Comment, reply)
|
node.Comment = append(node.Comment, reply)
|
||||||
@@ -355,14 +358,11 @@ func buildPostNode(pv *appbsky.FeedDefs_PostView, replies []*appbsky.FeedDefs_Th
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildReplyNode builds a "shallow" DiscussionForumPosting for a reply
|
// buildReplyNode builds a schema.org Comment for a reply. Returns the zero
|
||||||
// comment: includes media but skips nested comment[], isBasedOn, and
|
// value if pv or pv.Author is nil; callers should treat that as "skip".
|
||||||
// sharedContent (per project decision to bound payload size). Returns the
|
func buildReplyNode(pv *appbsky.FeedDefs_PostView, hideLabels map[string]bool) comment {
|
||||||
// zero value if pv or pv.Author is nil — callers should treat that as
|
|
||||||
// "skip this entry".
|
|
||||||
func buildReplyNode(pv *appbsky.FeedDefs_PostView, hideLabels map[string]bool) discussionForumPosting {
|
|
||||||
if pv == nil || pv.Author == nil {
|
if pv == nil || pv.Author == nil {
|
||||||
return discussionForumPosting{}
|
return comment{}
|
||||||
}
|
}
|
||||||
embedHidden := postEmbedHidden(pv, hideLabels)
|
embedHidden := postEmbedHidden(pv, hideLabels)
|
||||||
images := extractPostMedia(pv, embedHidden)
|
images := extractPostMedia(pv, embedHidden)
|
||||||
@@ -370,9 +370,8 @@ func buildReplyNode(pv *appbsky.FeedDefs_PostView, hideLabels map[string]bool) d
|
|||||||
if len(images) > 0 {
|
if len(images) > 0 {
|
||||||
thumb = images[0]
|
thumb = images[0]
|
||||||
}
|
}
|
||||||
|
return comment{
|
||||||
node := discussionForumPosting{
|
Type: "Comment",
|
||||||
Type: "DiscussionForumPosting",
|
|
||||||
URL: bskyPostURLFromATURI(pv.Author.Handle, pv.Uri),
|
URL: bskyPostURLFromATURI(pv.Author.Handle, pv.Uri),
|
||||||
Identifier: pv.Uri,
|
Identifier: pv.Uri,
|
||||||
Author: buildAuthor(pv.Author),
|
Author: buildAuthor(pv.Author),
|
||||||
@@ -381,7 +380,6 @@ func buildReplyNode(pv *appbsky.FeedDefs_PostView, hideLabels map[string]bool) d
|
|||||||
ThumbnailURL: thumb,
|
ThumbnailURL: thumb,
|
||||||
DatePublished: pv.IndexedAt,
|
DatePublished: pv.IndexedAt,
|
||||||
}
|
}
|
||||||
return node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildPostJSONLD marshals the top-level WebPage envelope for a post page.
|
// buildPostJSONLD marshals the top-level WebPage envelope for a post page.
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ func TestBuildPostJSONLD_TextEscaping(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildPostJSONLD_Replies(t *testing.T) {
|
func TestBuildPostJSONLD_Comments(t *testing.T) {
|
||||||
pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "main")
|
pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "main")
|
||||||
*pv.ReplyCount = 14
|
*pv.ReplyCount = 14
|
||||||
|
|
||||||
@@ -361,9 +361,14 @@ func TestBuildPostJSONLD_Replies(t *testing.T) {
|
|||||||
if first["identifier"] != "at://did:plc:rep00/app.bsky.feed.post/reply00" {
|
if first["identifier"] != "at://did:plc:rep00/app.bsky.feed.post/reply00" {
|
||||||
t.Errorf("first comment should be reply00, got %v", first["identifier"])
|
t.Errorf("first comment should be reply00, got %v", first["identifier"])
|
||||||
}
|
}
|
||||||
// Each comment must NOT have nested comment[]/isBasedOn/sharedContent.
|
// Each comment must be schema.org Comment (not DiscussionForumPosting —
|
||||||
|
// that type is invalid for the comment property) and must NOT have
|
||||||
|
// nested comment[] / isBasedOn / sharedContent.
|
||||||
for i, c := range comments {
|
for i, c := range comments {
|
||||||
cm := c.(map[string]any)
|
cm := c.(map[string]any)
|
||||||
|
if cm["@type"] != "Comment" {
|
||||||
|
t.Errorf("reply %d wrong type: got %v, want Comment", i, cm["@type"])
|
||||||
|
}
|
||||||
if _, present := cm["comment"]; present {
|
if _, present := cm["comment"]; present {
|
||||||
t.Errorf("reply %d should not have nested comment[]", i)
|
t.Errorf("reply %d should not have nested comment[]", i)
|
||||||
}
|
}
|
||||||
@@ -373,9 +378,6 @@ func TestBuildPostJSONLD_Replies(t *testing.T) {
|
|||||||
if _, present := cm["sharedContent"]; present {
|
if _, present := cm["sharedContent"]; present {
|
||||||
t.Errorf("reply %d should not have sharedContent", i)
|
t.Errorf("reply %d should not have sharedContent", i)
|
||||||
}
|
}
|
||||||
if cm["@type"] != "DiscussionForumPosting" {
|
|
||||||
t.Errorf("reply %d wrong type: %v", i, cm["@type"])
|
|
||||||
}
|
|
||||||
if cm["url"] == nil || cm["identifier"] == nil {
|
if cm["url"] == nil || cm["identifier"] == nil {
|
||||||
t.Errorf("reply %d missing url/identifier", i)
|
t.Errorf("reply %d missing url/identifier", i)
|
||||||
}
|
}
|
||||||
@@ -486,6 +488,28 @@ func TestBuildPostJSONLD_NilAuthorReply(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildPostJSONLD_CommentMedia(t *testing.T) {
|
||||||
|
// Comments with media embeds should expose image[] and thumbnailUrl.
|
||||||
|
pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "main")
|
||||||
|
*pv.ReplyCount = 1
|
||||||
|
thumb := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:bob/x@jpg"
|
||||||
|
reply := makePostView("bob.bsky.social", "did:plc:bob", "rep1", "with image",
|
||||||
|
withImages(thumb))
|
||||||
|
replies := []*appbsky.FeedDefs_ThreadViewPost_Replies_Elem{
|
||||||
|
{FeedDefs_ThreadViewPost: &appbsky.FeedDefs_ThreadViewPost{Post: reply}},
|
||||||
|
}
|
||||||
|
out, _ := buildPostJSONLD(pv, replies, "u", hideEmbedLabels)
|
||||||
|
main := unmarshalLD(t, out)["mainEntity"].(map[string]any)
|
||||||
|
c := main["comment"].([]any)[0].(map[string]any)
|
||||||
|
imgs, ok := c["image"].([]any)
|
||||||
|
if !ok || len(imgs) != 1 || imgs[0] != thumb {
|
||||||
|
t.Errorf("comment image[] wrong: %v", c["image"])
|
||||||
|
}
|
||||||
|
if c["thumbnailUrl"] != thumb {
|
||||||
|
t.Errorf("comment thumbnailUrl wrong: %v", c["thumbnailUrl"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildProfileJSONLD_Basic(t *testing.T) {
|
func TestBuildProfileJSONLD_Basic(t *testing.T) {
|
||||||
pv := &appbsky.ActorDefs_ProfileViewDetailed{
|
pv := &appbsky.ActorDefs_ProfileViewDetailed{
|
||||||
Did: "did:plc:alice",
|
Did: "did:plc:alice",
|
||||||
|
|||||||
Reference in New Issue
Block a user