Bringing the Feed Back

Published in Content Management, PHP, Statamic on May 9, 2021

After I recently launched the new look for this web site, I noticed something interesting about my traffic: a lot of 404 responses for /feed or /feed.xml.

grep feed /var/log/nginx/access.log | grep -v Mozilla | grep -ahoE '"[^"]+"$' | sort | uniq -c | sort -nr
    189 "davefeedread v0.4.14"
    176 "PlanetPHPAggregator/0.2 (PHP5) (http://planet-php.org/)"
    124 "Tiny Tiny RSS/21.02-a42e8aad9 (http://tt-rss.org/)"
    108 "Tiny Tiny RSS/20.11-328d7b55c (http://tt-rss.org/)"
     99 "Tiny Tiny RSS/20.06-06cc6e3 (http://tt-rss.org/)"
     92 "Fever/1.31 (Feed Parser; http://feedafever.com; Allow like Gecko)"
     85 "Tiny Tiny RSS/17.12 (http://tt-rss.org/)"
     85 "MagpieRSS/0.72 \x5C(+http://magpierss.sf.net\x5C)"
     85 "MagpieRSS/0.72 (+http://magpierss.sf.net)"
     49 "Fever/1.39 (Feed Parser; http://feedafever.com; Allow like Gecko)"
     18 "rogerbot/1.0 (http://www.moz.com/dp/rogerbot, rogerbot-crawler@moz.com)"
     14 "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
      4 "Feedspot/1.0 (+https://www.feedspot.com/fs/fetcher; like FeedFetcher-Google)"
      2 "NetNewsWire (RSS Reader; https://ranchero.com/netnewswire/)"
      1 "RSSMicro.com RSS/Atom Feed Robot"
      1 "Feedly/1.0 (+http://www.feedly.com/fetcher.html; 9 subscribers; like FeedFetcher-Google)"

I originally suspected that the feed from my old site wasn't in wide use, so I didn't bother porting it over when I migrated the site. However, the new site launch provided traffic that disproved my suspicion. So, I decided to bring the feed back.

But now that the new site was using Statamic, I wasn't sure of the best way to go about it. I had to do some research to come up with a solution and thought I'd take a post to discuss the particulars.

No Luck with Addons

A cursory search turned up an addon that, upon closer inspection, turned out to be for Statamic 2, where I'm using Statamic 3.

I found and joined the Statamic Discord and ended up speaking to the author of that addon, who linked me to a partially done port for Statamic 3.

I wasn't able to get this port to work and filed a few issues against its repository. The author encouraged me to submit pull requests or create my own fork, but with related documentation for extending Statamic being on the sparse side, I suspected that was more effort than I wanted to invest.

A Laravel Solution

The solution I settled on, while not as easily reusable as I would like, will work well enough as an interim measure until I can figure out how to convert it into an addon.

Statamic is built on Laravel, so I opted to pull the spatie/laravel-feed Laravel package into my web site project.

Per the package's installation instructions, I added a call to its Route::feeds() macro to the routes/web.php file.

When I tried to publish the config file, I received this error: Unable to locate publishable resources. However, it turns out this wasn't necessary and I was able to manually copy the config file example from the package README to config/feed.php. The package worked fine once I had configured it properly.

The Configuration

Getting this configuration right took some digging through source code and dd() calls. I'm using the Cool Writings starter kit, but you should be able to tweak this to work with your installation.

<?php

use Spatie\Feed\FeedItem;
use Statamic\Entries\Entry;
use Statamic\Facades\User;

return [
    'feeds' => [
        'main' => [
            'items' => fn() => Entry::query()
                ->where('collection', 'articles')
                ->where('published', true)
                ->where('date', '<=', now())
                ->orderBy('date', 'desc')
                ->orderBy('title')
                ->limit(20)
                ->get()
                ->map(function(Entry $entry): FeedItem {
                    return FeedItem::create([
                        'id' => $entry->url(),
                        'title' => $entry->get('title'),
                        'summary' => $entry->excerpt ?? '',
                        'updated' => $entry->date(),
                        'link' => $entry->url(),
                        'author' => implode(', ', $entry->authors()->map(
                            fn($id) => User::find($id)->name
                        )->all()),
                    ]);
                }),
            'url' => '/feed.xml',
            'title' => 'Matthew Turland',
            'description' => 'Blog posts from matthewturland.com',
            'language' => 'en-US',
            'view' => 'feed::feed',
        ],
    ],
];

This fetches the 20 most recent published entries with a date on or before today and converts them into FeedItem instances that the spatie/laravel-feed package can then render as feed entries.

Feed Links

Now that I had a rendering feed, I needed to link to it in the <head> tag of my Antlers layout template.

Unfortunately, spatie/laravel-feed uses Blade, the Cool Writings starter kit templates use Antlers, and it turns out it's not possible to embed Blade templates in Antlers templates, so I wasn't able to use the view macro provided by spatie/laravel-feed.

However, adding this snippet of markup to my layout template seemed to do the trick.

{{ foreach:config:feed.feeds as="feed" }}
<link rel="alternate" type="{{ feed.type ?? 'application/atom+xml' }}" href="{{ feed.url }}" title="{{ feed.title }}">
{{ /foreach:config:feed.feeds }}

Static Generation

I'm using statamic/ssg to generate a static build of my site, which is what you're viewing now.

By default, this addon doesn't generate static versions of routes like the one that spatie/laravel-feed creates for the feed.

To include this route in the build process, I had to open config/statamic/ssg.php and modify the array it returns to include this segment.

<?php

return [
	'urls' => [
		'/feed.xml',
	],

	// ...
];

Ideally, this would dynamically pull in all defined feeds, but I haven't been successful in accessing content or configuration from other addons from the statamic/ssg configuration. Hopefully, this is a shortcoming that I can address in an addon.

Lastly, the base URL for links needs to be set in configuration for it to be correct in the static build. I found the relevant setting in config/app.php.

<?php

return [

    // ...

    // Commented this out
    // 'url' => env('APP_URL', 'http://localhost'),

    // Added this
    'url' => 'https://matthewturland.com',

    // ...

];

Some Finishing Touches

One drawback of statamic/ssg is that the static output of the /feed.xml route is actually written to /feed.xml/index.html with no option to customize the destination.

To address this, I tacked some additional commands onto the build script in my composer.json file to move the file to the correct path in the build.

mv ./storage/app/static/feed.xml/index.html /tmp/feed.xml
rm -fR ./storage/app/static/feed.xml
mv /tmp/feed.xml ./storage/app/static/feed.xml

Lastly, in order to allow /feed to resolve as well as /feed.xml, I added a rewrite directive to my nginx configuration.

rewrite ^/feed/?$ /feed.xml permanent;

Epilogue

I hope that the Statamic community grows and that the barrier to entry for extending it becomes lower over time.

That said, I'm glad I was at least able to hack together a working solution for those accessing my content via feeds in the meantime.

I hope you found this post useful, or at least interesting. Thanks for reading!