Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RSS feed for Markdown blog posts #413

Merged
merged 40 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6108f26
Create RssFeedService.php
caendesilva May 18, 2022
d590ee6
Add RssFeedService test file
caendesilva May 18, 2022
9cb9b30
Add the initial channel items
caendesilva May 18, 2022
51d77ef
Escape XML values
caendesilva May 18, 2022
fd8b5ca
Add note that site_url is required to use sitemaps and RSS feeds
caendesilva May 18, 2022
6f887f2
Add helper to get canonical post links
caendesilva May 18, 2022
37b41b7
Add helper to get post description
caendesilva May 18, 2022
189c26c
Add basic blog post items to the feed
caendesilva May 18, 2022
77228a1
Add pubDate support in additional item data helper
caendesilva May 18, 2022
18c36dc
Add link to the RSS 2.0 spec
caendesilva May 18, 2022
707c522
Add getName() helper
caendesilva May 18, 2022
9158b38
Add author property
caendesilva May 18, 2022
130d9cb
Wrap additional data in conditionals
caendesilva May 18, 2022
425c55b
Replace author with dc:creator
caendesilva May 18, 2022
3902608
Add additional channel data
caendesilva May 18, 2022
7e862d8
Add guid element to items
caendesilva May 18, 2022
eb5c23e
Remove debug processing time
caendesilva May 18, 2022
eb98a73
Add Atom link
caendesilva May 18, 2022
024ce28
Add XML namespace to root element
caendesilva May 18, 2022
15bbf88
Add post category support
caendesilva May 18, 2022
b719b0f
Formatting
caendesilva May 18, 2022
0604d0e
Support local image attributes
caendesilva May 19, 2022
956f307
Fix test namespaces
caendesilva May 19, 2022
22d1e7e
Add type annotation
caendesilva May 19, 2022
a21596f
Add the RSSFeedService test
caendesilva May 19, 2022
856f59d
Apply fixes from StyleCI
StyleCIBot May 19, 2022
96156e9
Merge pull request #410 from hydephp/analysis-GD9j3E
May 19, 2022
53786cd
Make the feed filename configurable
caendesilva May 19, 2022
0d08e24
Merge branch 'add-rss-feed' of github.com:hydephp/framework into add-…
caendesilva May 19, 2022
ff45211
Apply fixes from StyleCI
StyleCIBot May 19, 2022
e3b8777
Merge pull request #411 from hydephp/analysis-x0kneb
May 19, 2022
7f71154
Add static canGenerateFeed helper
caendesilva May 19, 2022
fa389b0
Add static generateFeed helper
caendesilva May 19, 2022
83ca387
Generate RSS feed after build process
caendesilva May 19, 2022
5f1b137
Add better action information output
caendesilva May 19, 2022
fe93f5b
Update sitemap tests and add rss feed tests
caendesilva May 19, 2022
dde4a60
Apply fixes from StyleCI
StyleCIBot May 19, 2022
4c580a0
Merge pull request #412 from hydephp/analysis-YjVNg9
May 19, 2022
2ceb60d
Test that RSS filename can be changed
caendesilva May 19, 2022
ebcaa01
Merge branch 'add-rss-feed' of github.com:hydephp/framework into add-…
caendesilva May 19, 2022
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
2 changes: 2 additions & 0 deletions config/hyde.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
|
| Here are some configuration options for URL generation.
|
| A site_url is required to use sitemaps and RSS feeds.
|
| `site_url` is used to create canonical URLs and permalinks.
| `prettyUrls` will when enabled create links that do not end in .html.
| `generateSitemap` determines if a sitemap.xml file should be generated.
Expand Down
18 changes: 16 additions & 2 deletions src/Commands/HydeBuildStaticSiteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Hyde\Framework\Models\MarkdownPage;
use Hyde\Framework\Models\MarkdownPost;
use Hyde\Framework\Services\DiscoveryService;
use Hyde\Framework\Services\RssFeedService;
use Hyde\Framework\Services\SitemapService;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
Expand Down Expand Up @@ -172,9 +173,17 @@ public function postBuildActions(): void
}

