A major improvement to our Statamic Blog
IMPORTANT NOTE: Your mileage may very, but in our case, updating from Feedamic from version 2 - 3 WAS NOT PAINLESS. Our feeds broke and getting it to work again required a bit of work. However, the end result is a much better system. So if you find yourself in a similiar situation, maybe this will help you. Now there is a page on Feedamic documentation site that covers updating: https://docs.mity.com.au/feedamic/3/upgrade but for us, our site required a bit more work than what is covered there. So with all that being said...
Welcome to our comprehensive guide on implementing and maintaining RSS and Atom feeds using Feedamic - the powerful feed generation package for Statamic CMS.
Feedamic has become an essential tool for content syndication, allowing our Laravel application to generate clean, standards-compliant RSS and Atom feeds that can be consumed by feed readers, social media platforms, and other applications. Whether you're looking to set up feeds for the first time or maintain an existing implementation, this guide covers everything from basic configuration to advanced troubleshooting.
The Feedamic package provides a robust foundation for feed generation, but as we discovered during our upgrade to version 3.0.1, it sometimes requires custom solutions to handle edge cases and ensure optimal performance. This documentation captures our complete implementation, including the custom controller and middleware system we developed to overcome XML corruption issues and improve reliability.
In this guide, you'll find detailed explanations of our feed architecture, complete source code examples, and a comprehensive walkthrough of our upgrade process from Feedamic 2.x to 3.0.1. We've documented every change, every challenge we faced, and every solution we implemented to help you maintain a robust feeds system.
Import Parts
Here is a quick / brief overview of the different, important, elements required to get our RSS feeds to populate correctly.
1. Feedamic Package
- Package:
mitydigital/feedamic
(version 3.0.1) - Purpose: Core feed generation functionality
- Configuration: Managed through
resources/addons/feedamic.yaml
2. Custom Feed Controller
- File:
app/Http/Controllers/CustomFeedController.php
- Purpose: Bypasses problematic XML processing in Feedamic package
- Routes: Handles
/feed/atom
and/feed
endpoints
3. Middleware System
- File:
app/Http/Middleware/InterceptFeedamic.php
- Purpose: Intercepts feed routes and redirects to custom controller
- Integration: Registered in
app/Http/Kernel.php
4. Error Suppression (IMPORTANT)
- File:
public/index.php
- Purpose: Prevents deprecation warnings from corrupting XML output
- Scope: Applied specifically to feed routes
PART 1 | Feedamic Package + Config
Some of the main things that are set up with the new feedamic.yaml file are the same as when you were using the old config/feedamic.php file.
-
Routes: Define the URL endpoints for feeds
- Atom feed:
/feed/atom
- RSS feed:
/feed
- Atom feed:
-
Collections: Specify which content collections to include
blog
: Main blog postsinterviews
: Interview content
-
Content Mapping: Define how content fields are mapped to feed elements
- Summary: Uses
introduction
,notes
, andcontent
fields - Content: Uses
introduction
,content
, andtranscript
fields
- Summary: Uses
-
Author Configuration: Define how author information is handled
- Model:
MityDigital\Feedamic\Models\FeedamicAuthor
- Fallback:
Huement.com Blog
with emailpublic@huement.com
- Model:
PART 2 | Custom Feed Controller
File: app/Http/Controllers/CustomFeedController.php
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\Response;
use MityDigital\Feedamic\Facades\Feedamic;
use Statamic\Facades\Site;
class CustomFeedController extends Controller
{
public function atom()
{
// Suppress all errors to prevent XML corruption
$oldErrorReporting = error_reporting(0);
ini_set('display_errors', 0);
ini_set('log_errors', 0);
// Set a custom error handler to completely suppress warnings
set_error_handler(function ($severity, $message, $file, $line) {
return true; // Suppress all errors
});
try {
\Log::info('CustomFeedController: atom() method called');
$config = Feedamic::getConfig('/feed/atom', 'default');
if (!$config) {
\Log::info('CustomFeedController: No config found for /feed/atom');
abort(404);
}
$entries = Feedamic::getEntries($config);
// Generate XML directly without XMLReader/XMLWriter processing
$xml = view('feedamic::atom', [
'id' => request()->url(),
'config' => $config,
'entries' => $entries,
'site' => Site::current(),
'updated' => Carbon::now(),
'url' => request()->url(),
])->render();
// Clean up any XML corruption from deprecation warnings
$xml = preg_replace('/^[^<]*/', '', $xml); // Remove anything before the first <
$xml = trim($xml);
return response($xml, 200, [
'Content-Type' => 'application/xml; charset=utf-8',
'Cache-Control' => 'public, max-age=3600',
]);
} catch (\Exception $e) {
// Return a basic feed structure if there's an error
$basicFeed =
'<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
<id>' .
request()->url() .
'</id>
<title type="text">Huement Blog Main Feed</title>
<subtitle type="text">Feed temporarily unavailable</subtitle>
<updated>' .
Carbon::now()->toRfc3339String() .
'</updated>
</feed>';
return response($basicFeed, 200, [
'Content-Type' => 'application/xml; charset=utf-8',
]);
} finally {
// Restore original error reporting
error_reporting($oldErrorReporting);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
restore_error_handler();
}
}
public function rss()
{
// Suppress all errors to prevent XML corruption
$oldErrorReporting = error_reporting(0);
ini_set('display_errors', 0);
ini_set('log_errors', 0);
// Set a custom error handler to completely suppress warnings
set_error_handler(function ($severity, $message, $file, $line) {
return true; // Suppress all errors
});
try {
$config = Feedamic::getConfig('/feed', 'default');
if (!$config) {
abort(404);
}
$entries = Feedamic::getEntries($config);
// Generate XML directly without XMLReader/XMLWriter processing
$xml = view('feedamic::rss', [
'id' => request()->url(),
'config' => $config,
'entries' => $entries,
'site' => Site::current(),
'updated' => Carbon::now(),
'url' => request()->url(),
])->render();
return response($xml, 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8',
'Cache-Control' => 'public, max-age=3600',
]);
} catch (\Exception $e) {
// Return a basic RSS structure if there's an error
$basicFeed =
'<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Huement Blog Main Feed</title>
<description>Feed temporarily unavailable</description>
<link>' .
config('app.url') .
'</link>
<lastBuildDate>' .
Carbon::now()->toRfc2822String() .
'</lastBuildDate>
</channel>
</rss>';
return response($basicFeed, 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8',
]);
} finally {
// Restore original error reporting
error_reporting($oldErrorReporting);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
restore_error_handler();
}
}
}
Key Features
- Error Suppression: Prevents deprecation warnings from corrupting XML output
- Direct XML Generation: Bypasses problematic XMLReader/XMLWriter processing
- Fallback Handling: Provides basic feed structure if errors occur
- Proper Headers: Sets correct Content-Type and caching headers
- Logging: Includes debug logging for troubleshooting
PART 3 | Middleware System
File: app/Http/Middleware/InterceptFeedamic.php
<?php
namespace App\Http\Middleware;
use App\Http\Controllers\CustomFeedController;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class InterceptFeedamic
{
/**
- Handle an incoming request.
-
- @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// Intercept feed routes and redirect to our custom controller
if ($request->is('feed/atom') || $request->path() === 'feed/atom') {
\Log::info('InterceptFeedamic: Intercepting /feed/atom');
$controller = new CustomFeedController();
return $controller->atom();
}
if ($request->is('feed') || $request->path() === 'feed') {
\Log::info('InterceptFeedamic: Intercepting /feed');
$controller = new CustomFeedController();
return $controller->rss();
}
return $next($request);
}
}
Middleware Registration
File: app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Spatie\CookieConsent\CookieConsentMiddleware::class,
\App\Http\Middleware\SetCanonicalUrl::class,
\App\Http\Middleware\SuppressWarningsForFeeds::class,
\App\Http\Middleware\InterceptFeedamic::class,
\Statamic\StaticCaching\Middleware\Cache::class,
],
// ... other middleware groups
];
Part 4 | Error Suppression
This part was solved via Cursor.ai, after a little bit of head scratching. Out of the gate we were getting a lot of errors when we first updated, different errors like: error on line 4 at column 54: xmlParseEntityRef: no name
and various XML corruptions causing problems, to fix that we added a special catch for the /feed route.
File: public/index.php
<?php
// Aggressively suppress all errors for feed routes to prevent XML corruption
if (
isset($_SERVER['REQUEST_URI']) &&
strpos($_SERVER['REQUEST_URI'], '/feed') !== false
) {
// Completely suppress all errors for feed routes
error_reporting(0);
ini_set('display_errors', 0);
ini_set('log_errors', 0);
ini_set('html_errors', 0);
// Suppress all errors at the PHP level
set_error_handler(function ($severity, $message, $file, $line) {
return true; // Suppress all errors for feed routes
});
// Also suppress output buffering issues
ob_start();
}
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// ... rest of the file
Comments
No Comments Yet!
Would you like to be the first?
Comment Moderation is ON for this post. All comments must be approved before they will be visible.