Skip to content

Blog Feed

Rumen Damyanov edited this page Jul 29, 2025 · 1 revision

Blog Feed Example

This is a complete example of implementing a blog feed system with multiple feed types, categories, authors, and advanced features.

Complete Blog Feed Implementation

Database Schema

First, let's set up the database structure for a typical blog:

-- Posts table
CREATE TABLE posts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    content TEXT NOT NULL,
    excerpt TEXT,
    author_id INT NOT NULL,
    featured_image VARCHAR(255),
    published BOOLEAN DEFAULT FALSE,
    published_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_published (published, published_at),
    INDEX idx_author (author_id),
    INDEX idx_slug (slug)
);

-- Categories table
CREATE TABLE categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Post-Category relationship
CREATE TABLE post_categories (
    post_id INT NOT NULL,
    category_id INT NOT NULL,
    PRIMARY KEY (post_id, category_id),
    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
    FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);

-- Authors table
CREATE TABLE authors (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    bio TEXT,
    avatar VARCHAR(255),
    website VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Comments table
CREATE TABLE comments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    post_id INT NOT NULL,
    author_name VARCHAR(100) NOT NULL,
    author_email VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    approved BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
    INDEX idx_post_approved (post_id, approved),
    INDEX idx_created (created_at)
);

Plain PHP Blog Feed Implementation

Blog Feed Manager

<?php
require 'vendor/autoload.php';

use Rumenx\Feed\FeedFactory;

class BlogFeedManager
{
    private PDO $pdo;
    private string $baseUrl;
    private string $siteName;
    private array $cache = [];

    public function __construct(PDO $pdo, string $baseUrl, string $siteName)
    {
        $this->pdo = $pdo;
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->siteName = $siteName;
    }

