Skip to content

News Feed

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

News Feed Example

This is a comprehensive example of implementing a news feed system with multiple categories, breaking news alerts, live updates, and media-rich content.

News Feed Architecture

A news website typically needs:

  • Breaking news feeds with high update frequency
  • Category-based news feeds (Politics, Sports, Technology, etc.)
  • Regional/geographical news feeds
  • Media-rich content with images and videos
  • Real-time updates and notifications
  • SEO-optimized feed distribution

Database Schema

-- News articles table
CREATE TABLE news_articles (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    headline VARCHAR(500),
    content TEXT NOT NULL,
    excerpt TEXT,
    author_id INT NOT NULL,
    editor_id INT,
    featured_image VARCHAR(255),
    video_url VARCHAR(255),
    audio_url VARCHAR(255),
    source_url VARCHAR(255),
    is_breaking BOOLEAN DEFAULT FALSE,
    is_featured BOOLEAN DEFAULT FALSE,
    published BOOLEAN DEFAULT FALSE,
    published_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    priority INT DEFAULT 1,
    view_count INT DEFAULT 0,
    INDEX idx_published (published, published_at),
    INDEX idx_breaking (is_breaking, published_at),
    INDEX idx_featured (is_featured, published_at),
    INDEX idx_priority (priority, published_at),
    INDEX idx_author (author_id),
    FULLTEXT idx_content (title, headline, content)
);

-- News categories
CREATE TABLE news_categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    color VARCHAR(7), -- Hex color code
    icon VARCHAR(50),
    sort_order INT DEFAULT 0,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Article-Category relationship
CREATE TABLE article_categories (
    article_id INT NOT NULL,
    category_id INT NOT NULL,
    is_primary BOOLEAN DEFAULT FALSE,
    PRIMARY KEY (article_id, category_id),
    FOREIGN KEY (article_id) REFERENCES news_articles(id) ON DELETE CASCADE,
    FOREIGN KEY (category_id) REFERENCES news_categories(id) ON DELETE CASCADE
);