if (SitemapService::canGenerateSitemap()) {
$this->info('Generating sitemap.xml');
$actionTime = microtime(true);
$this->comment('Generating sitemap...');
file_put_contents(Hyde::getSiteOutputPath('sitemap.xml'), SitemapService::generateSitemap());
$this->newLine();
$this->line(' > Created <info>sitemap.xml</> in '.$this->getExecutionTimeInMs($actionTime)."ms\n");
}

if (RssFeedService::canGenerateFeed()) {
$actionTime = microtime(true);
$this->comment('Generating RSS feed...');
file_put_contents(Hyde::getSiteOutputPath(RssFeedService::getDefaultOutputFilename()), RssFeedService::generateFeed());
$this->line(' > Created <info>'.RssFeedService::getDefaultOutputFilename().'</> in '.$this->getExecutionTimeInMs($actionTime)."ms\n");
}
}

Expand All @@ -198,4 +207,9 @@ private function runNodeCommand(string $command, string $message, ?string $actio
$output ?? '<fg=red>Could not '.($actionMessage ?? 'run script').'! Is NPM installed?</>'
);
}

protected function getExecutionTimeInMs(float $timeStart): float
{
return number_format(((microtime(true) - $timeStart) * 1000), 2);
}
}
10 changes: 10 additions & 0 deletions src/Models/Author.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,14 @@ public function __construct(string $username, ?array $data = [])
$this->website = $data['website'];
}
}

/**
* Get the author's name.
*
* @return string
*/
public function getName(): string
{
return $this->name ?? $this->username;
}
}
11 changes: 11 additions & 0 deletions src/Models/MarkdownPost.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Hyde\Framework\Concerns\HasAuthor;
use Hyde\Framework\Concerns\HasDateString;
use Hyde\Framework\Concerns\HasFeaturedImage;
use Hyde\Framework\Hyde;
use Hyde\Framework\Models\Parsers\MarkdownPostParser;

class MarkdownPost extends MarkdownDocument
Expand Down Expand Up @@ -36,4 +37,14 @@ public function getCurrentPagePath(): string
{
return 'posts/'.$this->slug;
}

public function getCanonicalLink(): string
{
return Hyde::uriPath(Hyde::pageLink($this->getCurrentPagePath().'.html'));
}

public function getPostDescription(): string
{
return $this->matter['description'] ?? substr($this->body, 0, 125).'...';
}
}
137 changes: 137 additions & 0 deletions src/Services/RssFeedService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

namespace Hyde\Framework\Services;

use Hyde\Framework\Hyde;
use Hyde\Framework\Models\MarkdownPost;
use SimpleXMLElement;

