os/rss-bundle
OS RSS Bundle adds RSS feed generation to Symfony applications, letting you define feeds and expose content such as news or blog posts through configurable syndication endpoints with controller and template integration.
Read this first: it is not a Laravel package. os/rss-bundle is a Symfony2 bundle. It ships an Extension/Bundle class, a Resources/config/services.yml, and Twig templates rendered through Symfony's container and templating stack. There is no service provider, no facade, no config publish. In Laravel you do not composer require it — you read it as a reference implementation and either port the ~2 files that matter or replace it with spatie/laravel-feed.
Where to look, in this order:
composer.json — the symfony/symfony and php constraints tell you the era (Symfony 2.x, PHP 5.x). This alone usually rules out coexistence with a modern Laravel app.DependencyInjection/*Extension.php and Resources/config/services.yml — where the feed generator/service is defined and what it needs injected.Controller/*.php — the action that assembles items and returns the XML response. This is the actual business logic worth copying.Resources/views/*.twig — the RSS 2.0 template (<rss>, <channel>, <item>). This is the second thing worth copying.Minimal first use case (the pattern the bundle exists to serve): given a set of Eloquent models, produce a valid RSS 2.0 feed at /feed.
In the bundle's native form (Symfony2): register the bundle in AppKernel, import its routing, point the config at your data source, hit the route.
In Laravel, the equivalent first step is a single controller action:
Route::get('/feed', function () {
$posts = Post::latest()->limit(50)->get();
return response()
->view('feed.rss', ['posts' => $posts])
->header('Content-Type', 'application/rss+xml; charset=UTF-8');
});
{{-- resources/views/feed/rss.blade.php --}}
{!! '<'.'?xml version="1.0" encoding="UTF-8"?>' !!}
<rss version="2.0">
<channel>
<title>{{ config('app.name') }}</title>
<link>{{ url('/') }}</link>
<description>Latest posts</description>
@foreach ($posts as $post)
<item>
<title>{{ $post->title }}</title>
<link>{{ route('posts.show', $post) }}</link>
<guid isPermaLink="true">{{ route('posts.show', $post) }}</guid>
<pubDate>{{ $post->published_at->toRfc2822String() }}</pubDate>
<description><![CDATA[{!! $post->excerpt !!}]]></description>
</item>
@endforeach
</channel>
</rss>
That is 90% of what the bundle gives you, and it is what you should actually ship.
Feed-as-action. The bundle's model is: one controller action → one XML view. Reproduce that as a dedicated invokable controller (FeedController) rather than stuffing it into an existing resource controller. Feeds have different caching, different headers, and different auth expectations.
Separate the item mapping from the transport. The reusable idea in the bundle is a thin item array (title, link, description, pubDate, guid) fed into a dumb template. Keep that split in Laravel so you can swap RSS for Atom or JSON Feed without touching your query layer:
class FeedBuilder
{
public function items(Collection $posts): array
{
return $posts->map(fn ($p) => [
'title' => $p->title,
'link' => route('posts.show', $p),
'guid' => route('posts.show', $p),
'pubDate' => $p->published_at->toRfc2822String(),
'description' => $p->excerpt,
])->all();
}
}
Cache aggressively and conditionally. Feeds are the most-polled, least-changing endpoint in most apps. Wrap generation in Cache::remember("feed.{$key}", 600, ...) and emit ETag / Last-Modified, returning 304 on If-None-Match. Aggressive pollers will otherwise dominate your request logs.
Scoped feeds as routes. The common day-two request is "feed for a tag/author/category." Model it as /feed/{type}/{slug} resolving to the same builder with a different query scope. Validate the type against an allowlist — never pass it into a dynamic relation name.
Content negotiation. Set Content-Type: application/rss+xml; charset=UTF-8 explicitly. Laravel's response()->view() defaults to text/html, and some readers tolerate it while browsers render the XML as text — which looks like a bug even when it isn't.
Testing pattern. Assert Content-Type, assert the body parses (simplexml_load_string returns non-false), assert item count matches published count, and assert an unpublished draft is absent. That last assertion catches the most common real bug.
If you genuinely must run Symfony-era code, isolate it: it belongs behind symfony/* version pins in a separate service, exposed to Laravel over HTTP, not loaded into the same Composer autoloader.
Adoption risk is the headline. Six stars, zero dependents, Symfony2 (EOL since 2018). There is no maintainer response path if something breaks, and the PHP/Symfony constraints will conflict with essentially any current dependency graph. Treat it as documentation, not a dependency.
Twig/Blade autoescaping breaks XML. Both engines HTML-escape by default, which is correct for <title> but wrong inside <description> when you intend markup. Use CDATA (and be aware ]]> in user content will terminate the section — sanitize it) or escape explicitly. Never use Blade's {!! !!} on user-controlled fields without stripping ]]>.
Whitespace before the XML declaration. The single most common "feed doesn't validate" cause. A stray newline, BOM, or @php block emitting output before <?xml makes the document unparseable. Note the {!! '<'.'?xml …' !!} trick above exists precisely because Blade will try to interpret a raw <?xml tag.
Date format. RSS 2.0 requires RFC 2822 (toRfc2822String()), not ISO 8601. ISO 8601 in <pubDate> is an Atom convention; many readers silently ignore the field, so your feed "works" but never sorts correctly. This is a silent-failure class of bug.
Relative URLs. <link> and <guid> must be absolute. url()/route() in Laravel do this; hardcoded paths or asset() in a queued/CLI context may not if APP_URL is unset. Set it.
Config/service aliasing quirks (if reading the Symfony side). In Symfony2 the DI extension alias had to exactly match the bundle name segment, and services were public by default; in modern Symfony they're private. If you lift services.yml into a modern app, expect "service not found" from the container. Similarly, Laravel's container has no equivalent to tagged compiler passes — the bundle's !tagged wiring does not translate.
Debugging loop. Pipe the output through xmllint --noout feed.xml in a test, or hit the W3C Feed Validator once during development. Do not eyeball XML in a browser — Chrome's viewer hides exactly the structural problems that break readers.
Extension points worth borrowing. The bundle's design lets you override the template per-app, which is the right instinct: keep the channel metadata (title, description, logo, ttl) in config, not in the template, so multiple feeds can share one view.
Recommendation. For a Laravel codebase, use spatie/laravel-feed (routes, caching, multiple feeds, well-maintained) or laminas/laminas-feed if you need programmatic Atom/RSS construction. Take from os/rss-bundle only two things: the items-array-then-template separation, and the reminder to override feed metadata per channel rather than hardcoding it.
How can I help you explore Laravel packages today?