Bookmark-Cards in Kirby 5
Ein Custom Block mit iframely-Vorschau
Wie man in Kirby 5 einen Ghost-ähnlichen Bookmark-Block baut, der externe Links als Vorschau-Karte mit Bild, Titel und Beschreibung darstellt – inklusive iframely-Dienst via Docker Compose und Panel-Integration.
Wer von Ghost zu Kirby wechselt, vermisst schnell eine Funktion: die Bookmark-Card. In Ghost fügt man eine URL ein, und Ghost holt automatisch Titel, Beschreibung und Vorschaubild ab – fertig ist eine elegante Link-Vorschau. In Kirby existiert das nicht von Haus aus. Wer es haben möchte, muss es selbst bauen. Dieser Artikel beschreibt, was dafür nötig ist.
Was eine Bookmark-Card ist
Eine Bookmark-Card ist eine strukturierte Vorschau eines externen Links. Sie zeigt Titel, Beschreibung, Favicon und ein Vorschaubild der verlinkten Seite – ähnlich wie Mastodon oder andere Plattformen einen geteilten Link darstellen. Das Ergebnis ist deutlich ansprechender als ein nackter Link im Fließtext.
Das Problem: OG-Daten zuverlässig abholen
Der erste Schritt ist das Abrufen der Open-Graph-Metadaten der Zielseite. Das klingt einfacher als es ist. Viele große Websites – darunter BMW, Cloudflare-geschützte Domains und andere – blockieren automatisierte HTTP-Anfragen vom Server. Ein einfaches PHP-Scraping funktioniert vielleicht bei 60 bis 70 Prozent aller URLs, beim Rest kommt eine leere Antwort oder ein Fehler.
Die zuverlässigere Lösung ist ein spezialisierter Dienst: iframely. iframely ist ein Open-Source-Node.js-Dienst, der URLs analysiert und strukturierte Metadaten zurückliefert. Er unterstützt über 1800 Domains mit eigenen Parsern und ist deutlich robuster als direktes PHP-Scraping. Für nicht-kommerzielle Nutzung ist er kostenlos selbst hostbar.
iframely via Docker Compose
iframely läuft als eigenständiger Dienst und wird am einfachsten über Docker Compose betrieben. Das docker-compose.yml ist überschaubar:
services:
iframely:
image: itteco/iframely:latest
ports:
- "127.0.0.1:8061:8061"
volumes:
- ./config.local.js:/iframely/config.local.cjs
restart: unless-stopped
Die Konfigurationsdatei config.local.cjs definiert das Caching und die erlaubten Ursprungsdomains:
module.exports = {
CACHE_ENGINE: 'node-cache',
CACHE_TTL: 86400000,
ALLOWED_ORIGINS: ['https://deine-kirby-domain.de'],
};
Der Dienst läuft nach dem Start auf localhost:8061 und ist von außen nicht erreichbar – nur Kirby selbst fragt ihn intern an. Der Ressourcenverbrauch ist gering: im Ruhezustand etwa 80 bis 150 MB RAM, CPU nahezu null.
Das Kirby-Plugin
Kirby braucht eine API-Route, die das Panel ansprechen kann, um OG-Daten zu einem Link abzuholen. Das Plugin liegt unter site/plugins/bookmark/index.php und registriert genau diese Route:
Kirby::plugin('site/bookmark', [
'api' => [
'routes' => [
[
'pattern' => 'bookmark-preview',
'method' => 'GET',
'action' => function () {
$url = get('url');
$base = option('site.bookmark.iframely', 'http://localhost:8061');
$r = Remote::get($base . '/iframely?uri=' . urlencode($url), ['timeout' => 10]);
$d = $r->json();
return [
'title' => $d['meta']['title'] ?? '',
'description' => $d['meta']['description'] ?? '',
'image' => $d['links']['thumbnail'][0]['href'] ?? '',
'favicon' => $d['links']['icon'][0]['href'] ?? '',
'site' => $d['meta']['site'] ?? parse_url($url, PHP_URL_HOST),
];
}
]
]
]
]);
Die iframely-URL ist über die Kirby-Konfiguration überschreibbar, Standard ist localhost:8061.
Das Blueprint
Das Blueprint definiert den Block im Panel. Es liegt unter site/blueprints/blocks/bookmark.yml und beschreibt alle Felder die eine Bookmark-Card benötigt:
name: Bookmark
icon: url
fields:
url:
type: url
label: URL
width: 1/1
og_title:
type: text
label: Titel
og_description:
type: textarea
label: Beschreibung
size: small
og_image:
type: text
label: Vorschaubild-URL
og_favicon:
type: text
label: Favicon-URL
width: 1/2
og_site:
type: text
label: Seitenname
width: 1/2
Die Felder werden beim Klick auf „Vorschau laden” automatisch befüllt – können aber auch manuell eingetragen werden, etwa für Seiten die iframely nicht auflösen kann.
Das Panel-JavaScript
Das JavaScript unter site/plugins/bookmark/index.js registriert den Block im Kirby Panel, rendert die Vorschau und stellt den „Vorschau laden”-Button bereit. Es kommuniziert mit der API-Route und schreibt die abgerufenen Daten direkt in die Block-Felder:
panel.plugin('site/bookmark', {
blocks: {
bookmark: {
// ... render-Funktion mit Button und Vorschau-Karte
}
}
});
Die Vorschau im Panel zeigt Titel, Beschreibung, Favicon und Bild – identisch zum späteren Frontend-Rendering.
Das Frontend-Snippet
Das Template unter site/snippets/blocks/bookmark.php rendert die gespeicherten Daten als HTML:
<?php if ($block->url()->isNotEmpty()): ?>
<a class="bookmark-card" href="<?= $block->url()->esc() ?>" target="_blank" rel="noopener noreferrer">
<div class="bookmark-content">
<div class="bookmark-title">
<?= $block->og_title()->isNotEmpty() ? $block->og_title()->esc() : $block->url()->esc() ?>
</div>
<?php if ($block->og_description()->isNotEmpty()): ?>
<div class="bookmark-description"><?= $block->og_description()->esc() ?></div>
<?php endif ?>
<div class="bookmark-meta">
<?php if ($block->og_favicon()->isNotEmpty()): ?>
<img class="bookmark-favicon" src="<?= $block->og_favicon()->esc() ?>" alt="" loading="lazy">
<?php endif ?>
<span><?= $block->og_site()->isNotEmpty() ? $block->og_site()->esc() : parse_url($block->url(), PHP_URL_HOST) ?></span>
</div>
</div>
<?php if ($block->og_image()->isNotEmpty()): ?>
<div class="bookmark-image">
<img src="<?= $block->og_image()->esc() ?>" alt="<?= $block->og_title()->esc() ?>" loading="lazy">
</div>
<?php endif ?>
</a>
<?php endif ?>
Das CSS
Das Styling liegt in assets/css/custom.css und wird in site/snippets/layouts/default.php eingebunden. Es sorgt für das Ghost-ähnliche Layout mit Text links und Bild rechts:
.bookmark-card {
display: flex;
justify-content: space-between;
border: 1px solid #e0e0e0;
border-radius: 6px;
overflow: hidden;
text-decoration: none;
color: inherit;
margin: 1.5rem 0;
}
.bookmark-image {
width: 160px;
min-width: 160px;
max-height: 130px;
overflow: hidden;
}
.bookmark-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
Was nötig war – die Übersicht
Um diesen einen Menüpunkt im Blocks-Editor zu realisieren, waren folgende Dateien nötig:
site/plugins/bookmark/index.php– API-Route für den OG-Datenabrufsite/plugins/bookmark/index.js– Panel-Block mit Vorschau und Buttonsite/blueprints/blocks/bookmark.yml– Block-Definition mit Feldernsite/snippets/blocks/bookmark.php– Frontend-Templateassets/css/custom.css– Styling der Kartesite/snippets/layouts/default.php– Einbindung der CSS-Dateidocker-compose.yml+config.local.js– iframely als Hintergrunddienst
Dazu musste der Block noch im Layout-Blueprint unter den erlaubten Fieldsets registriert werden, damit er im Blocks-Menü unter der Gruppe „Mixed” erscheint.
Grenzen des Systems
iframely löst deutlich mehr URLs auf als direktes PHP-Scraping, aber nicht alle. Websites die aggressiv Bot-Traffic blockieren – große Unternehmensseiten, stark Cloudflare-geschützte Domains – liefern auch iframely eine leere Antwort. Für solche Fälle bleiben die Felder im Panel offen und können manuell befüllt werden. Die Karte funktioniert in jedem Fall – sie zeigt dann eben nur was eingetragen wurde.
Der Aufwand ist beträchtlich für eine Funktion die in Ghost mit einem Klick funktioniert. Aber das ist der Preis der Flexibilität: Kirby gibt dir die vollständige Kontrolle – und die Verantwortung, sie zu nutzen.
Hier ein paar Beispiele wie die Bookmark Cards dann aussehen: