Skip to content

Commit 9ce47b3

Browse files
committed
Add blog posts to the homepage
1 parent 379fc13 commit 9ce47b3

File tree

3 files changed

+127
-2
lines changed

3 files changed

+127
-2
lines changed

src/Controller/WebController.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use App\Entity\Version;
1919
use App\Search\Algolia;
2020
use App\Search\Query;
21+
use App\Service\BlogRssFetcher;
2122
use App\Util\Killswitch;
2223
use Predis\Client as RedisClient;
2324
use Predis\Connection\ConnectionException;
@@ -36,13 +37,15 @@
3637
class WebController extends Controller
3738
{
3839
#[Route('/', name: 'home')]
39-
public function index(Request $req): RedirectResponse|Response
40+
public function index(Request $req, BlogRssFetcher $blogRssFetcher): RedirectResponse|Response
4041
{
4142
if ($resp = $this->checkForQueryMatch($req)) {
4243
return $resp;
4344
}
4445

45-
return $this->render('web/index.html.twig');
46+
return $this->render('web/index.html.twig', [
47+
'newsItems' => $blogRssFetcher->getNewsItems(),
48+
]);
4649
}
4750

4851
#[Route('/sponsor/', name: 'sponsor')]

src/Service/BlogRssFetcher.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of Packagist.
5+
*
6+
* (c) Jordi Boggiano <j.boggiano@seld.be>
7+
* Nils Adermann <naderman@naderman.de>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
namespace App\Service;
14+
15+
use Composer\Pcre\Preg;
16+
use Psr\Log\LoggerInterface;
17+
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
18+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerAction;
19+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
20+
use Symfony\Contracts\Cache\CacheInterface;
21+
use Symfony\Contracts\Cache\ItemInterface;
22+
use Symfony\Contracts\HttpClient\HttpClientInterface;
23+
24+
class BlogRssFetcher
25+
{
26+
private const string RSS_URL = 'https://blog.packagist.com/rss/';
27+
private const int CACHE_TTL = 3600;
28+
private const int MAX_ITEMS = 5;
29+
30+
public function __construct(
31+
private readonly HttpClientInterface $httpClient,
32+
private readonly CacheInterface $cache,
33+
private readonly LoggerInterface $logger,
34+
) {
35+
}
36+
37+
/**
38+
* @return list<array{title: string, link: string, description: string, datePublished: \DateTimeInterface|null}>
39+
*/
40+
public function getNewsItems(): array
41+
{
42+
try {
43+
return $this->cache->get('blog_news_items', function (ItemInterface $item): array {
44+
$item->expiresAfter(self::CACHE_TTL);
45+
46+
return $this->fetchAndParseRss();
47+
});
48+
} catch (\Exception $e) {
49+
$this->logger->error('Failed to fetch blog RSS feed', [
50+
'error' => $e->getMessage(),
51+
]);
52+
return [];
53+
}
54+
}
55+
56+
/**
57+
* @return list<array{title: string, link: string, description: string, datePublished: \DateTimeInterface|null}>
58+
*/
59+
private function fetchAndParseRss(): array
60+
{
61+
$response = $this->httpClient->request('GET', self::RSS_URL);
62+
$content = $response->getContent();
63+
64+
$xml = new \SimpleXMLElement($content);
65+
$items = [];
66+
67+
foreach ($xml->channel->item as $entry) {
68+
// Extract categories
69+
$categories = [];
70+
foreach ($entry->category as $category) {
71+
$categories[] = strtolower((string) $category);
72+
}
73+
74+
// Only include items with composer or packagist.org category
75+
if (!in_array('composer', $categories, true) && !in_array('packagist.org', $categories, true)) {
76+
continue;
77+
}
78+
79+
$pubDate = null;
80+
if (isset($entry->pubDate)) {
81+
try {
82+
$pubDate = new \DateTimeImmutable((string) $entry->pubDate);
83+
} catch (\Exception) {
84+
// Ignore invalid dates
85+
}
86+
}
87+
88+
$desc = $entry->description;
89+
$desc = str_replace('><', '> <', (string) $desc);
90+
$desc = trim(html_entity_decode(strip_tags($desc), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
91+
92+
$items[] = [
93+
'title' => (string) $entry->title,
94+
'link' => (string) $entry->link,
95+
'description' => $desc,
96+
'datePublished' => $pubDate,
97+
];
98+
99+
if (count($items) >= self::MAX_ITEMS) {
100+
break;
101+
}
102+
}
103+
104+
return $items;
105+
}
106+
}

templates/web/index.html.twig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,20 @@
6565
</div>
6666
</section>
6767
</section>
68+
69+
{% if newsItems is defined and newsItems|length > 0 %}
70+
<section class="row">
71+
<section class="col-lg-6">
72+
<h2 class="title">Latest News</h2>
73+
{% for item in newsItems %}
74+
<h3><a href="{{ item.link }}" rel="noopener noreferrer">{{ item.title }}</a> <small class="text-muted">- {{ item.datePublished|date('M d, Y') }}</small></h3>
75+
<p>
76+
{{ item.description|length > 200 ? item.description[:200] ~ '...' : item.description }}
77+
{% if item.datePublished %}
78+
{% endif %}
79+
</p>
80+
{% endfor %}
81+
</section>
82+
</section>
83+
{% endif %}
6884
{% endblock %}

0 commit comments

Comments
 (0)