Missing sidebar widget after migration – what happened?

I’m writing this blog post on my new server. The old one will be shut down in about two hours. As it had quite a few WordPress sites and one Matomo instance hosted, migrating everything was quite a task.

Migrating the sites I’ve followed my “5 minute migration process” and it went all smoothly as expected. Only when opening my blog after the DNS changes, the sidebar was missing the first two text widgets. What happened here?

Emojis ?

For quite a while, WordPress natively supports emojis. You can just use them in a blog post and WordPress will show them. In the past, they were replaced with an SVG sprite. But since modern operating systems and browsers support them natively, that’s not necessary anymore.

But why was using emojis and issue after the migration? On the old server, I’ve used MySQL 8 and on the new one MariaDB 10. When exporting the database, I’ve just run the wp db export command as always. But after the import, the UTF-8 multibyte characters were broken. Instead of an emoji, I only got some ?? signs.

This most probably broke the unserialize() function WordPress is using, and the whole widget was broken. This caused the first two text widgets not being shown, even though they were in the database.

Exporting the database with utf8mb4

After finding this issue and some research, I’ve found a WP-CLI issue about this effect. I was able to get a working export, adding a flag for the default charset:

$ wp db export --default-character-set=utf8mb4

Importing this new SQL dump fixed the issue, and the widgets appeared again. And I also got emojis in the content of my blog posts.

Conclusion: Always update WP-CLI!

So I successfully managed to repair the database, but still wanted to know why this (still) happened. The issue states, that this was fixed in the db-command, and release with version 2.5.0 of WP-CLI. Now on that server, I was still using version 2.4.0 of the WP-CLI which didn’t include the fix.

After updating to the current version (2.7.1), I didn’t need to add the flag anymore and the export had emojis and other utf8mb4 characters encoded correctly.

So before you do important maintenance tasks, better always update the WP-CLI to the latest version, so you don’t run into issue, you might not find right away. If I had recognized this issue only in some days/week, fixing/synching the database would have become really challenging.

Handling timezones – how not to do it!

Last week, I had quite an interesting issue to deal with. The site had a custom post type for webinars. When you are registered, you were supposed to see a “Join” button some minutes before the webinar starts. There was also a setting in the backend to set the number of minutes, the button should be shown before the event.

For some reason, it was not working. As I had identified another issue with time calculations on this plugin, I quickly set the time to 300 minutes before the event, since the webinar had already started 5 minutes ago. This fixed the issue for the moment. But why has it happened at all? Investigating the issue showed some “clever code”.

Investigating the data

The plugin was using data from an external API. This API returned multiple fields, which were saved into postmeta. These are some of the fields that were used:

| post_id | meta_key                      | meta_value                   |
| 1234567 | webinar_created_by            | api                          |
| 1234567 | event_id                      | 12345                        |
| ...     | ...                           | ...                          |
| 1234567 | to_date                       | Montag, 27. März 2023        |
| 1234567 | date                          | Montag, 27. März 2023        |
| 1234567 | date_en                       | 27 Mar 2023                  |
| 1234567 | to_date_en                    | 27 Mar 2023                  |
| 1234567 | start_time                    | 19:00:00                     |
| 1234567 | end_time                      | 20:45:00                     |
| 1234567 | time_zone                     | Mitteleuropäische Sommerzeit |
| ...     | ...                           | ...                          |

Can you recognize something here? Yes, this is localized data. The timezone “Mitteleuropäische Sommerzeit” is the German name for “Central European Summer Time” (CEST, GMT+2). So the time is two hours ahead of UTC. That’s why I’ve set a value greater than 120 minutes, which safely fixed it. But when the timezone is set correctly, why does it fail?

Converting timezone strings

In that plugin, there was a custom function converting timezone strings to UTC offsets. The function looked something like this:

