atelier-socle/podcast-feed-maker-vapor
Vapor middleware for serving podcast RSS feeds with caching, streaming, Podping, and Fluent integration. Built on PodcastFeedMaker.
Features
- Middleware — X-Generator header, ETag/304 HTTP caching, CORS with preflight
- Route Builder —
app.podcastFeed("feed.xml")DSL for feed routes - Streaming — Chunked XML streaming via
StreamingFeedGeneratorfor large catalogs - Pagination — Query params parsing (
?limit=N&offset=N) with safe clamping - Podping — Feed update notifications via webhook and real-time WebSocket
- Batch Audit — Parallel feed quality scoring with grades and recommendations
- Fluent Mapping — Pure Swift protocols for model-to-feed conversion
- Redis Cache — Optional
FeedCacheStoreprotocol + Redis implementation - Queue Workers — Optional background feed regeneration via Vapor Queues
- Metrics — Feed request counters, latency timers, and response size recording via swift-metrics
- Strict concurrency — All types Sendable, Swift 6.2 strict concurrency
Installation
Requirements
- Swift 6.2+ with strict concurrency
- Vapor 4.121+
- PodcastFeedMaker 0.3.0+
- Platforms: macOS 14+ · Linux (Ubuntu 22.04+)
Swift Package Manager
Add the dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/atelier-socle/podcast-feed-maker-vapor.git", from: "0.3.0")
]Four products are available — import only what you need:
// Core (required) — middleware, routes, encoding, Fluent mapping
.product(name: "PodcastFeedVapor", package: "podcast-feed-maker-vapor")
// Optional — Redis-backed feed cache
.product(name: "PodcastFeedVaporRedis", package: "podcast-feed-maker-vapor")
// Optional — Background feed regeneration jobs
.product(name: "PodcastFeedVaporQueues", package: "podcast-feed-maker-vapor")
// Optional — Feed serving metrics (Prometheus, StatsD, etc.)
.product(name: "PodcastFeedVaporMetrics", package: "podcast-feed-maker-vapor")Quick Start
import PodcastFeedVapor
func configure(_ app: Application) throws {
app.feedConfiguration = FeedConfiguration(
ttl: .hours(1),
prettyPrint: false,
generatorHeader: "MyApp/1.0"
)
app.healthCheck()
app.middleware.use(CORSFeedMiddleware())
app.middleware.use(PodcastFeedMiddleware())
app.grouped(FeedCacheMiddleware()).podcastFeed("feed.xml") { req in
try await loadFeed(on: req.db)
}
}Key Concepts
Middleware Stack
Three middlewares add production headers to feed responses. Stack them in order: CORS (outermost), generator header, cache (innermost):
app.middleware.use(CORSFeedMiddleware())
app.middleware.use(PodcastFeedMiddleware())
app.grouped(FeedCacheMiddleware(ttl: .hours(2)))
.podcastFeed("feed.xml") { _ in feed }Feed Routes
Register feed routes with the podcastFeed() DSL. Supports static paths and dynamic parameters:
app.podcastFeed("feed.xml") { _ in feed }
app.podcastFeed("shows", ":showId", "feed.xml") { req in
let showId = req.parameters.get("showId") ?? "unknown"
return try await loadFeed(for: showId, on: req.db)
}HTTP Caching
FeedCacheMiddleware adds ETag (SHA256), Last-Modified, and Cache-Control headers. Returns 304 Not Modified when the client's cached version is still valid:
app.grouped(FeedCacheMiddleware(ttl: .minutes(15)))
.podcastFeed("feed.xml") { _ in feed }Fluent Mapping
Three protocols map your types to feeds — no Fluent dependency required:
extension Show: FeedMappable {
func toPodcastFeed() -> PodcastFeed { ... }
}
extension Episode: ItemMappable {
func toItem() -> Item { ... }
}
let items = episodes.toItems() // Array extensionPodping
Webhook notifications via PodpingNotifier and real-time WebSocket via PodpingWebSocketManager:
// Webhook (fire-and-forget)
let notifier = PodpingNotifier(client: req.client, authToken: "token")
try await notifier.notify(feedURL: "https://example.com/feed.xml")
// WebSocket (real-time, persistent)
app.podpingWebSocket("podping")
await app.podpingWebSocketManager.broadcast(
feedURL: "https://example.com/feed.xml"
)Batch Audit
Score feed quality in parallel:
app.batchAudit("feeds", "audit")
// GET /feeds/audit?urls=https://a.com/feed.xml,https://b.com/feed.xmlMetrics
Optional target for feed serving observability. Uses Apple's swift-metrics API — plug in any backend:
import PodcastFeedVaporMetrics
// Add metrics before other feed middleware
app.middleware.use(FeedMetricsMiddleware())
app.middleware.use(PodcastFeedMiddleware())Emits: pfv_feed_requests_total (counter), pfv_feed_request_duration_seconds (timer), pfv_feed_response_size_bytes (recorder), pfv_feed_active_streams (gauge).
Redis and Queues
Optional targets for production scale — no Redis/Queues dependency in core:
// Redis cache
import PodcastFeedVaporRedis
let cache = app.redisFeedCache(keyPrefix: "myapp:feed:", defaultTTL: 600)
try await cache.set(identifier: "show-123", xml: feedXML, ttl: 300)
// Queue workers
import PodcastFeedVaporQueues
app.registerFeedRegenerationJob(handler: MyFeedRegenerator())
try await req.queue.dispatch(
FeedRegenerationJob.self,
FeedRegenerationPayload(feedIdentifier: "show-123", reason: "episode_added")
)Architecture
Sources/
PodcastFeedVapor/ # Core (Vapor + PodcastFeedMaker + Fluent)
PodcastFeedMiddleware.swift
FeedCacheMiddleware.swift
CORSFeedMiddleware.swift
FeedRouteBuilder.swift
FeedResponseEncoder.swift
FeedConfiguration.swift
FeedPagination.swift
StreamingFeedResponse.swift
FluentFeedMapping.swift
PodpingNotifier.swift
PodpingMessage.swift
PodpingWebSocketManager.swift
PodpingWebSocketRoute.swift
BatchAuditEndpoint.swift
HealthCheck.swift
Extensions/
Request+Feed.swift
Response+XML.swift
PodcastFeedVaporRedis/ # Optional Redis cache
RedisFeedCache.swift
PodcastFeedVaporQueues/ # Optional queue workers
FeedRegenerationJob.swift
PodcastFeedVaporMetrics/ # Optional metrics middleware
FeedMetricsMiddleware.swift
FeedMetricsConfiguration.swift
FeedActiveStreamsGauge.swiftSample Server
A companion test server demonstrates all features with real HTTP endpoints. Run the setup script to scaffold it as a sibling directory:
./scripts/setup-sample-server.sh
cd ../sample-vapor-server
swift runOr specify a custom path:
./scripts/setup-sample-server.sh /path/to/my-test-serverThen test with curl:
curl http://localhost:8080/health
curl http://localhost:8080/feed.xml
curl -sI http://localhost:8080/feed.xml | grep etag
curl "http://localhost:8080/feeds/audit?urls=https://feeds.simplecast.com/54nAGcIl"See scripts/setup-sample-server.sh for the full list of endpoints and test scenarios.
Roadmap
- [x] Streaming cache — Stream-through caching for very large feeds
- [x] Metrics — Prometheus/StatsD middleware for feed serving metrics
- [x] WebSocket Podping — Real-time podping via WebSocket instead of webhook
Specification References
Documentation
Full API documentation is available as a DocC catalog hosted on GitHub Pages and bundled with the package. Open the project in Xcode and select Product > Build Documentation to browse it locally.
Guides:
| Guide | Content | |-------|---------| | Getting Started | Installation and first feed route | | Middleware Guide | Caching, CORS, and generator header | | Feed Serving Guide | Route builder DSL, streaming, and pagination | | Fluent Integration | Protocol-based model mapping | | Advanced Features | Podping, batch audit, and health check | | Redis and Queues | Optional production targets | | Metrics | Feed serving metrics via swift-metrics |
Contributing
See CONTRIBUTING.md for guidelines.
License
This project is licensed under the Apache License 2.0.
Copyright 2026 Atelier Socle SAS. See NOTICE for details.
Package Metadata
Repository: atelier-socle/podcast-feed-maker-vapor
Homepage: https://atelier-socle.com/en/solutions/podcast-feed-vapor
Stars: 1
Forks: 0
Open issues: 0
Default branch: main
Primary language: swift
License: Apache-2.0
Topics: cache, fluent, linux, middleware, podcast, podcast-namespace, podping, rss, rss-feed, server-side-swift, swift, vapor, xml
README: README.md