Add StructuredData element for JSON-LD structured data#882
Conversation
Adds a new HeadElement that renders <script type="application/ld+json">
blocks. Supports both generic schema creation from a type + properties
dictionary, and convenience factories for common Schema.org types:
- .article() — auto-generates from current article YAML front matter
- .organization() — Organization schema
- .webSite() — WebSite schema
- .event() — Event schema with location and organizer
- .breadcrumbs() — auto-generates BreadcrumbList from page context
The generic initializer accepts any Schema.org type string and properties
dictionary, so users can create any schema type without modifying the
framework:
StructuredData("Product", properties: ["name": "Widget", "price": "9.99"])
12 tests covering generic schemas, raw JSON, convenience methods (Organization, WebSite, Event, BreadcrumbList, Article), optional parameter handling, and custom context support.
Add article context testing via withEnvironment() helper, covering: - Article with headline, date, description, author, image, publisher - dateModified included/omitted based on whether it differs from date - Relative vs absolute image URL handling - Publisher with and without URL - Fully populated article with all fields Also adds tests for previously uncovered paths: - Nested dictionary and array properties in generic schemas - Organization with custom parentOrganizationType - WebSite without description - Event with custom status/attendanceMode and non-US country - Breadcrumbs on homepage (emits nothing) - Breadcrumbs with specific page title and URL
Aligns with TDD directive requirements: Edge cases: special characters, unicode, empty raw JSON, empty sameAs, deeply nested schemas (3 levels), HTML entities in article titles, deeply nested page paths. Invalid inputs: malformed JSON string (no crash), empty type string. Property-based: output structure invariant (all non-empty outputs wrapped in script tags), @context/@type present in all schema outputs, article required fields (headline/datePublished/url), empty output invariant for non-applicable contexts. Stress: 500-property dictionary, 10,000-character description, 50 sameAs URLs. Documentation verification: all three doc comment examples compile and execute without error. Total: 51 tests, 102 test cases.
Replace @mainactor isolation with Sendable conformance to match upstream protocol requirements. Pre-serialize schema dictionaries at init time so the Content enum is fully Sendable. Migrate all 51 tests from the deprecated `arguments: await Self.sites` pattern to .publishingContext().
The two breadcrumb tests that didn't use withPageContext relied on the default test environment having a non-root page URL. On CI the default page is root, triggering the homepage guard and returning empty output.
MrSkwiggs
left a comment
There was a problem hiding this comment.
I like the work done here, but I'm a bit torn with adding this and supporting this directly from within Ignite.
I'm starting to think it might actually make more sense to vend this as a separate plugin. That way we could make the plugin exhaustive and offer full parity with Schema.org without overloading Ignite directly?
Something akin to how Vapor registers additional functionality might be interesting to look into, but yeah... that'd require quite a significant rework to properly support.
| /// StructuredData(json: customJSONString) | ||
| /// } | ||
| /// ``` | ||
| public struct StructuredData: HeadElement, Sendable { |
There was a problem hiding this comment.
Since this is specifically about JSON-LD which stands for Linked Data, maybe this makes more sense here?
| public struct StructuredData: HeadElement, Sendable { | |
| public struct LinkedData: HeadElement, Sendable { |
or even
| public struct StructuredData: HeadElement, Sendable { | |
| public struct SchemaData: HeadElement, Sendable { |
| /// - sameAs: URLs for the organization's social media profiles or external pages. | ||
| /// - parentOrganization: An optional parent organization as (name, url). | ||
| /// - parentOrganizationType: The Schema.org type for the parent. Defaults to "Organization". | ||
| public static func organization( |
There was a problem hiding this comment.
Since we're providing a few convenience constructors here, would it make sense to provide full parity from the get-go? IMHO if we introduce this feature with limited support, it wouldn't be quite user-friendly.
Now with that said, peeking at the amount of different types there are, maybe we could support most top-level constructors at least?
Summary
Adds a new
StructuredDatahead element that renders JSON-LD<script type="application/ld+json">tags for Schema.org structured data.Organization,WebSite,Event,Article, andBreadcrumbListwith typed parametersArticleandBreadcrumbListauto-populate from the publishing context (article metadata, page URL, site URL)HeadElementandSendable; 51 tests; DocC catalog entryUsage
StructuredDatais aHeadElement, so you use it inside aHead { }block in your Layout — the same place you'd put aMetaTag,MetaLink, orScript:No site-level configuration is needed. Each
StructuredDataelement renders an independent<script type="application/ld+json">tag in the page's<head>, and you can include as many as you need per page. Context-aware schemas likeArticleandBreadcrumbListautomatically emit nothing when they don't apply (e.g., breadcrumbs on the homepage, article schema on non-article pages).Test plan
Sendableconformance, no@MainActor).publishingContext()trait (current upstream pattern)