-
Notifications
You must be signed in to change notification settings - Fork 94
Feed Links
Rumen Damyanov edited this page Jul 29, 2025
·
1 revision
This guide shows how to generate and implement feed discovery links, enabling automatic feed detection by feed readers and browsers.
Feed auto-discovery allows browsers and feed readers to automatically find your RSS/Atom feeds. This is done using <link>
elements in your HTML <head>
section.
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<!-- RSS Feed Discovery -->
<link rel="alternate" type="application/rss+xml" title="My Website RSS Feed" href="https://example.com/feed.xml">
<!-- Atom Feed Discovery -->
<link rel="alternate" type="application/atom+xml" title="My Website Atom Feed" href="https://example.com/feed.atom">
</head>
<body>
<!-- Your content here -->
</body>
</html>
<head>
<!-- Main site feed -->
<link rel="alternate" type="application/rss+xml" title="My Website - All Posts" href="https://example.com/feed.xml">
<!-- Category-specific feeds -->
<link rel="alternate" type="application/rss+xml" title="My Website - Technology" href="https://example.com/feeds/technology.xml">
<link rel="alternate" type="application/rss+xml" title="My Website - Reviews" href="https://example.com/feeds/reviews.xml">
<!-- Comments feed -->
<link rel="alternate" type="application/rss+xml" title="My Website - Comments" href="https://example.com/feeds/comments.xml">
<!-- JSON Feed -->
<link rel="alternate" type="application/feed+json" title="My Website - JSON Feed" href="https://example.com/feed.json">
</head>
Create a utility class to generate feed discovery links:
<?php
require 'vendor/autoload.php';
class FeedLinkGenerator
{
private string $baseUrl;
private array $feeds;
public function __construct(string $baseUrl)
{
$this->baseUrl = rtrim($baseUrl, '/');
$this->feeds = [];
}
public function addFeed(string $title, string $url, string $type = 'application/rss+xml'): self
{
$this->feeds[] = [
'title' => $title,
'url' => $this->resolveUrl($url),
'type' => $type
];
return $this;
}
public function addRssFeed(string $title, string $url): self
{
return $this->addFeed($title, $url, 'application/rss+xml');
}
public function addAtomFeed(string $title, string $url): self
{
return $this->addFeed($title, $url, 'application/atom+xml');
}
public function addJsonFeed(string $title, string $url): self
{
return $this->addFeed($title, $url, 'application/feed+json');
}
public function generateHtml(): string
{
$html = '';
foreach ($this->feeds as $feed) {
$html .= sprintf(
'<link rel="alternate" type="%s" title="%s" href="%s">' . "\n",
htmlspecialchars($feed['type']),
htmlspecialchars($feed['title']),
htmlspecialchars($feed['url'])
);
}
return $html;
}
public function generateArray(): array
{
return $this->feeds;
}
public function generateJson(): string
{
return json_encode($this->feeds, JSON_PRETTY_PRINT);
}
private function resolveUrl(string $url): string
{
if (filter_var($url, FILTER_VALIDATE_URL)) {
return $url;
}
return $this->baseUrl . '/' . ltrim($url, '/');
}
}
// Usage example
$feedLinks = new FeedLinkGenerator('https://example.com');
$feedLinks
->addRssFeed('Main Blog Feed', '/feed.xml')
->addAtomFeed('Main Blog Feed (Atom)', '/feed.atom')
->addRssFeed('Technology Posts', '/feeds/technology.xml')
->addRssFeed('Reviews', '/feeds/reviews.xml')
->addJsonFeed('JSON Feed', '/feed.json');
// Generate HTML for <head> section
echo $feedLinks->generateHtml();
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Controller;
use Illuminate\View\View;
class PageController extends Controller
{
public function home(): View
{
$feedLinks = $this->generateFeedLinks();
return view('home', compact('feedLinks'));
}
public function blog(): View
{
$feedLinks = $this->generateBlogFeedLinks();
return view('blog.index', compact('feedLinks'));
}
private function generateFeedLinks(): array
{
return [
[
'title' => config('app.name') . ' - All Posts',
'url' => route('feeds.main'),
'type' => 'application/rss+xml'
],
[
'title' => config('app.name') . ' - Latest Comments',
'url' => route('feeds.comments'),
'type' => 'application/rss+xml'
]
];
}
private function generateBlogFeedLinks(): array
{
$categories = \App\Models\Category::where('has_feed', true)->get();
$links = [];
// Main blog feed
$links[] = [
'title' => 'All Blog Posts',
'url' => route('feeds.blog'),
'type' => 'application/rss+xml'
];
// Category feeds
foreach ($categories as $category) {
$links[] = [
'title' => 'Blog Posts - ' . $category->name,
'url' => route('feeds.category', $category->slug),
'type' => 'application/rss+xml'
];
}
return $links;
}
}
<?php
// app/View/Components/FeedLinks.php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class FeedLinks extends Component
{
public array $feeds;
public function __construct(array $feeds = [])
{
$this->feeds = $feeds;
}
public function render(): View
{
return view('components.feed-links');
}
}
{{-- resources/views/components/feed-links.blade.php --}}
@foreach($feeds as $feed)
<link rel="alternate"
type="{{ $feed['type'] }}"
title="{{ $feed['title'] }}"
href="{{ $feed['url'] }}">
@endforeach
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html>
<head>
<title>@yield('title') - {{ config('app.name') }}</title>
{{-- Feed discovery links --}}
<x-feed-links :feeds="$feedLinks ?? []" />
{{-- Or manually --}}
@isset($feedLinks)
@foreach($feedLinks as $feed)
<link rel="alternate"
type="{{ $feed['type'] }}"
title="{{ $feed['title'] }}"
href="{{ $feed['url'] }}">
@endforeach
@endisset
</head>
<body>
@yield('content')
</body>
</html>
<?php
// src/Twig/FeedLinksExtension.php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class FeedLinksExtension extends AbstractExtension
{
private UrlGeneratorInterface $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function getFunctions(): array
{
return [
new TwigFunction('feed_links', [$this, 'generateFeedLinks'], ['is_safe' => ['html']]),
new TwigFunction('feed_link', [$this, 'generateFeedLink'], ['is_safe' => ['html']]),
];
}
public function generateFeedLinks(array $feeds): string
{
$html = '';
foreach ($feeds as $feed) {
$html .= $this->generateFeedLink(
$feed['title'],
$feed['route'] ?? $feed['url'],
$feed['type'] ?? 'application/rss+xml',
$feed['parameters'] ?? []
);
}
return $html;
}
public function generateFeedLink(
string $title,
string $route,
string $type = 'application/rss+xml',
array $parameters = []
): string {
$url = filter_var($route, FILTER_VALIDATE_URL)
? $route
: $this->urlGenerator->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL);
return sprintf(
'<link rel="alternate" type="%s" title="%s" href="%s">' . "\n",
htmlspecialchars($type),
htmlspecialchars($title),
htmlspecialchars($url)
);
}
}
<?php
// src/Controller/PageController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class PageController extends AbstractController
{
#[Route('/', name: 'homepage')]
public function home(): Response
{
$feedLinks = [
[
'title' => 'Main Feed',
'route' => 'feed_main',
'type' => 'application/rss+xml'
],
[
'title' => 'News Feed',
'route' => 'feed_news',
'type' => 'application/rss+xml'
]
];
return $this->render('page/home.html.twig', [
'feed_links' => $feedLinks
]);
}
#[Route('/blog', name: 'blog_index')]
public function blog(): Response
{
$feedLinks = $this->getBlogFeedLinks();
return $this->render('blog/index.html.twig', [
'feed_links' => $feedLinks
]);
}
private function getBlogFeedLinks(): array
{
return [
[
'title' => 'All Blog Posts',
'route' => 'feed_blog',
'type' => 'application/rss+xml'
],
[
'title' => 'Blog Posts - Technology',
'route' => 'feed_category',
'parameters' => ['category' => 'technology'],
'type' => 'application/rss+xml'
],
[
'title' => 'Blog Posts - Reviews',
'route' => 'feed_category',
'parameters' => ['category' => 'reviews'],
'type' => 'application/rss+xml'
]
];
}
}
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Welcome!{% endblock %}</title>
{# Feed discovery links #}
{% if feed_links is defined %}
{{ feed_links(feed_links) }}
{% endif %}
{# Or individual feed links #}
{{ feed_link('Main RSS Feed', 'feed_main') }}
{{ feed_link('News Feed', 'feed_news', 'application/rss+xml') }}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Implement WordPress-style automatic feed discovery:
<?php
class WordPressFeedLinks
{
private string $baseUrl;
private string $title;
public function __construct(string $baseUrl, string $title)
{
$this->baseUrl = rtrim($baseUrl, '/');
$this->title = $title;
}
public function generateWordPressStyleLinks(): string
{
$links = [
// Main RSS feed
[
'title' => $this->title,
'url' => $this->baseUrl . '/feed/',
'type' => 'application/rss+xml'
],
// RDF feed
[
'title' => $this->title . ' RDF Feed',
'url' => $this->baseUrl . '/feed/rdf/',
'type' => 'application/rdf+xml'
],
// RSS2 feed
[
'title' => $this->title . ' RSS2 Feed',
'url' => $this->baseUrl . '/feed/rss2/',
'type' => 'application/rss+xml'
],
// Atom feed
[
'title' => $this->title . ' Atom Feed',
'url' => $this->baseUrl . '/feed/atom/',
'type' => 'application/atom+xml'
],
// Comments feed
[
'title' => $this->title . ' Comments Feed',
'url' => $this->baseUrl . '/comments/feed/',
'type' => 'application/rss+xml'
]
];
$html = '';
foreach ($links as $link) {
$html .= sprintf(
'<link rel="alternate" type="%s" title="%s" href="%s">' . "\n",
htmlspecialchars($link['type']),
htmlspecialchars($link['title']),
htmlspecialchars($link['url'])
);
}
return $html;
}
public function generateCommentsFeedLink(int $postId): string
{
return sprintf(
'<link rel="alternate" type="application/rss+xml" title="Comments for post %d" href="%s/comments/feed/?post_id=%d">',
$postId,
$this->baseUrl,
$postId
);
}
}
// Usage
$wpFeeds = new WordPressFeedLinks('https://myblog.com', 'My Awesome Blog');
echo $wpFeeds->generateWordPressStyleLinks();
Generate feed links dynamically based on current page:
<?php
class DynamicFeedDiscovery
{
private string $baseUrl;
private array $routes;
public function __construct(string $baseUrl, array $routes = [])
{
$this->baseUrl = rtrim($baseUrl, '/');
$this->routes = $routes;
}
public function generateContextualFeeds(string $currentPath, array $context = []): string
{
$feeds = [];
// Always include main feed
$feeds[] = [
'title' => 'Main Feed',
'url' => $this->baseUrl . '/feed.xml',
'type' => 'application/rss+xml'
];
// Context-specific feeds
if (strpos($currentPath, '/blog') === 0) {
$feeds = array_merge($feeds, $this->getBlogFeeds($context));
} elseif (strpos($currentPath, '/news') === 0) {
$feeds = array_merge($feeds, $this->getNewsFeeds($context));
} elseif (strpos($currentPath, '/category/') === 0) {
$feeds = array_merge($feeds, $this->getCategoryFeeds($context));
} elseif (strpos($currentPath, '/author/') === 0) {
$feeds = array_merge($feeds, $this->getAuthorFeeds($context));
}
return $this->renderFeedLinks($feeds);
}
private function getBlogFeeds(array $context): array
{
$feeds = [
[
'title' => 'Blog Posts',
'url' => $this->baseUrl . '/feeds/blog.xml',
'type' => 'application/rss+xml'
]
];
if (isset($context['category'])) {
$feeds[] = [
'title' => 'Blog Posts - ' . $context['category'],
'url' => $this->baseUrl . '/feeds/blog/' . $context['category'] . '.xml',
'type' => 'application/rss+xml'
];
}
return $feeds;
}
private function getNewsFeeds(array $context): array
{
return [
[
'title' => 'News Updates',
'url' => $this->baseUrl . '/feeds/news.xml',
'type' => 'application/rss+xml'
],
[
'title' => 'Breaking News',
'url' => $this->baseUrl . '/feeds/breaking-news.xml',
'type' => 'application/rss+xml'
]
];
}
private function getCategoryFeeds(array $context): array
{
if (!isset($context['category_slug'])) {
return [];
}
return [
[
'title' => 'Category: ' . ($context['category_name'] ?? $context['category_slug']),
'url' => $this->baseUrl . '/feeds/category/' . $context['category_slug'] . '.xml',
'type' => 'application/rss+xml'
]
];
}
private function getAuthorFeeds(array $context): array
{
if (!isset($context['author_slug'])) {
return [];
}
return [
[
'title' => 'Posts by ' . ($context['author_name'] ?? $context['author_slug']),
'url' => $this->baseUrl . '/feeds/author/' . $context['author_slug'] . '.xml',
'type' => 'application/rss+xml'
]
];
}
private function renderFeedLinks(array $feeds): string
{
$html = '';
foreach ($feeds as $feed) {
$html .= sprintf(
'<link rel="alternate" type="%s" title="%s" href="%s">' . "\n",
htmlspecialchars($feed['type']),
htmlspecialchars($feed['title']),
htmlspecialchars($feed['url'])
);
}
return $html;
}
}
// Usage
$discovery = new DynamicFeedDiscovery('https://example.com');
// On a blog category page
$currentPath = '/blog/category/technology';
$context = [
'category' => 'technology',
'category_name' => 'Technology'
];
echo $discovery->generateContextualFeeds($currentPath, $context);
Validate that your feed links are working correctly:
<?php
class FeedLinkValidator
{
private int $timeout;
public function __construct(int $timeout = 10)
{
$this->timeout = $timeout;
}
public function validateFeedLinks(array $feeds): array
{
$results = [];
foreach ($feeds as $feed) {
$results[] = $this->validateSingleFeed($feed);
}
return $results;
}
private function validateSingleFeed(array $feed): array
{
$context = stream_context_create([
'http' => [
'timeout' => $this->timeout,
'method' => 'HEAD'
]
]);
$start = microtime(true);
$headers = @get_headers($feed['url'], 1, $context);
$responseTime = (microtime(true) - $start) * 1000;
$result = [
'url' => $feed['url'],
'title' => $feed['title'],
'type' => $feed['type'],
'response_time_ms' => round($responseTime),
'status' => 'error',
'status_code' => null,
'content_type' => null,
'errors' => []
];
if ($headers === false) {
$result['errors'][] = 'Failed to connect to feed URL';
return $result;
}
// Parse status code
if (isset($headers[0])) {
preg_match('/HTTP\/\d\.\d\s+(\d+)/', $headers[0], $matches);
$result['status_code'] = isset($matches[1]) ? (int)$matches[1] : null;
}
// Check content type
$contentType = $headers['Content-Type'] ?? $headers['content-type'] ?? '';
if (is_array($contentType)) {
$contentType = $contentType[0];
}
$result['content_type'] = $contentType;
// Validate response
if ($result['status_code'] === 200) {
if ($this->isValidFeedContentType($contentType, $feed['type'])) {
$result['status'] = 'valid';
} else {
$result['status'] = 'warning';
$result['errors'][] = "Content-Type '{$contentType}' doesn't match expected type '{$feed['type']}'";
}
} else {
$result['errors'][] = "HTTP error: {$result['status_code']}";
}
return $result;
}
private function isValidFeedContentType(string $actualType, string $expectedType): bool
{
$actualType = strtolower(trim(explode(';', $actualType)[0]));
$expectedType = strtolower(trim($expectedType));
$validMappings = [
'application/rss+xml' => ['application/rss+xml', 'application/xml', 'text/xml'],
'application/atom+xml' => ['application/atom+xml', 'application/xml', 'text/xml'],
'application/feed+json' => ['application/json', 'application/feed+json', 'text/json']
];
return in_array($actualType, $validMappings[$expectedType] ?? []);
}
public function generateValidationReport(array $results): string
{
$report = "Feed Link Validation Report\n";
$report .= str_repeat("=", 50) . "\n\n";
foreach ($results as $result) {
$status = strtoupper($result['status']);
$report .= "Feed: {$result['title']}\n";
$report .= "URL: {$result['url']}\n";
$report .= "Status: {$status}\n";
$report .= "Response Time: {$result['response_time_ms']}ms\n";
if ($result['status_code']) {
$report .= "HTTP Status: {$result['status_code']}\n";
}
if ($result['content_type']) {
$report .= "Content-Type: {$result['content_type']}\n";
}
if (!empty($result['errors'])) {
$report .= "Errors:\n";
foreach ($result['errors'] as $error) {
$report .= " - {$error}\n";
}
}
$report .= "\n";
}
return $report;
}
}
// Usage
$validator = new FeedLinkValidator();
$feeds = [
['title' => 'Main Feed', 'url' => 'https://example.com/feed.xml', 'type' => 'application/rss+xml'],
['title' => 'News Feed', 'url' => 'https://example.com/news.xml', 'type' => 'application/rss+xml']
];
$results = $validator->validateFeedLinks($feeds);
echo $validator->generateValidationReport($results);
- Always Include Title: Make feed titles descriptive and user-friendly
- Use Absolute URLs: Always use absolute URLs for feed discovery
- Multiple Formats: Offer both RSS and Atom when possible
- Category Feeds: Provide category-specific feeds for better user experience
- Validation: Regularly validate your feed links to ensure they work
- Context-Aware: Show relevant feeds based on current page context
- Performance: Cache feed discovery results when possible
- Learn about Basic Feed Generation to create the feeds
- Check Multiple Feeds for managing different feed types
- See Caching for optimizing feed performance