Skip to content

Feed Links

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

Feed Links Examples

This guide shows how to generate and implement feed discovery links, enabling automatic feed detection by feed readers and browsers.

Feed Auto-Discovery

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.

Basic Feed Discovery Links

Simple RSS Discovery

<!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>

Multiple Feed Discovery

<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>

PHP Feed Link Generator

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();

Laravel Feed Links Integration

Controller Method

<?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;
    }
}

Blade Component for Feed 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

Usage in Blade Templates

{{-- 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>

Symfony Feed Links Integration

Twig Extension

<?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)
        );
    }
}

Symfony Controller

<?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'
            ]
        ];
    }
}

Twig Template Usage

{# 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>

WordPress-Style Feed Discovery

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();

Dynamic Feed Discovery

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);

Feed Link Validation

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);

Best Practices

  1. Always Include Title: Make feed titles descriptive and user-friendly
  2. Use Absolute URLs: Always use absolute URLs for feed discovery
  3. Multiple Formats: Offer both RSS and Atom when possible
  4. Category Feeds: Provide category-specific feeds for better user experience
  5. Validation: Regularly validate your feed links to ensure they work
  6. Context-Aware: Show relevant feeds based on current page context
  7. Performance: Cache feed discovery results when possible

Next Steps

Clone this wiki locally