Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"require-dev": {
"danielescherzer/common-phpcs": "0.0.2",
"laminas/laminas-feed": "2.26.1",
"phpunit/phpunit": "~12.0",
"php-parallel-lint/php-parallel-lint": "^1.4",
"samdark/sitemap": "2.4.1"
Expand Down
3 changes: 3 additions & 0 deletions sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,7 @@
<url>
<loc>https://scherzer.dev/Blog/20260510-release-manager-notes-2</loc>
</url>
<url>
<loc>https://scherzer.dev/Blog/20260517-blog-rss</loc>
</url>
</urlset>
135 changes: 135 additions & 0 deletions src/Blog/feed-rss.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Daniel Scherzer's Blog</title>
<description>Entries from Daniel Scherzer' personal blog</description>
<pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate>
<generator>https://github.com/DanielEScherzer/website-content</generator>
<link>https://scherzer.dev/Blog</link>
<item>
<title>Blog RSS Feed</title>
<pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260517-blog-rss</link>
</item>
<item>
<title>Notes for PHP Release Managers, Part 2</title>
<pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260510-release-manager-notes-2</link>
</item>
<item>
<title>The Story of PHP 8.5.6 Release Candidate 3</title>
<pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260430-php856-rc-3</link>
</item>
<item>
<title>PHP 8.6 Release Manager</title>
<pubDate>Thu, 16 Apr 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260416-php86-release-manager</link>
</item>
<item>
<title>Introducing define_deprecated() for PHP</title>
<pubDate>Fri, 10 Apr 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260410-define-deprecated</link>
</item>
<item>
<title>Friends in PHP</title>
<pubDate>Mon, 09 Mar 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260309-php-friends</link>
</item>
<item>
<title>ConFoo 2026</title>
<pubDate>Mon, 02 Mar 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260302-confoo</link>
</item>
<item>
<title>Notes for PHP Release Managers</title>
<pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260218-release-manager-notes</link>
</item>
<item>
<title>This Year in PHP (2025)</title>
<pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20260120-php-2025</link>
</item>
<item>
<title>Project Euler in Rust, Part 2</title>
<pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20251201-rust-euler-2</link>
</item>
<item>
<title>Unplanned Downtime, November 2025</title>
<pubDate>Tue, 25 Nov 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20251125-unplanned-downtime</link>
</item>
<item>
<title>MergePHP, November 2025</title>
<pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20251118-mergephp-talk</link>
</item>
<item>
<title>The Story of PHP 8.5.0 Release Candidate 5</title>
<pubDate>Thu, 13 Nov 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20251113-release-candidate-5</link>
</item>
<item>
<title>Longhorn PHP 2025</title>
<pubDate>Wed, 29 Oct 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20251029-longhorn-php</link>
</item>
<item>
<title>Project Euler in Rust</title>
<pubDate>Fri, 19 Sep 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250919-rust-euler</link>
</item>
<item>
<title>"Hello, World!" in Rust</title>
<pubDate>Fri, 12 Sep 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250912-hello-world-rust</link>
</item>
<item>
<title>First Rust Patch</title>
<pubDate>Sat, 06 Sep 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250906-mago-rust</link>
</item>
<item>
<title>Out-of-Band Signaling</title>
<pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250829-out-of-band</link>
</item>
<item>
<title>#[\DelayedTargetValidation] Attribute Explained</title>
<pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250820-delayed-target-validation</link>
</item>
<item>
<title>No PHP 8.5.0alpha3</title>
<pubDate>Fri, 01 Aug 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250801-no-alpha-3</link>
</item>
<item>
<title>Pygments Syntax Highlighting for Markdown</title>
<pubDate>Fri, 18 Jul 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250718-pygments-highlighting</link>
</item>
<item>
<title>A Tale of Two RFCs</title>
<pubDate>Sun, 22 Jun 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250622-two-rfcs</link>
</item>
<item>
<title>Attributes on Constants</title>
<pubDate>Tue, 29 Apr 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250429-attributes-on-constants</link>
</item>
<item>
<title>PHP 8.5 Release Manager</title>
<pubDate>Thu, 17 Apr 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250417-php85-release-manager</link>
</item>
<item>
<title>Website Launch</title>
<pubDate>Wed, 09 Apr 2025 00:00:00 +0000</pubDate>
<link>https://scherzer.dev/Blog/20250409-website-launch</link>
</item>
</channel>
</rss>
95 changes: 95 additions & 0 deletions src/Blog/posts/20260517-blog-rss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: Blog RSS Feed
extensions:
pygments: true
---

# Blog RSS Feed

Last month, I received [a request][rss-req] to add an RSS feed to my blog. I am
happy to share that I have added such a feed, available at
[/Blog/feed/rss.xml][rss-link].

RSS (short for "Really Simple Syndication") is a web feed specification built on
top of XML. Rather than generating the XML entirely from scratch (potentially
using the PHP dom extension), I opted to use an open source library,
[laminas/laminas-feed][pgk-laminas], to create the XML content. While the
library certainly helped, I ran into a few challenges along the way.

## Laminas package

The first was the fact that the library unconditionally enables a "Slash"
extension which adds some extra elements to the output. Rather than manually
removing those elements from the resulting XML, I wanted to work within the
constraints of the library API. The result? A custom extension manager that
behaves differently based on the backtrace. The Laminas package still believes
that the extension exists, but the slash extension is skipped during
registration.

```php startinline=True
$old = Writer::getExtensionManager();
$manager = new class( $old ) extends ExtensionManager {
public function has( $entryName ) {
if ( $entryName === 'Slash\Renderer\Entry' ) {
// Needs to return true when checking if the extension
// exists, otherwise Laminas throws an exception
$bt = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 );
if (
$bt[1]['class'] === Writer::class
&& $bt[1]['function'] === 'hasExtension'
) {
return true;
}
return false;
}
return parent::has( $entryName );
}
};
Writer::setExtensionManager( $manager );
```

While I'm not the most pleased with this approach, it does work, and it presents
a great example for my talk at PHPTek next week about what is and is not part of
the public API of PHP libraries.

Another issue that I encountered was the unconditional addition of `<guid>` tags
in the output - for these, I just removed them from the XML result with a
post-processing step.

Of course, I could have used another library, or forked the Laminas package,
but the Laminas package seemed like the easiest to use.

## Forward compatibility

I had initially planned for the RSS feed to live at `.../Blog/rss.xml`, but
realized that I might want to support other feed formats (e.g. Atom) in the
future. To keep all feeds organized together, I put the RSS feed at
`.../Blog/feed/rss.xml`, allowing other feeds to be added more easily. If you
visit the feed page without specifying a feed format ([/Blog/feed][feeds-link]),
a list of available feeds is shown; it is also shown when an unknown feed is
requested.

## Caching

Rather than regenerating the RSS feed on every request, I generate it ahead of
time and store the resulting XML in my website's git repository. At runtime, the
cached file is simply read from disk. Tests exist to ensure that the cached
version remains up-to-date.

Thinking about this further, the same approach could be taken to every other
known page on my website. Other than HTTP 404 and 405 pages that depend on
what path or method were used, the rest of my pages are static and could be
similarly cached. If performance ever becomes a concern, I may extend the same
approach to the rest of the site.

## Field testing

This blog post was published at the same time the RSS feed was created, so it
does not serve as a test of the feed working properly. After PHPTek next week,
I plan to write up another post about my experiences there, which will serve as
a great opportunity to confirm that the RSS feed behaves as expected.

[feeds-link]: /Blog/feed
[pgk-laminas]: https://packagist.org/packages/laminas/laminas-feed
[rss-link]: /Blog/feed/rss.xml
[rss-req]: https://github.com/DanielEScherzer/website-content/issues/79
86 changes: 86 additions & 0 deletions src/Pages/BlogFeedPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php
declare( strict_types = 1 );

namespace DanielWebsite\Pages;

use DanielEScherzer\HTMLBuilder\FluentHTML;
use DanielWebsite\WebResponse;

class BlogFeedPage extends BasePage {

private ?string $feedName;

public function __construct( array $params ) {
parent::__construct();
$this->feedName = $params['feed'] ?? null;
$this->head->append(
FluentHTML::fromTag( 'title' )->addChild( 'Blog feeds' )
);
}

public function getResponse(): WebResponse {
if ( $this->feedName === 'rss.xml' ) {
// Bypass BasePage handling and return raw xml
$content = file_get_contents( dirname( __DIR__ ) . '/Blog/feed-rss.xml' );
return new WebResponse(
$content,
[ 'Content-Type: application/xml' ],
200
);
}
return parent::getResponse();
}

protected function build(): void {
if ( $this->feedName === 'rss.xml' ) {
return;
}
if ( $this->feedName !== null ) {
$this->addStyleSheet( 'error-styles.css' );
$feed = $this->feedName;
$this->contentWrapper->append(
FluentHTML::make(
'div',
[ 'class' => 'error-box' ],
[
FluentHTML::make( 'h1', [], 'Error' ),
FluentHTML::make(
'p',
[],
"The requested blog feed `$feed` is not recognized"
),
]
)
);
$this->setResponseCode( 404 );
}
$this->contentWrapper->append(
FluentHTML::make( 'h1', [], 'Known feed types' ),
FluentHTML::make(
'ul',
[],
[
FluentHTML::make(
'li',
[],
FluentHTML::make(
'a',
[ 'href' => '/Blog' ],
'HTML index of blog posts'
)
),
FluentHTML::make(
'li',
[],
FluentHTML::make(
'a',
[ 'href' => '/Blog/feed/rss.xml' ],
'RSS feed of blog posts'
)
),
]
)
);
}

}
3 changes: 3 additions & 0 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace DanielWebsite;

use DanielWebsite\Pages\AbstractPage;
use DanielWebsite\Pages\BlogFeedPage;
use DanielWebsite\Pages\BlogIndexPage;
use DanielWebsite\Pages\BlogPostPage;
use DanielWebsite\Pages\Error404Page;
Expand Down Expand Up @@ -68,6 +69,8 @@ public static function addRoutesCb( RouteCollector $r ): void {
$r->addRoute( 'GET', 'Thesis', ThesisPage::class );
$r->addRoute( 'GET', 'Work', WorkPage::class );
$r->addRoute( 'GET', 'Blog', BlogIndexPage::class );
$r->addRoute( 'GET', 'Blog/feed', BlogFeedPage::class );
$r->addRoute( 'GET', 'Blog/feed/{feed}', BlogFeedPage::class );
$r->addRoute( 'GET', 'Blog/{slug}', BlogPostPage::class );
$r->addRoute( 'GET', 'Tools', ToolPage::class );
$r->addRoute( 'GET', 'Tools/{tool}', ToolPage::class );
Expand Down
Loading
Loading