mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Previously, content that was already HTML-escaped — topic `fancy_title`s on the 404/oops pages, GitHub onebox labels, and topic/group/category `<meta>` descriptions — got escaped a second time, so readers (and crawlers) saw literal entities like `’` and `&` instead of `'` and `&`. This change escapes every value exactly once, by converting it to plain text before it reaches the renderer instead of flagging anything `html_safe`: - `Emoji.codes_to_img` escapes its text segments with `html_escape_once` so the already-escaped `fancy_title` and sanitized GitHub labels aren't escaped again. All of its callers render into element content, and the attribute escaping inside `emoji_img_tag` is untouched. - the new `ExcerptParser.to_plain_text` converts a stored excerpt (escaped text, the `…` truncation marker, and the hashtag placeholder markup the parser keeps) back to plain text, exposed as `Topic#plain_text_excerpt`. It's a tag-strip plus a memoized `htmlentities` decode — pure Ruby, a few microseconds per excerpt, no Nokogiri in the hot path. - `Category#plain_text_description` caches the pre-escape plain text that `description_text` previously discarded, and the category meta descriptions use it directly. This also fixes the double-escaped `og:description`/`twitter:description` on category pages, where `gsub_emoji_to_unicode` silently drops the `html_safe` flag of `description_text`. - the new `Group#bio_summary` mirrors `UserProfile#bio_summary`: a plain-text excerpt of the bio with links and images stripped and hashtags rendered as plain `#slug`. Marking the stored values `html_safe` would have worked for the column data — every writer of `topics.excerpt` escapes — but `topic.excerpt` can be swapped in-memory with a localized excerpt that isn't guaranteed to be escaped, so treating everything as unsafe plain text is both simpler and safer.
203 lines
9.6 KiB
Text
Vendored
203 lines
9.6 KiB
Text
Vendored
<% if @topic_view %>
|
|
<div id="topic-title">
|
|
<h1>
|
|
<%= render_topic_title(@topic_view.topic) %>
|
|
</h1>
|
|
|
|
<% if @breadcrumbs.present? %>
|
|
<div class="topic-category" itemscope itemtype="http://schema.org/BreadcrumbList">
|
|
<% @breadcrumbs.each_with_index do |c, i| %>
|
|
<span itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
|
|
<a href="<%= c[:url] %>" class="badge-wrapper bullet" itemprop="item">
|
|
<span class='badge-category-bg' style='background-color: #<%= c[:color] %>'></span>
|
|
<span class='badge-category clear-badge'>
|
|
<span class='category-name' itemprop='name'><%= c[:name] %></span>
|
|
</span>
|
|
</a>
|
|
<meta itemprop="position" content="<%= i + 1 %>" />
|
|
</span>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<% if @tags.present? %>
|
|
<div class="topic-category">
|
|
<div class='discourse-tags list-tags'>
|
|
<% @tags.each_with_index do |tag, i| %>
|
|
<a href='<%= "#{Discourse.base_url}/tag/#{tag.name}" %>' class='discourse-tag' rel="tag"><%= tag.name -%></a><% if i < @tags.size - 1 %>, <% end %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<%= server_plugin_outlet "topic_header" %>
|
|
|
|
<%- if include_crawler_content? %>
|
|
<% main_entity_schema = crawler_topic_main_entity_schema(@topic_view.topic) %>
|
|
<div <%= crawler_topic_container_schema(@topic_view.topic) %>>
|
|
<meta itemprop='<%= main_entity_schema ? "name" : "headline" %>' content='<%= @topic_view.title %>'>
|
|
<link itemprop='url' href='<%= @topic_view.absolute_url %>'>
|
|
<meta itemprop='datePublished' content='<%= @topic_view.topic.created_at.to_formatted_s(:iso8601) %>'>
|
|
<meta itemprop='keywords' content='<%= @tags.map(&:name).join(', ') %>'>
|
|
<% unless main_entity_schema %>
|
|
<% if @topic_view.topic.category.present? %>
|
|
<meta itemprop='articleSection' content='<%= @topic_view.topic.category.name %>'>
|
|
<% end %>
|
|
<div itemprop='publisher' itemscope itemtype="http://schema.org/Organization">
|
|
<meta itemprop='name' content='<%= SiteSetting.company_name.presence || SiteSetting.title %>'>
|
|
<% if application_logo_url.present? %>
|
|
<div itemprop='logo' itemscope itemtype="http://schema.org/ImageObject">
|
|
<meta itemprop='url' content='<%= application_logo_url %>'>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<% if main_entity_schema %>
|
|
<div <%= main_entity_schema %>>
|
|
<%= build_plugin_html 'server:topic-main-entity-meta-crawler' %>
|
|
<% end %>
|
|
|
|
<% if @topic_view.crawler_posts&.first && !@topic_view.crawler_posts.first.is_first_post? %>
|
|
<% if @topic_view.topic.user.present? %>
|
|
<span itemprop='author' itemscope itemtype="http://schema.org/Person">
|
|
<meta itemprop='name' content='<%= @topic_view.topic.user.username %>'>
|
|
<link itemprop='url' rel='nofollow' href='<%= Discourse.base_url %>/u/<%= @topic_view.topic.user.username %>'>
|
|
</span>
|
|
<% end %>
|
|
<meta itemprop='text' content='<%= @topic_view.topic.plain_text_excerpt %>'>
|
|
<% end %>
|
|
|
|
<% @topic_view.crawler_posts.each do |post| %>
|
|
<% next if crawler_post_schema_skip?(post, @topic_view.topic) %>
|
|
<% if (u = post.user) && !post.hidden && post.cooked && !post.cooked.strip.empty? %>
|
|
<% emit_md = crawler_post_emits_microdata?(post, @topic_view.topic) %>
|
|
<div id='post_<%= post.post_number %>' <%= crawler_post_schema(post, @topic_view.topic) %> class='topic-body crawler-post'>
|
|
<div class='crawler-post-meta'>
|
|
<span class="creator" <%= tag.attributes(itemprop: "author", itemscope: true, itemtype: "http://schema.org/Person") if emit_md %>>
|
|
<a rel='nofollow' href='<%= Discourse.base_url %>/u/<%= u.username %>'><span <%= tag.attributes(itemprop: "name") if emit_md %>><%= u.username %></span></a>
|
|
<% if emit_md %>
|
|
<meta itemprop='url' content='<%= Discourse.base_url %>/u/<%= u.username %>'>
|
|
<% end %>
|
|
<%= "(#{u.name})" if (SiteSetting.display_name_on_posts && SiteSetting.enable_names? && !u.name.blank?) %>
|
|
<%
|
|
post_custom_fields = @topic_view.post_custom_fields[post.id] || {}
|
|
who_username = post_custom_fields["action_code_who"] || ""
|
|
small_action_href = post_custom_fields["action_code_path"] || ""
|
|
if post.action_code
|
|
%>
|
|
<%= t("js.action_codes.#{post.action_code}", when: "", who: who_username, href: small_action_href).html_safe %>
|
|
<% end %>
|
|
</span>
|
|
|
|
<% if post.is_first_post? && !main_entity_schema %>
|
|
<link itemprop="mainEntityOfPage" href="<%= post.topic.url %>">
|
|
<% end %>
|
|
|
|
<% if post.image_url && emit_md %>
|
|
<link itemprop="image" href="<%= post.image_url %>">
|
|
<% end %>
|
|
|
|
<span class="crawler-post-infos">
|
|
<time <%= tag.attributes(itemprop: "datePublished") if emit_md && !post.is_first_post? %> datetime='<%= post.created_at.to_formatted_s(:iso8601) %>' class='post-time'>
|
|
<%= l post.created_at, format: :long %>
|
|
</time>
|
|
<% if emit_md %>
|
|
<% modified_at = post.version > 1 ? post.last_version_at : post.created_at %>
|
|
<meta itemprop='dateModified' content='<%= modified_at.to_formatted_s(:iso8601) %>'>
|
|
<% end %>
|
|
<span <%= tag.attributes(itemprop: "position") if emit_md %>><%= post.post_number %></span>
|
|
</span>
|
|
</div>
|
|
<div class='post' <%= tag.attributes(itemprop: "text") if emit_md %>>
|
|
<%= post.hidden ? t('flagging.user_must_edit').html_safe : post.cooked.html_safe %>
|
|
</div>
|
|
|
|
<% unless crawler_post_schema_overridden?(post, @topic_view.topic) %>
|
|
<div <%= tag.attributes(itemprop: "interactionStatistic", itemscope: true, itemtype: "http://schema.org/InteractionCounter") if emit_md %>>
|
|
<% if emit_md %>
|
|
<meta itemprop="interactionType" content="http://schema.org/LikeAction"/>
|
|
<meta itemprop="userInteractionCount" content="<%= post.like_count %>" />
|
|
<% end %>
|
|
<span class='post-likes'><%= post.like_count > 0 ? t('post.has_likes', count: post.like_count) : '' %></span>
|
|
</div>
|
|
<% end %>
|
|
|
|
<% if (linkbacks = @topic_view.linkbacks_for(post)).present? %>
|
|
<div class='crawler-linkback-list'>
|
|
<% linkbacks.each do |link| %>
|
|
<div>
|
|
<a href="<%=link[:url]%>"><%=link[:title]%></a>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%= build_plugin_html "server:topic-show-crawler-post-end", post: post %>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
|
|
<% if main_entity_schema %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<% if @topic_view.single_post_request? || @topic_view.prev_page || @topic_view.next_page %>
|
|
<div role='navigation' itemscope itemtype='http://schema.org/SiteNavigationElement' class="topic-body crawler-post">
|
|
<% if @topic_view.single_post_request? %>
|
|
<span itemprop='name'>
|
|
<%= link_to t(:show_post_in_topic), "#{@topic_view.current_page_path}#post_#{@topic_view.crawler_posts.first&.post_number}", itemprop: 'url' %>
|
|
</span>
|
|
<% else %>
|
|
<% if @topic_view.prev_page %>
|
|
<span itemprop='name'><%= link_to t(:prev_page), @topic_view.prev_page_path, rel: 'prev', itemprop: 'url' %></span>
|
|
<% end %>
|
|
<% if @topic_view.next_page %>
|
|
<span itemprop='name'><b><%= link_to t(:next_page), @topic_view.next_page_path, rel: 'next', itemprop: 'url' %></b></span>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%= build_plugin_html 'server:topic-show-after-posts-crawler' %>
|
|
<% else %>
|
|
<%= t(:crawler_content_hidden) %>
|
|
<% end %>
|
|
|
|
<% content_for :head do %>
|
|
<%= auto_discovery_link_tag(@topic_view, {action: :feed, slug: @topic_view.topic.slug, topic_id: @topic_view.topic.id}, rel: 'alternate nofollow', title: t('rss_posts_in_topic', topic: @topic_view.title), type: 'application/rss+xml') %>
|
|
<%= raw crawlable_meta_data(
|
|
title: @topic_view.title,
|
|
description: @topic_view.summary(strip_images: true),
|
|
image: @topic_view.image_url,
|
|
image_width: @topic_view.image_width,
|
|
image_height: @topic_view.image_height,
|
|
image_type: @topic_view.image_type,
|
|
read_time: @topic_view.read_time,
|
|
like_count: @topic_view.like_count,
|
|
ignore_canonical: true,
|
|
published_time: @topic_view.published_time,
|
|
breadcrumbs: @breadcrumbs,
|
|
tags: @tags.map(&:name),
|
|
) %>
|
|
|
|
<% if !@topic_view.single_post_request? && (@topic_view.prev_page || @topic_view.next_page) %>
|
|
<% if @topic_view.prev_page %>
|
|
<link rel="prev" href="<%= @topic_view.prev_page_path -%>">
|
|
<% end %>
|
|
<% if @topic_view.next_page %>
|
|
<link rel="next" href="<%= @topic_view.next_page_path -%>">
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|
|
|
|
<% content_for(:title) { @title || "#{gsub_emoji_to_unicode(@topic_view.page_title)} - #{SiteSetting.title}" } %>
|
|
|
|
<% if @topic_view.print %>
|
|
<% content_for :after_body do %>
|
|
<%= preload_script('js/print-page') %>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|