mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-06-26 04:59:35 +00:00
Add per-post Image frontmatter and richer OG/article meta tags
Pass an optional Image field through PostMetadata -> Post -> PostWriter so individual posts (especially link posts) can set their own og:image instead of always falling back to the site-wide image. Also emit article:published_time and article:tag for article pages, and switch twitter:card to summary_large_image when a post-specific image is set.
This commit is contained in:
parent
24bc321f2b
commit
760c13b0b6
8 changed files with 122 additions and 5 deletions
|
|
@ -6,7 +6,7 @@ module Pressa
|
|||
class PostMetadata
|
||||
REQUIRED_FIELDS = %w[Title Author Date Timestamp].freeze
|
||||
|
||||
attr_reader :title, :author, :date, :formatted_date, :link, :tags
|
||||
attr_reader :title, :author, :date, :formatted_date, :link, :tags, :image
|
||||
|
||||
def initialize(yaml_hash)
|
||||
@raw = yaml_hash
|
||||
|
|
@ -39,6 +39,7 @@ module Pressa
|
|||
@formatted_date = @raw["Date"]
|
||||
@link = @raw["Link"]
|
||||
@tags = parse_tags(@raw["Tags"])
|
||||
@image = @raw["Image"]
|
||||
end
|
||||
|
||||
def parse_tags(value)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ module Pressa
|
|||
attribute :formatted_date, Types::String
|
||||
attribute :link, Types::String.optional.default(nil)
|
||||
attribute :tags, Types::Array.of(Types::String).default([].freeze)
|
||||
attribute :image, Types::String.optional.default(nil)
|
||||
attribute :body, Types::String
|
||||
attribute :markdown_body, Types::String.default("".freeze)
|
||||
attribute :excerpt, Types::String
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ module Pressa
|
|||
formatted_date: metadata.formatted_date,
|
||||
link: metadata.link,
|
||||
tags: metadata.tags,
|
||||
image: metadata.image,
|
||||
body: html_body,
|
||||
markdown_body: body_markdown,
|
||||
excerpt:,
|
||||
|
|
|
|||
|
|
@ -117,7 +117,10 @@ module Pressa
|
|||
canonical_url: @site.url_for(post.path),
|
||||
content: content_view,
|
||||
page_description: post.excerpt,
|
||||
page_type: "article"
|
||||
page_type: "article",
|
||||
page_image: post.image,
|
||||
page_tags: post.tags,
|
||||
page_published_time: post.date.iso8601
|
||||
)
|
||||
|
||||
file_path = File.join(target_path, post.path.sub(/^\//, ""), "index.html")
|
||||
|
|
@ -161,7 +164,10 @@ module Pressa
|
|||
canonical_url:,
|
||||
content:,
|
||||
page_description: nil,
|
||||
page_type: "website"
|
||||
page_type: "website",
|
||||
page_image: nil,
|
||||
page_tags: [],
|
||||
page_published_time: nil
|
||||
)
|
||||
layout = Views::Layout.new(
|
||||
site: @site,
|
||||
|
|
@ -169,6 +175,9 @@ module Pressa
|
|||
canonical_url:,
|
||||
page_description:,
|
||||
page_type:,
|
||||
page_image:,
|
||||
page_tags:,
|
||||
page_published_time:,
|
||||
content:
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ module Pressa
|
|||
:page_subtitle,
|
||||
:page_description,
|
||||
:page_type,
|
||||
:page_image,
|
||||
:page_tags,
|
||||
:page_published_time,
|
||||
:canonical_url,
|
||||
:page_scripts,
|
||||
:page_styles,
|
||||
|
|
@ -18,6 +21,9 @@ module Pressa
|
|||
canonical_url:, page_subtitle: nil,
|
||||
page_description: nil,
|
||||
page_type: "website",
|
||||
page_image: nil,
|
||||
page_tags: [],
|
||||
page_published_time: nil,
|
||||
page_scripts: [],
|
||||
page_styles: [],
|
||||
content: nil
|
||||
|
|
@ -26,6 +32,9 @@ module Pressa
|
|||
@page_subtitle = page_subtitle
|
||||
@page_description = page_description
|
||||
@page_type = page_type
|
||||
@page_image = page_image
|
||||
@page_tags = page_tags
|
||||
@page_published_time = page_published_time
|
||||
@canonical_url = canonical_url
|
||||
@page_scripts = page_scripts
|
||||
@page_styles = page_styles
|
||||
|
|
@ -54,7 +63,12 @@ module Pressa
|
|||
meta(property: "og:image", content: og_image_url) if og_image_url
|
||||
meta(property: "og:type", content: page_type)
|
||||
meta(property: "article:author", content: site.author)
|
||||
meta(name: "twitter:card", content: "summary")
|
||||
meta(name: "twitter:card", content: twitter_card_type)
|
||||
|
||||
if page_type == "article"
|
||||
meta(property: "article:published_time", content: page_published_time) if page_published_time
|
||||
page_tags.each { |tag| meta(property: "article:tag", content: tag) }
|
||||
end
|
||||
|
||||
link(
|
||||
rel: "alternate",
|
||||
|
|
@ -108,9 +122,18 @@ module Pressa
|
|||
end
|
||||
|
||||
def og_image_url
|
||||
if page_image
|
||||
return page_image if page_image.start_with?("http://", "https://")
|
||||
return absolute_asset(page_image)
|
||||
end
|
||||
|
||||
site.image_url
|
||||
end
|
||||
|
||||
def twitter_card_type
|
||||
page_image ? "summary_large_image" : "summary"
|
||||
end
|
||||
|
||||
def all_styles
|
||||
site.styles + page_styles
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class Pressa::Posts::PostMetadataTest < Minitest::Test
|
|||
Timestamp: 2025-11-05T10:00:00-08:00
|
||||
Tags: Ruby, Testing
|
||||
Link: https://example.net/external
|
||||
Image: /images/blog/test-post.png
|
||||
---
|
||||
|
||||
This is the post body.
|
||||
|
|
@ -25,6 +26,7 @@ class Pressa::Posts::PostMetadataTest < Minitest::Test
|
|||
assert_equal(5, metadata.date.day)
|
||||
assert_equal("https://example.net/external", metadata.link)
|
||||
assert_equal(["Ruby", "Testing"], metadata.tags)
|
||||
assert_equal("/images/blog/test-post.png", metadata.image)
|
||||
end
|
||||
|
||||
def test_parse_raises_error_when_required_fields_are_missing
|
||||
|
|
@ -56,6 +58,7 @@ class Pressa::Posts::PostMetadataTest < Minitest::Test
|
|||
|
||||
assert_equal([], metadata.tags)
|
||||
assert_nil(metadata.link)
|
||||
assert_nil(metadata.image)
|
||||
end
|
||||
|
||||
def test_parse_raises_error_when_front_matter_is_missing
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ class Pressa::Posts::PostWriterTest < Minitest::Test
|
|||
link: "https://example.net/linked",
|
||||
body: "<p>linked body</p>",
|
||||
excerpt: "linked body...",
|
||||
path: "/posts/2025/11/link-post"
|
||||
path: "/posts/2025/11/link-post",
|
||||
image: "/images/blog/link-post.png"
|
||||
)
|
||||
regular_post = Pressa::Posts::Post.new(
|
||||
slug: "regular-post",
|
||||
|
|
@ -73,6 +74,22 @@ class Pressa::Posts::PostWriterTest < Minitest::Test
|
|||
end
|
||||
end
|
||||
|
||||
def test_write_posts_uses_post_image_for_og_image_and_article_tags
|
||||
Dir.mktmpdir do |dir|
|
||||
writer.write_posts(target_path: dir)
|
||||
|
||||
linked = File.join(dir, "posts/2025/11/link-post/index.html")
|
||||
html = File.read(linked)
|
||||
|
||||
assert_includes(html, %(<meta property="og:image" content="https://samhuri.net/images/blog/link-post.png">))
|
||||
assert_includes(html, %(<meta property="article:published_time" content="2025-11-05T10:00:00-08:00">))
|
||||
|
||||
regular = File.join(dir, "posts/2024/10/regular-post/index.html")
|
||||
regular_html = File.read(regular)
|
||||
assert_includes(regular_html, %(<meta property="article:tag" content="ruby">))
|
||||
end
|
||||
end
|
||||
|
||||
def test_write_recent_posts_writes_index_page
|
||||
Dir.mktmpdir do |dir|
|
||||
writer.write_recent_posts(target_path: dir, limit: 1)
|
||||
|
|
|
|||
|
|
@ -140,4 +140,66 @@ class Pressa::Views::LayoutTest < Minitest::Test
|
|||
refute_includes(html, "techhub.social")
|
||||
refute_includes(html, "github.com/samsonjs")
|
||||
end
|
||||
|
||||
def test_page_image_overrides_og_image_and_uses_large_image_card
|
||||
html = Pressa::Views::Layout.new(
|
||||
site:,
|
||||
canonical_url: "https://samhuri.net/posts/",
|
||||
page_image: "/images/blog/post.png",
|
||||
content: content_view
|
||||
).call
|
||||
|
||||
assert_includes(html, %(<meta property="og:image" content="https://samhuri.net/images/blog/post.png">))
|
||||
assert_includes(html, %(<meta name="twitter:card" content="summary_large_image">))
|
||||
end
|
||||
|
||||
def test_page_image_preserves_absolute_urls
|
||||
html = Pressa::Views::Layout.new(
|
||||
site:,
|
||||
canonical_url: "https://samhuri.net/posts/",
|
||||
page_image: "https://cdn.example.net/preview.png",
|
||||
content: content_view
|
||||
).call
|
||||
|
||||
assert_includes(html, %(<meta property="og:image" content="https://cdn.example.net/preview.png">))
|
||||
end
|
||||
|
||||
def test_default_twitter_card_is_summary_without_page_image
|
||||
html = Pressa::Views::Layout.new(
|
||||
site:,
|
||||
canonical_url: "https://samhuri.net/posts/",
|
||||
content: content_view
|
||||
).call
|
||||
|
||||
assert_includes(html, %(<meta name="twitter:card" content="summary">))
|
||||
end
|
||||
|
||||
def test_article_tags_render_published_time_and_tags
|
||||
html = Pressa::Views::Layout.new(
|
||||
site:,
|
||||
canonical_url: "https://samhuri.net/posts/2025/11/05/post/",
|
||||
page_type: "article",
|
||||
page_published_time: "2025-11-05T10:00:00-08:00",
|
||||
page_tags: ["ruby", "rails"],
|
||||
content: content_view
|
||||
).call
|
||||
|
||||
assert_includes(html, %(<meta property="article:published_time" content="2025-11-05T10:00:00-08:00">))
|
||||
assert_includes(html, %(<meta property="article:tag" content="ruby">))
|
||||
assert_includes(html, %(<meta property="article:tag" content="rails">))
|
||||
end
|
||||
|
||||
def test_article_tags_do_not_render_for_non_article_pages
|
||||
html = Pressa::Views::Layout.new(
|
||||
site:,
|
||||
canonical_url: "https://samhuri.net/posts/",
|
||||
page_type: "website",
|
||||
page_published_time: "2025-11-05T10:00:00-08:00",
|
||||
page_tags: ["ruby"],
|
||||
content: content_view
|
||||
).call
|
||||
|
||||
refute_includes(html, "article:published_time")
|
||||
refute_includes(html, "article:tag")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue