Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add enum for MetaTag properties and names outlined in #348. #373

Merged
merged 3 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 85 additions & 41 deletions Sources/Ignite/Elements/MetaTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,57 @@ import Foundation
/// An item of metadata that helps browsers and search engines understand
/// your page better.
public struct MetaTag: HeadElement, Sendable {
/// Metadata names for social media cards.
public enum `Type`: String, Sendable {
/// The image to display in Twitter cards.
case twitterImage = "twitter:image"
/// The title to display in Twitter cards.
case twitterTitle = "twitter:title"
/// The domain to display in Twitter cards.
case twitterDomain = "twitter:domain"
/// The type of Twitter card to use.
case twitterCard = "twitter:card"
/// Whether to disable Twitter tracking.
case twitterDoNotTrack = "twitter:dnt"
/// The description to display in Twitter cards.
case twitterDescription = "twitter:description"
/// The software used to generate the document.
case generator
/// The viewport configuration for responsive web design.
case viewport

/// The title of the shared content.
case openGraphTitle = "og:title"
/// A description of the shared content.
case openGraphDescription = "og:description"
/// The image to display when sharing.
case openGraphImage = "og:image"
/// The canonical URL of the shared content.
case openGraphURL = "og:url"
/// The name of the website.
case openGraphSiteName = "og:site_name"

/// The meta attribute key.
var key: String {
switch self {
case .twitterImage, .twitterTitle, .twitterDomain, .twitterCard,
.twitterDoNotTrack, .twitterDescription, .generator, .viewport:
"name"
case .openGraphTitle, .openGraphDescription, .openGraphImage,
.openGraphURL, .openGraphSiteName:
"property"
}
}
}

/// Allows mobile browsers to scale this page appropriately for the device.
public static let flexibleViewport = MetaTag(name: "viewport", content: "width=device-width, initial-scale=1")
public static let flexibleViewport = MetaTag(.viewport, content: "width=device-width, initial-scale=1")

/// Marks this page as using UTF-8 content.
public static let utf8 = MetaTag(characterSet: "utf-8")

/// Marks this page as having been automatically generated by Ignite.
public static let generator = MetaTag(name: "generator", content: Ignite.version)
public static let generator = MetaTag(.generator, content: Ignite.version)

/// The content and behavior of this HTML.
public var body: some HTML { self }
Expand All @@ -33,64 +76,66 @@ public struct MetaTag: HeadElement, Sendable {
private var type: String

/// The value to be placed into the "name" attribute.
var name: String
var value: String

/// The content to use for this metadata.
var content: String

/// A character set string that describes this page's content.
var charset: String

/// Creates a new `MetaTag` instance from the name and content provided.
/// A convenience initializer for when you want a piece of data with a
/// URL as its content. Creates a new `MetaTag` instance from the type
/// and URL provided.
/// - Parameters:
/// - name: The name for the metadata.
/// - type: The type of metadata.
/// - content: The URL value for the metadata.
public init(_ type: `Type`, content: URL) {
self.init(type, content: content.absoluteString)
}

/// Creates a new `MetaTag` instance from a metadata type and content provided.
/// - Parameters:
/// - type: The type of metadata.
/// - content: The value for the metadata.
public init(name: String, content: String) {
self.type = "name"
public init(_ type: `Type`, content: String) {
self.type = type.key

self.name = name
self.value = type.rawValue
self.content = content
self.charset = ""
}

/// A convenience initializer for when you want a named piece of data with a
/// URL as its content. Creates a new `MetaTag` instance from the name
/// and URL provided.
/// Creates a new `MetaTag` instance from the custom name and content provided.
/// - Parameters:
/// - name: The name for the metadata.
/// - content: The URL value for the metadata.
public init(name: String, content: URL) {
self.init(name: name, content: content.absoluteString)
/// - content: The value for the metadata.
public init(name: String, content: String) {
self.type = "name"

self.value = name
self.content = content
self.charset = ""
}

/// Creates a new `MetaTag` instance from the property and content provided.
/// Creates a new `MetaTag` instance from the custom property and content provided.
/// - Parameters:
/// - property: The property name for the metadata.
/// - content: The value for the metadata.
public init(property: String, content: String) {
self.type = "property"

self.name = property
self.value = property
self.content = content
self.charset = ""
}

/// A convenience initializer for when you want a named property of data with a
/// URL as its content. Creates a new `MetaTag` instance from the property
/// and URL provided.
/// - Parameters:
/// - property: The name for the metadata.
/// - content: The URL value for the metadata.
public init(property: String, content: URL) {
self.init(property: property, content: content.absoluteString)
}

/// Creates a new piece of metadata that sets a specific character set for
/// the current page.
public init(characterSet: String) {
self.type = "characterSet"

self.name = ""
self.value = ""
self.content = ""
self.charset = characterSet
}
Expand All @@ -104,29 +149,29 @@ public struct MetaTag: HeadElement, Sendable {
/// into your page header to enable social sharing.
@ElementBuilder<MetaTag> public static func socialSharingTags(for page: Page) -> [MetaTag] {
let site = PublishingContext.default.site
MetaTag(property: "og:site_name", content: site.name)
MetaTag(.openGraphSiteName, content: site.name)

if let image = page.image {
MetaTag(property: "og:image", content: image)
MetaTag(property: "twitter:image", content: image)
MetaTag(.openGraphImage, content: image)
MetaTag(.twitterImage, content: image)
}

MetaTag(property: "og:title", content: page.title)
MetaTag(property: "twitter:title", content: page.title)
MetaTag(.openGraphTitle, content: page.title)
MetaTag(.twitterTitle, content: page.title)

if page.description.isEmpty == false {
MetaTag(property: "og:description", content: page.description)
MetaTag(name: "twitter:description", content: page.description)
MetaTag(.openGraphDescription, content: page.description)
MetaTag(.twitterDescription, content: page.description)
}

MetaTag(property: "og:url", content: page.url)
MetaTag(.openGraphURL, content: page.url)

if let domain = page.url.removingWWW {
MetaTag(name: "twitter:domain", content: domain)
MetaTag(.twitterDomain, content: domain)
}

MetaTag(name: "twitter:card", content: "summary_large_image")
MetaTag(name: "twitter:dnt", content: "on")
MetaTag(.twitterCard, content: "summary_large_image")
MetaTag(.twitterDoNotTrack, content: "on")
}

/// Renders this element using publishing context passed in.
Expand All @@ -137,9 +182,8 @@ public struct MetaTag: HeadElement, Sendable {

if charset.isEmpty {
attributes.append(customAttributes:
.init(name: type, value: name),
.init(name: "content", value: content)
)
.init(name: type, value: value),
.init(name: "content", value: content))
} else {
attributes.append(customAttributes:
.init(name: "charset", value: charset)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Ignite/Rendering/Attribute+Convenience.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// BooleanAttribute.swift
// Attributes+Convenience.swift
// Ignite
// https://www.github.com/twostraws/Ignite
// See LICENSE for license information.
Expand Down
63 changes: 27 additions & 36 deletions Tests/IgniteTesting/Elements/MetaTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ struct MetaTagTests {
try PublishingContext.initialize(for: TestSite(), from: #filePath)
}

@Test("Meta tag with name and content both strings")
func withNameAndContentBothStrings() async throws {
let element = MetaTag(name: "tagname", content: "my content")
@Test("Meta tag with type enum and content a URL")
func withEnumAndContentURL() async throws {
let element = MetaTag(.twitterDomain, content: URL(string: "https://example.com?s=searching#target")!)

let output = element.render()

#expect(output == "<meta content=\"my content\" name=\"tagname\" />")
#expect(output == "<meta content=\"https://example.com?s=searching#target\" name=\"twitter:domain\" />")
}

@Test("Meta tag with name a string and content a URL")
func withNameStringAndContentURL() async throws {
let element = MetaTag(name: "tagname", content: URL(string: "https://example.com?s=searching#target")!)
@Test("Meta tag with name and content both strings")
func withNameAndContentBothStrings() async throws {
let element = MetaTag(name: "tagname", content: "my content")

let output = element.render()

#expect(output == "<meta content=\"https://example.com?s=searching#target\" name=\"tagname\" />")
#expect(output == "<meta content=\"my content\" name=\"tagname\" />")
}

@Test("Meta tag with property and content both strings")
Expand All @@ -45,15 +45,6 @@ struct MetaTagTests {
#expect(output == "<meta content=\"my value\" property=\"unique\" />")
}

@Test("Meta tag with property a string and content as URL")
func withPropertyStringAndContentURL() async throws {
let element = MetaTag(property: "different", content: URL(string: "https://example.com?s=searching#target")!)

let output = element.render()

#expect(output == "<meta content=\"https://example.com?s=searching#target\" property=\"different\" />")
}

@Test("Meta tag with character set only")
func withCharacterSet() async throws {
let element = MetaTag(characterSet: "UTF-16")
Expand All @@ -64,7 +55,7 @@ struct MetaTagTests {
}

func metaTagCoreFieldsMatch(_ actual: MetaTag, _ expected: MetaTag) -> Bool {
actual.name == expected.name && actual.content == expected.content && actual.charset == expected.charset
actual.value == expected.value && actual.content == expected.content && actual.charset == expected.charset
}

@Test("Social sharing tags with no image, description, or www")
Expand All @@ -79,13 +70,13 @@ struct MetaTagTests {
let tags = MetaTag.socialSharingTags(for: page)

let expectedTags: [MetaTag] = [
MetaTag(property: "og:site_name", content: "My Test Site"),
MetaTag(property: "og:title", content: "My Page Title"),
MetaTag(property: "twitter:title", content: "My Page Title"),
MetaTag(property: "og:url", content: "https://example.com"),
MetaTag(property: "twitter:domain", content: "example.com"),
MetaTag(property: "twitter:card", content: "summary_large_image"),
MetaTag(property: "twitter:dnt", content: "on")
MetaTag(.openGraphSiteName, content: "My Test Site"),
MetaTag(.openGraphTitle, content: "My Page Title"),
MetaTag(.twitterTitle, content: "My Page Title"),
MetaTag(.openGraphURL, content: "https://example.com"),
MetaTag(.twitterDomain, content: "example.com"),
MetaTag(.twitterCard, content: "summary_large_image"),
MetaTag(.twitterDoNotTrack, content: "on")
]

#expect(tags.count == expectedTags.count)
Expand All @@ -108,17 +99,17 @@ struct MetaTagTests {
let actualTags = MetaTag.socialSharingTags(for: page)

let expectedTags: [MetaTag] = [
MetaTag(property: "og:site_name", content: "My Test Site"),
MetaTag(property: "og:image", content: "https://example.com/image.png"),
MetaTag(property: "twitter:image", content: "https://example.com/image.png"),
MetaTag(property: "og:title", content: "My Page Title"),
MetaTag(property: "twitter:title", content: "My Page Title"),
MetaTag(property: "og:description", content: "describing the page"),
MetaTag(property: "twitter:description", content: "describing the page"),
MetaTag(property: "og:url", content: "https://www.example.com"),
MetaTag(property: "twitter:domain", content: "example.com"),
MetaTag(property: "twitter:card", content: "summary_large_image"),
MetaTag(property: "twitter:dnt", content: "on")
MetaTag(.openGraphSiteName, content: "My Test Site"),
MetaTag(.openGraphImage, content: "https://example.com/image.png"),
MetaTag(.twitterImage, content: "https://example.com/image.png"),
MetaTag(.openGraphTitle, content: "My Page Title"),
MetaTag(.twitterTitle, content: "My Page Title"),
MetaTag(.openGraphDescription, content: "describing the page"),
MetaTag(.twitterDescription, content: "describing the page"),
MetaTag(.openGraphURL, content: "https://www.example.com"),
MetaTag(.twitterDomain, content: "example.com"),
MetaTag(.twitterCard, content: "summary_large_image"),
MetaTag(.twitterDoNotTrack, content: "on")
]

#expect(actualTags.count == expectedTags.count)
Expand Down