Generating RSS Feeds and Sitemaps in Laravel

· 4 min read

RSS is not dead. In the developer community, RSS remains the gold standard for consuming content. If your blog doesn't have an RSS feed (/feed), many developers (including myself) won't subscribe.

Similarly, a Sitemap (/sitemap.xml) is the map you hand to Google's crawlers.

Here is how to implement both in a clean, minimal Laravel way.

1. The RSS Feed

We want to generate an XML response that adheres to the RSS 2.0 or Atom specification.

Route

// routes/web.php
Route::get('/feed', [FeedController::class, 'index'])->name('feed');

Controller

namespace App\Http\Controllers;

use App\Facades\Markdown;
use Illuminate\Http\Response;

class FeedController extends Controller
{
    public function index()
    {
        $posts = Markdown::getAllPosts()
            ->reject(fn($post) => $post->draft)
            ->take(20);

        $content = view('feeds.rss', [
            'posts' => $posts,
            'meta' => [
                'title' => 'My Technical Blog',
                'description' => 'Coding, Laravel, and Thoughts.',
                'link' => url('/'),
                'lang' => 'en-us',
            ]
        ])->render();

        return response($content, 200, [
            'Content-Type' => 'application/xml; charset=UTF-8',
        ]);
    }
}

The View (resources/views/feeds/rss.blade.php)

<?= '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>{{ $meta['title'] }}</title>
        <link>{{ $meta['link'] }}</link>
        <description>{{ $meta['description'] }}</description>
        <language>{{ $meta['lang'] }}</language>
        <atom:link href="{{ url('/feed') }}" rel="self" type="application/rss+xml" />
        
        @foreach($posts as $post)
        <item>
            <title><![CDATA[{{ $post->title }}]]></title>
            <link>{{ route('blog.show', $post->slug) }}</link>
            <guid isPermaLink="true">{{ route('blog.show', $post->slug) }}</guid>
            <description><![CDATA[{{ $post->description }}]]></description>
            <pubDate>{{ $post->date->toRssString() }}</pubDate>
            <author>me@example.com (Your Name)</author>
        </item>
        @endforeach
    </channel>
</rss>

Pro Tip: Laravel's Carbon instance on $post->date has a handy toRssString() method specifically for this.

2. The Sitemap

Sitemaps are simpler. They strictly list URLs and modification dates.

Controller Method

public function sitemap()
{
    $posts = Markdown::getAllPosts()->reject(fn($p) => $p->draft);
    
    return response()->view('feeds.sitemap', [
        'posts' => $posts
    ])->header('Content-Type', 'text/xml');
}

The View (resources/views/feeds/sitemap.blade.php)

<?= '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    {{-- Static Pages --}}
    <url>
        <loc>{{ url('/') }}</loc>
        <changefreq>daily</changefreq>
        <priority>1.0</priority>
    </url>
    <url>
        <loc>{{ url('/about') }}</loc>
        <changefreq>monthly</changefreq>
        <priority>0.5</priority>
    </url>

    {{-- Blog Posts --}}
    @foreach($posts as $post)
    <url>
        <loc>{{ route('blog.show', $post->slug) }}</loc>
        <lastmod>{{ $post->date->toAtomString() }}</lastmod>
        <changefreq>monthly</changefreq>
        <priority>0.8</priority>
    </url>
    @endforeach
</urlset>

Packages vs Custom

There is a great package spatie/laravel-feed that handles this configurations.

Why go custom?

  1. Dependencies: For a simple personal blog, pulling in a package just to loop over an array and print XML might be overkill.
  2. Control: You have bite-sized control over the exact output format.
  3. Learning: It's good to understand the underlying XML structure.

However, if you have multiple feed streams or complex requirements, Spatie's package is the way to go.

Final Polish

Add a link tag to your main layout <head> so browsers and RSS readers can auto-discover the feed:

<link rel="alternate" type="application/rss+xml" title="My Tech Blog RSS Feed" href="{{ route('feed') }}" />

Now your content is open to the world, consumable by bots and humans alike.

Comments