Skip to content

Add Atom and JSON Feed generators#877

Open
jpurnell wants to merge 7 commits intotwostraws:mainfrom
jpurnell:feature/atom-json-feeds-clean
Open

Add Atom and JSON Feed generators#877
jpurnell wants to merge 7 commits intotwostraws:mainfrom
jpurnell:feature/atom-json-feeds-clean

Conversation

@jpurnell
Copy link
Copy Markdown
Contributor

@jpurnell jpurnell commented Apr 14, 2026

Summary

  • Add Atom (RFC 4287) and JSON Feed (v1.1) generators alongside the existing RSS 2.0 generator, giving site authors three industry-standard syndication options
  • Introduce FeedFormat enum and extend FeedConfiguration with formats and per-format paths, fully backward compatible (defaults to [.rss] with existing behavior)
  • Add <link rel="alternate"> auto-discovery tags to <head> so feed readers can find feeds automatically
  • Extract shared String.xmlEscaped and Date.asRFC3339() utilities used by both XML-based generators
  • JSON Feed uses Encodable + JSONEncoder for safe serialization with no escaping bugs by construction
  • 61 new and updated tests across 8 suites

Test plan

  • All 61 feed-related tests pass (AtomFeedGenerator, JSONFeedGenerator, FeedConfiguration, FeedGenerator, FeedLink, Head, StringXMLEscaping, DateRFC3339)
  • Existing RSS generator tests unchanged and passing
  • Zero compiler warnings on Ignite target
  • Backward compatibility: existing sites with default feedConfiguration generate only RSS at /feed.rss with no code changes
  • Validate generated Atom output with W3C Feed Validation Service
  • Validate generated JSON Feed output against jsonfeed.org spec — all required fields present; added content_text fallback in descriptionOnly mode for strict compliance

Foundation for multi-format feed generation (RSS, Atom, JSON Feed):
- Add FeedFormat enum with .rss, .atom, .json cases
- Extend FeedConfiguration with formats and per-format paths
- Extract xmlEscape() from FeedGenerator into shared String.xmlEscaped
- Add Date.asRFC3339() for Atom and JSON Feed date formatting
- Fix pre-existing Sendable conformance in ContentFinder tests
- Maintain full backward compatibility with existing RSS configuration
- AtomFeedGenerator: RFC 4287 compliant Atom XML feed generation
  with proper XML escaping, CDATA wrapping, RFC 3339 dates, and
  support for description-only and full-content modes
- JSONFeedGenerator: JSON Feed v1.1 compliant output using Encodable
  types and JSONEncoder for safe serialization (no escaping bugs)
- Both generators use format-specific paths (paths[.atom], paths[.json])
  instead of the RSS path for their self-referencing URLs
- 28 new tests covering golden path, content modes, XML escaping,
  author handling, tags, images, timezone formatting, and count limiting
- generateFeed() now loops over all enabled formats, dispatching to
  the correct generator (RSS, Atom, or JSON Feed) for each
- FeedLink renders one link per configured format with format-specific
  display names (RSS, Atom, JSON Feed) in consistent sorted order
- Backward compatible: existing sites with default config continue
  to generate only RSS at /feed.rss
- Add <link rel="alternate"> tags to <head> for each enabled feed
  format, enabling automatic feed discovery by feed readers
- Atom feeds now emit both <icon> and <logo> elements when an image
  is configured, covering both compact and expanded display contexts
- JSON Feed now emits both "icon" and "favicon" fields when an image
  is configured, matching the v1.1 spec's dual-image support
- New MetaLink.feedDiscoveryLinks(for:) generates properly typed
  alternate links with correct MIME types per format
@MrSkwiggs
Copy link
Copy Markdown
Collaborator

Amazing work @jpurnell, thanks for submitting!

I see your checklist is still missing the final step, still something you want to handle before we proceed?

JSON Feed v1.1 requires either content_html or content_text per item.
In descriptionOnly mode, items previously had only summary (which the
spec treats as distinct from content). Now sets content_text to the
article description as a fallback, ensuring every item has at least
one content field per the spec.

Validated against https://www.jsonfeed.org/version/1.1/
@jpurnell
Copy link
Copy Markdown
Contributor Author

Thanks for the catch! That final checkbox is now done.

Validated against the JSON Feed v1.1 spec — all required fields (version, title, items, item id) are present. I also found that the spec requires either content_html or content_text per item, and in descriptionOnly mode we only had summary (which the spec treats as distinct from content). Fixed in 98c7843descriptionOnly mode now sets content_text as a fallback so every item is strictly spec-compliant.

@twostraws
Copy link
Copy Markdown
Owner

Hello! This is a great set of changes – thank you!

A couple of questions:

  1. How is the new date extension different from the existing Date.asISO8601 extension we have?
  2. How is the XML escaping extension different from the existing FeedGenerator.xmlEscape() method?

Thank you!

@jpurnell
Copy link
Copy Markdown
Contributor Author

The date extension mostly just gives an offset for Time Zone…on my site, I like to have the time of the post, but it's UTC with the existing extension. I wasn't sure if changing the signature would break other people's work, and technically, Atom is looking for RFC3339, so thought it better to make that explicit.

XMLEscape is more flexible as a shared extension rather than private to the FeedGenerator in the RSS fix, so it can be used for the Atom Generator now too.

@twostraws
Copy link
Copy Markdown
Owner

The date extension mostly just gives an offset for Time Zone…on my site, I like to have the time of the post, but it's UTC with the existing extension. I wasn't sure if changing the signature would break other people's work, and technically, Atom is looking for RFC3339, so thought it better to make that explicit.

The current extension uses ISO 8601, but the specific defaults Apple chose are RFC3339. If you want to modify it to accept a time zone we can do that, but I think it should be the same extension rather than a second one. Would that be okay?

XMLEscape is more flexible as a shared extension rather than private to the FeedGenerator in the RSS fix, so it can be used for the Atom Generator now too.

In that case, we should perhaps delete the version from FeedGenerator, and make it match the format in String-EscapedForJavaScript.swift, which would mean escapedForXML() – mostly because it keeps the namespace clear, given that we're extending all strings for everyone.

@twostraws
Copy link
Copy Markdown
Owner

@jpurnell I'm just checking in: what should we do with this PR?

Per @twostraws's review on twostraws#877:

- Date: replace `Date.asRFC3339(timeZone:)` with a new
  `Date.asISO8601(timeZone:)` method on the existing extension. RFC 3339
  is the format Apple's `ISO8601DateFormatter` produces with
  `.withInternetDateTime`, so a separate name was redundant. The
  existing `var asISO8601` is preserved as a UTC convenience that
  delegates to the method, so no callers break.

- String: rename `xmlEscaped` to `escapedForXML()` and make it a public
  method, matching the namespace style of the existing
  `escapedForJavascript()` extension.

Callers in AtomFeedGenerator, JSONFeedGenerator, FeedGenerator (RSS),
and Time updated. Test files renamed and merged accordingly.
@jpurnell
Copy link
Copy Markdown
Contributor Author

Sorry…lot of life events. Made some changes:

Date.asISO8601 — Folded the timezone capability into the existing extension as asISO8601(timeZone:). Kept var asISO8601 as a UTC convenience that delegates to the method, so existing callers (including Time.swift)
keep working unchanged. Deleted the separate Date-RFC3339.swift.

String.escapedForXML() — Renamed xmlEscaped to escapedForXML() to match the escapedForJavascript() namespace convention, made it a public extension, and consolidated all the RSS/Atom call sites onto it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants