Open Graph
There are now Open Graph informations and images available when a URL is shared.
For the image, I create a SVG image. I did not create JPEG or PNG. I did something simple and easily maintainable.
However, some improvements are still needed, particularly for the photos. When a title is too long, it appears under the photo, and I have to correct it. Despite this, I decided to implement them because it’s cool.
To see the result of the OG images, just add .svg
at the end of the URL like that: alienlebarge.ch/changelogs/2025-05-07-15-53.svg
Here’s the code of the template
<?php
use Kirby\Toolkit\Str;
/**
* Text formatting configuration
*/
$params = $params ?? [];
$TITLE_LENGTH = $params['titleLength'] ?? 50; // Maximum title length in characters
$MAX_TITLE_LENGTH = $params['maxTitleLength'] ?? 30; // Maximum characters per line for title
$EXCERPT_LENGTH = $params['excerptLength'] ?? 180; // Maximum excerpt length
$MAX_EXCERPT_LENGTH = $params['maxExcerptLength'] ?? 50; // Maximum characters per line for excerpt
/**
* Split text into lines respecting maximum length
*
* @param string $text Text to split
* @param int $maxLength Maximum length per line
* @return array Array of text lines
*/
function splitTextIntoLines(string $text, int $maxLength): array {
if (empty($text)) {
return [];
}
$words = explode(' ', $text);
$lines = [];
$currentLine = '';
foreach ($words as $word) {
if (strlen($currentLine . ' ' . $word) <= $maxLength) {
$currentLine .= ($currentLine === '' ? '' : ' ') . $word;
} else {
$lines[] = $currentLine;
$currentLine = $word;
}
}
if ($currentLine !== '') {
$lines[] = $currentLine;
}
return $lines;
}
/**
* Prepare and clean data
*/
$title = $page->title()->value() ?? '';
$title = Str::short($title, $TITLE_LENGTH);
$title = html_entity_decode($title, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$excerpt = $page->summary()->exists()
? $page->summary()->value()
: $page->text()->excerpt($EXCERPT_LENGTH)->value() ?? '';
$excerpt = html_entity_decode($excerpt, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Split title and excerpt into lines
$titleLines = splitTextIntoLines($title, $MAX_TITLE_LENGTH);
$excerptLines = splitTextIntoLines($excerpt, $MAX_EXCERPT_LENGTH);
?>
<svg width="1200" height="630"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1200 630"
role="img"
aria-label="<?= htmlspecialchars($title) ?>">
<!-- Styles -->
<style>
/* Fonts */
@font-face {
font-family: "Untitled";
src: url("https://alienlebarge.ch/assets/fonts/Untitled.woff2") format("woff2");
}
@font-face {
font-family: "UntitledMono";
src: url("https://alienlebarge.ch/assets/fonts/UntitledMono.woff2") format("woff2");
}
/* Base styles */
text {
font-family: "Untitled", sans-serif;
}
/* Heading styles */
.heading {
font-size: 72px;
font-weight: 800;
font-variation-settings: "wght" 800;
hanging-punctuation: first;
letter-spacing: 0;
}
/* Body text styles */
.body {
font-size: 40px;
font-weight: 400;
font-variant: oldstyle-nums;
font-variation-settings: "wght" var(--font-weight-body, 400);
font-weight: var(--font-weight-body, 400);
hanging-punctuation: first;
hyphens: auto;
}
/* Logo styles */
.logo-text {
font-size: 30px;
font-weight: 600;
}
.logo svg {
border-radius: 50%;
height: 2em;
width: 2em;
}
/* Reference styles */
.reference {
font-family: "UntitledMono", sans-serif;
font-size: 18px;
font-weight: 400;
}
</style>
<!-- Definitions -->
<defs>
<clipPath id="circleClip">
<circle cx="250" cy="250" r="250" />
</clipPath>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow
dx="0"
dy="4"
stdDeviation="8"
flood-color="rgba(0,0,0,0.2)"
/>
</filter>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="hsl(0deg 0% 96% / 100%)" />
<!-- Logo -->
<?php if ($file = $site->files()->filterBy('extension', 'svg')->first()):
$scale = 50 / $file->width();
?>
<g class="logo"
transform="translate(100, 20), scale(<?= $scale ?>)"
clip-path="url(#circleClip)"
role="img"
aria-label="Site logo">
<?= svg($file) ?>
</g>
<?php endif ?>
<!-- Logo text -->
<text class="logo-text"
y="55"
dominant-baseline="middle"
text-anchor="start"
role="banner"
aria-level="1">
<tspan x="170"><?= $site->title() ?></tspan><tspan fill="hsl(0deg 0% 55% / 100%)">.ch</tspan>
</text>
<!-- Title -->
<text class="heading"
y="200"
dominant-baseline="middle"
text-anchor="start"
role="heading"
aria-level="1">
<?php foreach ($titleLines as $index => $line): ?>
<tspan x="100" dy="<?= $index === 0 ? '0' : '1.2em' ?>">
<?= htmlspecialchars($line) ?>
</tspan>
<?php endforeach; ?>
</text>
<!-- Excerpt -->
<text class="body"
y="370"
dominant-baseline="middle"
text-anchor="start"
role="doc-abstract">
<?php foreach ($excerptLines as $index => $line): ?>
<tspan x="100" dy="<?= $index === 0 ? '0' : '1.2em' ?>">
<?= htmlspecialchars($line) ?>
</tspan>
<?php endforeach; ?>
</text>
<?php if ($page->template()->name() === 'photo'): ?>
<!-- Images -->
<?php
$images = $page->images();
if ($image1 = $images->first()): ?>
<!-- First image -->
<g transform="rotate(10, 975, 375)">
<!-- White background for image -->
<rect
x="780"
y="175"
width="350"
height="400"
fill="white"
filter="url(#shadow)"
/>
<!-- Photo image -->
<image
href="<?= $image1->thumb(['width' => 300, 'height' => 300])->url() ?>"
x="805"
y="200"
width="300"
height="300"
preserveAspectRatio="xMidYMid slice"
role="img"
aria-label="<?= $image1->alt()->or($page->title()) ?>"
/>
</g>
<?php endif; ?>
<?php if ($image2 = $images->nth(1)): ?>
<!-- Second image -->
<g transform="rotate(-5, 975, 375)">
<!-- White background for image -->
<rect
x="780"
y="175"
width="350"
height="400"
fill="white"
filter="url(#shadow)"
/>
<!-- Photo image -->
<image
href="<?= $image2->thumb(['width' => 300, 'height' => 300])->url() ?>"
x="805"
y="200"
width="300"
height="300"
preserveAspectRatio="xMidYMid slice"
role="img"
aria-label="<?= $image2->alt()->or($page->title()) ?>"
/>
</g>
<?php endif; ?>
<?php endif ?>
<!-- Date -->
<text class="reference"
y="600"
dominant-baseline="middle"
text-anchor="start"
role="doc-abstract">
<tspan x="100" fill="hsl(0deg 0% 55% / 100%)">
<?= $page->date()->toDate('Y-m-d') ?>
</tspan>
</text>
</svg>