-- News regions/locations
CREATE TABLE news_regions (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    country_code CHAR(2),
    timezone VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Article-Region relationship
CREATE TABLE article_regions (
    article_id INT NOT NULL,
    region_id INT NOT NULL,
    PRIMARY KEY (article_id, region_id),
    FOREIGN KEY (article_id) REFERENCES news_articles(id) ON DELETE CASCADE,
    FOREIGN KEY (region_id) REFERENCES news_regions(id) ON DELETE CASCADE
);

-- Journalists/Authors
CREATE TABLE journalists (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    bio TEXT,
    avatar VARCHAR(255),
    twitter_handle VARCHAR(50),
    linkedin_url VARCHAR(255),
    specialization VARCHAR(255),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- News tags
CREATE TABLE news_tags (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Article-Tag relationship
CREATE TABLE article_tags (
    article_id INT NOT NULL,
    tag_id INT NOT NULL,
    PRIMARY KEY (article_id, tag_id),
    FOREIGN KEY (article_id) REFERENCES news_articles(id) ON DELETE CASCADE,
    FOREIGN KEY (tag_id) REFERENCES news_tags(id) ON DELETE CASCADE
);

Plain PHP News Feed Implementation

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

use Rumenx\Feed\FeedFactory;

class NewsFeedManager
{
    private PDO $pdo;
    private string $baseUrl;
    private string $siteName;
    private array $defaultCategories;

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

    public function generateMainNewsFeed(int $limit = 30): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName . ' - Latest News');
        $feed->setDescription('Breaking news and latest updates from ' . $this->siteName);
        $feed->setLink($this->baseUrl);

        // Get latest news with priority ordering
        $articles = $this->getLatestNews($limit);

        foreach ($articles as $article) {
            $categories = $this->getArticleCategories($article['id']);
            $regions = $this->getArticleRegions($article['id']);
            $tags = $this->getArticleTags($article['id']);

            $item = [
                'title' => $article['title'],
                'author' => $article['author_name'],
                'link' => $this->baseUrl . '/news/' . $article['slug'],
                'pubdate' => $article['published_at'],
                'description' => $article['excerpt'] ?: $this->generateExcerpt($article['content']),
                'content' => $article['content'],
                'category' => array_merge($categories, $regions, $tags),
                'guid' => $this->baseUrl . '/news/' . $article['slug']
            ];

            // Add breaking news prefix
            if ($article['is_breaking']) {
                $item['title'] = '[BREAKING] ' . $item['title'];
            }

            // Add media enclosures
            if ($article['featured_image']) {
                $item['enclosure'] = [
                    'url' => $this->baseUrl . '/' . $article['featured_image'],
                    'type' => 'image/jpeg',
                    'length' => $this->getFileSize($article['featured_image'])
                ];
            }

            if ($article['video_url']) {
                $item['media'] = [
                    'url' => $article['video_url'],
                    'type' => 'video/mp4',
                    'medium' => 'video'
                ];
            }

            $feed->addItem($item);
        }

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

    public function generateBreakingNewsFeed(int $limit = 10): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName . ' - Breaking News');
        $feed->setDescription('Breaking news alerts from ' . $this->siteName);
        $feed->setLink($this->baseUrl . '/breaking');

        $articles = $this->getBreakingNews($limit);

        foreach ($articles as $article) {
            $categories = $this->getArticleCategories($article['id']);

            $feed->addItem([
                'title' => '[BREAKING] ' . $article['title'],
                'author' => $article['author_name'],
                'link' => $this->baseUrl . '/news/' . $article['slug'],
                'pubdate' => $article['published_at'],
                'description' => $article['headline'] ?: $article['excerpt'],
                'content' => $article['content'],
                'category' => array_merge(['Breaking News'], $categories),
                'guid' => $this->baseUrl . '/news/' . $article['slug'],
                'priority' => 'high'
            ]);
        }

        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'] . ' News');
        $feed->setDescription($category['description'] ?: 'Latest ' . $category['name'] . ' news');
        $feed->setLink($this->baseUrl . '/category/' . $categorySlug);

        $articles = $this->getNewsByCategory($category['id'], $limit);

        foreach ($articles as $article) {
            $item = [
                'title' => $article['title'],
                'author' => $article['author_name'],
                'link' => $this->baseUrl . '/news/' . $article['slug'],
                'pubdate' => $article['published_at'],
                'description' => $article['excerpt'] ?: $this->generateExcerpt($article['content']),
                'content' => $article['content'],
                'category' => [$category['name']],
                'guid' => $this->baseUrl . '/news/' . $article['slug']
            ];

            if ($article['is_breaking']) {
                $item['title'] = '[BREAKING] ' . $item['title'];
            }

            $feed->addItem($item);
        }

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

    public function generateRegionalFeed(string $regionSlug, int $limit = 20): string
    {
        $region = $this->getRegionBySlug($regionSlug);
        if (!$region) {
            throw new InvalidArgumentException("Region '{$regionSlug}' not found");
        }

        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName . ' - ' . $region['name'] . ' News');
        $feed->setDescription('Latest news from ' . $region['name']);
        $feed->setLink($this->baseUrl . '/region/' . $regionSlug);

        $articles = $this->getNewsByRegion($region['id'], $limit);

        foreach ($articles as $article) {
            $categories = $this->getArticleCategories($article['id']);

            $feed->addItem([
                'title' => $article['title'],
                'author' => $article['author_name'],
                'link' => $this->baseUrl . '/news/' . $article['slug'],
                'pubdate' => $article['published_at'],
                'description' => $article['excerpt'] ?: $this->generateExcerpt($article['content']),
                'content' => $article['content'],
                'category' => array_merge([$region['name']], $categories),
                'guid' => $this->baseUrl . '/news/' . $article['slug']
            ]);
        }

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

    public function generateJournalistFeed(int $journalistId, int $limit = 20): string
    {
        $journalist = $this->getJournalistById($journalistId);
        if (!$journalist) {
            throw new InvalidArgumentException("Journalist with ID '{$journalistId}' not found");
        }

        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName . ' - Articles by ' . $journalist['name']);
        $feed->setDescription('Latest articles by ' . $journalist['name']);
        $feed->setLink($this->baseUrl . '/journalist/' . $journalistId);

        $articles = $this->getNewsByJournalist($journalistId, $limit);

        foreach ($articles as $article) {
            $categories = $this->getArticleCategories($article['id']);

            $feed->addItem([
                'title' => $article['title'],
                'author' => $journalist['name'],
                'link' => $this->baseUrl . '/news/' . $article['slug'],
                'pubdate' => $article['published_at'],
                'description' => $article['excerpt'] ?: $this->generateExcerpt($article['content']),
                'content' => $article['content'],
                'category' => $categories,
                'guid' => $this->baseUrl . '/news/' . $article['slug']
            ]);
        }

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

    public function generateTopStoriesFeed(int $hours = 24, int $limit = 15): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName . ' - Top Stories');
        $feed->setDescription('Most viewed stories from the last ' . $hours . ' hours');
        $feed->setLink($this->baseUrl . '/top-stories');

        $articles = $this->getTopStories($hours, $limit);

        foreach ($articles as $article) {
            $categories = $this->getArticleCategories($article['id']);

            $feed->addItem([
                'title' => $article['title'],
                'author' => $article['author_name'],
                'link' => $this->baseUrl . '/news/' . $article['slug'],
                'pubdate' => $article['published_at'],
                'description' => $article['excerpt'] ?: $this->generateExcerpt($article['content']),
                'content' => $article['content'],
                'category' => $categories,
                'guid' => $this->baseUrl . '/news/' . $article['slug'],
                'views' => $article['view_count']
            ]);
        }

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

    public function generateMediaRichFeed(int $limit = 20): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle($this->siteName . ' - Media News');
        $feed->setDescription('News with photos, videos, and audio content');
        $feed->setLink($this->baseUrl . '/media');

        $articles = $this->getMediaRichNews($limit);

        foreach ($articles as $article) {
            $categories = $this->getArticleCategories($article['id']);

            $item = [
                'title' => $article['title'],
                'author' => $article['author_name'],
                'link' => $this->baseUrl . '/news/' . $article['slug'],
                'pubdate' => $article['published_at'],
                'description' => $article['excerpt'] ?: $this->generateExcerpt($article['content']),
                'content' => $article['content'],
                'category' => $categories,
                'guid' => $this->baseUrl . '/news/' . $article['slug']
            ];

            // Add media enclosures
            if ($article['featured_image']) {
                $item['enclosure'] = [
                    'url' => $this->baseUrl . '/' . $article['featured_image'],
                    'type' => 'image/jpeg',
                    'length' => $this->getFileSize($article['featured_image'])
                ];
            }

            if ($article['video_url']) {
                $item['media'] = [
                    'url' => $article['video_url'],
                    'type' => 'video/mp4',
                    'medium' => 'video'
                ];
            }

            if ($article['audio_url']) {
                $item['audio'] = [
                    'url' => $article['audio_url'],
                    'type' => 'audio/mpeg',
                    'medium' => 'audio'
                ];
            }

            $feed->addItem($item);
        }

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

    // Database query methods
    private function getLatestNews(int $limit): array
    {
        $sql = "
            SELECT 
                na.id, na.title, na.slug, na.content, na.excerpt, na.headline,
                na.published_at, na.featured_image, na.video_url, na.audio_url,
                na.is_breaking, na.is_featured, na.priority, na.view_count,
                j.name as author_name
            FROM news_articles na
            JOIN journalists j ON na.author_id = j.id
            WHERE na.published = 1 AND na.published_at <= NOW()
            ORDER BY 
                na.is_breaking DESC,
                na.priority DESC,
                na.published_at DESC
            LIMIT ?
        ";

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

    private function getBreakingNews(int $limit): array
    {
        $sql = "
            SELECT 
                na.id, na.title, na.slug, na.content, na.excerpt, na.headline,
                na.published_at, na.featured_image,
                j.name as author_name
            FROM news_articles na
            JOIN journalists j ON na.author_id = j.id
            WHERE na.published = 1 AND na.is_breaking = 1 AND na.published_at <= NOW()
            ORDER BY na.published_at DESC
            LIMIT ?
        ";

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

    private function getNewsByCategory(int $categoryId, int $limit): array
    {
        $sql = "
            SELECT 
                na.id, na.title, na.slug, na.content, na.excerpt,
                na.published_at, na.is_breaking,
                j.name as author_name
            FROM news_articles na
            JOIN journalists j ON na.author_id = j.id
            JOIN article_categories ac ON na.id = ac.article_id
            WHERE na.published = 1 AND na.published_at <= NOW()
            AND ac.category_id = ?
            ORDER BY na.published_at DESC
            LIMIT ?
        ";

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

    private function getNewsByRegion(int $regionId, int $limit): array
    {
        $sql = "
            SELECT 
                na.id, na.title, na.slug, na.content, na.excerpt,
                na.published_at, na.is_breaking,
                j.name as author_name
            FROM news_articles na
            JOIN journalists j ON na.author_id = j.id
            JOIN article_regions ar ON na.id = ar.article_id
            WHERE na.published = 1 AND na.published_at <= NOW()
            AND ar.region_id = ?
            ORDER BY na.published_at DESC
            LIMIT ?
        ";

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

    private function getNewsByJournalist(int $journalistId, int $limit): array
    {
        $sql = "
            SELECT 
                na.id, na.title, na.slug, na.content, na.excerpt,
                na.published_at, na.is_breaking
            FROM news_articles na
            WHERE na.published = 1 AND na.published_at <= NOW()
            AND na.author_id = ?
            ORDER BY na.published_at DESC
            LIMIT ?
        ";

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

    private function getTopStories(int $hours, int $limit): array
    {
        $sql = "
            SELECT 
                na.id, na.title, na.slug, na.content, na.excerpt,
                na.published_at, na.view_count,
                j.name as author_name
            FROM news_articles na
            JOIN journalists j ON na.author_id = j.id
            WHERE na.published = 1 
            AND na.published_at <= NOW()
            AND na.published_at >= DATE_SUB(NOW(), INTERVAL ? HOUR)
            ORDER BY na.view_count DESC, na.published_at DESC
            LIMIT ?
        ";

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

    private function getMediaRichNews(int $limit): array
    {
        $sql = "
            SELECT 
                na.id, na.title, na.slug, na.content, na.excerpt,
                na.published_at, na.featured_image, na.video_url, na.audio_url,
                j.name as author_name
            FROM news_articles na
            JOIN journalists j ON na.author_id = j.id
            WHERE na.published = 1 AND na.published_at <= NOW()
            AND (na.featured_image IS NOT NULL OR na.video_url IS NOT NULL OR na.audio_url IS NOT NULL)
            ORDER BY na.published_at DESC
            LIMIT ?
        ";

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

    private function getArticleCategories(int $articleId): array
    {
        $sql = "
            SELECT nc.name
            FROM news_categories nc
            JOIN article_categories ac ON nc.id = ac.category_id
            WHERE ac.article_id = ?
            ORDER BY ac.is_primary DESC, nc.name
        ";

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

    private function getArticleRegions(int $articleId): array
    {
        $sql = "
            SELECT nr.name
            FROM news_regions nr
            JOIN article_regions ar ON nr.id = ar.region_id
            WHERE ar.article_id = ?
        ";

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

    private function getArticleTags(int $articleId): array
    {
        $sql = "
            SELECT nt.name
            FROM news_tags nt
            JOIN article_tags at ON nt.id = at.tag_id
            WHERE at.article_id = ?
        ";

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

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

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

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

    private function getActiveCategories(): array
    {
        $stmt = $this->pdo->query("SELECT * FROM news_categories WHERE is_active = 1 ORDER BY sort_order, name");
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

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

    private function getFileSize(string $filePath): int
    {
        $fullPath = $_SERVER['DOCUMENT_ROOT'] . '/' . ltrim($filePath, '/');
        return file_exists($fullPath) ? filesize($fullPath) : 0;
    }
}

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

    $newsFeed = new NewsFeedManager($pdo, 'https://news-site.com', 'News Site');

    $path = $_SERVER['REQUEST_URI'] ?? '/';
    
    header('Content-Type: application/rss+xml; charset=utf-8');
    header('Cache-Control: public, max-age=300'); // 5 minutes cache for news

    if ($path === '/feed.xml' || $path === '/feed') {
        echo $newsFeed->generateMainNewsFeed();
    } elseif ($path === '/feed/breaking.xml') {
        echo $newsFeed->generateBreakingNewsFeed();
    } elseif (preg_match('/^\/feed\/category\/([^\/]+)\.xml$/', $path, $matches)) {
        echo $newsFeed->generateCategoryFeed($matches[1]);
    } elseif (preg_match('/^\/feed\/region\/([^\/]+)\.xml$/', $path, $matches)) {
        echo $newsFeed->generateRegionalFeed($matches[1]);
    } elseif (preg_match('/^\/feed\/journalist\/(\d+)\.xml$/', $path, $matches)) {
        echo $newsFeed->generateJournalistFeed((int)$matches[1]);
    } elseif ($path === '/feed/top-stories.xml') {
        echo $newsFeed->generateTopStoriesFeed();
    } elseif ($path === '/feed/media.xml') {
        echo $newsFeed->generateMediaRichFeed();
    } 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('News feed error: ' . $e->getMessage());
}

Laravel News Feed Implementation

Eloquent Models

<?php
// app/Models/NewsArticle.php

namespace App\Models;

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

class NewsArticle extends Model
{
    protected $table = 'news_articles';

    protected $fillable = [
        'title', 'slug', 'headline', 'content', 'excerpt', 'author_id',
        'featured_image', 'video_url', 'audio_url', 'source_url',
        'is_breaking', 'is_featured', 'published', 'published_at', 'priority'
    ];

    protected $casts = [
        'is_breaking' => 'boolean',
        'is_featured' => 'boolean',
        'published' => 'boolean',
        'published_at' => 'datetime',
        'priority' => 'integer'
    ];

    public function author(): BelongsTo
    {
        return $this->belongsTo(Journalist::class, 'author_id');
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(NewsCategory::class, 'article_categories', 'article_id', 'category_id')
                    ->withPivot('is_primary');
    }

    public function regions(): BelongsToMany
    {
        return $this->belongsToMany(NewsRegion::class, 'article_regions', 'article_id', 'region_id');
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(NewsTag::class, 'article_tags', 'article_id', 'tag_id');
    }

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

    public function scopeBreaking($query)
    {
        return $query->where('is_breaking', true);
    }

    public function scopeFeatured($query)
    {
        return $query->where('is_featured', true);
    }

    public function scopeOrderByPriority($query)
    {
        return $query->orderBy('is_breaking', 'desc')
                    ->orderBy('priority', 'desc')
                    ->orderBy('published_at', 'desc');
    }

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

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

Laravel News Feed Controller

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

namespace App\Http\Controllers;

use App\Models\NewsArticle;
use App\Models\NewsCategory;
use App\Models\NewsRegion;
use App\Models\Journalist;
use Illuminate\Http\Controller;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Rumenx\Feed\FeedFactory;

class NewsFeedController extends Controller
{
    public function main(): Response
    {
        $feedXml = Cache::remember('news:feed:main', now()->addMinutes(5), function () {
            return $this->generateMainNewsFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    public function breaking(): Response
    {
        $feedXml = Cache::remember('news:feed:breaking', now()->addMinutes(2), function () {
            return $this->generateBreakingNewsFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    public function category(NewsCategory $category): Response
    {
        $feedXml = Cache::remember("news:feed:category:{$category->slug}", now()->addMinutes(10), function () use ($category) {
            return $this->generateCategoryFeed($category);
        });

        return $this->responseWithXml($feedXml);
    }

    public function region(NewsRegion $region): Response
    {
        $feedXml = Cache::remember("news:feed:region:{$region->slug}", now()->addMinutes(10), function () use ($region) {
            return $this->generateRegionalFeed($region);
        });

        return $this->responseWithXml($feedXml);
    }

    public function journalist(Journalist $journalist): Response
    {
        $feedXml = Cache::remember("news:feed:journalist:{$journalist->id}", now()->addMinutes(30), function () use ($journalist) {
            return $this->generateJournalistFeed($journalist);
        });

        return $this->responseWithXml($feedXml);
    }

    public function topStories(): Response
    {
        $feedXml = Cache::remember('news:feed:top-stories', now()->addMinutes(15), function () {
            return $this->generateTopStoriesFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    public function media(): Response
    {
        $feedXml = Cache::remember('news:feed:media', now()->addMinutes(10), function () {
            return $this->generateMediaRichFeed();
        });

        return $this->responseWithXml($feedXml);
    }

    private function generateMainNewsFeed(): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - Latest News');
        $feed->setDescription('Breaking news and latest updates');
        $feed->setLink(url('/'));

        $articles = NewsArticle::with(['author', 'categories', 'regions', 'tags'])
            ->published()
            ->orderByPriority()
            ->limit(30)
            ->get();

        foreach ($articles as $article) {
            $item = [
                'title' => $article->is_breaking ? '[BREAKING] ' . $article->title : $article->title,
                'author' => $article->author->name,
                'link' => $article->url,
                'pubdate' => $article->published_at,
                'description' => $article->excerpt,
                'content' => $article->content,
                'category' => $this->buildCategoriesList($article),
                'guid' => $article->url
            ];

            if ($article->featured_image) {
                $item['enclosure'] = [
                    'url' => asset($article->featured_image),
                    'type' => 'image/jpeg',
                    'length' => 0
                ];
            }

            $feed->addItem($item);
        }

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

    private function generateBreakingNewsFeed(): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - Breaking News');
        $feed->setDescription('Breaking news alerts');
        $feed->setLink(url('/breaking'));

        $articles = NewsArticle::with(['author', 'categories'])
            ->published()
            ->breaking()
            ->latest('published_at')
            ->limit(10)
            ->get();

        foreach ($articles as $article) {
            $feed->addItem([
                'title' => '[BREAKING] ' . $article->title,
                'author' => $article->author->name,
                'link' => $article->url,
                'pubdate' => $article->published_at,
                'description' => $article->headline ?: $article->excerpt,
                'content' => $article->content,
                'category' => array_merge(['Breaking News'], $article->categories->pluck('name')->toArray()),
                'guid' => $article->url,
                'priority' => 'high'
            ]);
        }

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

    private function generateCategoryFeed(NewsCategory $category): string
    {
        $feed = FeedFactory::create();
        $feed->setTitle(config('app.name') . ' - ' . $category->name);
        $feed->setDescription($category->description ?: 'Latest ' . $category->name . ' news');
        $feed->setLink(route('news.category', $category->slug));

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

        foreach ($articles as $article) {
            $feed->addItem([
                'title' => $article->is_breaking ? '[BREAKING] ' . $article->title : $article->title,
                'author' => $article->author->name,
                'link' => $article->url,
                'pubdate' => $article->published_at,
                'description' => $article->excerpt,
                'content' => $article->content,
                'category' => [$category->name],
                'guid' => $article->url
            ]);
        }

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

    private function buildCategoriesList(NewsArticle $article): array
    {
        $categories = [];
        
        // Add primary categories
        $categories = array_merge($categories, $article->categories->pluck('name')->toArray());
        
        // Add regions
        $categories = array_merge($categories, $article->regions->pluck('name')->toArray());
        
        // Add tags
        $categories = array_merge($categories, $article->tags->pluck('name')->toArray());
        
        return array_unique($categories);
    }

    private function responseWithXml(string $xml): Response
    {
        return response($xml)
            ->header('Content-Type', 'application/rss+xml; charset=utf-8')
            ->header('Cache-Control', 'public, max-age=300'); // 5 minutes
    }
}

Real-time News Feed Updates

WebSocket Integration

<?php

class RealTimeNewsFeed
{
    private $websocketServer;
    private $subscribers;

    public function __construct()
    {
        $this->subscribers = new \SplObjectStorage();
    }

    public function broadcastBreakingNews(array $article): void
    {
        $notification = [
            'type' => 'breaking_news',
            'title' => $article['title'],
            'link' => $article['url'],
            'timestamp' => time(),
            'priority' => 'high'
        ];

        $this->broadcastToSubscribers($notification);
        $this->updateBreakingNewsFeed($article);
    }

    public function broadcastNewsUpdate(array $article): void
    {
        $notification = [
            'type' => 'news_update',
            'title' => $article['title'],
            'link' => $article['url'],
            'categories' => $article['categories'],
            'timestamp' => time()
        ];

        $this->broadcastToSubscribers($notification);
        $this->invalidateFeedCache($article);
    }

    private function broadcastToSubscribers(array $notification): void
    {
        $message = json_encode($notification);
        
        foreach ($this->subscribers as $subscriber) {
            $subscriber->send($message);
        }
    }

    private function updateBreakingNewsFeed(array $article): void
    {
        // Immediately update breaking news feed cache
        Cache::forget('news:feed:breaking');
        
        // Pre-generate new breaking news feed
        $newsFeed = app(NewsFeedController::class);
        $feedXml = $newsFeed->generateBreakingNewsFeed();
        Cache::put('news:feed:breaking', $feedXml, now()->addMinutes(2));
    }

    private function invalidateFeedCache(array $article): void
    {
        // Clear relevant caches
        Cache::forget('news:feed:main');
        
        foreach ($article['categories'] as $category) {
            Cache::forget("news:feed:category:{$category}");
        }
        
        if (isset($article['author_id'])) {
            Cache::forget("news:feed:journalist:{$article['author_id']}");
        }
    }
}

News Feed Analytics

<?php

class NewsFeedAnalytics
{
    public static function trackFeedAccess(string $feedType, array $metadata = []): void
    {
        $data = [
            'feed_type' => $feedType,
            'user_agent' => request()->userAgent(),
            'ip_address' => request()->ip(),
            'referer' => request()->header('referer'),
            'timestamp' => now(),
            'metadata' => $metadata
        ];

        // Log to analytics service
        Log::channel('feed_analytics')->info('Feed accessed', $data);

        // Store in database for reporting
        DB::table('feed_analytics')->insert([
            'feed_type' => $feedType,
            'user_agent' => $data['user_agent'],
            'ip_address' => $data['ip_address'],
            'referer' => $data['referer'],
            'accessed_at' => $data['timestamp'],
            'metadata' => json_encode($metadata)
        ]);
    }

    public static function getPopularCategories(int $days = 30): array
    {
        return DB::table('feed_analytics')
            ->where('feed_type', 'LIKE', 'category:%')
            ->where('accessed_at', '>=', now()->subDays($days))
            ->select(DB::raw('SUBSTRING(feed_type, 10) as category_name, COUNT(*) as access_count'))
            ->groupBy('category_name')
            ->orderBy('access_count', 'desc')
            ->limit(10)
            ->get()
            ->toArray();
    }

    public static function getFeedReaderStats(): array
    {
        $userAgents = DB::table('feed_analytics')
            ->where('accessed_at', '>=', now()->subDays(30))
            ->select('user_agent', DB::raw('COUNT(*) as count'))
            ->groupBy('user_agent')
            ->orderBy('count', 'desc')
            ->get();

        $feedReaders = [];
        foreach ($userAgents as $ua) {
            $reader = 'Unknown';
            if (stripos($ua->user_agent, 'feedly') !== false) {
                $reader = 'Feedly';
            } elseif (stripos($ua->user_agent, 'newsblur') !== false) {
                $reader = 'NewsBlur';
            } elseif (stripos($ua->user_agent, 'inoreader') !== false) {
                $reader = 'Inoreader';
            }

            if (!isset($feedReaders[$reader])) {
                $feedReaders[$reader] = 0;
            }
            $feedReaders[$reader] += $ua->count;
        }

        return $feedReaders;
    }
}

News Feed SEO & Performance

SEO-Optimized Headers

<?php

class NewssSeoHeaders
{
    public static function setOptimalHeaders(string $feedType, int $cacheTime = 300): void
    {
        header('Content-Type: application/rss+xml; charset=utf-8');
        header("Cache-Control: public, max-age={$cacheTime}");
        header('Vary: Accept-Encoding, User-Agent');
        header('X-Robots-Tag: noindex, follow');
        
        // News-specific headers
        if ($feedType === 'breaking') {
            header('X-Feed-Priority: high');
            header('X-Update-Frequency: immediate');
        } else {
            header('X-Update-Frequency: hourly');
        }
    }
}

Performance Monitoring

<?php

class NewsFeedMonitor
{
    public static function monitorFeedGeneration(string $feedType, callable $generator): string
    {
        $startTime = microtime(true);
        $startMemory = memory_get_usage(true);

        try {
            $result = $generator();
            $success = true;
            $error = null;
        } catch (Exception $e) {
            $success = false;
            $error = $e->getMessage();
            $result = '';
        }

        $endTime = microtime(true);
        $endMemory = memory_get_usage(true);

        $metrics = [
            'feed_type' => $feedType,
            'execution_time' => ($endTime - $startTime) * 1000, // milliseconds
            'memory_usage' => $endMemory - $startMemory,
            'memory_peak' => memory_get_peak_usage(true),
            'success' => $success,
            'error' => $error,
            'timestamp' => now()
        ];

        // Log performance metrics
        Log::channel('feed_performance')->info('Feed generation metrics', $metrics);

        if (!$success) {
            throw new Exception($error);
        }

        return $result;
    }
}

Best Practices for News Feeds

  1. Real-time Updates: Implement WebSocket or Server-Sent Events for breaking news
  2. Cache Strategy: Short cache times (2-5 minutes) for breaking news, longer for other content
  3. Priority System: Use priority levels to ensure important news appears first
  4. Media Support: Include images, videos, and audio in feeds
  5. Mobile Optimization: Ensure feeds work well on mobile devices
  6. SEO: Optimize feed titles and descriptions for search engines
  7. Analytics: Track feed usage to understand reader preferences
  8. Performance: Monitor generation times and optimize database queries
  9. Security: Sanitize content and validate input data
  10. Standards Compliance: Follow RSS 2.0, Atom 1.0, and media RSS standards

Next Steps

Clone this wiki locally