innhold/custom/plugins/page/news-preview.php
Ruben 1766b370dd Add news preview plugin with three display sizes
The plugin displays latest news items from /nyheter/ section with three
size options:
- small: horizontal scrolling cards
- medium: responsive grid
- large: responsive grid with summaries

Usage: Add to metadata.ini and call with <?= news_preview() ?> or <?=
news_preview(5, 'large') ?>
2026-02-07 17:16:06 +01:00

322 lines
9.1 KiB
PHP

<?php
/**
* News Preview Plugin
*
* Displays a preview of the latest news items from the /nyheter/ section.
* Can be included on any page as a reusable component.
*
* Usage: Add to metadata.ini:
* plugins = "news-preview"
*
* Then in your PHP content file:
* <?= news_preview() ?>
* <?= news_preview(3, 'medium') ?>
* <?= news_preview(5, 'large') ?>
*
* Available sizes:
* - 'small': Horizontal scrolling row of compact cards (cover, title, date)
* - 'medium': Responsive wrapping grid (cover, title, date)
* - 'large': Responsive wrapping grid with summary (cover, title, date, summary)
*/
function newsPreviewT(string $key): string {
$ctx = $GLOBALS['ctx'] ?? null;
$translations = $ctx?->get('translations', []) ?? [];
$fallbacks = [
'heading' => 'Siste nytt',
'read_more' => 'Les mer',
'see_all' => 'Se alle nyheter',
];
return $translations["news_preview.$key"] ?? $fallbacks[$key] ?? $key;
}
function newsPreviewBuildItems(int $count): array {
$ctx = $GLOBALS['ctx'] ?? null;
if (!$ctx) return [];
$newsDir = $ctx->contentDir . '/nyheter';
if (!is_dir($newsDir)) return [];
$langPrefix = $ctx->get('langPrefix', '');
$subdirs = getSubdirectories($newsDir);
$items = [];
foreach ($subdirs as $dir) {
$itemPath = "$newsDir/$dir";
$metadata = loadMetadata($itemPath);
$title = $metadata['title'] ?? extractTitle($itemPath) ?? $dir;
$rawDate = $metadata['date'] ?? extractRawDateFromFolder($dir);
$date = $rawDate ? Hooks::apply(Hook::PROCESS_CONTENT, $rawDate, 'date_format') : null;
$cover = findCoverImage($itemPath);
$slug = $metadata['slug'] ?? $dir;
$items[] = [
'title' => $title,
'url' => $langPrefix . '/nyheter/' . urlencode($slug) . '/',
'date' => $date,
'rawDate' => $rawDate,
'summary' => $metadata['summary'] ?? extractMetaDescription($itemPath, $metadata),
'cover' => $cover ? $langPrefix . '/nyheter/' . urlencode($dir) . '/' . $cover : null,
];
}
usort($items, fn($a, $b) => strcmp($b['rawDate'] ?? '', $a['rawDate'] ?? ''));
return array_slice($items, 0, $count);
}
function newsPreviewRenderSmall(array $items, string $langPrefix): string {
$heading = htmlspecialchars(newsPreviewT('heading'));
$seeAll = htmlspecialchars(newsPreviewT('see_all'));
$seeAllUrl = htmlspecialchars($langPrefix . '/nyheter/');
$cards = '';
foreach ($items as $item) {
$title = htmlspecialchars($item['title']);
$url = htmlspecialchars($item['url']);
$date = $item['date'] ? '<p class="news-preview-date">' . htmlspecialchars($item['date']) . '</p>' : '';
$cover = $item['cover']
? '<a href="' . $url . '"><img src="' . htmlspecialchars($item['cover']) . '" alt="' . $title . '"></a>'
: '';
$cards .= <<<HTML
<article class="news-preview-card">
{$cover}
<div class="news-preview-card-body">
<h3><a href="{$url}">{$title}</a></h3>
{$date}
</div>
</article>
HTML;
}
return <<<HTML
<section class="news-preview news-preview-small">
<div class="news-preview-header">
<h1>{$heading}</h1>
<a href="{$seeAllUrl}" class="button">{$seeAll}</a>
</div>
<div class="news-preview-scroll">
{$cards}
</div>
</section>
HTML;
}
function newsPreviewRenderMedium(array $items, string $langPrefix): string {
$heading = htmlspecialchars(newsPreviewT('heading'));
$seeAll = htmlspecialchars(newsPreviewT('see_all'));
$seeAllUrl = htmlspecialchars($langPrefix . '/nyheter/');
$cards = '';
foreach ($items as $item) {
$title = htmlspecialchars($item['title']);
$url = htmlspecialchars($item['url']);
$date = $item['date'] ? '<p class="news-preview-date">' . htmlspecialchars($item['date']) . '</p>' : '';
$cover = $item['cover']
? '<a href="' . $url . '"><img src="' . htmlspecialchars($item['cover']) . '" alt="' . $title . '"></a>'
: '';
$cards .= <<<HTML
<article class="news-preview-card">
{$cover}
<h3><a href="{$url}">{$title}</a></h3>
{$date}
</article>
HTML;
}
return <<<HTML
<section class="news-preview news-preview-medium">
<div class="news-preview-header">
<h1>{$heading}</h1>
<a href="{$seeAllUrl}" class="button">{$seeAll}</a>
</div>
<div class="news-preview-grid">
{$cards}
</div>
</section>
HTML;
}
function newsPreviewRenderLarge(array $items, string $langPrefix): string {
$heading = htmlspecialchars(newsPreviewT('heading'));
$readMore = htmlspecialchars(newsPreviewT('read_more'));
$seeAll = htmlspecialchars(newsPreviewT('see_all'));
$seeAllUrl = htmlspecialchars($langPrefix . '/nyheter/');
$cards = '';
foreach ($items as $item) {
$title = htmlspecialchars($item['title']);
$url = htmlspecialchars($item['url']);
$date = $item['date'] ? '<p class="news-preview-date">' . htmlspecialchars($item['date']) . '</p>' : '';
$cover = $item['cover']
? '<a href="' . $url . '"><img src="' . htmlspecialchars($item['cover']) . '" alt="' . $title . '"></a>'
: '';
$summary = $item['summary']
? '<p class="news-preview-summary">' . htmlspecialchars($item['summary']) . '</p>'
: '';
$cards .= <<<HTML
<article class="news-preview-card">
{$cover}
<h3><a href="{$url}">{$title}</a></h3>
{$date}
{$summary}
<a href="{$url}" class="button">{$readMore}</a>
</article>
HTML;
}
return <<<HTML
<section class="news-preview news-preview-large">
<div class="news-preview-header">
<h1>{$heading}</h1>
<a href="{$seeAllUrl}" class="button">{$seeAll}</a>
</div>
<div class="news-preview-grid">
{$cards}
</div>
</section>
HTML;
}
function newsPreviewGetStyles(): string {
static $included = false;
if ($included) return '';
$included = true;
return <<<'STYLES'
<style>
/* News Preview Plugin */
.news-preview {
margin-top: 2rem;
.news-preview-header {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 0 1rem;
h1 { margin-top: .5em }
.button { margin-top: 1.3em }
}
.news-preview-grid,
.news-preview-scroll {
margin-top: 1.5rem;
}
}
.news-preview-card {
background: white;
overflow: hidden;
img {
width: 100%;
height: 12rem;
object-fit: cover;
display: block;
}
h3 {
margin-top: .6rem;
padding: 0 .8rem;
font-size: 1.1rem;
}
.news-preview-date {
padding: 0 .8rem;
font-size: .85rem;
opacity: .7;
}
.news-preview-summary {
padding: 0 .8rem;
font-size: .95rem;
line-height: 1.4;
}
.button {
margin: .8rem .8rem 1rem;
}
}
/* Small: horizontal scroll */
.news-preview-small {
.news-preview-scroll {
display: flex;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
padding-bottom: .5rem;
}
.news-preview-card {
flex: 0 0 16rem;
scroll-snap-align: start;
img { height: 9rem }
h3 { font-size: 1rem }
}
}
/* Medium: wrapping grid, no summary */
.news-preview-medium {
.news-preview-grid {
display: flex;
flex-wrap: wrap;
gap: 1.2rem;
}
.news-preview-card {
flex: 1 1 clamp(14rem, (30rem - 100%) * 999, 100%);
padding-bottom: .8rem;
}
}
/* Large: wrapping grid with summary */
.news-preview-large {
.news-preview-grid {
display: flex;
flex-wrap: wrap;
gap: 1.2rem;
}
.news-preview-card {
flex: 1 1 clamp(14rem, (30rem - 100%) * 999, 100%);
padding-bottom: .2rem;
}
}
</style>
STYLES;
}
/**
* Render a news preview section.
*
* @param int $count Number of news items to show (default: 3)
* @param string $size Display size: 'small', 'medium', or 'large' (default: 'medium')
* @return string The complete HTML for the news preview section
*/
function news_preview(int $count = 3, string $size = 'medium'): string {
$ctx = $GLOBALS['ctx'] ?? null;
if (!$ctx) return '';
$items = newsPreviewBuildItems($count);
if (empty($items)) return '';
$langPrefix = $ctx->get('langPrefix', '');
$html = newsPreviewGetStyles();
$html .= match ($size) {
'small' => newsPreviewRenderSmall($items, $langPrefix),
'large' => newsPreviewRenderLarge($items, $langPrefix),
default => newsPreviewRenderMedium($items, $langPrefix),
};
return $html;
}