139 lines
5.0 KiB
PHP
139 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\RSS;
|
|
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* RSSFeedBuilder
|
|
*
|
|
* Responsible for:
|
|
* - normalising feed items from Eloquent collections
|
|
* - enforcing feed limits (max 20 items)
|
|
* - rendering RSS 2.0 XML via the rss.channel Blade template
|
|
* - returning a properly typed HTTP Response
|
|
*/
|
|
final class RSSFeedBuilder
|
|
{
|
|
/** Hard item limit per feed (spec §7). */
|
|
public const FEED_LIMIT = 20;
|
|
|
|
// ── Public builders ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Build an RSS 2.0 Response from an Artwork Eloquent collection.
|
|
* Artworks must have 'user' and 'categories' relations preloaded.
|
|
*/
|
|
public function buildFromArtworks(
|
|
string $channelTitle,
|
|
string $channelDescription,
|
|
string $feedUrl,
|
|
Collection $artworks,
|
|
): Response {
|
|
$items = $artworks->take(self::FEED_LIMIT)->map(fn ($a) => $this->artworkToItem($a));
|
|
|
|
return $this->buildResponse($channelTitle, $channelDescription, url('/'), $feedUrl, $items);
|
|
}
|
|
|
|
/**
|
|
* Build an RSS 2.0 Response from a BlogPost Eloquent collection.
|
|
* Posts must have 'author' relation preloaded.
|
|
*/
|
|
public function buildFromBlogPosts(
|
|
string $channelTitle,
|
|
string $channelDescription,
|
|
string $feedUrl,
|
|
Collection $posts,
|
|
): Response {
|
|
$items = $posts->take(self::FEED_LIMIT)->map(fn ($p) => $this->blogPostToItem($p));
|
|
|
|
return $this->buildResponse($channelTitle, $channelDescription, url('/blog'), $feedUrl, $items);
|
|
}
|
|
|
|
// ── Private helpers ───────────────────────────────────────────────────────
|
|
|
|
private function buildResponse(
|
|
string $channelTitle,
|
|
string $channelDescription,
|
|
string $channelLink,
|
|
string $feedUrl,
|
|
Collection $items,
|
|
): Response {
|
|
$xml = view('rss.channel', [
|
|
'channelTitle' => trim($channelTitle) . ' — Skinbase',
|
|
'channelDescription' => $channelDescription,
|
|
'channelLink' => $channelLink,
|
|
'feedUrl' => $feedUrl,
|
|
'items' => $items,
|
|
'buildDate' => now()->toRfc2822String(),
|
|
])->render();
|
|
|
|
return response($xml, 200, [
|
|
'Content-Type' => 'application/rss+xml; charset=utf-8',
|
|
'Cache-Control' => 'public, max-age=300',
|
|
]);
|
|
}
|
|
|
|
/** Convert an Artwork model to an RSS item array. */
|
|
private function artworkToItem(object $artwork): array
|
|
{
|
|
$link = url('/art/' . $artwork->id . '/' . ($artwork->slug ?? ''));
|
|
$thumb = method_exists($artwork, 'thumbUrl') ? $artwork->thumbUrl('sm') : null;
|
|
|
|
// Primary category from eagerly loaded relation (avoid N+1)
|
|
$primaryCategory = ($artwork->relationLoaded('categories'))
|
|
? $artwork->categories->first()
|
|
: null;
|
|
|
|
// Build HTML description embedded in CDATA
|
|
$descParts = [];
|
|
if ($thumb) {
|
|
$descParts[] = '<img src="' . htmlspecialchars($thumb, ENT_XML1) . '" '
|
|
. 'alt="' . htmlspecialchars((string) $artwork->title, ENT_XML1) . '" />';
|
|
}
|
|
if (!empty($artwork->description)) {
|
|
$descParts[] = '<p>' . htmlspecialchars(strip_tags((string) $artwork->description), ENT_XML1) . '</p>';
|
|
}
|
|
|
|
return [
|
|
'title' => (string) $artwork->title,
|
|
'link' => $link,
|
|
'guid' => $link,
|
|
'description' => implode('', $descParts),
|
|
'pubDate' => $artwork->published_at?->toRfc2822String(),
|
|
'author' => $artwork->user?->username ?? 'Unknown',
|
|
'category' => $primaryCategory?->name,
|
|
'enclosure' => $thumb ? [
|
|
'url' => $thumb,
|
|
'length' => 0,
|
|
'type' => 'image/jpeg',
|
|
] : null,
|
|
];
|
|
}
|
|
|
|
/** Convert a BlogPost model to an RSS item array. */
|
|
private function blogPostToItem(object $post): array
|
|
{
|
|
$link = url('/blog/' . $post->slug);
|
|
$excerpt = $post->excerpt ?? strip_tags((string) ($post->body ?? ''));
|
|
|
|
return [
|
|
'title' => (string) $post->title,
|
|
'link' => $link,
|
|
'guid' => $link,
|
|
'description' => $excerpt,
|
|
'pubDate' => $post->published_at?->toRfc2822String(),
|
|
'author' => $post->author?->username ?? 'Skinbase',
|
|
'category' => null,
|
|
'enclosure' => !empty($post->featured_image) ? [
|
|
'url' => $post->featured_image,
|
|
'length' => 0,
|
|
'type' => 'image/jpeg',
|
|
] : null,
|
|
];
|
|
}
|
|
}
|