Extra tags and Social Card integration in the Infrastructure package (#28)

This PR contains the work done to extends the `Page` protocol with the head tags that control search snippets and link previews: a meta description, a canonical URL, and a social card rendered as _Open Graph_ and _Twitter_ meta tags. All three are optional with nil defaults, so existing conformers compile and render unchanged.

Reviewed-on: rock-n-code/loud-amsterdam#28
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
2026-08-01 10:57:29 +00:00
committed by javier
parent ea00b94841
commit efc933d5d0
8 changed files with 483 additions and 12 deletions
@@ -47,6 +47,56 @@ struct PageTests {
#expect(content.lowerBound < script.lowerBound)
}
@Test
func `omits the summary and canonical tags by default`() {
let html = StubPage().render()
#expect(!html.contains(#"name="description""#))
#expect(!html.contains(#"rel="canonical""#))
}
@Test
func `renders the summary and canonical tags when provided`() {
let html = StubPage(
canonicalURL: "https://stub.example/",
summary: "A stub page."
).render()
#expect(html.contains(#"<meta name="description" content="A stub page.">"#))
#expect(html.contains(#"<link rel="canonical" href="https://stub.example/">"#))
}
@Test
func `omits the social card tags by default`() {
let html = StubPage().render()
#expect(!html.contains(#"property="og:"#))
#expect(!html.contains(#"name="twitter:card""#))
}
@Test
func `renders the social card tags when provided`() {
let html = StubPage(socialCard: .init(
title: "Stub Page",
summary: "A stub page.",
url: "https://stub.example/",
image: .init(
url: "https://stub.example/img/card.png",
width: 2400,
height: 1260
)
)).render()
#expect(html.contains(#"<meta property="og:type" content="website">"#))
#expect(html.contains(#"<meta property="og:title" content="Stub Page">"#))
#expect(html.contains(#"<meta property="og:description" content="A stub page.">"#))
#expect(html.contains(#"<meta property="og:url" content="https://stub.example/">"#))
#expect(html.contains(#"<meta property="og:image" content="https://stub.example/img/card.png">"#))
#expect(html.contains(#"<meta property="og:image:width" content="2400">"#))
#expect(html.contains(#"<meta property="og:image:height" content="1260">"#))
#expect(html.contains(#"<meta name="twitter:card" content="summary_large_image">"#))
}
@Test
func `appends the version token to the asset URLs`() {
let html = StubPage(assetVersion: "0123456789abcdef").render()
@@ -0,0 +1,84 @@
import Testing
@testable import Infrastructure
@Suite(
"SocialCard type",
.tags(.type)
)
struct SocialCardTests {
// MARK: Functional tests
@Test
func `derives the full tag list from a complete card`() {
let card = SocialCard(
title: "A Title",
summary: "A summary.",
url: "https://site.example/",
siteName: "A Site",
locale: "en",
image: .init(
url: "https://site.example/img/card.png",
width: 2400,
height: 1260,
alt: "An image."
)
)
#expect(card.tags == [
.init(attribute: .property, content: "website", name: .type),
.init(attribute: .property, content: "A Site", name: .siteName),
.init(attribute: .property, content: "A Title", name: .title),
.init(attribute: .property, content: "A summary.", name: .description),
.init(attribute: .property, content: "https://site.example/", name: .url),
.init(attribute: .property, content: "en", name: .locale),
.init(attribute: .property, content: "https://site.example/img/card.png", name: .image),
.init(attribute: .property, content: "2400", name: .imageWidth),
.init(attribute: .property, content: "1260", name: .imageHeight),
.init(attribute: .property, content: "An image.", name: .imageAlt),
.init(attribute: .name, content: "summary_large_image", name: .twitter),
])
}
@Test
func `omits the tags of the facts a minimal card does not carry`() {
let card = SocialCard(title: "A Title")
#expect(card.tags == [
.init(attribute: .property, content: "website", name: .type),
.init(attribute: .property, content: "A Title", name: .title),
.init(attribute: .name, content: "summary_large_image", name: .twitter),
])
}
@Test
func `omits the image alt tag when the image carries none`() {
let card = SocialCard(
title: "A Title",
image: .init(
url: "https://site.example/img/card.png",
width: 2400,
height: 1260
)
)
let names = card.tags.map(\.name)
#expect(names.contains(.image))
#expect(!names.contains(.imageAlt))
}
@Test
func `carries the type and style it is given`() {
let card = SocialCard(
title: "A Title",
type: "article",
style: .summary
)
#expect(card.tags.contains(.init(attribute: .property, content: "article", name: .type)))
#expect(card.tags.contains(.init(attribute: .name, content: "summary", name: .twitter)))
}
}
@@ -9,6 +9,6 @@ extension Tag {
@Tag static var middleware: Tag
/// Tests exercising a protocol scaffolding of the Infrastructure package.
@Tag static var `protocol`: Tag
/// Tests exercising an internal type of the Infrastructure package.
/// Tests exercising a type of the Infrastructure package.
@Tag static var type: Tag
}
@@ -10,9 +10,18 @@ struct StubPage: Page {
/// The version token appended to the page's asset URLs, or `nil` to leave them unversioned.
let assetVersion: String?
/// The canonical URL rendered in the document head, or `nil` to omit it.
let canonicalURL: String?
/// The locale the page content is localized to.
let locale: Locale
/// The card rendered as link-preview tags in the document head, or `nil` to omit them.
let socialCard: SocialCard?
/// The summary rendered in the document head, or `nil` to omit it.
let summary: String?
// MARK: Initializers
/// Creates a stub page.
@@ -20,12 +29,24 @@ struct StubPage: Page {
/// - locale: the locale the page content is localized to. Defaults to `en`.
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the
/// default) to leave them unversioned.
/// - canonicalURL: the canonical URL rendered in the document head, or `nil` (the default)
/// to omit it.
/// - socialCard: the card rendered as link-preview tags in the document head, or `nil`
/// (the default) to omit them.
/// - summary: the summary rendered in the document head, or `nil` (the default)
/// to omit it.
init(
locale: Locale = .init(identifier: "en"),
assetVersion: String? = nil
assetVersion: String? = nil,
canonicalURL: String? = nil,
socialCard: SocialCard? = nil,
summary: String? = nil
) {
self.assetVersion = assetVersion
self.canonicalURL = canonicalURL
self.locale = locale
self.socialCard = socialCard
self.summary = summary
}
// MARK: Computed