nilsmielke’s blog. Read what I wrote! en The mental game Thu, 19 Oct 2023 00:00:00 +0200 (Nils Mielke)

The most important part is your mental game… I would say it’s 90 % mental and the rest is in your head.

—Cam Zink
on Red Bull TV

The 2023 (and 2010) Red Bull Rampage champion was talking about the mental strength required to compete in the most prestigious—and likely most dangerous—freeride mountainbiking event in the world.

This quote makes me chuckle every time I hear it or even think of it.

Airbus’ open source font Fri, 15 Sep 2023 00:00:00 +0200 (Nils Mielke) Thanks to a post on Mastodon, I came across Airbus’s open source font “B612”.

It was “designed and tested to be used on aircraft cockpit screens”, which makes it a perfect fit for small—even low-res—applications.

I like well-designed fonts with purpose—and I like, when companies share their findings and ultimately their resulting assets.

See for yourself and make good use of it:

Using cURL to get records from Airtable Mon, 20 Mar 2023 00:00:00 +0100 (Nils Mielke) I was having a hard time, figuring out, how to use cURL to retrieve Airtable records with their new token-based API. Granted, I am a slow—albeit eager—learner. But their API documentation wasn’t that much help with references like curl -X POST{YOUR_BASE_ID}/{YOUR_TABLE_ID_OR_TABLE_NAME}. What’s that supposed to mean? Where does that go?
For future me, here is what I found.

Where I’m coming from

With Airtable’s previous API one was able to import table records into PHP with a simple

$json = file_get_contents('{YOUR_BASE_ID}/{YOUR_TABLE_ID_OR_TABLE_NAME}?api_key={YOUR_API_KEY}');

Convert that to an array with $array = json_decode($json, true);—boom. Done.

As for the cURL approach…
There is not that much helpful documentation available, that is actually digestable by a newbie to the matter. Most articles seem to explain using cURL in the command line, too.

Fishing in the mud

So, I started searching around the interwebs and stitching together what I could find.

This is, what a very basic version of a cURL call in PHP looks like:

$url = "…";
$ch = curl_init();  
curl_setopt($ch, CURLOPT_URL, $url);  
$result = curl_exec($ch);  

Make that an Airtable API call:

$base_id = '{YOUR_BASE_ID}';
$table_id = '{YOUR_TABLE_ID_OR_TABLE_NAME}';
$token = '{YOUR_BEARER_TOKEN}';
$url = "" . $base_id . "/" . $table_id;
$headers = [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $token
$ch = curl_init();  
curl_setopt($ch, CURLOPT_URL, $url); 
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);   
$result = curl_exec($ch);  

Be aware, that you do need to generate the bearer token in your Airtable account. When logged in, go to and hit “Create new token”. Name your new token, give it an appropriate scope—data.records:read will do in our case—and select the base you want to access with it.

The above code does return something—a JSON object to be exact. But that something gets echoed in the browser. Not all that helpful. But it’s a start.

The full code example

Digging around some more, I figured, curlsetopt($ch, CURLOPTRETURNTRANSFER,1); must be, what’s missing. And it was.

This is, what the full example from API call to array looks like:

$base_id = '{YOUR_BASE_ID}';
$table_id = '{YOUR_TABLE_ID_OR_TABLE_NAME}';
$token = '{YOUR_BEARER_TOKEN}';
$url = "" . $base_id . "/" . $table_id;  
$headers = [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $token
$ch = curl_init();  
curl_setopt($ch, CURLOPT_URL, $url);  
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);  
$result = curl_exec($ch);
$array = json_decode($result, true);

Much more complicated than file_get_contents, but (supposedly) much more secure, too.

BTW, props to Airtable for announcing their switch to the new API a whole year up front. 🙏
And congratulation to cURL to its 25th anniversary, which is—wait for it—today. 🎉

Humans, ey? Fri, 10 Mar 2023 00:00:00 +0100 (Nils Mielke)

[…] humans don’t communicate very well by default.

—Jen Simmons
on The Big Web Show with Jeffrey Zeldman

I keep repeating this, but Jen said it on air, so …

Perch “standard” to Perch Runway migration—the missing steps Wed, 01 Mar 2023 00:00:00 +0100 (Nils Mielke) I have recently migrated a bunch of websites, that were on Perch version 3.2 (a.k.a. the last “regular” release), to Perch Runway 4.x. It is not always an easy route, is what I have learnt.

