Compare commits

...
Author SHA1 Message Date
99f306ede1
Replace RSS icon with a JSON Feed icon in the header
The nav's feed link already points at the JSON feed, but was using
the feather RSS icon. Adds an SVG recreation of the official JSON
Feed mark (traced from jsonfeed.org's icon) and swaps it in, using a
single dark-green fill throughout so it stays legible against both
the light and dark theme backgrounds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXyzNvQZPQgBoSZwW7cLG8
2026-08-22 10:42:23 +01:00
714cd95d79
Drop RSS/Atom feeds, support JSON feeds only
Removes the RSS and Atom feed routes, controller methods, and views
for both the blog and notes feeds, keeping JSON (and JF2) as the only
supported feed formats. Also serves the JSON feeds with the
spec-required application/feed+json MIME type instead of the generic
application/json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXyzNvQZPQgBoSZwW7cLG8
2026-08-22 10:42:16 +01:00
9 changed files with 24 additions and 210 deletions

View file

@ -7,66 +7,13 @@ namespace App\Http\Controllers;
use App\Models\Article; use App\Models\Article;
use App\Models\Note; use App\Models\Note;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class FeedsController extends Controller class FeedsController extends Controller
{ {
/**
* Returns the blog RSS feed.
*/
public function blogRss(): Response
{
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
$buildDate = $articles->first()->updated_at->toRssString();
return response()
->view('articles.rss', compact('articles', 'buildDate'))
->header('Content-Type', 'application/rss+xml; charset=utf-8');
}
/**
* Returns the blog Atom feed.
*/
public function blogAtom(): Response
{
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
return response()
->view('articles.atom', compact('articles'))
->header('Content-Type', 'application/atom+xml; charset=utf-8');
}
/**
* Returns the notes RSS feed.
*/
public function notesRss(): Response
{
$notes = Note::latest()->take(20)->get();
$buildDate = $notes->first()->updated_at->toRssString();
return response()
->view('notes.rss', compact('notes', 'buildDate'))
->header('Content-Type', 'application/rss+xml; charset=utf-8');
}
/**
* Returns the notes Atom feed.
*/
public function notesAtom(): Response
{
$notes = Note::latest()->take(20)->get();
return response()
->view('notes.atom', compact('notes'))
->header('Content-Type', 'application/atom+xml; charset=utf-8');
}
/** @todo sort out return type for json responses */
/** /**
* Returns the blog JSON feed. * Returns the blog JSON feed.
*/ */
public function blogJson(): array public function blogJson(): JsonResponse
{ {
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get(); $articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
$data = [ $data = [
@ -94,13 +41,15 @@ class FeedsController extends Controller
]; ];
} }
return $data; return response()->json($data, 200, [
'Content-Type' => 'application/feed+json',
]);
} }
/** /**
* Returns the notes JSON feed. * Returns the notes JSON feed.
*/ */
public function notesJson(): array public function notesJson(): JsonResponse
{ {
$notes = Note::latest()->with('media', 'place', 'tags')->take(20)->get(); $notes = Note::latest()->with('media', 'place', 'tags')->take(20)->get();
$data = [ $data = [
@ -130,7 +79,9 @@ class FeedsController extends Controller
} }
} }
return $data; return response()->json($data, 200, [
'Content-Type' => 'application/feed+json',
]);
} }
/** /**

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Atom feed for {{ config('user.display_name') }}s blog</title>
<link rel="self" href="{{ config('app.url') }}/blog/feed.atom" />
<id>{{ config('app.url')}}/blog</id>
<updated>{{ $articles[0]->updated_at->toAtomString() }}</updated>
@foreach($articles as $article)
<entry>
<title>{{ $article->title }}</title>
<link href="{{ config('app.url') }}{{ $article->link }}" />
<id>{{ config('app.url') }}{{ $article->link }}</id>
<updated>{{ $article->updated_at->toAtomString() }}</updated>
<content>{{ $article->main }}</content>
<author>
<name>{{ config('user.display_name') }}</name>
</author>
</entry>
@endforeach
</feed>

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ config('user.display_name') }}</title>
<atom:link href="{{ config('app.url') }}/blog/feed.rss" rel="self" type="application/rss+xml" />
<description>An RSS feed of the blog posts found on {{ config('app.url') }}</description>
<link>{{ config('app.url') }}/blog</link>
<lastBuildDate>{{ $buildDate }}</lastBuildDate>
<ttl>1800</ttl>
@foreach($articles as $article)
<item>
<title>{{ strip_tags($article->title) }}</title>
<description>
<![CDATA[
{{ $article->main }}
@if($article->url)<p><a href="{{ config('app.url') }}{{ $article->link }}">Permalink</a></p>@endif
]]>
</description>
<link>@if($article->url != ''){{ $article->url }}@else{{ config('app.url') }}{{ $article->link }}@endif</link>
<guid>{{ config('app.url') }}{{ $article->link }}</guid>
<pubDate>{{ $article->pubdate }}</pubDate>
</item>
@endforeach
</channel>
</rss>

View file

@ -0,0 +1,11 @@
@php
if (isset($title)) {
$uniqueId = bin2hex(random_bytes(6));
}
@endphp
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 200 200" class="feather feather-json-feed"
@if($title)aria-labelledby="{{ $uniqueId }}"@endif
>
@if($title)<title id="{{ $uniqueId }}">{{ $title }}</title>@endif
<path fill="#497714" transform="translate(0,200) scale(0.1,-0.1)" d="M373 1547 c-88 -95 -127 -171 -127 -252 -1 -90 28 -140 164 -280 150 -155 160 -168 160 -219 0 -31 -9 -55 -41 -101 -34 -51 -39 -65 -34 -95 3 -20 18 -46 36 -62 44 -41 89 -38 163 12 32 22 67 40 78 40 46 0 99 -38 227 -164 169 -165 209 -190 305 -190 92 -1 158 32 258 125 l73 67 -59 61 -60 60 -39 -36 c-22 -19 -60 -44 -84 -55 -73 -32 -109 -12 -278 152 -77 76 -156 145 -175 155 -47 24 -110 22 -159 -6 -23 -12 -44 -19 -48 -15 -4 4 2 25 15 46 24 42 29 110 12 154 -5 15 -66 84 -135 156 -138 143 -165 180 -165 227 0 47 15 78 61 128 l42 45 -54 59 c-30 32 -57 59 -61 60 -3 0 -37 -32 -75 -72z M1474 1631 c-36 -22 -58 -75 -48 -115 10 -41 59 -76 106 -76 107 0 137 147 41 199 -31 16 -64 14 -99 -8z M1229 1385 c-55 -30 -73 -89 -44 -145 19 -37 43 -50 95 -50 110 0 139 144 40 195 -36 18 -57 18 -91 0z M949 1111 c-86 -87 29 -228 130 -159 40 27 52 62 41 115 -12 51 -41 73 -97 73 -36 0 -50 -6 -74 -29z"/>
</svg>

View file

@ -7,13 +7,9 @@
<title>@yield('title'){{ config('app.name') }}</title> <title>@yield('title'){{ config('app.name') }}</title>
<link rel="stylesheet" href="/assets/highlight/nord.css"> <link rel="stylesheet" href="/assets/highlight/nord.css">
<link rel="stylesheet" href="/assets/css/app.css"> <link rel="stylesheet" href="/assets/css/app.css">
<link rel="alternate" type="application/rss+xml" title="Blog RSS Feed" href="{{ route('feed.blog.rss') }}"> <link rel="alternate" type="application/feed+json" title="Blog JSON Feed" href="{{ route('feed.blog.json') }}">
<link rel="alternate" type="application/atom+xml" title="Blog Atom Feed" href="{{ route('feed.blog.atom') }}">
<link rel="alternate" type="application/json" title="Blog JSON Feed" href="{{ route('feed.blog.json') }}">
<link rel="alternate" type="application/jf2feed+json" title="Blog JF2 Feed" href="{{ route('feed.blog.jf2') }}"> <link rel="alternate" type="application/jf2feed+json" title="Blog JF2 Feed" href="{{ route('feed.blog.jf2') }}">
<link rel="alternate" type="application/rss+xml" title="Notes RSS Feed" href="{{ route('feed.notes.rss') }}"> <link rel="alternate" type="application/feed+json" title="Notes JSON Feed" href="{{ route('feed.notes.json') }}">
<link rel="alternate" type="application/atom+xml" title="Notes Atom Feed" href="{{ route('feed.notes.atom') }}">
<link rel="alternate" type="application/json" title="Notes JSON Feed" href="{{ route('feed.notes.json') }}">
<link rel="alternate" type="application/jf2feed+json" title="Notes JF2 Feed" href="{{ route('feed.notes.jf2') }}"> <link rel="alternate" type="application/jf2feed+json" title="Notes JF2 Feed" href="{{ route('feed.notes.jf2') }}">
<link rel="indieauth-metadata" href="{{ route('indieauth.metadata') }}"> <link rel="indieauth-metadata" href="{{ route('indieauth.metadata') }}">
<link rel="authorization_endpoint" href="{{ route('indieauth.start') }}"> <link rel="authorization_endpoint" href="{{ route('indieauth.start') }}">
@ -40,7 +36,7 @@
<a href="/likes">Likes</a> <a href="/likes">Likes</a>
<a href="/contacts">Contacts</a> <a href="/contacts">Contacts</a>
<a href="/projects">Projects</a> <a href="/projects">Projects</a>
<a href="{{ route('feed.notes.json') }}" class="rss-icon">@include('icons.rss', ['title' => 'RSS Feed'])</a> <a href="{{ route('feed.notes.json') }}" class="rss-icon">@include('icons.json-feed', ['title' => 'JSON Feed'])</a>
</nav> </nav>
<div id="theme-selector"> <div id="theme-selector">
<button class="toggle" popovertarget="theme-selector-dropdown"> <button class="toggle" popovertarget="theme-selector-dropdown">

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Atom feed for {{ config('user.display_name') }}s notes</title>
<link rel="self" href="{{ config('app.url') }}/notes/feed.atom" />
<id>{{ config('app.url')}}/notes</id>
<updated>{{ $notes[0]->updated_at->toAtomString() }}</updated>
@foreach($notes as $note)
<entry>
<title>{{ strip_tags($note->note) }}</title>
<link href="{{ $note->uri }}" />
<id>{{ $note->uri }}</id>
<updated>{{ $note->updated_at->toAtomString() }}</updated>
<content type="html">{{ $note->note }}</content>
<author>
<name>{{ config('user.display_name') }}</name>
</author>
</entry>
@endforeach
</feed>

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ config('user.display_name') }}</title>
<atom:link href="{{ config('app.url') }}/notes/feed.rss" rel="self" type="application/rss+xml" />
<description>An RSS feed of the notes found on {{ config('app.url') }}</description>
<link>{{ config('app.url') }}/notes</link>
<lastBuildDate>{{ $buildDate }}</lastBuildDate>
<ttl>1800</ttl>
@foreach($notes as $note)
<item>
<title>{{ strip_tags($note->note) }}</title>
<description>
<![CDATA[
{!! $note->note !!}
]]>
</description>
<link>{{ $note->uri }}</link>
<guid>{{ $note->uri}}</guid>
<pubDate>{{ $note->pubdate }}</pubDate>
</item>
@endforeach
</channel>
</rss>

View file

@ -170,8 +170,6 @@ Route::middleware(MyAuthMiddleware::class)->prefix('admin')->group(function () {
// Blog pages using ArticlesController // Blog pages using ArticlesController
Route::prefix('blog')->group(function () { Route::prefix('blog')->group(function () {
Route::get('/feed.rss', [FeedsController::class, 'blogRss'])->name('feed.blog.rss');
Route::get('/feed.atom', [FeedsController::class, 'blogAtom'])->name('feed.blog.atom');
Route::get('/feed.json', [FeedsController::class, 'blogJson'])->name('feed.blog.json'); Route::get('/feed.json', [FeedsController::class, 'blogJson'])->name('feed.blog.json');
Route::get('/feed.jf2', [FeedsController::class, 'blogJf2'])->name('feed.blog.jf2'); Route::get('/feed.jf2', [FeedsController::class, 'blogJf2'])->name('feed.blog.jf2');
Route::get('/s/{id}', [ArticlesController::class, 'onlyIdInURL']); Route::get('/s/{id}', [ArticlesController::class, 'onlyIdInURL']);
@ -182,8 +180,6 @@ Route::prefix('blog')->group(function () {
// Notes pages using NotesController // Notes pages using NotesController
Route::prefix('notes')->group(function () { Route::prefix('notes')->group(function () {
Route::get('/', [NotesController::class, 'index']); Route::get('/', [NotesController::class, 'index']);
Route::get('/feed.rss', [FeedsController::class, 'notesRss'])->name('feed.notes.rss');
Route::get('/feed.atom', [FeedsController::class, 'notesAtom'])->name('feed.notes.atom');
Route::get('/feed.json', [FeedsController::class, 'notesJson'])->name('feed.notes.json'); Route::get('/feed.json', [FeedsController::class, 'notesJson'])->name('feed.notes.json');
Route::get('/feed.jf2', [FeedsController::class, 'notesJf2'])->name('feed.notes.jf2'); Route::get('/feed.jf2', [FeedsController::class, 'notesJf2'])->name('feed.notes.jf2');
Route::get('/new', [NotesController::class, 'create']); Route::get('/new', [NotesController::class, 'create']);

View file

@ -15,42 +15,6 @@ class FeedsTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
/**
* Test the blog RSS feed.
*/
#[Test]
public function blog_rss_feed_is_present(): void
{
Article::factory()->count(3)->create();
$response = $this->get('/blog/feed.rss');
$response->assertHeader('Content-Type', 'application/rss+xml; charset=utf-8');
$response->assertOk();
}
/**
* Test the notes RSS feed.
*/
#[Test]
public function notes_rss_feed_is_present(): void
{
Note::factory()->count(3)->create();
$response = $this->get('/notes/feed.rss');
$response->assertHeader('Content-Type', 'application/rss+xml; charset=utf-8');
$response->assertOk();
}
/**
* Test the blog RSS feed.
*/
#[Test]
public function blog_atom_feed_is_present(): void
{
Article::factory()->count(3)->create();
$response = $this->get('/blog/feed.atom');
$response->assertHeader('Content-Type', 'application/atom+xml; charset=utf-8');
$response->assertOk();
}
#[Test] #[Test]
public function blog_jf2_feed_is_present(): void public function blog_jf2_feed_is_present(): void
{ {
@ -73,18 +37,6 @@ class FeedsTest extends TestCase
]); ]);
} }
/**
* Test the notes RSS feed.
*/
#[Test]
public function notes_atom_feed_is_present(): void
{
Note::factory()->count(3)->create();
$response = $this->get('/notes/feed.atom');
$response->assertHeader('Content-Type', 'application/atom+xml; charset=utf-8');
$response->assertOk();
}
/** /**
* Test the blog JSON feed. * Test the blog JSON feed.
*/ */
@ -93,7 +45,7 @@ class FeedsTest extends TestCase
{ {
Article::factory()->count(3)->create(); Article::factory()->count(3)->create();
$response = $this->get('/blog/feed.json'); $response = $this->get('/blog/feed.json');
$response->assertHeader('Content-Type', 'application/json'); $response->assertHeader('Content-Type', 'application/feed+json');
$response->assertOk(); $response->assertOk();
} }
@ -105,7 +57,7 @@ class FeedsTest extends TestCase
{ {
Note::factory()->count(3)->create(); Note::factory()->count(3)->create();
$response = $this->get('/notes/feed.json'); $response = $this->get('/notes/feed.json');
$response->assertHeader('Content-Type', 'application/json'); $response->assertHeader('Content-Type', 'application/feed+json');
$response->assertOk(); $response->assertOk();
} }