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-Datenabruf
  • site/plugins/bookmark/index.js – Panel-Block mit Vorschau und Button
  • site/blueprints/blocks/bookmark.yml – Block-Definition mit Feldern
  • site/snippets/blocks/bookmark.php – Frontend-Template
  • assets/css/custom.css – Styling der Karte
  • site/snippets/layouts/default.php – Einbindung der CSS-Datei
  • docker-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:

BAYERWALD.SOCIAL
Eine Mastodon-Instanz für die Region Niederbayern und Bayerischer Wald. Wer sich mit der Region verbunden fühlt, ist hier ebenfalls herzlich willkommen.
Mastodon hosted on bayerwald.social
BAYERWALD.SOCIAL
WATZMANN.SOCIAL
Testserver
Mastodon hosted on watzmann.social
WATZMANN.SOCIAL
PixelGalaxy.NET
Pixelfed is an image sharing platform, an ethical alternative to centralized platforms.
PixelGalaxy.NET
PixelGalaxy.NET
Home
GRAFFITI | BAYERWALD.SOCIAL
GRAFFITI
Home
WATZMANN.SOCIAL
Thoughts, stories and ideas.
WATZMANN.SOCIAL
WATZMANN.SOCIAL
BAYERWALD.BLOG
Thoughts, stories and ideas.
BAYERWALD.BLOG
BAYERWALD.BLOG
BLOG.BAYERWALD.SOCIAL
Thoughts, stories and ideas.
BLOG.BAYERWALD.SOCIAL
BLOG.BAYERWALD.SOCIAL

Share