AI Powered


This is how I realized the blogroll on my website

In my blog, there has been a blogroll for quite some time now. I have already described what it's all about here. At this point, I would like to explain how I implemented my blogroll using Kirby.

My blogroll is quite simple: In my blog blueprint, I have a structure field that consists of two fields:

  1. The URL of the blog
  2. The title of the blog

Previously, the blogroll was displayed in icon form under my blog listing. I want to expand it so that the blogroll is also accessible under /blog/blogroll, creating a deep link.

Since I don't want the blogroll to be a standalone page in the CMS, I will create a route for the blogroll and play a virtual page there.

The Route

In the site/config/config.php file, I create a new route. I want the blogroll to be accessible under blog/blogroll for all languages:

    'pattern' => 'blog/blogroll',
    'language' => '*',
    'action' => function ($language) {
      // […]

This allows the page to be delivered under the corresponding URL. However, if we were to call this URL now, we would get a 404 error. A route always needs either an output or must redirect to another route.

Therefore, in the next step, I create a virtual page that we can return:

    $data = [
        'slug' => 'blogroll',
        'parent' => page('blog'),
        'template' => 'listing-blogroll',
        'translations' => []

    $page = Page::factory($data);
    site()->visit($page, $language);

    return $page;

First, I set the slug to 'blogroll', then I specify the parent page, in my case, the blog. Finally, the page needs a template to be rendered. Here, I use a special blogroll template. I will talk about translations later.


I won't describe the complete template here, just the relevant parts. These are essentially the page header with a short description and the actual listing.

The page header consists of the title and a short description:

<div class="page-intro">
    <h1><?= $page->title(); ?></h1>
    <?= $page->intro()->kt(); ?>

The listing uses a snippet that I already use in my notes:

<ul class="note-list">
    <?php foreach ($blogroll as $blog) : ?>
        <?php $site->organism('list-entry-note', ['note' => $blog]); ?>
    <?php endforeach; ?>

Here, it's worth noting that I use two site methods for snippets, which I have written myself. In this case, organism(). Basically, they work the same as Kirby's snippet() function. I use my methods to add a bit more organization to my snippets. I'll probably talk more about this in another post.

So, my list-entry-note snippet receives a blog from my blogroll and displays it. It needs three pieces of information:

  1. A title
  2. A URL
  3. An icon

The data comes from the corresponding controller.

The Controller

In Kirby, controllers are used to separate data logic from templates. I like this approach and try to keep my templates as simple as possible. Anything related to data should ideally happen in the controller.

This is what the controller for the blogroll looks like:

return function ($page) {
    $blogroll = page('blog')->blogroll()->toStructure();
    $blogEntries = [];

  foreach ($blogroll as $blog) {

    $url = str_replace(['https://', 'http://', '/'], ['', '', ''], Url::stripPath($blog->url()->value()));
    $icon = $page->getSiteIcon($url);

    $blogEntries[] = new StructureObject([
        'content' => [
            'title' => $blog->title(),
            'url' => $blog->url(),
            'icon' => $icon,
            'intendedTemplate' => 'blogroll'

    $blogrollStructure = new Structure($blogEntries);

  return [
    'blogroll' => $blogrollStructure,

First, I get the blog because that's where the data is stored. I directly access the blogroll field and retrieve it as a structure. Then, I populate the $blogEntries array with data. It gets the title, the URL, and an icon.

For the icon, I use a custom method that tries to fetch the favicon of a page and provides a fallback if necessary. The result is a data:image/png;base64 string, not a image URL. This allows me to cache the favicons for a while. I don't have to query numerous other sites on every page load (I'll talk more about this in another post).

Finally, I create a new structure from the array because my note entry snippet expects it that way (it usually receives a note, and that is a Kirby page).

Completing the Virtual Page

Now I'm almost there; a few pieces of information are still missing in the virtual page. It needs a title and a description, among other things. These are hidden in the translations. I decided not to maintain this data in the panel for now because I probably won't adjust it very often:

'translations' => [
    'en' => [
        'code' => 'en',
        'content' => [
            'title' => 'Blogroll',
            'date' => '2024-01-30',
            'intro' => 'Blogs I read regularly and can recommend.',
    'de' => [
        'code' => 'de',
        'content' => [
            'title' => 'Blogroll',
            'date' => '2024-01-30',
            'intro' => 'Blogs, die ich regelmäßig lese und die ich empfehlen kann.',
            'uuid' => Uuid::generate(),

As you can see, the necessary data is now in the virtual page. The German language variant additionally gets a UUID; it is my main language.

The Finished Blogroll

The page is now accessible under blog/blogroll. The template receives data from the controller and renders it using the snippet. You can see the result here.

I can continue to add new entries easily in the blog blueprint, and the virtual page takes care of the rest without having to create an extra page for it in the panel.

What you could do now

If you (don't) like this post, you can comment, write about it elsewhere, or share it. If you want to read more posts like this, you can follow me via RSS or ActivityPub, or you can view similar posts.