Skip to content

Commit 58d298c

Browse files
authored
FIX: remove ItemList schema from linkbacks in crawler view (#36608)
Linkbacks (reflection links showing "topics that link here") were incorrectly using schema.org/ItemList markup. This caused Google Rich Results Test errors ("Multiple ListItem elements defined on page") when combined with plugins like discourse-ai that add their own ItemList for related topics. ItemList is intended for curated/ranked lists (e.g., "Related Topics"). Linkbacks are automatic citations, not recommendations, so plain links are semantically correct. Also extracts `linkbacks_for(post)` helper to TopicView for cleaner template code. Internal ref - t/170560
1 parent f363ae5 commit 58d298c

File tree

5 files changed

+29
-33
lines changed

5 files changed

+29
-33
lines changed

app/views/topics/show.html.erb

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,15 @@
108108
<span class='post-likes'><%= post.like_count > 0 ? t('post.has_likes', count: post.like_count) : '' %></span>
109109
</div>
110110

111-
<% if @topic_view.link_counts[post.id] && @topic_view.link_counts[post.id].filter { |l| l[:reflection] }.length > 0 %>
112-
<div class='crawler-linkback-list' itemscope itemtype='http://schema.org/ItemList'>
113-
<% @topic_view.link_counts[post.id].each_with_index do |link, i| %>
114-
<% if link[:reflection] && link[:title].present? %>
115-
<div itemprop='itemListElement' itemscope itemtype='http://schema.org/ListItem'>
116-
<a itemprop='url' href="<%=link[:url]%>"><%=link[:title]%></a>
117-
<meta itemprop='position' content='<%= i+1 %>'>
118-
</div>
119-
<% end %>
111+
<% if (linkbacks = @topic_view.linkbacks_for(post)).present? %>
112+
<div class='crawler-linkback-list'>
113+
<% linkbacks.each do |link| %>
114+
<div>
115+
<a href="<%=link[:url]%>"><%=link[:title]%></a>
116+
</div>
120117
<% end %>
121118
</div>
122-
<% end %>
119+
<% end %>
123120

124121
<%= build_plugin_html "server:topic-show-crawler-post-end", post: post %>
125122
</div>

lib/topic_view.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,10 @@ def link_counts
714714
)
715715
end
716716

717+
def linkbacks_for(post)
718+
link_counts[post.id]&.select { |l| l[:reflection] && l[:title].present? }
719+
end
720+
717721
def pm_params
718722
@pm_params ||= TopicQuery.new(@user).get_pm_params(topic)
719723
end

spec/fixtures/onebox/discourse_topic.response

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ And that too in just over an year, way to go! [boom]">
280280
<meta itemprop="userInteractionCount" content="0" />
281281
</div>
282282

283-
<div class='crawler-linkback-list' itemscope itemtype='http://schema.org/ItemList'>
283+
<div class='crawler-linkback-list'>
284284
</div>
285285

286286
</div>

spec/fixtures/onebox/discourse_topic_reply.response

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ And that too in just over an year, way to go! [boom]">
272272
<meta itemprop="userInteractionCount" content="0" />
273273
</div>
274274

275-
<div class='crawler-linkback-list' itemscope itemtype='http://schema.org/ItemList'>
275+
<div class='crawler-linkback-list'>
276276
</div>
277277

278278
</div>
Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,31 @@
11
# frozen_string_literal: true
22

3-
require "ostruct"
4-
53
RSpec.describe "topics/show.html.erb" do
6-
fab!(:category)
7-
fab!(:topic) { Fabricate(:topic, category: category) }
4+
fab!(:topic) { Fabricate(:topic, category: Fabricate(:category)) }
85

9-
it "add nofollow to RSS alternate link for topic" do
6+
it "adds nofollow to RSS alternate link" do
107
topic_view = OpenStruct.new(topic: topic, posts: [], crawler_posts: [])
118
topic_view.stubs(:summary).returns("")
129
view.stubs(:crawler_layout?).returns(false)
13-
view.stubs(:url_for).returns("https://www.example.com/test.rss")
10+
view.stubs(:url_for).returns("https://example.com/test.rss")
1411
view.instance_variable_set("@topic_view", topic_view)
1512
assign(:tags, [])
1613

1714
render template: "topics/show", formats: [:html]
1815

1916
expect(view.content_for(:head)).to match(
20-
%r{<link rel="alternate nofollow" type="application/rss\+xml" title="[^"]+" href="https://www.example.com/test\.rss" />},
17+
%r{<link rel="alternate nofollow" type="application/rss\+xml"},
2118
)
2219
end
2320

24-
it "adds structured data" do
21+
it "renders linkbacks as plain links without ItemList schema" do
2522
view.stubs(:include_crawler_content?).returns(true)
2623
post = Fabricate(:post, topic: topic)
2724
TopicLink.create!(
2825
topic_id: post.topic_id,
2926
post_id: post.id,
3027
user_id: post.user_id,
31-
url: "https://example.com/",
28+
url: "https://example.com/linked",
3229
domain: "example.com",
3330
link_topic_id: Fabricate(:topic).id,
3431
reflection: true,
@@ -38,27 +35,25 @@
3835

3936
render template: "topics/show", formats: [:html]
4037

41-
links_list = Nokogiri::HTML5.fragment(rendered).css(".crawler-linkback-list")
42-
first_item = links_list.css('[itemprop="itemListElement"]')
43-
expect(first_item.css('[itemprop="position"]')[0]["content"]).to eq("1")
44-
expect(first_item.css('[itemprop="url"]')[0]["href"]).to eq("https://example.com/")
38+
doc = Nokogiri::HTML5.fragment(rendered)
39+
linkbacks = doc.css(".crawler-linkback-list")
40+
expect(linkbacks.css("a[href='https://example.com/linked']")).to be_present
41+
expect(linkbacks.css('[itemtype*="ItemList"]')).to be_empty
4542
end
4643

47-
it "uses comment scheme type for replies" do
44+
it "uses DiscussionForumPosting with Comment schema for replies" do
4845
view.stubs(:crawler_layout?).returns(true)
4946
view.stubs(:include_crawler_content?).returns(true)
50-
Fabricate(:post, topic: topic)
51-
Fabricate(:post, topic: topic)
52-
Fabricate(:post, topic: topic)
47+
3.times { Fabricate(:post, topic: topic) }
5348
assign(:topic_view, TopicView.new(topic))
5449
assign(:tags, [])
5550

5651
render template: "topics/show", formats: [:html]
5752

5853
doc = Nokogiri::HTML5.fragment(rendered)
59-
topic_schema = doc.css('[itemtype="http://schema.org/DiscussionForumPosting"]')
60-
expect(topic_schema.size).to eq(1)
61-
expect(topic_schema.css('[itemtype="http://schema.org/Comment"]').size).to eq(2)
62-
expect(topic_schema.css('[itemprop="articleSection"]')[0]["content"]).to eq(topic.category.name)
54+
posting = doc.css('[itemtype*="DiscussionForumPosting"]')
55+
expect(posting.size).to eq(1)
56+
expect(posting.css('[itemtype*="Comment"]').size).to eq(2) # replies only, not OP
57+
expect(posting.css('[itemprop="articleSection"]').first["content"]).to eq(topic.category.name)
6358
end
6459
end

0 commit comments

Comments
 (0)