/**
* @see \Tests\Feature\Services\RssFeedServiceTest
* @see https://validator.w3.org/feed/docs/rss2.html
*/
class RssFeedService
{
public SimpleXMLElement $feed;

public function __construct()
{
$this->feed = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" />');
$this->feed->addChild('channel');

$this->addInitialChannelItems();
}

public function generate(): self
{
/** @var MarkdownPost $post */
foreach (Hyde::getLatestPosts() as $post) {
$this->addItem($post);
}

return $this;
}

public function getXML(): string
{
return $this->feed->asXML();
}

protected function addItem(MarkdownPost $post): void
{
$item = $this->feed->channel->addChild('item');
$item->addChild('title', $post->findTitleForDocument());
$item->addChild('link', $post->getCanonicalLink());
$item->addChild('guid', $post->getCanonicalLink());
$item->addChild('description', $post->getPostDescription());

$this->addAdditionalItemData($item, $post);
}

protected function addAdditionalItemData(SimpleXMLElement $item, MarkdownPost $post): void
{
if (isset($post->date)) {
$item->addChild('pubDate', $post->date->dateTimeObject->format(DATE_RSS));
}

if (isset($post->author)) {
$item->addChild('dc:creator', $post->author->getName(), 'http://purl.org/dc/elements/1.1/');
}

if (isset($post->category)) {
$item->addChild('category', $post->category);
}

// Only support local images, as remote images would take extra time to make HTTP requests to get length
if (isset($post->image) && isset($post->image->path)) {
$image = $item->addChild('enclosure');
$image->addAttribute('url', Hyde::uriPath(str_replace('_media', 'media', $post->image->path)));
$image->addAttribute('type', str_ends_with($post->image->path, '.png') ? 'image/png' : 'image/jpeg');
$image->addAttribute('length', filesize(Hyde::path($post->image->path)));
}
}

protected function addInitialChannelItems(): void
{
$this->feed->channel->addChild('title', $this->getTitle());
$this->feed->channel->addChild('link', $this->getLink());
$this->feed->channel->addChild('description', $this->getDescription());

$atomLink = $this->feed->channel->addChild('atom:link', namespace: 'http://www.w3.org/2005/Atom');
$atomLink->addAttribute('href', $this->getLink().'/'.static::getDefaultOutputFilename());
$atomLink->addAttribute('rel', 'self');
$atomLink->addAttribute('type', 'application/rss+xml');

$this->addAdditionalChannelData();
}

protected function addAdditionalChannelData(): void
{
$this->feed->channel->addChild('language', config('hyde.language', 'en'));
$this->feed->channel->addChild('generator', 'HydePHP '.Hyde::version());
$this->feed->channel->addChild('lastBuildDate', date(DATE_RSS));
}

protected function getTitle(): string
{
return $this->xmlEscape(
config('hyde.name', 'HydePHP')
);
}

protected function getLink(): string
{
return $this->xmlEscape(
config('hyde.site_url') ?? 'http://localhost'
);
}

protected function getDescription(): string
{
return $this->xmlEscape(
config('hyde.rssDescription',
$this->getTitle().' RSS Feed')
);
}

protected function xmlEscape(string $string): string
{
return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8');
}

public static function getDefaultOutputFilename(): string
{
return config('hyde.rssFilename', 'feed.rss');
}

public static function generateFeed(): string
{
return (new static)->generate()->getXML();
}

public static function canGenerateFeed(): bool
{
return (Hyde::uriPath() !== false) && config('hyde.generateRssFeed', true);
}
}
47 changes: 46 additions & 1 deletion tests/Feature/Commands/BuildStaticSiteCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,55 @@ public function test_sitemap_is_generated_when_conditions_are_met()

unlinkIfExists(Hyde::path('_site/sitemap.xml'));
$this->artisan('build')
->expectsOutput('Generating sitemap.xml')
->expectsOutput('Generating sitemap...')
->assertExitCode(0);

$this->assertFileExists(Hyde::path('_site/sitemap.xml'));
unlink(Hyde::path('_site/sitemap.xml'));
}

public function test_rss_feed_is_not_generated_when_conditions_are_not_met()
{
config(['hyde.site_url' => '']);
config(['hyde.generateRssFeed' => false]);

unlinkIfExists(Hyde::path('_site/feed.rss'));
$this->artisan('build')
->assertExitCode(0);

$this->assertFileDoesNotExist(Hyde::path('_site/feed.rss'));
}

public function test_rss_feed_is_generated_when_conditions_are_met()
{
config(['hyde.site_url' => 'https://example.com']);
config(['hyde.generateRssFeed' => true]);

unlinkIfExists(Hyde::path('_site/feed.rss'));
$this->artisan('build')
->expectsOutput('Generating RSS feed...')
->assertExitCode(0);

$this->assertFileExists(Hyde::path('_site/feed.rss'));
unlink(Hyde::path('_site/feed.rss'));
}

public function test_rss_filename_can_be_changed()
{
config(['hyde.site_url' => 'https://example.com']);
config(['hyde.generateRssFeed' => true]);
config(['hyde.rssFilename' => 'blog.xml']);

unlinkIfExists(Hyde::path('_site/feed.rss'));
unlinkIfExists(Hyde::path('_site/blog.xml'));

$this->artisan('build')
->expectsOutput('Generating RSS feed...')
->assertExitCode(0);

$this->assertFileDoesNotExist(Hyde::path('_site/feed.rss'));
$this->assertFileExists(Hyde::path('_site/blog.xml'));
unlink(Hyde::path('_site/blog.xml'));
}

/**
Expand Down
Loading