index-main/site/snippets/card-open-graph.php
isUnknown f757906584
All checks were successful
Deploy / Deploy to Production (push) Successful in 12s
feat: impact-media page type with OG scraping and cache
- Add impact-media blueprint (entries field + linked investigation)
- Add minimal impact-media template
- Refactor card-open-graph snippet: accepts $url param, Kirby cache (6h TTL), decode HTML entities, empty alt on images
- Update impacts.yml to allow impact-media pages
- Render impact-media in investigation aside with dynamic count + details/summary
- Add OG cache config in config.php
- CSS formatting fixes (body, card-block-small, category)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:31:00 +01:00

109 lines
3.4 KiB
PHP

<?php
/**
* Card Open Graph snippet
* Fetches and displays OG data from an external URL with cache.
*
* @var string $url The URL to scrape OG data from
*/
if (empty($url)) return;
$cache = kirby()->cache('og');
$cacheKey = md5($url);
$ogData = $cache->get($cacheKey);
if ($ogData === null) {
$ogData = [
'title' => '',
'description' => '',
'image' => '',
'site_name' => '',
'url' => $url,
];
$context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'timeout' => 10,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$html = @file_get_contents($url, false, $context);
if ($html !== false) {
// Parse OG meta tags (both property...content and content...property orders)
preg_match_all('/<meta\s+(?:property=["\']og:([^"\']+)["\']\s+content=["\']([^"\']*)["\']|content=["\']([^"\']*?)["\']\s+property=["\']og:([^"\']+)["\'])/i', $html, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $index => $property) {
$prop = $property ?: $matches[4][$index];
$content = $matches[2][$index] ?: $matches[3][$index];
$content = html_entity_decode(trim($content), ENT_QUOTES | ENT_HTML5, 'UTF-8');
switch ($prop) {
case 'title':
$ogData['title'] = $content;
break;
case 'description':
$ogData['description'] = $content;
break;
case 'image':
$ogData['image'] = $content;
break;
case 'site_name':
$ogData['site_name'] = $content;
break;
}
}
}
// Fallback: use <title> if no og:title
if (empty($ogData['title'])) {
preg_match('/<title>([^<]+)<\/title>/i', $html, $titleMatch);
if (!empty($titleMatch[1])) {
$ogData['title'] = html_entity_decode($titleMatch[1], ENT_QUOTES, 'UTF-8');
}
}
}
// Always use domain as site_name
$parsed = parse_url($url);
$ogData['site_name'] = $parsed['host'] ?? '';
$cache->set($cacheKey, $ogData, 360); // 6 hours
}
?>
<div class="card--open-graph">
<div class="open-graph__inner">
<?php if (!empty($ogData['image'])): ?>
<figure>
<img src="<?= htmlspecialchars($ogData['image']) ?>" alt="">
</figure>
<?php endif ?>
<div class="content">
<?php if (!empty($ogData['site_name'])): ?>
<span class="site-name"><?= htmlspecialchars($ogData['site_name']) ?></span>
<?php endif ?>
<?php if (!empty($ogData['title'])): ?>
<h3 class="title">
<a href="<?= htmlspecialchars($ogData['url']) ?>" target="_blank">
<?= htmlspecialchars($ogData['title']) ?>
</a>
</h3>
<?php endif ?>
<?php if (!empty($ogData['description'])): ?>
<p class="description"><?= htmlspecialchars($ogData['description']) ?></p>
<?php endif ?>
</div>
<a class="link-block" href="<?= htmlspecialchars($ogData['url']) ?>" target="_blank"></a>
</div>
</div>