0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/spec/lib/topic_og_image_generator_spec.rb
Penar Musaraj 2d895de297
FEATURE: generated OpenGraph images for topics (#39194)
Adds the ability to automatically generate Open Graph images for topics
that don't have their own image. When enabled via the
generate_topic_og_image site setting, a background job creates a branded
1200×630 PNG for each new topic OP.

Key changes:
- TopicOgImageGenerator builds an SVG and converts it to PNG via
ImageMagick
  - Images generated via GenerateTopicOgImage Sidekiq job
- Generated images handled via og_image_upload_id column on the topics
table
- TopicView#image_url falls back to the generated OG image when no topic
image is available

This includes a preview for admins so they can see if they like this
feature or not:


https://github.com/user-attachments/assets/62d17704-4ee5-4859-ac37-b127fe27b85c


And it also respects the site's default theme: 

<img width="1200" height="630" alt="image"
src="https://github.com/user-attachments/assets/678f5c5d-e593-4385-b800-141196dfc221"
/>

---------

Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Grubba <gabriel@discourse.org>
2026-06-26 10:37:10 -04:00

138 lines
4.6 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe TopicOgImageGenerator do
fab!(:category) { Fabricate(:category, name: "Feature", color: "0088cc") }
fab!(:topic) do
Fabricate(
:topic,
title: "How to configure your Discourse site for best results",
category: category,
)
end
# 1x1 transparent PNG as a test fixture for data URI embedding
TINY_PNG_DATA_URI =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
before do
topic.update_columns(like_count: 42, posts_count: 13)
TopicOgImageGenerator.any_instance.stubs(:fetch_as_data_uri).returns(TINY_PNG_DATA_URI)
end
describe "#generate" do
it "generates a PNG upload for a topic" do
generator = described_class.new(topic)
# Stub the ImageMagick rasterization: rendering an SVG to PNG depends on the
# ImageMagick/font setup of the host, which isn't reliable across CI and dev
# environments. Here we assert that #generate turns rendered bytes into a
# proper PNG Upload.
generator.stubs(:render_png).returns(File.binread(file_from_fixtures("logo.png").path))
upload = generator.generate
expect(upload).to be_a(Upload)
expect(upload.errors).to be_empty
expect(upload.extension).to eq("png")
expect(upload.original_filename).to eq("topic-og-#{topic.id}.png")
end
end
describe ".eligible?" do
it "returns true for a public topic in a public category" do
expect(described_class.eligible?(topic)).to eq(true)
end
it "returns false when topic is nil" do
expect(described_class.eligible?(nil)).to eq(false)
end
it "returns false when login_required is enabled" do
SiteSetting.login_required = true
expect(described_class.eligible?(topic)).to eq(false)
end
it "returns false for a personal message" do
pm = Fabricate(:private_message_topic)
expect(described_class.eligible?(pm)).to eq(false)
end
it "returns false for a topic in a read-restricted category" do
private_category = Fabricate(:private_category, group: Fabricate(:group))
topic.update!(category: private_category)
expect(described_class.eligible?(topic)).to eq(false)
end
end
describe "#build_svg (via send)" do
it "includes the topic title limited to two lines" do
generator = described_class.new(topic)
svg = generator.send(:build_svg)
expect(svg).to include("How to configure your")
title_line_count = svg.scan(/font-size="62"/).length
expect(title_line_count).to eq(2)
end
it "adds ellipsis when title exceeds two lines" do
long_title =
"This is a very long topic title that will definitely wrap onto more than two lines and require truncation with an ellipsis at the end"
topic.update!(title: long_title)
generator = described_class.new(topic)
svg = generator.send(:build_svg)
title_line_count = svg.scan(/font-size="62"/).length
expect(title_line_count).to eq(TopicOgImageGenerator::MAX_TITLE_LINES)
expect(svg).to include("")
end
it "includes category name" do
generator = described_class.new(topic)
svg = generator.send(:build_svg)
expect(svg).to include("Feature")
expect(svg).to include("0088cc")
end
it "includes stats separated by middle dots" do
generator = described_class.new(topic)
svg = generator.send(:build_svg)
expect(svg).to include("12 replies · 42 likes")
end
it "includes author avatar and username" do
generator = described_class.new(topic)
svg = generator.send(:build_svg)
expect(svg).to include(topic.user.username)
expect(svg).to include("avatar-clip")
expect(svg).to include(topic.created_at.strftime("%b %-d, %Y"))
end
it "embeds images as data URIs" do
generator = described_class.new(topic)
svg = generator.send(:build_svg)
expect(svg).to include("data:image/png;base64,")
expect(svg).not_to match(%r{href="https?://})
end
it "escapes XML entities in title" do
topic.update!(title: "Using <script> tags & other HTML elements safely")
generator = described_class.new(topic)
svg = generator.send(:build_svg)
expect(svg).not_to include("<script>")
expect(svg).to include("&lt;script&gt;")
expect(svg).to include("&amp;")
end
it "does not include site name text" do
SiteSetting.title = "My Test Forum"
generator = described_class.new(topic)
svg = generator.send(:build_svg)
expect(svg).not_to include("My Test Forum")
end
end
end