diff --git a/composer.json b/composer.json
index d667ae7..9d931bc 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",
diff --git a/src/Http/Controllers/Internal/v1/LookupController.php b/src/Http/Controllers/Internal/v1/LookupController.php
index bec8952..464231b 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 0000000..73dbe32
--- /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 0000000..1ff3ba3
--- /dev/null
+++ b/tests/Unit/FleetbaseBlogTest.php
@@ -0,0 +1,67 @@
+
+
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('