Runway has been around for quite some time and was sort of the bigger sibling of standard Perch—some called the latter “Perch proper“.
After the CMS changing owners in early 2021, the standard version has been retired.

There is a helpful guide in the docs for migrating. That article lacks some minor details, though, that I’ll jot down here for future reference.

MySQL errors

Many times, when migrating, I got a massive list of Syntax error or access violations after replacing the core folder and reloading the admin panel in the browser.

A browser screenshot showing rows and rows of MySQL errors.

As per this old forum post, these can be ignored.
At the bottom of the page displaying the errors you can “accept these errors and continue.” I haven’t had any issues following that so far.


Although it being mentioned in the migration guide, the URL to an explanation of the rewrite rules required for Runway is broken. Here’s the correct document:
Basically, replace whatever rewrites you had in your .htaccess so far with this:

# Perch Runway
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/perch
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* /perch/core/runway/start.php [L]    

Include the runtime conditionally and relative to the site root

In the beginning of my migration journey I often came across errors notifying me, that I included the runtime.php more than once. When removing it from the page template affected, I would end up it being missing entirely in certain constellations. To avoid that, include the runtime like this:

if (!defined('PERCH_RUNWAY')) include($_SERVER['DOCUMENT_ROOT'] . '/perch/runtime.php');

Missing “error master pages”

If it is not there already, copy the directory /perch/templates/pages/errors/ from the 4.x folder you downloaded from the Perch website into the corresponding folder of your installation.
Coming from standard Perch, you likely rolled your own 404 page and redirected to that in your .htaccess.
In Runway you simply use the page template given in the errors folder. Rewriting is handled automatically. Alter the template to your liking, of course.

Amend page templates to account for missing physical files

If you decide to make “your Perch site more Runway-like”, remember to also update the page option to the perch_content_custom calls in your PHP templates. Given, you are retrieving content from another page than the one using the template, that is.
Instead of

