From cad87d4e417d170042cb418ac0d2d941d47c8006 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson"
Date: Tue, 19 May 2026 19:49:14 +0800
Subject: [PATCH 1/2] Fix Fleetbase blog Ghost feed lookup
---
.../Internal/v1/LookupController.php | 84 ++++++++-----
src/Support/FleetbaseBlog.php | 111 ++++++++++++++++++
tests/Unit/FleetbaseBlogTest.php | 67 +++++++++++
3 files changed, 230 insertions(+), 32 deletions(-)
create mode 100644 src/Support/FleetbaseBlog.php
create mode 100644 tests/Unit/FleetbaseBlogTest.php
diff --git a/src/Http/Controllers/Internal/v1/LookupController.php b/src/Http/Controllers/Internal/v1/LookupController.php
index bec89523..464231b8 100644
--- a/src/Http/Controllers/Internal/v1/LookupController.php
+++ b/src/Http/Controllers/Internal/v1/LookupController.php
@@ -3,6 +3,7 @@
namespace Fleetbase\Http\Controllers\Internal\v1;
use Fleetbase\Http\Controllers\Controller;
+use Fleetbase\Support\FleetbaseBlog;
use Fleetbase\Support\Http;
use Fleetbase\Types\Country;
use Fleetbase\Types\Currency;
@@ -186,8 +187,8 @@ public function country($code, Request $request)
*/
public function fleetbaseBlog(Request $request)
{
- $limit = $request->integer('limit', 6);
- $cacheKey = "fleetbase_blog_posts_{$limit}";
+ $limit = max(1, min($request->integer('limit', 6), 20));
+ $cacheKey = $this->getFleetbaseBlogCacheKey($limit);
$cacheTTL = now()->addDays(4); // 4 days as requested
// Try to get from cache
@@ -212,7 +213,8 @@ public function fleetbaseBlog(Request $request)
*/
protected function fetchBlogPosts(int $limit): array
{
- $rssUrl = 'https://www.fleetbase.io/post/rss.xml';
+ $limit = max(1, min($limit, 20));
+ $rssUrl = $this->getFleetbaseBlogFeedUrl();
$posts = [];
try {
@@ -230,31 +232,7 @@ protected function fetchBlogPosts(int $limit): array
return [];
}
- // Parse XML
- $rss = simplexml_load_string($response->body());
-
- if (!$rss || !isset($rss->channel->item)) {
- Log::error('[Blog] Invalid RSS feed structure');
-
- return [];
- }
-
- foreach ($rss->channel->item as $item) {
- $posts[] = [
- 'title' => (string) $item->title,
- 'link' => (string) $item->link,
- 'guid' => (string) $item->guid,
- 'description' => (string) $item->description,
- 'pubDate' => (string) $item->pubDate,
- 'media_content' => (string) data_get($item, 'media:content.url'),
- 'media_thumbnail' => (string) data_get($item, 'media:thumbnail.url'),
- ];
-
- // Early exit if we have enough
- if (count($posts) >= $limit) {
- break;
- }
- }
+ $posts = $this->parseBlogPostsFromRss($response->body(), $limit);
Log::info('[Blog] Successfully fetched blog posts', ['count' => count($posts)]);
} catch (\Exception $e) {
@@ -267,6 +245,48 @@ protected function fetchBlogPosts(int $limit): array
return array_slice($posts, 0, $limit);
}
+ /**
+ * Parse blog posts from an RSS payload.
+ */
+ protected function parseBlogPostsFromRss(string $rssXml, int $limit): array
+ {
+ return FleetbaseBlog::parseRss($rssXml, $limit, $this->getFleetbaseBlogUrl());
+ }
+
+ /**
+ * Get the cache key for the Fleetbase blog feed.
+ */
+ protected function getFleetbaseBlogCacheKey(int $limit): string
+ {
+ $sourceHash = md5($this->getFleetbaseBlogFeedUrl() . '|' . $this->getFleetbaseBlogUrl());
+
+ return "fleetbase_blog_posts_{$limit}_{$sourceHash}";
+ }
+
+ /**
+ * Get the public Fleetbase blog RSS feed URL.
+ */
+ protected function getFleetbaseBlogFeedUrl(): string
+ {
+ return FleetbaseBlog::getFeedUrl();
+ }
+
+ /**
+ * Get the canonical Fleetbase blog URL.
+ */
+ protected function getFleetbaseBlogUrl(): string
+ {
+ return FleetbaseBlog::getBlogUrl();
+ }
+
+ /**
+ * Rewrite Ghost publication links to the canonical Fleetbase.io blog URL.
+ */
+ protected function normalizeFleetbaseBlogLink(?string $link): string
+ {
+ return FleetbaseBlog::normalizeLink($link, $this->getFleetbaseBlogUrl());
+ }
+
/**
* Manually refresh blog cache (can be called via webhook or admin panel).
*
@@ -275,13 +295,13 @@ protected function fetchBlogPosts(int $limit): array
public function refreshBlogCache()
{
// Clear all blog caches
- Cache::forget('fleetbase_blog_posts_6');
- Cache::forget('fleetbase_blog_posts_10');
- Cache::forget('fleetbase_blog_posts_20');
+ Cache::forget($this->getFleetbaseBlogCacheKey(6));
+ Cache::forget($this->getFleetbaseBlogCacheKey(10));
+ Cache::forget($this->getFleetbaseBlogCacheKey(20));
// Warm up cache with default limit
$posts = $this->fetchBlogPosts(6);
- Cache::put('fleetbase_blog_posts_6', $posts, now()->addDays(4));
+ Cache::put($this->getFleetbaseBlogCacheKey(6), $posts, now()->addDays(4));
return response()->json([
'status' => 'success',
diff --git a/src/Support/FleetbaseBlog.php b/src/Support/FleetbaseBlog.php
new file mode 100644
index 00000000..73dbe327
--- /dev/null
+++ b/src/Support/FleetbaseBlog.php
@@ -0,0 +1,111 @@
+channel->item)) {
+ return [];
+ }
+
+ foreach ($rss->channel->item as $item) {
+ $publishedAt = self::getSimpleXmlText($item->pubDate);
+ $timestamp = $publishedAt ? strtotime($publishedAt) : false;
+
+ $posts[] = [
+ 'title' => self::getSimpleXmlText($item->title),
+ 'link' => self::normalizeLink(self::getSimpleXmlText($item->link), $blogUrl),
+ 'guid' => self::getSimpleXmlText($item->guid),
+ 'description' => self::getSimpleXmlText($item->description),
+ 'pubDate' => $publishedAt,
+ 'published_at' => $timestamp ? gmdate('c', $timestamp) : null,
+ 'author' => self::getSimpleXmlText($item->children('http://purl.org/dc/elements/1.1/')->creator),
+ 'media_content' => self::getSimpleXmlAttribute($item, 'http://search.yahoo.com/mrss/', 'content', 'url'),
+ 'media_thumbnail' => self::getSimpleXmlAttribute($item, 'http://search.yahoo.com/mrss/', 'thumbnail', 'url'),
+ ];
+
+ if (count($posts) >= $limit) {
+ break;
+ }
+ }
+
+ return $posts;
+ }
+
+ /**
+ * Rewrite Ghost publication links to the canonical Fleetbase.io blog URL.
+ */
+ public static function normalizeLink(?string $link, ?string $blogUrl = null): string
+ {
+ $link = trim((string) $link);
+ $blogUrl = self::getBlogUrl($blogUrl);
+
+ if (!$link) {
+ return $blogUrl;
+ }
+
+ $host = parse_url($link, PHP_URL_HOST);
+ $path = trim((string) parse_url($link, PHP_URL_PATH), '/');
+
+ if ($host && Str::contains($host, 'ghost.io') && $path) {
+ return $blogUrl . '/' . $path;
+ }
+
+ return $link;
+ }
+
+ /**
+ * Get the public Fleetbase blog RSS feed URL.
+ */
+ public static function getFeedUrl(?string $feedUrl = null): string
+ {
+ return rtrim($feedUrl ?: getenv('FLEETBASE_BLOG_FEED_URL') ?: 'https://fleetbase.ghost.io/rss/', '/') . '/';
+ }
+
+ /**
+ * Get the canonical Fleetbase blog URL.
+ */
+ public static function getBlogUrl(?string $blogUrl = null): string
+ {
+ return rtrim($blogUrl ?: getenv('FLEETBASE_BLOG_URL') ?: 'https://www.fleetbase.io/blog', '/');
+ }
+
+ /**
+ * Get trimmed text from a SimpleXML element.
+ */
+ protected static function getSimpleXmlText($node): string
+ {
+ return trim((string) $node);
+ }
+
+ /**
+ * Get an attribute from a namespaced SimpleXML child.
+ */
+ protected static function getSimpleXmlAttribute($item, string $namespace, string $childName, string $attributeName): string
+ {
+ $children = $item->children($namespace);
+
+ if (!isset($children->{$childName})) {
+ return '';
+ }
+
+ $attributes = $children->{$childName}->attributes();
+
+ return isset($attributes->{$attributeName}) ? (string) $attributes->{$attributeName} : '';
+ }
+}
diff --git a/tests/Unit/FleetbaseBlogTest.php b/tests/Unit/FleetbaseBlogTest.php
new file mode 100644
index 00000000..1ff3ba31
--- /dev/null
+++ b/tests/Unit/FleetbaseBlogTest.php
@@ -0,0 +1,67 @@
+
+
+
+ -
+
+ First excerpt.
]]>
+ https://fleetbase.ghost.io/first-ghost-post/
+ ghost-post-1
+
+ Wed, 06 May 2026 14:31:46 GMT
+
+
+
+ -
+
+ Second excerpt.]]>
+ https://fleetbase.ghost.io/second-ghost-post/
+ ghost-post-2
+
+ Wed, 06 May 2026 14:30:46 GMT
+
+
+
+XML;
+}
+
+test('fleetbase blog parser maps ghost rss posts to the widget response shape', function () {
+ $posts = FleetbaseBlog::parseRss(fleetbaseBlogRssFixture(), 6);
+
+ expect($posts)->toHaveCount(2)
+ ->and($posts[0])->toMatchArray([
+ 'title' => 'First Ghost Post',
+ 'link' => 'https://www.fleetbase.io/blog/first-ghost-post',
+ 'guid' => 'ghost-post-1',
+ 'description' => 'First excerpt.
',
+ 'pubDate' => 'Wed, 06 May 2026 14:31:46 GMT',
+ 'published_at' => '2026-05-06T14:31:46+00:00',
+ 'author' => 'Fleetbase Team',
+ 'media_content' => 'https://static.ghost.org/first.jpg',
+ 'media_thumbnail' => 'https://static.ghost.org/first-thumb.jpg',
+ ]);
+});
+
+test('fleetbase blog parser clamps limit to a small safe range', function () {
+ $posts = FleetbaseBlog::parseRss(fleetbaseBlogRssFixture(), 1);
+
+ expect($posts)->toHaveCount(1)
+ ->and($posts[0]['title'])->toBe('First Ghost Post');
+});
+
+test('fleetbase blog parser returns an empty array for malformed rss', function () {
+ $posts = FleetbaseBlog::parseRss('- ', 6);
+
+ expect($posts)->toBe([]);
+});
+
+test('fleetbase blog link normalization keeps non ghost links unchanged', function () {
+ expect(FleetbaseBlog::normalizeLink('https://www.fleetbase.io/blog/already-canonical'))->toBe('https://www.fleetbase.io/blog/already-canonical')
+ ->and(FleetbaseBlog::normalizeLink('https://fleetbase.ghost.io/ghost-post/'))->toBe('https://www.fleetbase.io/blog/ghost-post');
+});
From eb393389888dc23f28e38cad8f6920dad53e9975 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson"
Date: Wed, 20 May 2026 14:38:42 +0800
Subject: [PATCH 2/2] bump version to v1.6.48
---
composer.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/composer.json b/composer.json
index d667ae77..9d931bc2 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
"name": "fleetbase/core-api",
- "version": "1.6.47",
+ "version": "1.6.48",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",