    public function generateMainFeed(int $limit = 20): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName);
        $feed->setDescription('Latest blog posts from ' . $this->siteName);
        $feed->setLink($this->baseUrl);

        $posts = $this->getLatestPosts($limit);

        foreach ($posts as $post) {
            $feed->addItem([
                'title' => $post['title'],
                'author' => $post['author_name'],
                'link' => $this->baseUrl . '/posts/' . $post['slug'],
                'pubdate' => $post['published_at'],
                'description' => $post['excerpt'] ?: $this->generateExcerpt($post['content']),
                'content' => $post['content'],
                'category' => explode(',', $post['categories'] ?? ''),
                'guid' => $this->baseUrl . '/posts/' . $post['slug'],
                'image' => $post['featured_image'] ? $this->baseUrl . '/' . $post['featured_image'] : null
            ]);
        }

        return $feed->render('rss');
    }

    public function generateCategoryFeed(string $categorySlug, int $limit = 20): string
    {
        $category = $this->getCategoryBySlug($categorySlug);
        if (!$category) {
            throw new InvalidArgumentException("Category '{$categorySlug}' not found");
        }

        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName . ' - ' . $category['name']);
        $feed->setDescription('Latest posts from ' . $category['name'] . ' category');
        $feed->setLink($this->baseUrl . '/category/' . $categorySlug);

        $posts = $this->getPostsByCategory($category['id'], $limit);

        foreach ($posts as $post) {
            $feed->addItem([
                'title' => $post['title'],
                'author' => $post['author_name'],
                'link' => $this->baseUrl . '/posts/' . $post['slug'],
                'pubdate' => $post['published_at'],
                'description' => $post['excerpt'] ?: $this->generateExcerpt($post['content']),
                'content' => $post['content'],
                'category' => [$category['name']],
                'guid' => $this->baseUrl . '/posts/' . $post['slug']
            ]);
        }

        return $feed->render('rss');
    }

    public function generateAuthorFeed(int $authorId, int $limit = 20): string
    {
        $author = $this->getAuthorById($authorId);
        if (!$author) {
            throw new InvalidArgumentException("Author with ID '{$authorId}' not found");
        }

        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName . ' - Posts by ' . $author['name']);
        $feed->setDescription('Latest posts by ' . $author['name']);
        $feed->setLink($this->baseUrl . '/author/' . $author['id']);

        $posts = $this->getPostsByAuthor($authorId, $limit);

        foreach ($posts as $post) {
            $feed->addItem([
                'title' => $post['title'],
                'author' => $author['name'],
                'link' => $this->baseUrl . '/posts/' . $post['slug'],
                'pubdate' => $post['published_at'],
                'description' => $post['excerpt'] ?: $this->generateExcerpt($post['content']),
                'content' => $post['content'],
                'category' => explode(',', $post['categories'] ?? ''),
                'guid' => $this->baseUrl . '/posts/' . $post['slug']
            ]);
        }

        return $feed->render('rss');
    }

    public function generateCommentsFeed(int $postId = null, int $limit = 50): string
    {
        $feed = FeedFactory::create();
        
        if ($postId) {
            $post = $this->getPostById($postId);
            $feed->setTitle('Comments for: ' . $post['title']);
            $feed->setDescription('Latest comments on ' . $post['title']);
            $feed->setLink($this->baseUrl . '/posts/' . $post['slug'] . '#comments');
        } else {
            $feed->setTitle($this->siteName . ' - Latest Comments');
            $feed->setDescription('Latest comments from ' . $this->siteName);
            $feed->setLink($this->baseUrl . '/comments');
        }

        $comments = $this->getLatestComments($postId, $limit);

        foreach ($comments as $comment) {
            $feed->addItem([
                'title' => 'Comment on: ' . $comment['post_title'],
                'author' => $comment['author_name'],
                'link' => $this->baseUrl . '/posts/' . $comment['post_slug'] . '#comment-' . $comment['id'],
                'pubdate' => $comment['created_at'],
                'description' => $comment['content'],
                'guid' => $this->baseUrl . '/comments/' . $comment['id']
            ]);
        }

        return $feed->render('rss');
    }

    public function generateJsonFeed(int $limit = 20): string
    {
        $posts = $this->getLatestPosts($limit);

        $jsonFeed = [
            'version' => 'https://jsonfeed.org/version/1.1',
            'title' => $this->siteName,
            'description' => 'Latest blog posts from ' . $this->siteName,
            'home_page_url' => $this->baseUrl,
            'feed_url' => $this->baseUrl . '/feed.json',
            'language' => 'en',
            'items' => []
        ];

        foreach ($posts as $post) {
            $item = [
                'id' => $this->baseUrl . '/posts/' . $post['slug'],
                'title' => $post['title'],
                'content_html' => $post['content'],
                'summary' => $post['excerpt'] ?: $this->generateExcerpt($post['content']),
                'url' => $this->baseUrl . '/posts/' . $post['slug'],
                'date_published' => date('c', strtotime($post['published_at'])),
                'authors' => [
                    ['name' => $post['author_name']]
                ]
            ];

            if ($post['categories']) {
                $item['tags'] = explode(',', $post['categories']);
            }

            if ($post['featured_image']) {
                $item['image'] = $this->baseUrl . '/' . $post['featured_image'];
            }

            $jsonFeed['items'][] = $item;
        }

        return json_encode($jsonFeed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    private function getLatestPosts(int $limit): array
    {
        $sql = "
            SELECT 
                p.id, p.title, p.slug, p.content, p.excerpt, p.published_at, p.featured_image,
                a.name as author_name,
                GROUP_CONCAT(c.name ORDER BY c.name) as categories
            FROM posts p
            JOIN authors a ON p.author_id = a.id
            LEFT JOIN post_categories pc ON p.id = pc.post_id
            LEFT JOIN categories c ON pc.category_id = c.id
            WHERE p.published = 1 AND p.published_at <= NOW()
            GROUP BY p.id
            ORDER BY p.published_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getPostsByCategory(int $categoryId, int $limit): array
    {
        $sql = "
            SELECT 
                p.id, p.title, p.slug, p.content, p.excerpt, p.published_at, p.featured_image,
                a.name as author_name
            FROM posts p
            JOIN authors a ON p.author_id = a.id
            JOIN post_categories pc ON p.id = pc.post_id
            WHERE p.published = 1 AND p.published_at <= NOW()
            AND pc.category_id = ?
            ORDER BY p.published_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$categoryId, $limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getPostsByAuthor(int $authorId, int $limit): array
    {
        $sql = "
            SELECT 
                p.id, p.title, p.slug, p.content, p.excerpt, p.published_at, p.featured_image,
                GROUP_CONCAT(c.name ORDER BY c.name) as categories
            FROM posts p
            LEFT JOIN post_categories pc ON p.id = pc.post_id
            LEFT JOIN categories c ON pc.category_id = c.id
            WHERE p.published = 1 AND p.published_at <= NOW()
            AND p.author_id = ?
            GROUP BY p.id
            ORDER BY p.published_at DESC
            LIMIT ?
        ";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$authorId, $limit]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getLatestComments(?int $postId, int $limit): array
    {
        $sql = "
            SELECT 
                c.id, c.author_name, c.content, c.created_at,
                p.title as post_title, p.slug as post_slug
            FROM comments c
            JOIN posts p ON c.post_id = p.id
            WHERE c.approved = 1
        ";

        $params = [];
        if ($postId) {
            $sql .= " AND c.post_id = ?";
            $params[] = $postId;
        }

        $sql .= " ORDER BY c.created_at DESC LIMIT ?";
        $params[] = $limit;

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function getCategoryBySlug(string $slug): ?array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM categories WHERE slug = ?");
        $stmt->execute([$slug]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function getAuthorById(int $id): ?array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM authors WHERE id = ?");
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function getPostById(int $id): ?array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM posts WHERE id = ? AND published = 1");
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function generateExcerpt(string $content, int $length = 200): string
    {
        $text = strip_tags($content);
        if (strlen($text) <= $length) {
            return $text;
        }
        return substr($text, 0, $length) . '...';
    }
}

// Usage example
try {
    $pdo = new PDO('mysql:host=localhost;dbname=blog', $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $blogFeed = new BlogFeedManager($pdo, 'https://myblog.com', 'My Awesome Blog');

    // Route handling
    $path = $_SERVER['REQUEST_URI'] ?? '/';
    
    header('Content-Type: application/rss+xml; charset=utf-8');

    if ($path === '/feed.xml' || $path === '/feed') {
        echo $blogFeed->generateMainFeed();
    } elseif ($path === '/feed.json') {
        header('Content-Type: application/json; charset=utf-8');
        echo $blogFeed->generateJsonFeed();
    } elseif (preg_match('/^\/feed\/category\/([^\/]+)\.xml$/', $path, $matches)) {
        echo $blogFeed->generateCategoryFeed($matches[1]);
    } elseif (preg_match('/^\/feed\/author\/(\d+)\.xml$/', $path, $matches)) {
        echo $blogFeed->generateAuthorFeed((int)$matches[1]);
    } elseif ($path === '/feed/comments.xml') {
        echo $blogFeed->generateCommentsFeed();
    } elseif (preg_match('/^\/feed\/comments\/(\d+)\.xml$/', $path, $matches)) {
        echo $blogFeed->generateCommentsFeed((int)$matches[1]);
    } else {
        http_response_code(404);
        echo '<?xml version="1.0"?><error>Feed not found</error>';
    }

} catch (Exception $e) {
    http_response_code(500);
    echo '<?xml version="1.0"?><error>Feed generation failed</error>';
    error_log('Feed error: ' . $e->getMessage());
}

Laravel Blog Feed Implementation

Models

<?php
// app/Models/Post.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    protected $fillable = [
        'title', 'slug', 'content', 'excerpt', 'author_id', 
        'featured_image', 'published', 'published_at'
    ];

    protected $casts = [
        'published' => 'boolean',
        'published_at' => 'datetime'
    ];

    public function author(): BelongsTo
    {
        return $this->belongsTo(Author::class);
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class, 'post_categories');
    }

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }

    public function scopePublished($query)
    {
        return $query->where('published', true)
                    ->where('published_at', '<=', now());
    }

    public function getExcerptAttribute($value): string
    {
        return $value ?: Str::limit(strip_tags($this->content), 200);
    }

    public function getUrlAttribute(): string
    {
        return route('posts.show', $this->slug);
    }
}
<?php
// app/Models/Category.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Category extends Model
{
    protected $fillable = ['name', 'slug', 'description'];

    public function posts(): BelongsToMany
    {
        return $this->belongsToMany(Post::class, 'post_categories');
    }

    public function getUrlAttribute(): string
    {
        return route('categories.show', $this->slug);
    }
}

Laravel Feed Controller

<?php
// app/Http/Controllers/FeedController.php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\Category;
use App\Models\Author;
use App\Models\Comment;
use Illuminate\Http\Controller;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Rumenx\Feed\FeedFactory;

class FeedController extends Controller
{
    public function main(): Response
    {
        $feedXml = Cache::remember('feed:main', now()->addHour(), function () {
            return $this->generateMainFeed();
        });

        return response($feedXml)
            ->header('Content-Type', 'application/rss+xml; charset=utf-8');
    }

    public function category(string $slug): Response
    {
        $category = Category::where('slug', $slug)->firstOrFail();

        $feedXml = Cache::remember("feed:category:{$slug}", now()->addHour(), function () use ($category) {
            return $this->generateCategoryFeed($category);
        });

        return response($feedXml)
            ->header('Content-Type', 'application/rss+xml; charset=utf-8');
    }

    public function author(Author $author): Response
    {
        $feedXml = Cache::remember("feed:author:{$author->id}", now()->addHour(), function () use ($author) {
            return $this->generateAuthorFeed($author);
        });

        return response($feedXml)
            ->header('Content-Type', 'application/rss+xml; charset=utf-8');
    }

    public function comments(?Post $post = null): Response
    {
        $cacheKey = $post ? "feed:comments:post:{$post->id}" : 'feed:comments:all';
        
        $feedXml = Cache::remember($cacheKey, now()->addMinutes(15), function () use ($post) {
            return $this->generateCommentsFeed($post);
        });

        return response($feedXml)
            ->header('Content-Type', 'application/rss+xml; charset=utf-8');
    }

    public function json(): Response
    {
        $feedJson = Cache::remember('feed:json', now()->addHour(), function () {
            return $this->generateJsonFeed();
        });

        return response($feedJson)
            ->header('Content-Type', 'application/json; charset=utf-8');
    }

    private function generateMainFeed(): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name'));
        $feed->setDescription('Latest blog posts from ' . config('app.name'));
        $feed->setLink(url('/'));

        $posts = Post::with(['author', 'categories'])
            ->published()
            ->latest('published_at')
            ->limit(20)
            ->get();

        foreach ($posts as $post) {
            $feed->addItem([
                'title' => $post->title,
                'author' => $post->author->name,
                'link' => $post->url,
                'pubdate' => $post->published_at,
                'description' => $post->excerpt,
                'content' => $post->content,
                'category' => $post->categories->pluck('name')->toArray(),
                'guid' => $post->url,
                'image' => $post->featured_image ? asset($post->featured_image) : null
            ]);
        }

        return $feed->render('rss');
    }

    private function generateCategoryFeed(Category $category): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - ' . $category->name);
        $feed->setDescription('Latest posts from ' . $category->name . ' category');
        $feed->setLink($category->url);

        $posts = $category->posts()
            ->with(['author'])
            ->published()
            ->latest('published_at')
            ->limit(20)
            ->get();

        foreach ($posts as $post) {
            $feed->addItem([
                'title' => $post->title,
                'author' => $post->author->name,
                'link' => $post->url,
                'pubdate' => $post->published_at,
                'description' => $post->excerpt,
                'content' => $post->content,
                'category' => [$category->name],
                'guid' => $post->url
            ]);
        }

        return $feed->render('rss');
    }

    private function generateAuthorFeed(Author $author): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - Posts by ' . $author->name);
        $feed->setDescription('Latest posts by ' . $author->name);
        $feed->setLink(route('authors.show', $author));

        $posts = $author->posts()
            ->with(['categories'])
            ->published()
            ->latest('published_at')
            ->limit(20)
            ->get();

        foreach ($posts as $post) {
            $feed->addItem([
                'title' => $post->title,
                'author' => $author->name,
                'link' => $post->url,
                'pubdate' => $post->published_at,
                'description' => $post->excerpt,
                'content' => $post->content,
                'category' => $post->categories->pluck('name')->toArray(),
                'guid' => $post->url
            ]);
        }

        return $feed->render('rss');
    }

    private function generateCommentsFeed(?Post $post = null): string
    {
        $feed = FeedFactory::create();
        
        if ($post) {
            $feed->setTitle('Comments for: ' . $post->title);
            $feed->setDescription('Latest comments on ' . $post->title);
            $feed->setLink($post->url . '#comments');
            
            $comments = $post->comments()
                ->where('approved', true)
                ->latest()
                ->limit(50)
                ->get();
        } else {
            $feed->setTitle(config('app.name') . ' - Latest Comments');
            $feed->setDescription('Latest comments from ' . config('app.name'));
            $feed->setLink(url('/comments'));
            
            $comments = Comment::with('post')
                ->where('approved', true)
                ->latest()
                ->limit(50)
                ->get();
        }

        foreach ($comments as $comment) {
            $feed->addItem([
                'title' => 'Comment on: ' . $comment->post->title,
                'author' => $comment->author_name,
                'link' => $comment->post->url . '#comment-' . $comment->id,
                'pubdate' => $comment->created_at,
                'description' => $comment->content,
                'guid' => url('/comments/' . $comment->id)
            ]);
        }

        return $feed->render('rss');
    }

    private function generateJsonFeed(): string
    {
        $posts = Post::with(['author', 'categories'])
            ->published()
            ->latest('published_at')
            ->limit(20)
            ->get();

        $jsonFeed = [
            'version' => 'https://jsonfeed.org/version/1.1',
            'title' => config('app.name'),
            'description' => 'Latest blog posts from ' . config('app.name'),
            'home_page_url' => url('/'),
            'feed_url' => route('feeds.json'),
            'language' => 'en',
            'items' => []
        ];

        foreach ($posts as $post) {
            $item = [
                'id' => $post->url,
                'title' => $post->title,
                'content_html' => $post->content,
                'summary' => $post->excerpt,
                'url' => $post->url,
                'date_published' => $post->published_at->toISOString(),
                'authors' => [
                    ['name' => $post->author->name]
                ]
            ];

            if ($post->categories->isNotEmpty()) {
                $item['tags'] = $post->categories->pluck('name')->toArray();
            }

            if ($post->featured_image) {
                $item['image'] = asset($post->featured_image);
            }

            $jsonFeed['items'][] = $item;
        }

        return json_encode($jsonFeed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
}

Laravel Routes

<?php
// routes/web.php

use App\Http\Controllers\FeedController;

// Main feeds
Route::get('/feed.xml', [FeedController::class, 'main'])->name('feeds.main');
Route::get('/feed', [FeedController::class, 'main']);
Route::get('/feed.json', [FeedController::class, 'json'])->name('feeds.json');

// Category feeds
Route::get('/feed/category/{category:slug}.xml', [FeedController::class, 'category'])->name('feeds.category');

// Author feeds
Route::get('/feed/author/{author}.xml', [FeedController::class, 'author'])->name('feeds.author');

// Comment feeds
Route::get('/feed/comments.xml', [FeedController::class, 'comments'])->name('feeds.comments');
Route::get('/feed/comments/{post}.xml', [FeedController::class, 'comments'])->name('feeds.post_comments');

Laravel Events for Cache Invalidation

<?php
// app/Listeners/InvalidateFeedCache.php

namespace App\Listeners;

use Illuminate\Support\Facades\Cache;
use App\Events\PostPublished;
use App\Events\PostUpdated;
use App\Events\CommentApproved;

class InvalidateFeedCache
{
    public function handle(PostPublished|PostUpdated|CommentApproved $event): void
    {
        // Clear main feed cache
        Cache::forget('feed:main');
        Cache::forget('feed:json');
        
        if ($event instanceof PostPublished || $event instanceof PostUpdated) {
            $post = $event->post;
            
            // Clear author feed cache
            Cache::forget("feed:author:{$post->author_id}");
            
            // Clear category feeds
            foreach ($post->categories as $category) {
                Cache::forget("feed:category:{$category->slug}");
            }
        }
        
        if ($event instanceof CommentApproved) {
            // Clear comment feeds
            Cache::forget('feed:comments:all');
            Cache::forget("feed:comments:post:{$event->comment->post_id}");
        }
    }
}

Advanced Blog Feed Features

SEO-Optimized Feed Headers

<?php

class SeoFeedHeaders
{
    public static function setHeaders(string $feedUrl, int $cacheTime = 3600): void
    {
        header('Content-Type: application/rss+xml; charset=utf-8');
        header("Cache-Control: public, max-age={$cacheTime}");
        header('ETag: "' . md5($feedUrl . date('Y-m-d-H')) . '"');
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
        header('Vary: Accept-Encoding');
        
        // Feed discovery
        header('Link: <' . $feedUrl . '>; rel="self"; type="application/rss+xml"');
    }
}

Feed Analytics Tracking

<?php

class FeedAnalytics
{
    public static function trackFeedAccess(string $feedType, ?string $userAgent = null): void
    {
        $userAgent = $userAgent ?: ($_SERVER['HTTP_USER_AGENT'] ?? 'Unknown');
        
        // Detect feed readers
        $feedReader = 'Unknown';
        if (strpos($userAgent, 'Feedly') !== false) {
            $feedReader = 'Feedly';
        } elseif (strpos($userAgent, 'NewsBlur') !== false) {
            $feedReader = 'NewsBlur';
        } elseif (strpos($userAgent, 'Inoreader') !== false) {
            $feedReader = 'Inoreader';
        }
        
        // Log to database or analytics service
        DB::table('feed_analytics')->insert([
            'feed_type' => $feedType,
            'user_agent' => $userAgent,
            'feed_reader' => $feedReader,
            'ip_address' => request()->ip(),
            'accessed_at' => now()
        ]);
    }
}

Automatic Feed Sitemap Generation

<?php

class FeedSitemap
{
    public static function generate(): string
    {
        $feeds = [
            'https://myblog.com/feed.xml',
            'https://myblog.com/feed.json',
            'https://myblog.com/feed/comments.xml'
        ];
        
        // Add category feeds
        $categories = Category::all();
        foreach ($categories as $category) {
            $feeds[] = route('feeds.category', $category->slug);
        }
        
        // Add author feeds
        $authors = Author::all();
        foreach ($authors as $author) {
            $feeds[] = route('feeds.author', $author);
        }
        
        $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        $xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
        
        foreach ($feeds as $feed) {
            $xml .= '<url>' . "\n";
            $xml .= '<loc>' . htmlspecialchars($feed) . '</loc>' . "\n";
            $xml .= '<changefreq>daily</changefreq>' . "\n";
            $xml .= '<priority>0.8</priority>' . "\n";
            $xml .= '</url>' . "\n";
        }
        
        $xml .= '</urlset>';
        
        return $xml;
    }
}

Testing Blog Feeds

PHPUnit Tests

<?php
// tests/Feature/BlogFeedTest.php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\Post;
use App\Models\Author;
use App\Models\Category;
use Illuminate\Foundation\Testing\RefreshDatabase;

class BlogFeedTest extends TestCase
{
    use RefreshDatabase;

    public function test_main_feed_generation(): void
    {
        $author = Author::factory()->create();
        $category = Category::factory()->create();
        
        $post = Post::factory()->create([
            'author_id' => $author->id,
            'published' => true,
            'published_at' => now()
        ]);
        
        $post->categories()->attach($category);

        $response = $this->get('/feed.xml');

        $response->assertStatus(200);
        $response->assertHeader('Content-Type', 'application/rss+xml; charset=utf-8');
        
        $xml = simplexml_load_string($response->getContent());
        $this->assertEquals(config('app.name'), (string)$xml->channel->title);
        $this->assertEquals($post->title, (string)$xml->channel->item[0]->title);
    }

    public function test_category_feed_generation(): void
    {
        $category = Category::factory()->create(['slug' => 'technology']);
        $author = Author::factory()->create();
        
        $post = Post::factory()->create([
            'author_id' => $author->id,
            'published' => true,
            'published_at' => now()
        ]);
        
        $post->categories()->attach($category);

        $response = $this->get("/feed/category/{$category->slug}.xml");

        $response->assertStatus(200);
        
        $xml = simplexml_load_string($response->getContent());
        $this->assertStringContainsString($category->name, (string)$xml->channel->title);
        $this->assertEquals($post->title, (string)$xml->channel->item[0]->title);
    }

    public function test_json_feed_generation(): void
    {
        $author = Author::factory()->create();
        $post = Post::factory()->create([
            'author_id' => $author->id,
            'published' => true,
            'published_at' => now()
        ]);

        $response = $this->get('/feed.json');

        $response->assertStatus(200);
        $response->assertHeader('Content-Type', 'application/json; charset=utf-8');
        
        $json = $response->json();
        $this->assertEquals('https://jsonfeed.org/version/1.1', $json['version']);
        $this->assertEquals($post->title, $json['items'][0]['title']);
    }
}

Best Practices for Blog Feeds

  1. Performance: Always cache feed generation
  2. SEO: Include proper meta tags and feed discovery links
  3. Content: Provide both excerpt and full content options
  4. Categories: Organize feeds by categories and tags
  5. Comments: Consider providing comment feeds
  6. Analytics: Track feed usage to understand your audience
  7. Validation: Regularly validate feed XML/JSON structure
  8. Security: Sanitize content to prevent XSS
  9. Mobile: Ensure feeds work well with mobile feed readers
  10. Standards: Follow RSS 2.0, Atom 1.0, and JSON Feed specifications

Next Steps

Clone this wiki locally