perch_content_custom('Content', [
    'page' => '/news.php'

set the option to:

perch_content_custom('Content', [
    'page' => '/news'

Otherwise you will get “Template not found” notices instead of the content you were looking for.

Issues with localized dates

Most websites I do are in German. Thus, the localization of dates is unavoidable in some instances, i.e. if we want to display a month’s name, rather than its one- or two-digit representation.
After upgrading to Perch Runway 4.x, all the sites trying that showed broken dates—e.g. “%041 %2023” instead of “März 2023”.
I suspected this being related to the deprecation of strftime in PHP 8.1 (and it well might be), but the problem persisted after reverting to PHP 7.4. The Perch support team wasn’t able to help me with this and they were not able to reproduce the issue. So I wrote a template filter, that replaces the standard English-based names for weekdays and months with their German counterparts.
This is cumbersome, somewhat hacky, and the translations live inside the filter for now. Also it is not suited for multi-lingual setups. But it reinstated the correct format of the dates.
Here’s the filter:

class PerchTemplateFilter_localdatestrings extends PerchTemplateFilter 
    public function filterAfterProcessing($value, $valueIsMarkup = false)
        $strings = [
            'January'   => 'Januar',
            'February'  => 'Februar',
            'March'     => 'März',
            'April'     => 'April',
            'May'       => 'Mai',
            'June'      => 'Juni',
            'July'      => 'Juli',
            'August'    => 'August',
            'September' => 'September',
            'October'   => 'Oktober',
            'November'  => 'November',
            'December'  => 'Dezember',
            'Monday'  => 'Montag',
            'Tuesday'  => 'Dienstag',
            'Wednesday'  => 'Mittwoch',
            'Thursday'  => 'Donnerstag',
            'Friday'  => 'Freitag',
            'Satday'  => 'Samstag',
            'Sunday'  => 'Sonntag',
            'Mon'  => 'Mo',
            'Tue'  => 'Di',
            'Wed'  => 'Mi',
            'Thu'  => 'Do',
            'Fri'  => 'Fr',
            'Sat'  => 'Sa',
            'Sun'  => 'So',
        return $strings[$value];
PerchSystem::register_template_filter('localdatestrings', 'PerchTemplateFilter_localdatestrings');

After incorporating that into your filters setup, use it like this in your Perch template:

<perch:content id="date" type="date" format="F" filter="localdatestrings"> <perch:content id="date" type="date" format="Y">

The filter could be enhanced to do more complex convertions. But it serves its purpose for now.
Feel free to grab it and alter it to your requirements.


Don’t forget to redo your sitemap, too. It should no longer be a PHP file sitting in your folder structure, but a virtual page, just like all the other ones. Treat its template like those of the remaining pages, add the route sitemap.xml in the page’s details and you’re good to go.

Anything else? Anyone?

Have you found a missing piece you’d like to see added here?
Send me an email or hit me up on Mastodon!

Using PHP’s list() with varying length arrays Wed, 22 Feb 2023 00:00:00 +0100 (Nils Mielke) I enjoy using list() ( to disassemble arrays into named variables. It is a bit awkward at first, having a language construct on the left side of the is equal to character (=), but that’s okay.

Say, you want to dismantle a page’s path into named fragments.
This is, what that might look like for only one fragment (like "/contact"):

list(, $level1) = explode('/', $_SERVER['REQUEST_URI']);

The first fragment is always empty, in this case. That is why we ditch it with an empty list element at the front.

The above example is ignorant of any path fragments after the second one ("contact"), though.
For list() elements with explicit keys, one can define defaults. That doesn’t work for elements without keys.
Let’s fix that.

In the following example the page we are looking at is three levels deep.

list(, $level1, $level2, $level3) = explode('/', $_SERVER['REQUEST_URI']);


Unfortunately, if you use this on your contact page, which is only one level deep, listing that one fragment will fail. list() doesn’t like the input array being too short. How can we account for that?

Luckily, list() doesn’t care, if the input array is too long for its elements.
Thus, we can “fill” the array with empty elements at the end:

list(, $level1, $level2, $level3) = explode('/', $_SERVER['REQUEST_URI'] . '//');

Every time you hit a page, that is lower than the three levels, you now get those empty elements filled with empty strings. No errors thrown. Problem solved. ✅

Bonus: Shorthand

As of PHP 7.1 there is a shorthand for the list construct—and you know, I love a good shorthand:

[, $level1, $level2, $level3] = explode('/', $_SERVER['REQUEST_URI'] . '//');
Alias for your Mastodon handle—in the Kirby config.php Mon, 13 Feb 2023 00:00:00 +0100 (Nils Mielke) On Mastodon, there are a few posts about how to implement a custom alias for your actual Mastodon handle using different setups already. Phil Hawksworth wrote about it here (using a Netlify redirect rule) and Trey Piepmeier here (utilising the Redirects plugin in Kirby CMS).
This post here shows you, how you can achieve the same thing with a route in your Kirby config—without any plugin.

After implementation your handle might look something like this:
rather than this:
One of the advantages being, that you will be able to keep that custom handle even if moving to another Mastodon instance.

In your Kirby config.php add a new route like so…

return [
    'routes' => [
            'pattern' => '.well-known/webfinger',
            'action'  => function() {
                if (get('resource') == '') {
                    go('', 200);

…and it works beautifully:

Mastodon search result showing “” for the search term “”.

Wunderbar! 🎉

You gotta use your own alias and handle, of course!1!!

And now what?

Whenever you switch Mastodon instances in the future, your handle will inevitably change.
If you update above route in your Kirby site accordingly, your alias will point there, thus preventing you from having to get new business cards printed—or change whatever else you put your handle on.

Minimal PHP—less, but better Wed, 25 Jan 2023 00:00:00 +0100 (Nils Mielke) As always (😬), I am most likely late to the party, but have you enjoyed writing shorthand PHP, yet?

Shorter if statements

For me, it all started when I realize, that this block of code

if(condition) {
  echo ’some text’;
} else {
  echo ’some other text’;

can be turned into this:

echo (condition) ? ’some text’ : ’some other text’;

Neat, ey?
Likewise this can be used to define variables.

This syntax has its limits, of course. You wouldn’t want to write more complex statements like that. It is more of a one-liner thing.

(Am I boring you with my unspectacular findings already? Well,…if you’ve come this far, you might as well keep at it. 😉)

The <?= short tag

Only a few weeks ago I came across this little gem in the Kirby CMS docs:

<?= ’some text’ ?>

The <?= is a shorthand for “echo this piece of PHP”.

Bringing together, what belongs together

Taking the last code block a step further, you might end up with this:

<?= (condition) ? ’some text’ : ’some other text’ ?>

So cool!
This is particular handy, if you want to echo some small piece of conditional content inline in your HTML. E.g. when prefilling a form.

Once you get the hang of it, your code will even be much more readable.

Hope, you share my enthusiasm. 😊

I bet, there is tons more.
Send me your favorite minimal PHP snippet on Mastodon!

I failed you, German blog posts. Fri, 13 Jan 2023 00:00:00 +0100 (Nils Mielke) Here’s to a new era—of posts on this site only being published in English.We didn’t even have a good run, German posts. We’re only five posts in. I am sorry.

Oh no, why only one language from now on?

This is kind of a side project to me. Or better yet the side project.I am having a rough time making,—well…—time for writing posts as it is. Three kids and a main gig keep me on my toes more than I had hoped.

This here is supposed to be more of a relaxation practice. And if it isn’t relaxing, it just isn’t worth it.

In the beginning I thought it would be fun to have all posts in both languages. But soon I realized it is holding me back, really. So,…only one language it is.

I get it, but why English, you bald-headed, German potato?

Firstly, I do almost all of my reading and listening in English (see also post #2, about my favorite podcasts), I spent quite some time in English-speaking countries—Australia and Canada mostly—, and I just love the language. Don’t get me wrong, I love my mother tongue, too. To the point of being quite nitpicky about punctuation and orthography. (Don’t get me started on people who use a Deppenapostroph in the German genitive.) But English will also be of better service to others, who might stumble across a how-to piece on my blog.

And secondly, I just recently decided to let my hair grow—all of it—(for the first time in about 25 years) for as long as I can bear it. Let’s see how that goes.But that’s a whole other post, right there. Maybe I will write about it some day. In English.

So, zero German posts from here on out, ey?

Simply put: Yes.If I get the itch and can spare some time, I might start a blog on the website of my business FEUERWASSER. There I might “recycle” some of the technical posts from here, translated to German, and sprinkle in some business-specific articles. That would even give me an excuse to write posts in my working hours. 🤫😉

And what about the feeds?

As language-specific RSS feeds make no sense anymore, there will only be one going forward. And I even managed to make the URL look nicer:

Never lonely Fri, 16 Sep 2022 00:00:00 +0200 (Nils Mielke)
If you are scared of people, you never feel lonely.
—Ai Weiwei
in Debbie Millman’s podcast Design Matters
Essentialism Thu, 28 Jul 2022 00:00:00 +0200 (Nils Mielke)
You cannot overestimate the unimportance of practically everything.
—John Maxwell
from Greg McKeown’s book “Essentialism”
Loading Fathom Analytics’ script.js under a strict Content-Security-Policy Thu, 21 Jul 2022 00:00:00 +0200 (Nils Mielke) If you follow me on Twitter, you are most likely aware, that I am a big fan of the privacy-first website analytics solution Fathom Analytics. A less known fact may be, that I am no expert on the topic of Content-Security-Policies (CSP). 🤷‍♂️

The problem

When it came to introduce a strong CSP to a client’s web presence, I hit a wall, trying to load the Fathom script.js. The obvious solution was to add the custom domain used to circumvent add blockers (there’s nothing fishy about that, seriously—more info on the use of custom domains in Fathom’s documentation) as a resource to the script-src directive, of course.
Browsers kept throwing a 403 Forbidden Error for the above-mentioned request at me, all the same.
Disabling the CSP reinstated the analytics functionality, so the problem was obviously with it.

Searching up and down the interwebs, I didn’t get any wiser.
I tried introducing a nonce to the script-src directive. No luck.
Asking for help on Twitter and reaching out to Paul and Jack of Fathom via email didn’t give me any pointers. Although Paul replied in a very timely fashion—as usual. The problem was on my end, not their’s, after all.

The epiphany

When trying to explain what I was experiencing, to post that to StackOverflow in search for help, it hit me. I revisited the CSP, turning off one directive after another. I had done that before, but obviously had missed something.
Commenting out the “Referrer-Policy” solved the issue.
That Fathom cannot do its work, if they don’t know which website was calling the script, is a no-brainer. Why the server returns a 403—rather than just failing to do its thing—is beyond me, though.

Completing the CSP

But now I was left without a “Referrer-Policy”—in the CSP, that is. Luckily we had opted for rel="noreferrer" attributes on the anchor tags of external links before, so there was a policy in place. It wasn’t obvious to the CSP, though.
To mitigate for that, I added an empty directive to the policy—like so Header always set Referrer-Policy ""—, thus telling it, that there is an alternative approach in effect.

Lessons learned (again)

  • Do not hesitate to formulate your problem—in written form or in a discussion. That might trigger you thinking about it differently.
  • Always try testing by exclusion. Multiple times, if need be.
My favorite podcasts (a snapshot) Fri, 10 Jun 2022 00:00:00 +0200 (Nils Mielke) Do you listen to podcasts? I do and I have subscribed to many of them over the years—and listened to even more. I am a true fan of the format.

Some of the subscribed ones I listen to every single episode. That’s how good they are (to me, at least). Some I cherry-pick.
These categories are always in flux, of course. That’s, why this is a “snapshot”.
Here are my current go-tos—in alphabetic order:

  • 99% Invisible (en)
    The award-winning show is a must. The team around Roman Mars reports on the small—invisible—things of the built world. Every episode is a gem. And I have heard all of ’em.
    Listen to the first episode of 99% Invisible

  • ARD Radio Tatort (de)
    This German radio play series is an audio adaptation of a very successful crime TV series, that has been running for decades now. There are several settings (in different locations) and every case plays out in one of them.
    Visit the Tatort podcast archive

  • Criminal (en)
    This true crime show—hosted by Phoebe Judge (no pun intended, I assume)—reports on historical as well as contemporary, sometimes mysterious, crimes. Amazing stuff.
    Go to the Criminal website

  • Fake Doctors, Real Friends (en)
    Real-life friends Zack Braff and Donald Faison review the hit comedy series Scrubs, in which they played the main characters. One episode per—well—episode in chronological order. Sometimes they have other members of the cast or team on. This show is making me laugh a lot (although I unfortunately do not watch along, as many listeners do).
    Listen to the first episode of Fake Doctors, Real Friends

  • Fest & Flauschig (de)
    The most successful German podcast is a bi-weekly conversation between satirist Jan Böhmermann and musician/entertainer Olli Schulz. The show started out as an actual radio show—under the moniker Sanft & Sorgfältig—and hasn’t lost any of its appeal since moving to Spotify. Jan and Olli are just hilarious.
    Listen to the first episode of Fest & Flauschig

  • „Kein Mucks!“ (de)
    Bastian Pastewka is a famous German comedian and a real connoisseur of the German audio play scene. He unearths decades-old audio plays from the archives of the radio station Bremen 4 and presents them with a lot of background information on the speakers and the stories.
    Listen to the „Kein Mucks!“ episode “Strafentscheid

  • Planet Money (en)
    The fine folks of PM turn economical and financial topics into entertaining stories. Sometimes taking it to extremes. E.g. when they started their own comic book series to learn about the economics of the industry or when they had their own satellite launched. Fascinating!
    Listen to the Planet Money comic book episodes

  • Tagesschau (de)
    This is the audio track of the daily German TV news show Tagesschau. I almost never manage to watch it live at 8 p.m., so I help myself by listening to the podcast version while doing chores.
    Visit the Tagesschau podcasts listing

  • WTF with Marc Maron (en)
    Marc is an American comedian gone podcaster—interviewing colleagues from the industry as well as other famous people, like former US president Barack Obama, musician Flea (Red Hot Chili Peppers) or designer Aaron Draplin. The latter was the reason I started listening to WTF.
    Marc is artfully tickling very personal details out of each guest. Great entertainment.
    Listen to the Barack Obama episode of WTF

There are many more in my podcast app of choice, of course. Debbie Millman’s Design Matters (en), Große Frage, kleine Pause (de), Netlify’s Remotely Interesting (en), Snap Judgement (en), Jeffrey Zeldman’s The Big Web Show (en), The Truth (en), This American Life (en) to name but a few. They are all great, too. Just not my regulars—right now.

Have a favorite podcast, that is missing from my list? Let me know on (twitter: @nilsmielke text: Twitter rel: me).

Go! Fri, 13 May 2022 00:00:00 +0200 (Nils Mielke)
Done is better than perfect.
—Sheryl Sandberg

That’s what I thought, after—literally—years of tinkering around on this website.
Many a technology was tried, but there was no leisure to learn. Even more designs were iterated on, but long breaks inspired indecisiveness. Way to little spare time was available to achieve real progress—this is “just for fun”, after all.

So let’s get this over with and launch this…thing.
Hopefully there will be a lot happening here, though. From a content perspective, design-wise, and, last but not least, technically.

This shall become a collection of how-tos of my webdesign work, quotes from my favorite podcasts (and from elsewhere), World Wide Web finds, and the like.

But: This will never become “perfect”.