Skip to content

Commit

Permalink
Merge pull request #413 from hydephp/add-rss-feed
Browse files Browse the repository at this point in the history
Add RSS feed for Markdown blog posts
  • Loading branch information
caendesilva authored May 19, 2022
2 parents d0bdcdd + ebcaa01 commit 11fe780
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 4 deletions.
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

0 comments on commit 11fe780

Please sign in to comment.