Skip to content

Commit 20a7b67

Browse files
authored
Merge pull request #215 from fleetbase/feature/fix-ghost-blog-feed
Fix Fleetbase blog Ghost feed lookup
2 parents 2aa2412 + eb39338 commit 20a7b67

4 files changed

Lines changed: 231 additions & 33 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fleetbase/core-api",
3-
"version": "1.6.47",
3+
"version": "1.6.48",
44
"description": "Core Framework and Resources for Fleetbase API",
55
"keywords": [
66
"fleetbase",

src/Http/Controllers/Internal/v1/LookupController.php

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Fleetbase\Http\Controllers\Internal\v1;
44

55
use Fleetbase\Http\Controllers\Controller;
6+
use Fleetbase\Support\FleetbaseBlog;
67
use Fleetbase\Support\Http;
78
use Fleetbase\Types\Country;
89
use Fleetbase\Types\Currency;
@@ -186,8 +187,8 @@ public function country($code, Request $request)
186187
*/
187188
public function fleetbaseBlog(Request $request)
188189
{
189-
$limit = $request->integer('limit', 6);
190-
$cacheKey = "fleetbase_blog_posts_{$limit}";
190+
$limit = max(1, min($request->integer('limit', 6), 20));
191+
$cacheKey = $this->getFleetbaseBlogCacheKey($limit);
191192
$cacheTTL = now()->addDays(4); // 4 days as requested
192193

193194
// Try to get from cache
@@ -212,7 +213,8 @@ public function fleetbaseBlog(Request $request)
212213
*/
213214
protected function fetchBlogPosts(int $limit): array
214215
{
215-
$rssUrl = 'https://www.fleetbase.io/post/rss.xml';
216+
$limit = max(1, min($limit, 20));
217+
$rssUrl = $this->getFleetbaseBlogFeedUrl();
216218
$posts = [];
217219

218220
try {
@@ -230,31 +232,7 @@ protected function fetchBlogPosts(int $limit): array
230232
return [];
231233
}
232234

233-
// Parse XML
234-
$rss = simplexml_load_string($response->body());
235-
236-
if (!$rss || !isset($rss->channel->item)) {
237-
Log::error('[Blog] Invalid RSS feed structure');
238-
239-
return [];
240-
}
241-
242-
foreach ($rss->channel->item as $item) {
243-
$posts[] = [
244-
'title' => (string) $item->title,
245-
'link' => (string) $item->link,
246-
'guid' => (string) $item->guid,
247-
'description' => (string) $item->description,
248-
'pubDate' => (string) $item->pubDate,
249-
'media_content' => (string) data_get($item, 'media:content.url'),
250-
'media_thumbnail' => (string) data_get($item, 'media:thumbnail.url'),
251-
];
252-
253-
// Early exit if we have enough
254-
if (count($posts) >= $limit) {
255-
break;
256-
}
257-
}
235+
$posts = $this->parseBlogPostsFromRss($response->body(), $limit);
258236

259237
Log::info('[Blog] Successfully fetched blog posts', ['count' => count($posts)]);
260238
} catch (\Exception $e) {
@@ -267,6 +245,48 @@ protected function fetchBlogPosts(int $limit): array
267245
return array_slice($posts, 0, $limit);
268246
}
269247

248+
/**
249+
* Parse blog posts from an RSS payload.
250+
*/
251+
protected function parseBlogPostsFromRss(string $rssXml, int $limit): array
252+
{
253+
return FleetbaseBlog::parseRss($rssXml, $limit, $this->getFleetbaseBlogUrl());
254+
}
255+
256+
/**
257+
* Get the cache key for the Fleetbase blog feed.
258+
*/
259+
protected function getFleetbaseBlogCacheKey(int $limit): string
260+
{
261+
$sourceHash = md5($this->getFleetbaseBlogFeedUrl() . '|' . $this->getFleetbaseBlogUrl());
262+
263+
return "fleetbase_blog_posts_{$limit}_{$sourceHash}";
264+
}
265+
266+
/**
267+
* Get the public Fleetbase blog RSS feed URL.
268+
*/
269+
protected function getFleetbaseBlogFeedUrl(): string
270+
{
271+
return FleetbaseBlog::getFeedUrl();
272+
}
273+
274+
/**
275+
* Get the canonical Fleetbase blog URL.
276+
*/
277+
protected function getFleetbaseBlogUrl(): string
278+
{
279+
return FleetbaseBlog::getBlogUrl();
280+
}
281+
282+
/**
283+
* Rewrite Ghost publication links to the canonical Fleetbase.io blog URL.
284+
*/
285+
protected function normalizeFleetbaseBlogLink(?string $link): string
286+
{
287+
return FleetbaseBlog::normalizeLink($link, $this->getFleetbaseBlogUrl());
288+
}
289+
270290
/**
271291
* Manually refresh blog cache (can be called via webhook or admin panel).
272292
*
@@ -275,13 +295,13 @@ protected function fetchBlogPosts(int $limit): array
275295
public function refreshBlogCache()
276296
{
277297
// Clear all blog caches
278-
Cache::forget('fleetbase_blog_posts_6');
279-
Cache::forget('fleetbase_blog_posts_10');
280-
Cache::forget('fleetbase_blog_posts_20');
298+
Cache::forget($this->getFleetbaseBlogCacheKey(6));
299+
Cache::forget($this->getFleetbaseBlogCacheKey(10));
300+
Cache::forget($this->getFleetbaseBlogCacheKey(20));
281301

282302
// Warm up cache with default limit
283303
$posts = $this->fetchBlogPosts(6);
284-
Cache::put('fleetbase_blog_posts_6', $posts, now()->addDays(4));
304+
Cache::put($this->getFleetbaseBlogCacheKey(6), $posts, now()->addDays(4));
285305

286306
return response()->json([
287307
'status' => 'success',

src/Support/FleetbaseBlog.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace Fleetbase\Support;
4+
5+
use Illuminate\Support\Str;
6+
7+
class FleetbaseBlog
8+
{
9+
/**
10+
* Parse blog posts from an RSS payload.
11+
*/
12+
public static function parseRss(string $rssXml, int $limit, ?string $blogUrl = null): array
13+
{
14+
$limit = max(1, min($limit, 20));
15+
$posts = [];
16+
17+
$previousLibxmlState = libxml_use_internal_errors(true);
18+
$rss = simplexml_load_string($rssXml);
19+
libxml_clear_errors();
20+
libxml_use_internal_errors($previousLibxmlState);
21+
22+
if (!$rss || !isset($rss->channel->item)) {
23+
return [];
24+
}
25+
26+
foreach ($rss->channel->item as $item) {
27+
$publishedAt = self::getSimpleXmlText($item->pubDate);
28+
$timestamp = $publishedAt ? strtotime($publishedAt) : false;
29+
30+
$posts[] = [
31+
'title' => self::getSimpleXmlText($item->title),
32+
'link' => self::normalizeLink(self::getSimpleXmlText($item->link), $blogUrl),
33+
'guid' => self::getSimpleXmlText($item->guid),
34+
'description' => self::getSimpleXmlText($item->description),
35+
'pubDate' => $publishedAt,
36+
'published_at' => $timestamp ? gmdate('c', $timestamp) : null,
37+
'author' => self::getSimpleXmlText($item->children('http://purl.org/dc/elements/1.1/')->creator),
38+
'media_content' => self::getSimpleXmlAttribute($item, 'http://search.yahoo.com/mrss/', 'content', 'url'),
39+
'media_thumbnail' => self::getSimpleXmlAttribute($item, 'http://search.yahoo.com/mrss/', 'thumbnail', 'url'),
40+
];
41+
42+
if (count($posts) >= $limit) {
43+
break;
44+
}
45+
}
46+
47+
return $posts;
48+
}
49+
50+
/**
51+
* Rewrite Ghost publication links to the canonical Fleetbase.io blog URL.
52+
*/
53+
public static function normalizeLink(?string $link, ?string $blogUrl = null): string
54+
{
55+
$link = trim((string) $link);
56+
$blogUrl = self::getBlogUrl($blogUrl);
57+
58+
if (!$link) {
59+
return $blogUrl;
60+
}
61+
62+
$host = parse_url($link, PHP_URL_HOST);
63+
$path = trim((string) parse_url($link, PHP_URL_PATH), '/');
64+
65+
if ($host && Str::contains($host, 'ghost.io') && $path) {
66+
return $blogUrl . '/' . $path;
67+
}
68+
69+
return $link;
70+
}
71+
72+
/**
73+
* Get the public Fleetbase blog RSS feed URL.
74+
*/
75+
public static function getFeedUrl(?string $feedUrl = null): string
76+
{
77+
return rtrim($feedUrl ?: getenv('FLEETBASE_BLOG_FEED_URL') ?: 'https://fleetbase.ghost.io/rss/', '/') . '/';
78+
}
79+
80+
/**
81+
* Get the canonical Fleetbase blog URL.
82+
*/
83+
public static function getBlogUrl(?string $blogUrl = null): string
84+
{
85+
return rtrim($blogUrl ?: getenv('FLEETBASE_BLOG_URL') ?: 'https://www.fleetbase.io/blog', '/');
86+
}
87+
88+
/**
89+
* Get trimmed text from a SimpleXML element.
90+
*/
91+
protected static function getSimpleXmlText($node): string
92+
{
93+
return trim((string) $node);
94+
}
95+
96+
/**
97+
* Get an attribute from a namespaced SimpleXML child.
98+
*/
99+
protected static function getSimpleXmlAttribute($item, string $namespace, string $childName, string $attributeName): string
100+
{
101+
$children = $item->children($namespace);
102+
103+
if (!isset($children->{$childName})) {
104+
return '';
105+
}
106+
107+
$attributes = $children->{$childName}->attributes();
108+
109+
return isset($attributes->{$attributeName}) ? (string) $attributes->{$attributeName} : '';
110+
}
111+
}

tests/Unit/FleetbaseBlogTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
use Fleetbase\Support\FleetbaseBlog;
4+
5+
function fleetbaseBlogRssFixture(): string
6+
{
7+
return <<<'XML'
8+
<?xml version="1.0" encoding="UTF-8"?>
9+
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
10+
<channel>
11+
<item>
12+
<title><![CDATA[First Ghost Post]]></title>
13+
<description><![CDATA[<p>First excerpt.</p>]]></description>
14+
<link>https://fleetbase.ghost.io/first-ghost-post/</link>
15+
<guid isPermaLink="false">ghost-post-1</guid>
16+
<dc:creator><![CDATA[Fleetbase Team]]></dc:creator>
17+
<pubDate>Wed, 06 May 2026 14:31:46 GMT</pubDate>
18+
<media:content url="https://static.ghost.org/first.jpg" />
19+
<media:thumbnail url="https://static.ghost.org/first-thumb.jpg" />
20+
</item>
21+
<item>
22+
<title><![CDATA[Second Ghost Post]]></title>
23+
<description><![CDATA[<p>Second excerpt.</p>]]></description>
24+
<link>https://fleetbase.ghost.io/second-ghost-post/</link>
25+
<guid isPermaLink="false">ghost-post-2</guid>
26+
<dc:creator><![CDATA[Fleetbase Team]]></dc:creator>
27+
<pubDate>Wed, 06 May 2026 14:30:46 GMT</pubDate>
28+
</item>
29+
</channel>
30+
</rss>
31+
XML;
32+
}
33+
34+
test('fleetbase blog parser maps ghost rss posts to the widget response shape', function () {
35+
$posts = FleetbaseBlog::parseRss(fleetbaseBlogRssFixture(), 6);
36+
37+
expect($posts)->toHaveCount(2)
38+
->and($posts[0])->toMatchArray([
39+
'title' => 'First Ghost Post',
40+
'link' => 'https://www.fleetbase.io/blog/first-ghost-post',
41+
'guid' => 'ghost-post-1',
42+
'description' => '<p>First excerpt.</p>',
43+
'pubDate' => 'Wed, 06 May 2026 14:31:46 GMT',
44+
'published_at' => '2026-05-06T14:31:46+00:00',
45+
'author' => 'Fleetbase Team',
46+
'media_content' => 'https://static.ghost.org/first.jpg',
47+
'media_thumbnail' => 'https://static.ghost.org/first-thumb.jpg',
48+
]);
49+
});
50+
51+
test('fleetbase blog parser clamps limit to a small safe range', function () {
52+
$posts = FleetbaseBlog::parseRss(fleetbaseBlogRssFixture(), 1);
53+
54+
expect($posts)->toHaveCount(1)
55+
->and($posts[0]['title'])->toBe('First Ghost Post');
56+
});
57+
58+
test('fleetbase blog parser returns an empty array for malformed rss', function () {
59+
$posts = FleetbaseBlog::parseRss('<rss><channel><item>', 6);
60+
61+
expect($posts)->toBe([]);
62+
});
63+
64+
test('fleetbase blog link normalization keeps non ghost links unchanged', function () {
65+
expect(FleetbaseBlog::normalizeLink('https://www.fleetbase.io/blog/already-canonical'))->toBe('https://www.fleetbase.io/blog/already-canonical')
66+
->and(FleetbaseBlog::normalizeLink('https://fleetbase.ghost.io/ghost-post/'))->toBe('https://www.fleetbase.io/blog/ghost-post');
67+
});

0 commit comments

Comments
 (0)