-
Notifications
You must be signed in to change notification settings - Fork 94
News Feed
Rumen Damyanov edited this page Jul 29, 2025
·
1 revision
This is a comprehensive example of implementing a news feed system with multiple categories, breaking news alerts, live updates, and media-rich content.
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
-- 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
);
<?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());
}
<?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);
}
}
<?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
}
}
<?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']}");
}
}
}
<?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;
}
}
<?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');
}
}
}
<?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;
}
}
- Real-time Updates: Implement WebSocket or Server-Sent Events for breaking news
- Cache Strategy: Short cache times (2-5 minutes) for breaking news, longer for other content
- Priority System: Use priority levels to ensure important news appears first
- Media Support: Include images, videos, and audio in feeds
- Mobile Optimization: Ensure feeds work well on mobile devices
- SEO: Optimize feed titles and descriptions for search engines
- Analytics: Track feed usage to understand reader preferences
- Performance: Monitor generation times and optimize database queries
- Security: Sanitize content and validate input data
- Standards Compliance: Follow RSS 2.0, Atom 1.0, and media RSS standards
- Learn about Feed Links for discovery implementation
- Check Advanced Features for enterprise functionality
- See Caching for performance optimization