Social Card tweaks in the Infrastructure package. (#30)

This PR contains the work done to address certain tweaks in the newly-introduced _Social Card_ types in the **Infrastructure** package.

Reviewed-on: rock-n-code/loud-amsterdam#30
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
2026-08-01 11:25:36 +00:00
committed by javier
parent 5f4316b85b
commit 5c9fde9d41
5 changed files with 88 additions and 102 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ The package provides, grouped by role:
| Routing | `RouterController`, `RouteCollectionBuilder`, the `addController` extension on `RouterMethods` |
| Middlewares | `SecurityHeadersMiddleware`, `VaryMiddleware`, `RateLimitMiddleware`, `LocalizationMiddleware`, `NotFoundMiddleware` |
| Pages and assets | `Page`, `Asset`, `AssetExtension`, `FingerprintAssets` |
| Link previews | `SocialCard`, its `Image`, and the `SocialCardTag` meta tags it derives |
| Link previews | `SocialCard`, its `Image`, and the `Tag` meta tags it derives |
| Responses | `CachedHTMLResponse`, `LocalizedHTMLCollectionResponse` |
| Contexts | `LocalizedRequestContext` |
| Constants | The `HTTPField.Name` header names, `Int.RateLimit` limits, and `String.Security` header values the middlewares default to |
@@ -70,52 +70,16 @@ public struct SocialCard: Sendable {
/// The card's meta tags, in a stable order: the Open Graph type, site name, title, description, URL, and locale, then the image group, and
/// the Twitter card style last. A tag whose fact the card does not carry is left out.
public var tags: [SocialCardTag] {
let tags: [SocialCardTag?] = [
SocialCardTag(
attribute: .property,
content: type,
name: .type
),
siteName.map {
SocialCardTag(
attribute: .property,
content: $0,
name: .siteName
)
},
SocialCardTag(
attribute: .property,
content: title,
name: .title
),
summary.map {
SocialCardTag(
attribute: .property,
content: $0,
name: .description
)
},
url.map {
SocialCardTag(
attribute: .property,
content: $0,
name: .url
)
},
locale.map {
SocialCardTag(
attribute: .property,
content: $0,
name: .locale
)
},
public var tags: [Tag] {
let tags: [Tag?] = [
Tag(type, name: .type),
siteName.map { Tag($0, name: .siteName) },
Tag(title, name: .title),
summary.map { Tag($0, name: .description) },
url.map { Tag($0, name: .url) },
locale.map { Tag($0, name: .locale) },
] + (image?.tags ?? []) + [
SocialCardTag(
attribute: .name,
content: style.rawValue,
name: .twitter
),
Tag(style.rawValue, name: .twitter),
]
return tags.compactMap { $0 }
@@ -39,30 +39,12 @@ extension SocialCard {
// MARK: Computed
/// The image's meta tags, in a stable order: its URL, width, and height, then its alt text when it carries one.
public var tags: [SocialCardTag] {
let tags: [SocialCardTag?] = [
SocialCardTag(
attribute: .property,
content: url,
name: .image
),
SocialCardTag(
attribute: .property,
content: String(width),
name: .imageWidth
),
SocialCardTag(
attribute: .property,
content: String(height),
name: .imageHeight
),
alt.map {
SocialCardTag(
attribute: .property,
content: $0,
name: .imageAlt
)
},
public var tags: [Tag] {
let tags: [Tag?] = [
Tag(url, name: .image),
Tag(String(width), name: .imageWidth),
Tag(String(height), name: .imageHeight),
alt.map { Tag($0, name: .imageAlt) },
]
return tags.compactMap { $0 }
@@ -1,24 +1,44 @@
/// A head meta tag of a ``SocialCard``: which attribute keys it, and its name and content.
public struct SocialCardTag: Equatable, Sendable {
extension SocialCard {
/// A head meta tag of a ``SocialCard``: its name and content, keyed by the attribute its ``name`` dictates.
public struct Tag: Equatable, Sendable {
// MARK: Properties
// MARK: Properties
/// The attribute the tag is keyed by.
public let attribute: Attribute
/// The tag's value.
public let content: String
/// The tag's value.
public let content: String
/// The tag's name.
public let name: Name
// MARK: Initializers
/// The tag's name.
public let name: Name
/// Creates a head meta tag.
/// - Parameters:
/// - content: the tag's value.
/// - name: the tag's name, dictating the attribute the tag is keyed by.
public init(
_ content: String,
name: Name
) {
self.content = content
self.name = name
}
// MARK: Computed
/// The attribute the tag is keyed by, dictated by its ``name``.
public var attribute: Attribute {
name.attribute
}
}
}
// MARK: - Enumerations
extension SocialCardTag {
/// The meta attribute a ``SocialCardTag`` is keyed by, named by its raw value.
extension SocialCard.Tag {
/// The meta attribute a ``SocialCard/Tag`` is keyed by, named by its raw value.
public enum Attribute: String, Sendable {
/// The `name` attribute, keying the Twitter tags.
case name
@@ -26,7 +46,7 @@ extension SocialCardTag {
case property
}
/// The name of a ``SocialCardTag``, carried in its raw value.
/// The name of a ``SocialCard/Tag``, carried in its raw value.
public enum Name: String, Sendable {
/// The `og:description` tag, carrying the card's summary.
case description = "og:description"
@@ -34,10 +54,10 @@ extension SocialCardTag {
case image = "og:image"
/// The `og:image:alt` tag, carrying the share image's text for assistive technologies.
case imageAlt = "og:image:alt"
/// The `og:image:width` tag, carrying the share image's width in pixels.
case imageWidth = "og:image:width"
/// The `og:image:height` tag, carrying the share image's height in pixels.
case imageHeight = "og:image:height"
/// The `og:image:width` tag, carrying the share image's width in pixels.
case imageWidth = "og:image:width"
/// The `og:locale` tag, carrying the locale of the card's text.
case locale = "og:locale"
/// The `og:site_name` tag, carrying the name of the site the card belongs to.
@@ -51,5 +71,18 @@ extension SocialCardTag {
/// The `og:url` tag, carrying the absolute URL the card's page is served at.
case url = "og:url"
}
}
// MARK: - Implementations
public extension SocialCard.Tag.Name {
// MARK: Computed
/// The attribute keying a tag with this name: `property` for the Open Graph names, `name` for the Twitter ones.
var attribute: SocialCard.Tag.Attribute {
self == .twitter ? .name : .property
}
}
@@ -27,17 +27,17 @@ struct SocialCardTests {
)
#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),
.init("website", name: .type),
.init("A Site", name: .siteName),
.init("A Title", name: .title),
.init("A summary.", name: .description),
.init("https://site.example/", name: .url),
.init("en", name: .locale),
.init("https://site.example/img/card.png", name: .image),
.init("2400", name: .imageWidth),
.init("1260", name: .imageHeight),
.init("An image.", name: .imageAlt),
.init("summary_large_image", name: .twitter),
])
}
@@ -46,9 +46,9 @@ struct SocialCardTests {
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),
.init("website", name: .type),
.init("A Title", name: .title),
.init("summary_large_image", name: .twitter),
])
}
@@ -77,8 +77,15 @@ struct SocialCardTests {
style: .summary
)
#expect(card.tags.contains(.init(attribute: .property, content: "article", name: .type)))
#expect(card.tags.contains(.init(attribute: .name, content: "summary", name: .twitter)))
#expect(card.tags.contains(.init("article", name: .type)))
#expect(card.tags.contains(.init("summary", name: .twitter)))
}
@Test
func `keys a tag by the attribute its name dictates`() {
#expect(SocialCard.Tag.Name.twitter.attribute == .name)
#expect(SocialCard.Tag.Name.title.attribute == .property)
#expect(SocialCard.Tag("website", name: .type).attribute == .property)
}
}