function get_timezone_mapping( $name = '' ) {
	$mapping = [
		// ...
		'Central Time' => '-6',
		'Central Standard Time' => '-6',
		'Canada Central Standard Time' => '-6',
		// ...
		'Portugal Winter Time' => '+0',
		'India Standard Time' => '+05:30',
		// ...
		'Восточноевропейское время' => '+2',
		'Eastern European Summer Time (Athens)' => '+3',
		'Eastern European Summer Time' => '+3',
		// ...
		'北京时间' => '+8',
		'台北時間' => '+8',
		'Seoul Time' => '+9',
		'日本時間' => '+9',

	if ( ! empty( $name ) ) {
		if ( ! empty( $mapping[ $name ] ) ) {
			return $mapping[ $name ];
		} else {
			return false;

	return $mapping;

You would pass the strings of a timezone, and it would return the UTC offset. For some timezones, there were different variants, some even translated into other languages. Only our “Mitteleuropäische Sommerzeit” was not in that list, so it returned false in this case.

When things broke

Now, this function and its return value was used to create a Date object. It was then compared to the current time (of the server). The codes looked something like this (simplified):

// Data dynamically queried from the database.
$webinar_data = [
	'time_zone' => 'Mitteleuropäische Sommerzeit',
	'date_en' => '27 Mar 2023',
	'start_time' => '19:00:00',
// ...
$timezone = $webinar_data['timezone'];
$start_date = $webinar_data['date_en'];
$start_time = $webinar_data['start_time'];
// ...
$timezone_mapping = get_timezone_mapping( $timezone );
$date_timezone = ! empty( $timezone_mapping ) && ! is_array( $timezone_mapping ) ? new DateTimeZone( $timezone_mapping ) : null;

$current_datetime = new DateTime( 'now', $date_timezone );
$start_datetime = $start_date ? new DateTime( $start_date . ' ' . $start_time, $date_timezone ) : null;
// ...
$pre_buffer_minutes = empty($minutes) ? 15 : absint($minutes);
// ...
// Subtract the buffer from the webinar's starting date & time.
$start_datetime->sub( new DateInterval( "PT{$pre_buffer_minutes}M" ) );
$is_within_time = $current_datetime->getTimestamp() >= $start_datetime->getTimestamp();

So let’s break it down and see where the issue lies. The webinar’s timezone is passed to the function get_timezone_mapping(), but since the string “Mitteleuropäische Sommerzeit” cannot be mapped, this function returns false, which then results in using null for $date_timezone on line 13. This timezone is then used in both new Date() calls. But what happens here? Let’s take a look at the resulting Date object of the two calls and which time they represent, when being called at “19:00” (server time):

$current_datetime = (new DateTime('now', null))->format('Y-m-d H:i:s');
// 2023-03-27 17:00:00
$start_datetime = (new DateTime('27 Mar 2023 19:00:00', null))->format('Y-m-d H:i:s');
// 2023-03-27 19:00:00

Since the second Date object $start_datetime represents a valid “date string”, PHP will create an object with that exact time, ignoring the timezone. The first Date object $current_datetime however is using the now string and also no valid DateTimeZone object passed as a second parameter. If none is passed, PHP will always use UTC. So even if the server (or WordPress system) runs on Central European Summer Time, PHP will still use UTC. That results in the two-hour difference between the time we are expecting to get and the one we actually get. And finally, when the $is_within_time boolean gets created, it will be false, until $pre_buffer_minutes plus our two-hour difference if reached.

How to deal with timezones in a better way?

As you can see from the code above, handling timezones in such a way is a really bad idea. Especially, when you also have translated timezone strings. You can’t possibly maintain an array with all different variants. So what to use instead?

I would suggest to always use an “industry standard” string including the timezone, when you specify a time and date. In the table at the beginning of this blog post, you can see the date written in two ways: “Montag, 27. März 2023” and “27 Mar 2023”. The first one is translated, the second one is not. But by splitting up the date, time and timezones into different values, you have to combine them again (as seen at line 16), in order to create “date objects” in different programming languages. Why not use a format like this instead, having all the parts combined?

$webinar_start = 'Mon, 27 Mar 2023 19:00:00 +0200';

This string combines all information about the time, date and timezone in one string. This is what you would get, when you use the $date->format('r') output format. It follows the standards RFC 2822/RFC 5322. When someone consumes your API, they can then use this string, create a “date object” from it, and get the time, date, or whatever they might need from it.


Dealing times and dates is often not easy to do in programming. If you operate on different timezones as well, things become even harder. You can decide to serve parts of your time/date information as separate values, but please make sure to also provide a string like this. And if you decide not to do it, then please, don’t translate timezones, month names, or anything similar, as it can really cause (sometimes hard to debug) issues, as the one explained here.

As for this plugin, I have yet to find a solution. If the API doesn’t add such a string in the future, I probably have to extend the static array in that function with more and more (translated) timezone strings, but I would never be sure, if not one day, a new value would become available I don’t have, causing the issue all over again.

My CloudFest Hackathon 2023 review

Last weekend, I went to Europa-Park in Rust, to attend my second CloudFest Hackathon. This year’s event had 11 projects, and 7 of them were WordPress projects (some other were only slightly connected to WordPress). Last year, I joined the Pluginkollektiv team, working on the popular Antispam Bee plugin. This year, the team worked on Statify, a privacy-friendly statistics plugin.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Small steps to a new and better plugin

For the Antispam Bee project last year, we rewrote the whole plugin with the goal to release a new major version. This new version is now in its alpha phase, so even a year after the event, not quite ready. For Statify, the team lead Florian Brinkmann identified some issues an GitHub he wanted to see the team working at. Some of them were “low-hanging fruits”, other quite large issues or features.

A supposedly easy ticket

We had some people in our team that were new to contributions, and we tried to find some easy issues to work on. One issue with the title “Show title instead of permalink” sounded quite easy. The issue was almost 5 years old and there was already some constructive discussion. There was even a PR with a single line change to target one part of the issue. So this issue should be easy to resolve, right? Unfortunately, it was not. The issue was not closed since 2018 for a reason. Talking about it in the group, we found many edge-cases about why it needs more planning to resolve all of them.

Working on a solution and introducing new possibilities

After discussing many different approaches and testing some of them in a proof of concept, it became clear, that the best solution would be to store the actual post title in the database, when the page view was tracked by Statify. As the custom table of Statify is quite simple, we had two possible solutions:

  1. Add another column to the table
  2. Add a statifymeta table to store the title

We have decided to take the second approach. While it seems too much for such a little feature, the introduction of this new table would enable Statify (and any extending plugin) to store more metadata for every entry without the need to extend the table every time. It also makes updates of Statify easier, as updating existing tables can always cause issues on some environments.

Onboarding new people

I have to admit, that I have not written too much code. I only contributed a minor UX improvement. Instead, I helped others with their contributions. One member of the team, who does not write code on a regular basis, made his first contribution and the PR was reviewed and merged. Another team member working on the post title issue was also new to many things. After working for two days on the issue, it was time to commit the new code. Only then he mentioned, that he has never used git before, so I also gave him a quick-start into git, and he was able to commit the changes to his fork. By that time we realized, that some other changes created some merge conflicts, so he also learned a bit how to handle them. Now we have to test the new code before it can get merged. As this issue and other we worked on are quite large, in summary, we will probably release a new major version of Statify containing all the new bug fixes and features from the hackathon.

An event to visit!

While I cannot give you details on all the other teams, I would recommend to check out the #CFHack2023 hashtag to get an idea of the event. For me, it was another amazing event! Not only the “hacking”, but also the social parts of it. There have been some official parties and also some fun site activities like a Mario Kart tournament happening on Sunday evening.

On Monday, just after the announcement of the winners of the different categories, the German WordPress community held a spontaneous “meta meetup”, in which organizers from many different cities shared experiences and ideas from their meetups. We plan to have such a meeting more regularly now:

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

The CloudFest

In the afternoon, there was a “WordPress Day” kicking off the actual CloudFest event. While the main CloudFest event starting on Tuesday was more of an “industry event”, the WordPress Day had different topics focussing on the WordPress ecosystem. But not the typical sessions you would see on a WordCamp and some of them more of product presentations.

In the evening, there was the “Come2Gather in the Streets” event at the main entrance of Europa-Park, with food stands, drinks and some live music. I finally had the chance to talk to some people that were as busy as I was on the previous days or who just arrived for the main event.

Unfortunately, I had to leave Tuesday at 11 am and didn’t have much time to see any sessions. I briefly visited the “Cloud Fair” expo area and talked to some sponsors of the event. I also missed the chance to ride some of the rollercoaster that opened just for CloudFest attendees. But last year I did that quite a bit.


While I was a bit “disappointed” of myself in terms of active code contributions this year, I really enjoyed onboarding new people to contribute to open source. All of them were excited and motivated to get their first experiences, and they want to continue contributing in the future.

If nothing comes up, I will be there again next year. Maybe I can even come up with a project to submit. For the one idea I had for this year, there was just not enough time to prepare something. If you have never contributed to open source before, I can highly recommend finding a hackathon around our or a WordCamp with a Contributor Day. You learn new skills and find many like-minded people!

Using git for your server configuration files

I recently got myself a new web server. On this new server, I’m playing around with OpenLiteSpeed, which I have never used before. It comes with its own admin dashboard to manage the server. But it writes everything into configuration files.

Sometimes you also want or need to manipulate configurations files manually. If not for OpenLiteSpeed, then maybe the SQL or PHP configuration. But if you don’t really know what you do, you can easily screw things up.

Versioning your configuration files

This is why I have started to put my main configuration files under version control. This has several benefits:

  • You can try things and revert them, if things are get broken
  • You have a “backup” of your configuration
  • You can use the same/similar configuration files on all your servers and synchronize them
  • You can see changes made by other processes – like after an update or when using configuration tools

Only version the most important files and ignore sensitive information

On many Linux server systems, the configuration files are stored in the /etc folder. You may not think that you simply version that whole directory. But that’s not a good idea, as you might version files that contain sensitive information or that would even screw up your whole server, if you do something wrong. I once accidentally deleted the /etc/passwd file, which was not really good. ?

Ignoring nginx configuration files

So on my old server, I’ve versioned only the /etc/php and the /etc/nginx folders in two separate git repositories. For the nginx configurations, I used the following .gitignore file:

# /etc/nginx/.gitignore

The second line ignores the dhparam-4096.pem files being used for better Diffie-Hellman key. I also ignored the *-enabled folder, which are just symlinks to the *-available folders. You can version them, if you want to keep track of which sites and modules are enabled. The modules-available also didn’t have too much value in my opinion, so I left this one out as well.

Ignoring OpenLiteSpeed configuration files

On the new server, I’m still figuring out how configuration files are structured. The OpenLiteSpeed files are stored in the folder /usr/local/lsws and as of now, I use the following .gitignore file:

# /usr/local/lsws/.gitignore
# Ignore all files by default but decent into subfolders

# Allow just some files and subfolders

# Still ignore some files and folders in those subfolders

As the /usr/local/lsws folder also contains binaries and log files, I’ve first ignored all files and folder. I then added only those folders containing the configuration files I wanted to put under version control.

When I upgrade some packages, a lot of “backup configuration files” appeared, so I ignored all with those file extensions in the previously allowed folders.

I might update this ignore list in the future, but as of now, I have all my files I want to have under version control.

Push the repository to a remote

As mentioned in the benefits, you might also use this approach to have a “backup” of your configuration files. While git (or any other version control system) is not an alternative to a backup, it can be a bonus to have them stored on some external server. As those configuration files are often not meant to be public, I use private git repositories on GitLab.com as my remote.

This also has the benefit, that I clone the repositories to my laptop and edit the configuration files in PhpStorm, which is a lot easier than using vim on the server. And I can also easily compare changes and revert some commits.

Automatically version changes

I usually do the commits (and pushes) manually. But in case you have changes to those files made by other processes, and you want to version every change, you could set up a cron to commit all changes. This is an example of a cron, that would auto-commit and push all changes to the nginx configuration files every 30 minutes:

30 * * * * cd /etc/nginx && git add -A && git commit -m "auto commit on `date`" && git push

Even new and deleted files would be versioned. But having a cron like this could also potentially result in a lot of commits, if you don’t have your ignores set correctly, as some of the new files might have been a log file that is changed quite frequently. So if you use a cron, you should check the auto-commits from time to time.


I really love git! You can use it for so many different things. Versioning my configuration files has saved me a lot of time trying to figure out why things are suddenly broken or when I wanted to make changes to many configuration files at once (editing them locally in PhpStrom).

Do you use a similar approach and might want to share your setup here as well? Or do you use git for other things? Then please leave a comment.

WordCamp Asia 2023 – Finally, my first trip to Asia!

About two weeks ago, I was preparing myself for a long trip to Thailand. This would also be my first trip to Asia. After WordCamp Asia was canceled in 2020 just 9 days before the event, it finally happened in 2023! It was the first edition of this new flagship event.

Travelling to Thailand

I haven’t really planned the day perfectly. The flight from Berlin was going at 08:55 in the morning. As it Super Bowl Sunday, I was facing myself with very little sleep. (Un)fortunately I didn’t felt well Sunday evening, so I went to bed really early and skipped watching the Super Bowl live 🙁

But with some good sleep, I arrived a BER airport and took my first flight to Helsinki. From there I took the second flight to Bangkok. The around 11 hours went by quite fast, enjoying some entertainment. As I cannot sleep in planes, I arrived a little tired 06:00 local time. From the airport I took a train to the city and then the metro. Walking to the hotel, I got some first experiences on how Thailand is different to Europe, especially in terms of traffic. But I was also quite surprised of the many street food places on my walk. Checking was only at 14:00, but fortunately I was able to get my room at around 10:00 paying only 300 baht (around 9€) extra. So after almost 24 hours of traveling, I finally got some good sleep 🙂

On Tuesday and Wednesday I had the chance to see a bit of the city. Wednesday was the only day of my trip with some rain (at times quite heavy), but I managed to meet some people to experience China Town. It first seemed a bit odd to visit an Asian country and then go to China Town, but it was pretty fascinating. I also took one of the ferries on the river, one of the really fast ones!

Meeting the community

On Thursday, I moved hotels and met with my colleagues from Inpsyde. We stayed at the main hotel right next to the venue. Just hanging around in the lobby, you met many WordPress community members. Some of them I haven’t seen for quite a while. Well, even when they also have been to WCEU in Porto, I was just too busy to talk to them. Before heading to some side events, I briefly visited one of the many temples in Bangkok:

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

The first side event was “The WordPress Enterprise Gap” hosted by Human Made. It was like a WordPress meetup with some talks and a panel. The topics were around how to attract enterprise clients to WordPress.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

I would have loved to join the “GoDaddy Pro Island” party on a boat, as I really liked this event in Porto, but it overlapped with the other events. So I then joined some colleagues and friends at the “WooCommerce Community” event. It was a nice location next to the river and I had some good talks … even though I’m personally not too much into eCommerce in general 🙂

The last side event of the day was the “Superheroes Pride Party” organized by Yoast, codeable and Bluehost. As the location was the hotel I was staying, it was a perfect end of the day. And it was a lot of fun for many people.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Contributor Day

On Friday, WordCamp Asia officially started with its Contributor Day. I wasn’t really sure which team I would join, but as I’ve met Alex there, and he had some questions regarding the ticketing/registration on WordCamps, I joined the Meta Team again. We both tried to set up a local environment for WordCamp.org … but failed ? But I was able to fix some issues, created a PR with one fix and an issue for the other problem. Hopefully we can fix this for new contributors before the next WordCamp.

In the evening I went for dinner with my colleague Viola. First, we took a look at the many different food places in the venue, but were overwhelmed with the options. We then finally found a nice little restaurant just down the street serving some great Thai dishes.

The first conference day

Saturday was the first conference day. As for any flagship WordCamp, it started off with the Opening Remarks:

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

The schedule was packed with many great topics. But I’ve skipped the first slot to talk to some old WordPress friends I haven’t seen since WCEU Berlin in 2019.

Advanced Performance & Scalability for PHP Developers

In the second slot, I attended the session presented by Alain Schlesser. One topic of this talk was surrounding caches. He gave some great advice on how to cache things, on different levels and in a granular way, and how to invalidate the cache when necessary.

Think like a hacker: Attack your WordPress

After this I watched Matthias Held hacking a website live on stage of one of the attendees. But he was not hacking the live website, only a copy and the attendee volunteered for this 🙂 Even though there were not too many security issues, Matthias was able to showcase how a XSS attack could be used to steal the PHP session of a logged in (administrator) user through a manipulated link in an outdated and vulnerable plugin.

Success stories of HeroPress

After lunch break, Topher DeRosia shared some stories from HeroPress. I’m following his projects for many years now, and many friends from the WordPress community have already posted their essays there. Maybe one day I will share my story as well.

The Ultimate Newsroom QA: how to manage your editorial workflow like a boss

Being a solo blogger, things are easy for me. I just need to decide what I want to blog about and write it down. Blogging in a larger team is more challenging, and Francesca Marano gave a lot of insights on how she successfully ran an Italian blog with many different authors.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Meeting the sponsors

No WordCamp would be possible without the sponsors. In the next two slot, I skipped the sessions and talked to different sponsors. Many of my WordPress friends work for some of them, and it was nice to catch up how they have been in the last few years.

Migrating WordPress Core to GitHub Actions: A Retrospective

The last session I’ve attended on the first day was held by Jonathan Desrosiers in which he presented some insight on how WordPress migrated all automated tests to GitHub actions.

Thai dinner with the team

The first two days were quite busy, and we finally took the time to have a team dinner with all Inpsyders attending the event. We took a ferry to “Asiatique The Riverfront” and had a large variety of Thai food. On the way back, we took a tuk tuk, something I have never done before. Back at the hotel, we met some other WordCamp attendees in the bar at the top of the hotel.

The second conference day

As the previous day ended quite late – or early – I took some good sleep to have enough energy for the second half of talks.

AMA – Ask Matt Anything

That’s why I only watched the first talk using the live stream. Matt also couldn’t join, as he had to care for his family. But some of the questions he got asked were quite interesting. I still have to watch the full session, but he casually announced that Gutenberg phase 3 is starting now.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Stepping Back To Move Forward

After some coffee and a small breakfast snack, I joined the first lightning talk by Carole Olinger. Her talk was the first of three lightning talks focussing on mental health. These are the types of talks I really like to attend on WordCamps. One of the things she presented was the concept of JOMO – Joy of Missing Out – to make us aware that it’s OK not to always participate in all things possible.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

The Power Of Empathy

The second mental health lightning talk was focussing on empathy and presented by Ajit Bohra. This is the quote I liked most in his talk:

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Look for the Good

The last talk was held by Michelle Frechette, and she was speaking about times can be hard – like the past years with the pandemic – but also about how she and many of us have been finding new opportunities in those challenging times.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Panel: Building WordPress Communities in Your Country

Then I’ve attended the only panel of WordCamp Asia. Rahul D Sarker, M Asif Rahman, Miriam Schwab, Amit Bajracharya, Angela Jin and Hajime Ogushi were talking about their local communities and answered many questions from the audience on how to build a strong community in a country.

Ten Minutes on Five for the Future: A commitment to WordPress and the Open Web

After the lunch break, Hari Shanker presented the “Five for the Future” initiative of WordPress. The goal is to encourage every company using WordPress to give back 5% to the open source project. In the Q&A I’ve asked on how individual contributions could be highlighted a bit more, as currently, there is only a listing of companies. Hari also wrote a blog post on Make WordPress and I will probably make some suggestions on how we could show all these people helping the projects while not working for a company.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Spending more time with friends

Two people I was looking forward most at WordCamp Asia were Ellen and Manuel the couple behind Elmastudio and AinoBlocks. I have not talked to them since Contributor Day at WCEU 2019 and was really happy to catch up.

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Closing Remarks … and the announcement of the next host city

Following the tradition of flagship events, in the closing remarks all people involved were asked to join the organizers on stage. Around 1300 attendees made it to the first edition of WordCamp Asia. At the very end, the new host city was announced:

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

So next year we will meet in Taipei, Taiwan! I really hope that I can make it again to see yet another new Asian country. I want to thank all the organizers of WordCamp Asia 2023 and wish the team all the best!

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

After Party!!!

Sure, there was an after party at WordCamp Asia 😉 It was a large open air venue with some (live) music, amazing food and some dances I haven’t seen before 🙂

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

If you want to see more pictures, just follow up on the #WCAsia hashtag on Twitter or take a look at the shared gallery with pictures from many attendees.

Workation in Thailand

On Monday, we headed to a place around 3h south-west of Bangkok. I could only spend one day with my colleagues, but it was a nice finish of my trip:

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

A WordCamp of “first times”

Visiting WordCamp Asia was my first trip to Asia. It was also the first WordCamp Asia. It was the first (in-person) WordCamp being only an attendee for quite a while. And it was the first time I was attending a WordCamp since I started my new job at Inpsyde last October. My next WordCamp will probably be WordCamp Vienna, followed by WordCamp Europe in Athens, this time probably as a speaker. I want to close this blog post with the after movie my colleague Viola produced:

Click here to display content from Twitter.
Learn more in Twitter’s privacy policy.

Use SSH keys to sign Git commits

In November last year, I have explained, how you can use different Git settings for personal and company projects. In one of the snippets, I have used an SSH key to sign commits. But as in the past, commits were usually signed using GPG keys, I want to explain what you have to do, to use an SSH key to sign your commits.

Step 0: Create an SSH key pair

You probably already have a key pair. How else would you clone repositories from Git hosting platforms? But in case you don’t have an SSH key pair, run this command:

ssh-keygen -t ed25519 -C [email protected]

I would recommend using the ed25519 cipher algorithm. You can also set the comment, otherwise it would be user@machine from the machine you are creating the key on. I would also recommend setting a passphrase, but it’s not necessary for using the key to sign commits.

Step 1: Update your Git configuration

Now that we have an SSH key, we can add it to our configuration. We can do that with the following command:

git config --global user.signkey ~/.ssh/id_ed25519

As Git is still using GPG keys by default to sign commit, you have to tell it to use SSH keys instead. This can be done adding this settings:

git config --global gpg.format ssh

If you have never signed commits before and want to sign all commits by default, run this command:

git config --global commit.gpgsign true

Don’t get confused, that the setting is called gpgsign, it will still sign your commits using SSH.

Step 2: Define the “allowed signers” (optional)

When you commit something and then show the log including signatures, you will see the following error:

git log --show-signature
error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
commit eccdf56431b052772b09027c5cea585b8e7eee32 (HEAD -> master)
No signature
Author: Jo Doe <[email protected]>
Date:   Sun Jan 29 19:07:52 2023 +0000

This is because Git does not know if any of the public keys is valid. We can fix this by adding another setting to our git configuration:

git config --global gpg.ssh.allowedSignersFile ~/.git_allowed_signers

And now we also have to create this file, otherwise we would get a different error. You add one public key per line, with the email address of the key in the first “column” and the content of the public key after this. The file could look like this:

# ~/.git_allowed_signers
[email protected] ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ818qdUM98GriqpTKhqMmwYAgeK3iiCg07Qgya5NwN/ [email protected]

When you show the Git log again, you should see a valid signature:

git log --show-signature
commit eccdf56431b052772b09027c5cea585b8e7eee32 (HEAD -> master)
Good "git" signature for [email protected] with ED25519 key SHA256:/qGkPHs1/58u7jZgX95+hr5PNFs7gXswbkcRdfZuMWk
Author: Jo Doe <[email protected]>
Date:   Sun Jan 29 19:07:52 2023 +0000

After these steps, your Git configuration should (at least) look like this:

	email = [email protected]
	name = Jo Doe
	signingkey = ~/.ssh/id_ed25519
	gpgsign = true
	format = ssh
[gpg "ssh"]
	allowedSignersFile = ~/.git_allowed_signers

Step 3: Add key to Git hosting platforms

Many of you will probably use GitHub, GitLab or other Git hosting platforms. At least the two mentioned platforms also support SSH keys for signing in their UI.

Add the key to GitHub

Navigate to “Settings | SSH and GPG keys“. Then click the “New SSH key” Button. In the “Key type”, choose “Signing Key”. Add your public key and give it a title:

Screenshot of the "New SSH key" form with the inserted public key.

You can use the same key, as you might have already used for the “Authentication Key”.

Add the key to GitLab

Navigate to “Preferences (User Settings) | SSH Keys“. Here, you can insert the key into the form and either choose “Authentication & Signing” or just “Signing” in the “Usage type” dropdown:

Screenshot of the "Add an SSH key" form with the inserted public key.

By default, the keys at GitLab have an “Expiration date”, but you can also leave this empty. You can always extend the validation by deleting the key and adding it again. Since SSH keys do not expire (GPG keys usually do), you might want to give the default date a try.

Step 4: Verify that commits are signed

We have already checked on the terminal in the Git log, that our signature is correct. On GitHub and GitLab, you can see the signed signature check on most views with a commit hash. In the commits list of GitHub, it looks like this:

Screenshot of a "Verified" commit on GitHub.

In the commits lists of GitLab, it looks like this:

Screenshot of a "Verified" commit on GitLab.

The SSH key fingerprint is the same, as we have seen in the Git log on the terminal already: /qGkPHs1/58u7jZgX95+hr5PNFs7gXswbkcRdfZuMWk.


For a long time, GPG keys were the ones to use when signing off commits. But since GPG can be quite complex to set up and manage, not too many people used commit signing. If you don’t use GPG already – like for signing and/or encrypting emails – you might just use SSH keys instead to sign your commits. But you can also use both for different projects. In this case, just read my blog post about different Git configurations again, and don’t put the settings in the global, but in the included configuration files.

Cluster markers by state on a Leaflet map

As mentioned in my last blog post, I have a little bonus topic to the maps series. In the previous post, we have created a map with Leaflet in JavaScript. The map visualized the capital cities of all German states. Two of them are so close together, that there are hard to click. But how about having a map with all big cities with around 100,000 or more inhabitants:

© ammap.com | SVG map of Germany (low detail), used with Leaflet, modified by Bernhard Kau, CC BY-NC 4.0

Now, especially in North Rhine-Westphalia, it is impossible to see, hover and click all makers. But as we are using Leaflet there is a solution for this:

Use marker cluster

There is an extension for Leaflet called Leaflet.markercluster, which we can use to cluster the markers. In order to use it, we first have to import some more external CSS and JavaScript files:

<link rel="stylesheet"
      href="https://unpkg.com/[email protected]/dist/MarkerCluster.css"
<link rel="stylesheet"
      href="https://unpkg.com/[email protected]/dist/MarkerCluster.Default.css"

<script src="https://unpkg.com/[email protected]/dist/leaflet.markercluster.js"

Now we can create a markerClusterGroup and add the markers to this group. At the end, you have to add the marker group to the map:

const markers = L.markerClusterGroup();

cities.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    markers.addLayer( marker );
} );

markers.addTo( map );

By default, the markers are clusters with a max radius of 80 pixels. The map of Germany would then look like this:

© ammap.com | SVG map of Germany (low detail), used with Leaflet, modified by Bernhard Kau, CC BY-NC 4.0

While this might be the most efficient clustering, it doesn’t look really nice, as we have multiple clusters in one state and none in others. That’s why I am going to cluster them by state.

Cluster markers by state

We can add multiple markerClusterGroup objects to the map. We create 16 groups for the 16 states and then add the markers to the correct group. Let’s take a look at the complete code:

const stateMarkers = {};

// Get a unique list of all states from the cities array.
[ ...new Set( cities.map( city => city.state ) ) ].map( state => {
    // Create a markerClusterGroup per state.
    stateMarkers[state] = L.markerClusterGroup( {
        maxClusterRadius: 1000,
        spiderfyOnMaxZoom: false,
        showCoverageOnHover: false,
        disableClusteringAtZoom: 8,
    } );
} );

// Create city markers and add them to the correct markerClusterGroup.
cities.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    stateMarkers[city.state].addLayer( marker );
} );

// Add all markerClusterGroups to the map.
Object.keys( stateMarkers ).map( state => {
    stateMarkers[state].addTo( map );
} );

First, we create an object for the groups of all states. Then we do some JavaScript magic to get a unique list of all state names from the cities objects and create one marker group. For the cluster groups, we set the maxClusterRadius to 1000 pixels, as our map is smaller than 1000 pixels, which results in cluster groups being at least as big as any state on the map. We also disable two options, we don’t really need. We also define, that on a zoom level of 8, all clusters should be disabled. This will “uncluster” all markers when any of the clusters is clicked.

After we have created the groups, we create and add the city markers to those groups. At the end, we add all groups to the map. This will finally give us the following result (a screenshot):

© ammap.com | SVG map of Germany (low detail), used with Leaflet, modified by Bernhard Kau, CC BY-NC 4.0

Some of the states do not have more than one marker. In this case, they are just shown. All others have a cluster. For North Rhine-Westphalia, we have a cluster of 30 markers. If we click on that cluster, we get the following zoomed view (screenshot):

© ammap.com | SVG map of Germany (low detail), used with Leaflet, modified by Bernhard Kau, zoomed, CC BY-NC 4.0

If your map has more markers, or they are closer, you might have to tweak the options for the cluster groups. For this example, I would say they are now clickable.


This should be the last blog post to my little maps series. There are probably dozens of other possible topics around maps in general and Leaflet, but other blogs and documentation cover them already.

I really hope, that you are curious now to try to create your own (dynamic) maps. And finally, you can also find all examples from this blog post in a single HTML file in a new branch on GitHub.

If you still have topics you want me to cover, please leave a comment.

Use markers on a custom image with Leaflet

In the last two blog posts, we have written some PHP code for geocoding. This week, I want to show you how to do something similar with Leaflet, a well-known JavaScript library you can use to create maps. As it also has the feature to add a custom image layer, we will use it to create a map again.

Getting started with Leaftlet

When using Leaflet, you first have to include some (external) CSS and JavaScript. You can find more details in the quick start guide, but you will basically need the following:

 <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"

 <script src="https://unpkg.com/[email protected]/dist/leaflet.js"

<div id="leaflet-map"></div>

Besides the CSS and JS, you would need a container that the map is going to use. You also need some basic styles, but we get to them later.

Setting up the map

As with our previous examples, we have used a map of Germany with a specific size and some boundaries. We also have to define them when using Leaflet:

// Create the map object.
const map = L.map( 'leaflet-map', {
    center: [ 51.1642, 10.4541 ],
    maxZoom: 10,
    zoomDelta: 1,
    zoomSnap: 0,
    scrollWheelZoom: false,
    trackResize: true,
    attributionControl: false,
} );

// Define the image overlay and its boundaries.L.marker([52.5162746,13.3777041]).addTo(map);
const imageUrl = './ammap-germany-low.svg';
const imageBounds = [
    [ 55.051693, 5.864765 ],
    [ 47.269299, 15.043380 ],

// Add the overlay to the map.
L.imageOverlay( imageUrl, imageBounds ).addTo( map );

// Automatically zoom the map to the boundaries.
map.fitBounds( imageBounds );

We first create a map object with some basic configuration. Then we define the image source and its boundaries in GPS coordinates, before we add it to the map. In the last step, we fit the map into the container.

The container should have a good size to fit the map and its ratio. We just take the SVG image size in our CSS:

#leaflet-map {
    width: 585.506px;
    height: 791.999px;
    background: white;
    max-width: 100%;
    max-height: 100%;

It does not have to be this exact, and you can use a container that is larger/smaller than the SVG image as well.

Adding markers to the map

Now that we have the map, who do we add markers to it? Well, since we are using a mapping library, nothing easier than that. For the marker to the Brandenburg Gate, we just need one line:


That’s really it! No more transformation from GPS to pixel coordinates. This is all done by Leaflet. You will get the following result:

© ammap.com | SVG map of Germany (low detail), used with Leaflet, modified by Bernhard Kau, CC BY-NC 4.0

The only downside is that out of the box, Leaflet only supports the EPSG:3857 projection (Web Mercator). If you need a different projection, there is however a PROJ extension: Proj4Leaflet.

Adding the capitals of all Germany states to the map

Since you probably want to display more than one marker, let’s add some more to the map. I’ve made a list of all capitals of the 16 Germany states for this example:

var capitals = [
        'state': 'BB', 'name': 'Potsdam', 
        'lat': 52.4009309, 'lng': 13.0591397,
        'url': 'https://de.wikipedia.org/wiki/Potsdam'
        'state': 'BE', 'name': 'Berlin', 
        'lat': 52.5170365, 'lng': 13.3888599,
        'url': 'https://de.wikipedia.org/wiki/Berlin'
        'state': 'BW', 'name': 'Stuttgart', 
        'lat': 48.7784485, 'lng': 9.1800132,
        'url': 'https://de.wikipedia.org/wiki/Stuttgart'
        'state': 'BY', 'name': 'München', 
        'lat': 48.1371079, 'lng': 11.5753822,
        'url': 'https://de.wikipedia.org/wiki/München'
        'state': 'HB', 'name': 'Bremen', 
        'lat': 53.0758196, 'lng': 8.8071646,
        'url': 'https://de.wikipedia.org/wiki/Bremen'
        'state': 'HE', 'name': 'Wiesbaden', 
        'lat': 50.0820384, 'lng': 8.2416556,
        'url': 'https://de.wikipedia.org/wiki/Wiesbaden'
        'state': 'HH', 'name': 'Hamburg', 
        'lat': 53.550341, 'lng': 10.000654,
        'url': 'https://de.wikipedia.org/wiki/Hamburg'
        'state': 'MV', 'name': 'Schwerin', 
        'lat': 53.6288297, 'lng': 11.4148038,
        'url': 'https://de.wikipedia.org/wiki/Schwerin'
        'state': 'NI', 'name': 'Hannover', 
        'lat': 52.3744779, 'lng': 52.3744779,
        'url': 'https://de.wikipedia.org/wiki/Hannover'
        'state': 'NW', 'name': 'Düsseldorf', 
        'lat': 51.2254018, 'lng': 6.7763137,
        'url': 'https://de.wikipedia.org/wiki/Düsseldorf'
        'state': 'RP', 'name': 'Mainz', 
        'lat': 50.0012314, 'lng': 8.2762513,
        'url': 'https://de.wikipedia.org/wiki/Mainz'
        'state': 'SH', 'name': 'Kiel', 
        'lat': 54.3227085, 'lng': 10.135555,
        'url': 'https://de.wikipedia.org/wiki/Kiel'
        'state': 'SL', 'name': 'Saarbrücken', 
        'lat': 49.234362, 'lng': 6.996379,
        'url': 'https://de.wikipedia.org/wiki/Saarbrücken'
        'state': 'SN', 'name': 'Dresden', 
        'lat': 51.0493286, 'lng': 13.7381437,
        'url': 'https://de.wikipedia.org/wiki/Dresden'
        'state': 'ST', 'name': 'Magdeburg', 
        'lat': 52.1315889, 'lng': 11.6399609,
        'url': 'https://de.wikipedia.org/wiki/Magdeburg'
        'state': 'TH', 'name': 'Erfurt', 
        'lat': 50.9777974, 'lng': 11.0287364,
        'url': 'https://de.wikipedia.org/wiki/Erfurt'

Similar to the examples in the other two blog posts, we would also like to link the markers to a page. In this case, I just took the German Wikipedia pages for all of them. A marker can get several event handlers. We use the onclick here. Adding all markers from the list can be done with this code:

capitals.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    marker.addTo( map );
} );

This will get us the following result (when hovering over the markers, you would also see the city’s name as a title):

© ammap.com | SVG map of Germany (low detail), used with Leaflet, modified by Bernhard Kau, CC BY-NC 4.0

By default, we can zoom into the map, but also out. This is not really ideal for an image overlay. Also, when zooming out, we want to center our map into the container again. This can both be solved with the following code, I have used for the map:

// Set the current min zoom to the zoom level after init.
map.setMinZoom( map.getZoom() );

// Re-center the map when zoomed to minZoom level.
map.on( 'zoomend', function () {
    if ( map.getZoom() === map.getMinZoom() ) {
        map.fitBounds( imageBounds );
} );


In the last three blog posts, you have learned how you can use your own images to create your own individual maps. If you just need a static map, then the PHP approach might be best for you. If you need an interactive map with some features you know from Google Maps (like zooming, etc.), you might want to use Leaflet.

If you want to test the code yourself, you can again find a working version in a new branch on GitHub. I’ve combined everything in a single HTML file, but in a WordPress plugin/theme you can also split them to different files.

I still have an idea for a little “bonus blog post” in this series, so you might get another topic on interactive and individual map. ☺️

Adding markers to a satellite image

In the previous blog posts, I have shown you how to add a marker for a GPS coordinate to an SVG image. In this case, we calculated the x/y pixel position and have drawn an SVG circle or marker path. But what can we do if we have a satellite image – or any other pixel graphic?

Solution: use an SVG as well 😉

OK, not just any SVG. The technique we are using here could be called “a modern image map”. Those of you who are building websites a bit longer probably still know the <map> element which could be used to make an <area> of an <img> tag clickable. The shapes for such an image map however were quite limited and not as flexible and precise as SVG paths. So instead of using a traditional image map, we use an (empty) SVG and place it over an image to create a “clickable map”. Well, we actually just create a clickable SVG overlay, but this will get us the result we want to have. For this blog post, we are using the following satellite image of Berlin:

NASA Goddard Space Flight Center from Greenbelt, MD, USA, Berlin, Germany – Flickr – NASA Goddard Photo and Video1CC BY 2.0

Getting the image boundaries

As explained in the previous blog post, we need to define the boundaries of the map we want to add markers to. We start again by defining the boundaries of the image itself, which is the pixel size:

// Init PixelGeocoder using WGS84 and Mercato projection.
$pixel_geocoder = new PixelGeocoder( 'EPSG:4326', 'EPSG:3857' );
// Set boundaries for the map.
$pixel_geocoder->image_boundaries = [
	'xmin' => 0,
	'xmax' => 2400,
	'ymin' => 0,
	'ymax' => 1800,

Now we need the GPS boundaries. For this image, we don’t have them. In order to get them, I searched for spots on the edge of the map I could identify on Google Maps and then got their position. These are the ones I have chosen and added as reference points:

$map_edges = [
	[ 13.0467623, 52.5594922 ], // West.
	[ 13.1993623, 52.6484712 ], // North.
	[ 13.5841963, 52.4416892 ], // East.
	[ 13.2766553, 52.4069153 ], // South.


Now that we have the boundaries, we can calculate the coordinates for the Brandenburg Gate again:

// Calculate the coordinates.
$bb_gate_lat     = 13.3777041;
$bb_gate_lng     = 52.5162746;
$bb_gate_dst_arr = $pixel_geocoder->transformGPStoMapProjection( $bb_gate_lat, $bb_gate_lng );
$bb_gate_coords  = $pixel_geocoder->calculateCoordinatesToPixel( $bb_gate_dst_arr[0], $bb_gate_dst_arr[1] );

var_dump( bb_gate_coords );
 * array(2) {
 *   [0]=>
 *   float(1477.8750879177708)
 *   [1]=>
 *   float(986.3143837577029)
 * }

But how do we now get a marker as an overlay of the satellite image? This is where the SVG image map comes into play.

Generate the SVG image map

An SVG image map is just an empty “SVG canvas” you would place things. We define a single SVG tag with the same width and height as our satellite image. The image itself would just be a sibling node. We wrap both in a container element to add some styles:

<div class="image-map">
	<img class="image-map-background" src="./Berlin-Germany-Flickr-NASA-Goddard-Photo-and-Video1.jpg" alt="Berlin NASA image"/>
	<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="dynamic-map" width="2400" height="1800" viewBox="0 0 2400 1800"></svg>

To place the SVG over the image, we define a fixed with and height for the container element (this should have the same “aspect ratio” as the image):

.image-map {
	position: relative;
	width: 600px;
	height: 450px;

.dynamic-map {
	max-width: 100%;
	height: auto;

.dynamic-map {
	position: absolute;
	top: 0;
	left: 0;

Now we can add our markers. I would usually do that by getting a list of the markers either from a static array or using a WP_Query and some meta fields. Let’s take this static array with our single marker as an example:

$markers = [
		'name'  => 'brandenburg-gate',
		'title' => 'Brandenburg Gate',
		'x'	 => $bb_gate_coords[0],
		'y'	 => $bb_gate_coords[1],
		'url'   => 'https://en.wikipedia.org/wiki/Brandenburg_Gate',

As we are about to print the marker with some SVG paths and links, we can make this easier to read by using a marker template:

$marker_markup = '
	<a xlink:title="%1$s" target="_parent" class="marker" id="%2$s" xlink:href="/%3$s/" transform="translate(%4$s,%5$s)">
		<path fill="#c10926" fill-rule="evenodd" d="m -0.266,-28.261 a 4.504,4.504 0 0 0 3.204,-1.343 4.613,4.613 0 0 0 1.327,-3.242 4.615,4.615 0 0 0 -1.327,-3.244 4.508,4.508 0 0 0 -3.204,-1.343 4.512,4.512 0 0 0 -3.206,1.343 4.619,4.619 0 0 0 -1.327,3.244 c 0,1.215 0.478,2.382 1.327,3.242 a 4.51,4.51 0 0 0 3.206,1.343 m -0.613,27.98 -8.895,-28.49 h 0.013 a 10.555,10.555 0 0 1 -0.818,-4.074 c 0,-2.77 1.086,-5.425 3.02,-7.381 a 10.251,10.251 0 0 1 7.294,-3.056 c 2.735,0 5.358,1.099 7.293,3.056 a 10.502,10.502 0 0 1 3.021,7.38 c 0,1.414 -0.284,2.798 -0.819,4.076 h 0.012 z" clip-rule="evenodd"/>

Now we put it all together inside our SVG in a loop:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="dynamic-map" width="2400" height="1800" viewBox="0 0 2400 1800">
	<?php foreach ( $markers as $marker ) : ?>
	<?php endforeach; ?>

By using the printf() function, we can put the dynamic parts into the marker template. When you are using this in WordPress, make sure to use the esc_*() escape functions for the dynamic values.

I used the same approach to visualize the “map edges” as a cyan colored circle. The final result will look like this (it’s just a screenshot of the result):

NASA Goddard Space Flight Center from Greenbelt, MD, USA, Berlin, Germany – Flickr – NASA Goddard Photo and Video1, modified by Bernhard Kau, CC BY 2.0

As the marker we have used is pretty small, I have just scaled it up. As the maker scales by its point, this can be done with a single CSS definition:

.marker path {
	transform: scale(5);


In a very similar way, we were able to create an SVG image map with some clickable markers on a satellite image. For the image of a city, this can probably be done if any satellite image. For larger regions, you need to find images with the Mercator projection (or any other projection supported by the PROJ library). It took me quite a while to find a CC licensed image for my examples of this blog post.

If you want this code as well, you can find the new branch with all the different parts combined in a new PHP file on GitHub. You will also find the satellite images there.

I hope this showed you another nice way to use an SVG for an individual map. But we have used quite a bit of custom PHP code and an external library to achieve it. In the next (and probably final) blog post of this map series, I will introduce you with a library you might have already used, just not in this way. So gain, stay tuned. The year will end, but calendar week 52 has one more day in 2023! ?

Geo coordinates to pixel positions – build a map with your own images

This blog post will be a start of a little post series. For a project, I recently had to add markers of cities to a map of Germany. I did that manually in the past, but as the maps has grown to 100 markers, I was looking for a way to do that programmatically.

Map projections

Even though some people believe the earth is flat, the earth is round. But it is also not perfectly round. As our monitors are flat (and before that paper maps), there are man different “map projections“. One of the most common ones is the Mercator projection which is used by Google Maps, Bing Maps, OpenStreetMap and many other services. It’s also referred to by identifier EPSG:3857, which we will need a little later.

GPS coordinates

When giving the position of a place on earth, you usually use the World Geodetic System 1984 (WGS 84) with the latitude and longitude values for a specific place. For the Brandenburg Gate in Berlin this might be around 52.5162746 (latitude) and 13.3777041 (longitude). You might have seen coordinates like this before. You might also have seen a location in degrees, minutes and seconds like 52° 30' 58.59" (latitude) and 13° 22' 39.7338" (longitude). But as it’s easier to make calculations with numbers, the first format is easier to continue with.

Getting SVG maps

So now that we have talked a bit about the theory, let’s talk about code. First, you need to find a map. This can be anything, an aerial photo, a satellite image or an image. I like to use SVG images. You can find them on different platforms. Wikipedia is usually a good source, and maps are usually released using a Creative Commons license. We can find a nice map of Germany with borders to all states. Unfortunately, it uses a different projection system. That’s why, for this blog post, I have chosen the free SVG map from amCharts:

Map of Germany
© ammap.com | SVG map of Germany (low detail), code optimized by Bernhard Kau, CC BY-NC 4.0

There are also commercial services and special stock shops for vector maps. Which ever map you use, please make sure to respect the license.

Geocoding markers

We have a map, let’s start to add some markers. As you probably only know the GPS coordinates, we need a way to convert them to “x/y pixel coordinates” on the SVG map. After some research I’ve found the PROJ library and fortunately there is also a PHP variant of PROJ. We can install it using composer:

composer require proj4php/proj4php

Now we have to do some initialize. We have are converting between two projection systems which we have to set up as follow:

require 'vendor/autoload.php';

// Initialize the PROJ library.
$proj = new Proj4php();

// Set the source and destination projection.
$src_projection = new Proj( 'EPSG:4326', $proj );
$dst_projection = new Proj( 'EPSG:3857', $proj );

The EPSG:4326 projection system, our source projection, is the WGS 84 system used for GPS coordinates. The EPSG:3857 on the other hand is a “Spherical Mercator” projection system mentioned above and also for the SVG file we are using here. So the first one is defining coordinates on a “globe” (spherical map) while the second one is defining coordinates on a flat map (in one of the many projection systems).

Now that we have this setup, let’s do the first conversion. We take the GPS coordinates of Berlin, Germany and convert them to the Mercator coordinates:

$src_berlin = new Point( 13.3777041, 52.5162746, $src_projection );
print_r( $src_berlin->toArray() );
 * Array
 * (
 *     [0] => 52.5162746
 *     [1] => 13.3777041
 *     [2] => 0
 * )

$dst_berlin = $proj->transform( $dst_projection, $src_berlin );
print_r( $dst_berlin->toArray() );
 * Array
 * (
 *     [0] => 1489199.2083951
 *     [1] => 6894018.2850289
 *     [2] => 0
 * )

All right, we have some new numbers now. But how would you use them now? If we want to put them on the SVG map, we have to calculate the pixel coordinates on that image.

Getting the image boundaries

Now we need to get the min and max coordinates on the map. If we are lucky, we would get the coordinates of these points from the provider of the map. Unfortunately for the map from Wikipedia there are no such coordinates given. You can probably find those boundaries on with a search engine or use an open API like the one from OpenStreetMaps and other services. But if you don’t have such data or don’t know where to find them, you can just try to get a GPS coordinate for the points on the edges. So for the map of Germany, we would need to find the places all the way in the North, East, South and West. With these points, we then use the min/max values for the latitude and longitude. Fortunately for the amCharts map, they have it in the source code of the SVG itself:

<amcharts:ammap projection="mercator" leftLongitude="5.864765" topLatitude="55.051693" rightLongitude="15.043380" bottomLatitude="47.269299"></amcharts:ammap>

With these, we can just use two reference points (that are not within the country limits of Germany, but that’s not an issue) and do our calculation:

$swap_x = false;
$swap_y = true;

$dst_points_x = [];
$dst_points_y = [];

$src_points = [
	[ 15.043380, 47.269133 ],
	[ 5.865010, 55.057722 ],

foreach ( $src_points as $point ) {
	$src_point      = new Point( $point[0], $point[1], $src_projection );
	$dst_point      = $proj->transform( $dst_projection, $src_point );
	$dst_point_arr  = $dst_point->toArray();
	$dst_points_x[] = $dst_point_arr[0];
	$dst_points_y[] = $dst_point_arr[1];

$src_boundaries = [
	'xmin' => $swap_x ? max( $dst_points_x ) : min( $dst_points_x ),
	'xmax' => $swap_x ? min( $dst_points_x ) : max( $dst_points_x ),
	'ymin' => $swap_y ? max( $dst_points_y ) : min( $dst_points_y ),
	'ymax' => $swap_y ? min( $dst_points_y ) : max( $dst_points_y ),

var_dump( $src_boundaries );
 * array(4) {
 *   ["xmin"]=>
 *   float(653037.4250227585)
 *   ["xmax"]=>
 *   float(1668369.9214457471)
 *   ["ymin"]=>
 *   float(5986273.409259587)
 *   ["ymax"]=>
 *   float(7373214.063855921)
 * }

With the two “swap” flags, you can indicate that the coordinates are on opposite sites of 0° latitude or 0° longitude, so the maximum value would be used for the left boundary for example and the minimum for the right boundary. For Germany, we have to set $swap_y to true.

Now that we have the boundaries, we can calculate the coordinates on the image. We would also need the size of the image. Then the calculation would be like this:

$image_boundaries = [
	'xmin' => 0,
	'xmax' => 585.506,
	'ymin' => 0,
	'ymax' => 791.999,

$dst_berlin_arr = $dst_berlin->toArray();
$lng = $dst_berlin_arr[0];
$lat = $dst_berlin_arr[1];

$x_pos = ( $lng - $src_boundaries['xmin'] ) / ( $src_boundaries['xmax'] - $src_boundaries['xmin'] ) * ( $image_boundaries['xmax'] - $image_boundaries['xmin'] );

$y_pos = ( $lat - $src_boundaries['ymin'] ) / ( $src_boundaries['ymax'] - $src_boundaries['ymin'] ) * ( $image_boundaries['ymax'] - $image_boundaries['ymin'] );

var_dump( [ $x_pos, $y_pos ] );
 * array(2) {
 *   [0]=>
 *   float(487.1242093149932)
 *   [1]=>
 *   float(523.9253760603847)
 * }

First, we define the boundaries of our SVG map of Germany. Then we use the $dst_berlin point we have transformed before and put it into our calculation. By using $src_boundaries['xmax'] - $src_boundaries['xmin'] we get the “width” of the source projection boundaries (same for the image boundaries). We then subtract the left boundary from the longitude of the point. We divide this point by the width of the source projection and multiply it with the width of the destination projection. This will give us the pixel position on the image. We do the same for the y-axis. To visualize the point, we could simply draw a SVG circle. This would then look like this in code:

<circle cx="482.18464676347816" cy="273.64009871474894" r="5" stroke="red" stroke-width="2" fill="transparent" />

We then add this new node to the original SVG map and get the following:

Map of Germany with a circle on Berlin
© ammap.com | SVG map of Germany (low detail), code optimized and modified by Bernhard Kau, CC BY-NC 4.0

Putting it all together

In order to make this all a little easier, I’ve written a small PixelGeocoder class with the initialization steps, the helper function for the boundaries and a method to do get the coordinates for the image:

use proj4php\Proj4php;
use proj4php\Proj;
use proj4php\Point;

class PixelGeocoder {
	public $proj;
	public $src_proj;
	public $dst_proj;

	public $src_boundaries = [
		'xmin' => 0,
		'xmax' => 0,
		'ymin' => 0,
		'ymax' => 0,

	public $image_boundaries = [
		'xmin' => 0,
		'xmax' => 0,
		'ymin' => 0,
		'ymax' => 0,

	public function __construct( $src_proj_type = 'EPSG:4326', $dst_proj_type = 'EPSG:3857' ) {
		$this->proj     = new Proj4php();
		$this->src_proj = new Proj( $src_proj_type, $this->proj );
		$this->dst_proj = new Proj( $dst_proj_type, $this->proj );

	public function setDstBoundaries( $points, $swap_x = false, $swap_y = false ) {
		$dst_points_x = [];
		$dst_points_y = [];

		foreach ( $points as $point ) {
			$dst_point      = $this->transformGPStoMapProjection( $point[0], $point[1] );
			$dst_points_x[] = $dst_point[0];
			$dst_points_y[] = $dst_point[1];

		$this->src_boundaries = [
			'xmin' => $swap_x ? max( $dst_points_x ) : min( $dst_points_x ),
			'xmax' => $swap_x ? min( $dst_points_x ) : max( $dst_points_x ),
			'ymin' => $swap_y ? max( $dst_points_y ) : min( $dst_points_y ),
			'ymax' => $swap_y ? min( $dst_points_y ) : max( $dst_points_y ),

	public function transformGPStoMapProjection( $lng, $lat ) {
		$src_point = new Point( $lng, $lat, $this->src_proj );
		$dst_point = $this->proj->transform( $this->dst_proj, $src_point );

		return $dst_point->toArray();

	public function calculateCoordinatesToPixel( $lng, $lat ) {
		return [
			( $lng - $this->src_boundaries['xmin'] ) / ( $this->src_boundaries['xmax'] - $this->src_boundaries['xmin'] ) * ( $this->image_boundaries['xmax'] - $this->image_boundaries['xmin'] ),
			( $lat - $this->src_boundaries['ymin'] ) / ( $this->src_boundaries['ymax'] - $this->src_boundaries['ymin'] ) * ( $this->image_boundaries['ymax'] - $this->image_boundaries['ymin'] ),

Now if you want to use this class, this is:

require_once 'vendor/autoload.php';
require_once 'PixelGeocoder.php';

// Init PixelGeocoder using WGS84 and Mercato projection.
$pixel_geocoder = new PixelGeocoder( 'EPSG:4326', 'EPSG:3857' );
// Set boundaries for the map.
$pixel_geocoder->image_boundaries = [
	'xmin' => 0,
	'xmax' => 585.506,
	'ymin' => 0,
	'ymax' => 791.999,
		[ 15.043380, 47.269133 ],
		[ 5.865010, 55.057722 ],

// Calculate the coordinates.
$berlin_lat     = 13.3777041;
$berlin_lng     = 52.5162746;
$dst_berlin_arr = $pixel_geocoder->transformGPStoMapProjection( $berlin_lat, $berlin_lng );
$image_coords   = $pixel_geocoder->calculateCoordinatesToPixel( $dst_berlin_arr[0], $dst_berlin_arr[1] );

var_dump( $image_coords );
 * array(2) {
 *   [0]=>
 *   float(479.2493080704524)
 *   [1]=>
 *   float(273.55748351793665)
 * }

Bonus: use a clickable marker

The circle is easy and nice, but if you want to build a map, you probably want to use a marker, and it should be clickable. Since we are using an SVG image here, we can use a path for the marker and then move it around with the transform="translate(x,y)" attribute:

<a xlink:title="Link to berlin.de" target="_parent" xlink:href="https://berlin.de/" transform="translate(479.2493080704524,273.55748351793665)">
	<path fill="#c10926" fill-rule="evenodd" d="m -0.266,-28.261 a 4.504,4.504 0 0 0 3.204,-1.343 4.613,4.613 0 0 0 1.327,-3.242 4.615,4.615 0 0 0 -1.327,-3.244 4.508,4.508 0 0 0 -3.204,-1.343 4.512,4.512 0 0 0 -3.206,1.343 4.619,4.619 0 0 0 -1.327,3.244 c 0,1.215 0.478,2.382 1.327,3.242 a 4.51,4.51 0 0 0 3.206,1.343 m -0.613,27.98 -8.895,-28.49 h 0.013 a 10.555,10.555 0 0 1 -0.818,-4.074 c 0,-2.77 1.086,-5.425 3.02,-7.381 a 10.251,10.251 0 0 1 7.294,-3.056 c 2.735,0 5.358,1.099 7.293,3.056 a 10.502,10.502 0 0 1 3.021,7.38 c 0,1.414 -0.284,2.798 -0.819,4.076 h 0.012 z" clip-rule="evenodd"/>

When added to our map, the result would look like this:

Map of Germany with a clickable marker on Berlin
© ammap.com | SVG map of Germany (low detail), code optimized and modified by Bernhard Kau, CC BY-NC 4.0


We all love map, don’t we? But sometimes a Google Maps or OpenStreetMap version just doesn’t look nice. With some custom code and a little set-up work, we can create beautiful maps on top of (our own) images. I did the above for a map of Germany that would get the GPS coordinates from a WordPress custom post type and then dynamically created the SVG markup for the city markers in a shortcode (and later server-side-rendered block).

If you want to test the code yourself, you can find it all on GitHub. It is a “proof of concept” and works, but the PHP class can probably use a better architecture and maybe some more methods, so please feel free to adjust it to your need.

In my next blog post, I will show you how you use a picture instead of an SVG map, so stay tuned, the year is not over, yet! ?