My blog’s sweet sixteen – the start of a new chapter

For some of us, our 16th birthday was something special. In Germany, you could go to movies rated 16+ and be alone in the cinema until midnight. Others fulfilled their dream of getting a motorcycle license. In the US, you can even get a driver’s license for a car. My blog also turns 16 today and a new chapter begins.

Closure of more plugins in prospect

Last year, I had hoped that I would finally be able to close the Language Fallback plugin. But unfortunately, Preferred Languages has not landed in the Core. However, the now longer release cycles will bring some movement into the matter, as the plugin now has a good chance of finally being included in one of the next major versions. Important preliminary work for the necessary performance optimization has already been implemented in WordPress 6.5 into Core.

At WordCamp Leipzig, I also learned that an interface for my Embeds for ProvenExpert plugin will soon no longer be available. But I’m not too sad about that, because there is now an official plugin from ProvenExpert that also implements blocks. Mine currently only offers widgets, and I save myself the work on the branch that implements the blocks. However, I will of course try to implement a “migration path” for all those who are currently still using my plugin so that they can switch over without any problems.

The numbers for the past year

Visits to the English blog have increased by around 12%, but for the German blog, they have dropped by 23.1%, which I’d assume is also caused by the rise of AI, which presents answers without the need to visit the site. Looking at the Google Search Console, clicks have increased by around 43% and impressions by around 76%. The numbers for the German site are also positive, but not this high.

One interesting metric is the “visit duration”, which increased by 240% for English and 300% for German! So while AI might have caused a drop in overall traffic, the “traffic quality” has increased, and visitors read much longer, once they clicked a link. As I’m not pursuing a commercial purpose with the site, the numbers aren’t that important to me. I’m just happy when I can help others, and especially when someone leaves me a comment. Unfortunately, there were only 7 comments, 2 of them from me. You are all welcome to change that directly under this post 😉

If you want to see more stats for the German blog, you can find them on the translated post – which you might just translate to English, using AI, if you don’t speak German. 😀

Top3 in the past year

  1. Migrating the Media Library from one site to another
  2. Quick debugging for SSH connections
  3. Repair a broken Git Repository file system

The previous top post dropped to the third place. The new two top posts were both written in 2023 and climbed all the way to the top.

When you check the Top3 for German, you will find different topics. The one about how to debug errors in Contact Form 7 was not yet translated into English. But since it was so successful, I did that at the end of last year. I also took the opportunity to test the content again with the current version of WordPress and Contact Form 7 and updated all the screenshots. There is also another piece of news concerning this post, but more on that in a moment.

Classic Themes will still work in 2025

Just 12 months ago, I was optimistic that my project to reimplement the current theme as a block theme would be successful. But less than 2 weeks later, I had to admit to myself that the goal was not achievable in the desired way. In November, I abandoned the project for the time being. In the four months between these two posts, I also invested a lot of time in the theme and had to neglect blogging.

I still plan to replace the current theme, but no longer with the same goal. I will accompany the new attempt with posts again, but perhaps in a different way.

New project: supplementary videos for the blog

I have often faced the problem, that I want to write a blog post on a topic, but found it very difficult to write it in text, code examples and images. Some things are just too complex to explain them well enough in this form, especially when you need a lot of steps and don’t necessarily want to have 20 screenshots of every single one of these steps. That’s why I would like to take on the project of explaining such topics in a video. I already have a YouTube channel (there is also an English one, but currently without any videos) and have already produced a video as a test. And now we’re coming back to the second-placed German post, because this one is probably a good example of a blog post, to record a video for. As soon as it is finished, I will present it in a separate post and probably include it in the original post.

Conclusion

I was able to achieve some goals, but failed on others. I only managed my 26 posts through the Advent calendar in December, but I still really enjoy sharing new posts with you. If the videos work well, you can look forward to completely new topics in the future that I haven’t tried to tackle yet as blog posts.

Of course, there has to be a video at the end. Last year I shared the WordCamp Europe announcement with you, but of course it makes more sense to present my first (German) YouTube video here. Maybe you’d like to do me a favor and subscribe to the channel (or the English one). That might motivate me a bit more to record the next video even faster, or translate the current one. I haven’t tested these “AI video translations with your voice in a different language”, but maybe you can give it a try and tell me how it sounds in English (or you native language). 😉

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

WordCamp Europe 2025 – A trip to a beautiful city

Some of you might know, that I grew up in Mannheim, where I’ve lived for 26 years, before moving to Berlin. On the way to this year’s WordCamp Europe, I’ve spent some days in Mannheim, before taking a train to Basel, Switzerland. After only 121 minutes, we arrived – on time – in Basel. Taking the tram to our accommodation for the next days, we’ve realized what an absolutely beautiful city Basel is! How can I have lived so close to such a city for so many years and never visited it?

Some sightseeing and museums

As we’ve arrived on Tuesdays, and the Contributor Day would only start on Thursday, we had some time to explore the city. When you book the accommodation in Basel, you get the “Basel Card”, which gives you free public transport and discounts in many museums and other places. We’ve used it quite intensively, but Basel is also quite good to explore by just walking.

Our accommodation was next to “Marktplatz” (market square), so we started there and briefly went into the town hall courtyard. We continued walking through some old streets and took the chance to buy some nice things. Next, we went to the “Carnival Fountain” from Swiss artist Jean Tinguely. Different sculptures were moving and spilling water around. A very different and fascinating fountain. We liked it so much that we decided to visit the Tinguely museum the next day. But first we continued to the Basler Münster (Basel Minster), a cathedral from the 11th century. Behind the cathedral, we found a very unique type of transportation, we had to take: a “wooden ferry”, that does not need any “power” to cross the river. It would just steer around 45° into the flow of the Rhine river to make it to the other side.

The next day was rainy, as most days this week, and we only went to the Tinguely museum, which really had some very cool sculptures. In order to not wear the mechanics out too fast, you could only activate them every 9 minutes manually by pressing a button. In the evening, we had a nice team dinner with my colleagues from Syde.

Contributor Day

Like on previous flagship WordCamp events, I’ve volunteered to be a Table Lead at the Contributor Day for the Meta Team. Even though only one contributor joined my table, we had some great outcome. Xaver worked on a cool new feature, that would allow WordCamp organizers to check in attendees much faster, by scanning a QR code to find them in the list and mark them as attended.

As always, the Contributor Day is over much faster than you think. The evening was dedicated for “The Social”, the invite only event for all organizers, volunteers, sponsors and all other people making WordCamp Europe happen.

The first conference day

Syde had a booth in the sponsors area, but I did not have any “booth duties”, so I could take the chance to see some talks and talk to people.

WordPress without Borders — The Fight for Digital Freedom

After the opening remarks, I’ve stayed for the first talk, a keynote from Noel Tock. He shared some stats on WordPress and how the number of installations have been influenced by events in the past years, like the pandemic. But then he turned it into a presentation on how WordPress does help charities in their work. Since he himself has a charity that rescues dogs in Ukraine, he highlighted some other charities which operate in Ukraine and what they do. A very different and emotional start into WordCamp Europe.

Building Support as core of product

After taking a break and mainly talking to sponsors and attendees, I went back to track 1 for some lightning talks. For the first one, I was a bit late, but still got some cool tips from Anna on how to make support an important part of your company and not only a responsibility of one team.

3 WordPress Agency F*ckups and What I Learned from Them

The second one was a fun one. I had the chance to briefly talk to Jennifer at the Contributor Day about her talk. She told the audience of “nightmares” she experienced in various projects. With many of those, I could relate, since I’ve experienced them myself in the last 10 years working for WordPress agencies.

What we learned from building Site Kit and how you can apply this to your WordPress site

In the last lightning talk of this round, Mariya shared some stories about the Site Kit, the official WordPress plugin from Google, and how the learning from this plugin can help others in their own work. It is always difficult, if not even impossible, to create a plugin that serves everyone.

Connecting cultures at SWI Swissinfo.ch: The journey to international audience engagement

This was the first talk one of my collegues gave. Our Commercial Director at Syde, Ronny Marx, shared the stage with Isabelle Schrills from Swissinfo. They both shared insights on a shared project from the client and agency point of view and highlighted how well a project can turn out if both sides work collaboratively on a new website. Since Swissinfo publishes content in 10 different languages, the use of a WordPress Multisite combined with Syde’s own multilingual plugin solution MultilingualPress fitted perfectly for this type of project, and Isabelle reported, that everyone was really happy with the new system and was not looking back on the legacy one.

WordPress Speed Build

One of my highlights of WordCamp Europe 2024 in Torino was the Speed Build Challenge, hosted by Jamie Marsland. In this editon, Ellen Bauer and Fabian Kägy got a real “challenge”. Jamie presented them with a “fancy” ecommerce side, including some marquees, sliders, playful fonts and other things that were hard to recreate. Fabian managed to get more things done visually, but Ellen even went as far as setting up WooCommerce, so the results was a real shop. The task, however, was not really doable in 30 minutes, but both had great fun and entertained the audience in their attempt to recreate the page.

The second conference day

I’ve initially signed up for my first ever WCEU workshop, but somehow did not receive my confirmation. OK, I was also very late with registering, so it’s my own fault. And as we had to move to a different hotel for the last night, I took it slowly in the morning.

Multilingual WordPress for developers

Another colleague from Syde, Dennis Plöttner, gave a talk in track 2 about multilingual WordPress. This is a topic Dennis cares a lot about, since he also wrote a free plugin to create a multilingual website. Even though I have experience to make WordPress code translatable, I’ve learned some new things from his presentation. Such a topic always gathers a large audience at WordCamp Europe, and especially in a country with four official languages, like Switzerland.

WP Cafe: Five for the Future

After a small break, I joined other attendees to talk about the “Five for the Future” initiative. In light of recent events, there were many things to discuss. Many people shared great ideas, and I am very much looking forward to seeing some momentum in this crucial initiative in our community.

Why Block Themes Make WooCommerce Stores Better

Some of you might know that I don’t like ecommerce. It has nothing to do with WooCommerce or any other solution, I just don’t like the complexity around it and the time it takes me to get into it, when I only have the chance to work on ecommerce projects quite rarely. But I was looking forward to the talk from Ellen. It was packed, and I’ve learned some new things about how blocks can really turn WooCommerce into a new experience for merchants in the future. She was also presenting some first bits of a new WooCommerce “default theme”, I can’t wait to take a look at, once it will be released in about a month together with WooCommerce 10.

Fireside chat with Mary Hubbard and Matt Mullenweg

After many sponsors had their raffles – I did not win anything, but one of my colleagues won a Braille Lego set – I went to the final session. The first half was some talk between Mary – which I haven’t had met before – and Matt, in which Mary asked some questions. The second half was with questions from the audience, and both Mary and Matt allowed for some 15 minutes extra to answers all of them.

Closing Remarks

Always a special part: we could thank the amazing organizing team and all the volunteers for their work. I’ve tried to capture the moment, but the lens of my phone was now wide enough to get all in one shot. This shows, how many people help to make this week an event for all of us!

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

Then the moment all attendees waited for. The current Global Leads say goodbye and the new team is coming on stage to announce where WordCamp Europe is going to next year. I had a strong guess and was right. But see for yourself:

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

The After Party

Yes, there was also an After Party, as every year. I took the chance to speak to some people I haven’t had the time to talk to, yet. And still there were some attendees I have not spoken to this week. Even if this year was smaller with “only” 1723 attendees, there is just not enough time to catch up with everyone. But I hope that I will see many of them next year in Poland!

Conclusion

I’ve been to all WordCamp Europe editions, and being in Basel I’ve realized, that there have been as many WordCamp Europe event (including the online ones) since Berlin, than before Berlin. I can’t believe it’s been that long ago.

Basel was special in its own kind. The weather might have been the rainiest for a WCEU, but the city was just amazing, and I’m glad I was able to attend again. But Basel was also the most expensive one. Not only for the organizing team, who had to work around a very tight budget. With only 50€ per ticket, they still managed to find enough sponsors to dev lier a great overall experience. But unfortunately, for some many community members, Basel was just too expensive to take the journey this year. I believe that Poland will allow many more attendees to return, which will also result in a much more diverse group of attendees, not just the ones who can afford it or are sponsored to attended by their employers. I’ll hopefully see many of you next June, then!

Implementing a custom block style variation using only JSON files

For a small project, I’ve implemented my first Block Theme (that I finished) using the Create Block Theme plugin. In the PDF with the design, there was a “round button”, which made me wonder on how I would implement it.

It seemed easy at first. Just add a button, write some text inside and set the border-radius to 50%, to make it round. The problem is: that will only work if the button has exactly the same height and width. But since you don’t know in advance what text you would use in the button, and only the width increases, the button will have an oval shape:

The Block Editor with a button and a "border-radius" set to "50%", showing an oval button.

That doesn’t work. But how can we get the button perfectly round? We need to set the aspect-ratio for it to 1. While registering custom aspect ratios was introduced in WordPress 6.6, we cannot use it for any block. It’s currently only available for the Image, Feature Image and Cover block. How can we achieve it? That’s where Block Style Variation come into play.

What is a Block Style Variation?

As many of you know, one of the hardest things in tech is naming things, and there are many things in the “Gutenberg project” with similar names, making it all confusing. A Block Style Variation – sometimes referred to as only Block Style – is a feature that lets you create an alternative style for a block. When we think of the core/button block, we usually have the “Fill” and the “Outline” styles. The core/table block has the styles “Default” and “Striped”. WordPress Core comes with many of these block styles. But you can create your own custom ones.

Creating your own Block Style Variation

On the documentation page about Block Style Variations, you can (currently) find two different methods to register a custom block style: PHP and JavaScript. Those of you who wrote WordPress themes for some years, are probably (still) quite familiar with PHP, so let’s take a look at how to do it with PHP.

Register a Block Style with PHP

We want to have a round button, and all we have to do, is adding some PHP to our functions.php file like this:

function tt5_round_button_register_block_styles() {
	register_block_style(
		'core/button',
		[
			'name' => 'round',
			'label' => esc_attr__( 'Round', 'twentytwentyfive-round-button' ),
			'inline_style' => '.wp-block-button.is-style-round {
				aspect-ratio: 1;
				border-radius; 50%;
			}'
		]
	);
}
add_action( 'init', 'tt5_round_button_register_block_styles' );

With this code in place, we can now select the new block style from the “Style” settings in the “Block” sidebar:

The Block Editor with a button and the "Round" block style selected, showing the button as a perfect circle.

Now this looks nice! And it was not really a lot of code we had to use. But it doesn’t feel right to use PHP to register a block style, when we want to create a Block Theme. So how can we do it instead? Well, we could use JavaScript, and you can find an example for that in the documentation, but I like this solution even less.

When you scroll down that page, you will find this notice:

It is not currently possible to customize your custom-registered block styles via theme.json. You can only style those registered by core WordPress at the moment.

This is really unfortunate. But it’s also not true. I’ve found a solution to register a block style and also style it using the theme.json file.

Register and customize styles for a Block Style using JSON

The feature to use custom block styles was also introduced in WordPress 6.6, and in order to use it, you need to create a new file in the styles/blocks folder of your theme. So we could create a file styles/blocks/round.json with this content:

{
  "version": 3,
  "$schema": "https://schemas.wp.org/wp/6.8/theme.json",
  "title": "Round",
  "slug": "round",
  "blockTypes": [
    "core/button"
  ],
  "styles": {
    "border": {
      "radius": "50%"
    },
    "dimensions": {
      "aspectRatio": "1"
    }
  }
}

This is really all we have to do. We can give this file any name. And we don’t have to “load” it, this is done automatically by WordPress. You should use "version: 3" here and also set the $schema to use at least “6.8” or higher (or trunk, for the latest version).

Overwriting custom Block Styles

Even though the statement says that you can’t style your custom block styles using the theme.json file, you can do it. I’ve tested it successfully in a local WordPress 6.8 installation. You could do it like this:

{
  "version": 3,
  "$schema": "https://schemas.wp.org/wp/6.8/theme.json",
  "styles": {
    "blocks": {
      "core/button": {
        "variations": {
          "round": {
            "border": {
              "color": "currentColor",
              "style": "solid",
              "width": "3px"
            },
            "color": {
              "text": "currentColor",
              "background": "white"
            }
          }
        }
      }
    }
  }
}

This would overwrite the border and colors and give us a “round outline button” that would look like this:

The Block Editor with a button and the "Round" block style selected, showing the button as a perfect circle, that has a 3px "outline" style.

All of this was possible by only editing some JSON files. No need to write any “real code” in PHP or JavaScript.

Caveats

Even though we were able to create a round button without any “custom code”, the result is not perfect. We have used the dimensions.aspectRatio property for the button, but since it is not yet supported for a core/button block, we won’t be able to see and edit it in the global block styles for the theme.

Conclusion

Adding a custom block style to an existing core block can make it easier for content editors to create consistent layouts. If you want to avoid writing “real code”, you can add them using only JSON files.

If you want to learn more on that topics, I can highly recommend the page “Block style variations and section styles” on fullsiteediting.com for more examples.

I really hope that the aspectRatio and other options will be made available to other blocks as well, and also that we are able to create block styles using only the Site Editor in the future. There is already a ticket about this, but I understand that this is quite a large feature.

Adding a custom panel to Query Monitor

One of the questions I hear a lot in different settings is this: “Which plugin do you use on every site?”. I usually don’t have an answer for that, since in my opinion, there is no “mandatory plugin”. But one plugin I do use almost always, at least while developing a site, is the Query Monitor plugin. It helps me a lot in finding issues with the site. I even have it active on some production sites.

Since I work on some projects hosted on WordPress VIP, I was asked to take some VIP trainings. One of these is around all the ways in which you can debug a website. And sure enough, Query Monitor is also available on WordPress VIP sites.

Extending the Query Monitor plugin

As I’m a frequent user of Query Monitor, I am very familiar with most of its panels. Some of them I use all the time, others rather rarely. But I was not aware that you can “easily” add your own panels with your own debugging data to Query Monitor. Some plugin use this to add information, others have additional “debugging plugins” to add the panels. On the Query Monitor website, you can find a list of those “Add-On plugins”.

The VIP training had an example on how to add some random data to a new panel, and this was so fascinating to me, that I wanted to try it out. I also found a nice use-case for my blog. Since I am using MultilingualPress to translate my blog posts, I thought it would be nice to have a list of translated content on a page or post. So that’s what I am going to demonstrate here.

Collecting the data

The Query Monitor plugin is written using OOP (object-oriented programming) and the first thing we have to implement is a class extending the QM_DataCollector class from Query Monitor, to collect the data we want to display.

The data is stored in a “storage”. We don’t have to implement a custom one, but I wanted to implement a full solution, so I created this implementation of a QM_Data class first:

class QM_Data_MLP_Translations extends QM_Data {
	/**
	 * @var Translation[]
	 */
	public $translations;
}

The MultilingualPress API is using a custom class Translation and we would use an array of these objects in our data class. Now we can implement our QM_DataCollector implementation:

class QM_Collector_MLP_Translations extends QM_DataCollector {

	public $id = 'mlp_translations';

	private $current_locale;

	public function __construct() {
		parent::__construct();
		$this->current_locale = get_locale();
	}

	public function get_storage(): QM_Data {
		return new QM_Data_MLP_Translations();
	}

	public function process(): void {
		$args = TranslationSearchArgs::forContext(
			new WordPressContext()
		)->forSiteId( get_current_blog_id() )->includeBase();

		$translations = resolve( Translations::class )->searchTranslations( $args );

		$this->data->translations = [];
		foreach ( $translations as $translation ) {
			if ( $translation->language()->locale() !== $this->current_locale
				 && $translation->remoteContentId() !== 0 ) {
				$this->data->translations[] = $translation;
			}
		}
	}
}

Every QM_DataCollector needs an $id property, and we use a vendor prefixed mlp_translations value here. In the constructor, we also save the current locale, as for some reasons, it was later changed to the locale from the user profile. In the get_storage() function, we are using the QM_Data object we implemented first.

The most important part is the process() function. This is where we collect the data and add it to our storage. You don’t have to understand the first few lines, as these are the specific functions from MultilingualPress to get a list of all translations to a post or page. The collection of your data might be different, but you will probably end up having a loop to add the data you want to use into $this->data. In my case, I filter out the current translation and also only add those with a remoteContentId, which would give me only “singular content”.

Initializing the Collector

The last step is to initialize the “collector” object in a filter. This is done with the following filter:

function register_qm_collector_mlp_translations( array $collectors ) {
	$collectors['mlp_translations'] = new QM_Collector_MLP_Translations();

	return $collectors;
}

add_filter( 'qm/collectors', 'register_qm_collector_mlp_translations', 10 );

Nothing really fancy here. Now that we have our “collector” with the data we want to display, let’s take a look at how we can do that.

Adding our data to Query Monitor

Query Monitor has several panels, and you can either add your own panel, or you add data to an existing one as a “child panel”. I went with the second approach, as it seemed to me more logical to add my data to the “Languages” panel. This is the complete code (with comments removed):

class QM_Output_MLP_Translationsextends QM_Output_Html {

	protected $collector;

	public function __construct( QM_Collector $collector ) {
		parent::__construct( $collector );
		add_filter( 'qm/output/panel_menus', array( $this, 'panel_menu' ), 20 );
	}

	public function name() {
		return __( 'Translations', 'multilingualpress' );
	}

	public function output() {
		$data = $this->collector->get_data();

		if ( empty( $data->translations ) ) {
			$this->before_non_tabular_output();

			$notice = __(
				'No translations for this content.',
				'multilingualpress-qm'
			);
			echo $this->build_notice( $notice );

			$this->after_non_tabular_output();

			return;
		}

		$id = sprintf( 'qm-%s', $this->collector->id );

		$this->before_tabular_output( $id );

		$this->output_translation_table( $data->translations );

		$this->after_tabular_output();
	}

	protected function output_translation_table( array $translations ): void {
		echo '<thead>';
		echo '<tr>';
		printf( '<th>%s</th>', esc_html__( 'Locale', 'multilingualpress-qm' ) );
		printf( '<th>%s</th>', esc_html__( 'Title', 'multilingualpress-qm' ) );
		printf( '<th>%s</th>', esc_html__( 'Post ID', 'multilingualpress-qm' ) );
		printf( '<th>%s</th>', esc_html__( 'Site ID', 'multilingualpress-qm' ) );
		echo '</tr>';
		echo '<tbody>';

		foreach ( $translations as $translation ) {
			echo '<tr>';
			printf(
				'<th scope="row"><code>%s</code></th>',
				esc_html( $translation->language()->locale() )
			);
			printf(
				'<td class="qm-ltr">%s</td>',
				$this->build_link(
					$translation->remoteUrl(),
					$translation->remoteTitle()
				)
			);
			printf(
				'<td class="qm-num">%s</td>',
				esc_html( $translation->remoteContentId() )
			);
			printf(
				'<td class="qm-num">%s</td>',
				esc_html( $translation->remoteSiteId() )
			);
			echo '</tr>';
		}

		echo '</tbody>';
	}

	public function panel_menu( array $menu ) {
		if ( ! isset( $menu['qm-languages'] ) ) {
			return $menu;
		}

		$data = $this->collector->get_data();

		$menu['qm-languages']['children'][] = $this->menu(
			[
				'title' => sprintf(
					esc_html__(
						'Content Translations (%s)',
						'multilingualpress-qm'
					),
					number_format_i18n( count( $data->translations ) )
				)
			]
		);

		return $menu;
	}
}

Let me explain some of the important functions. In the constructor, we are using a filter to call our panel_menu() function, that adds the panel. Here you can see that I add the data to the children of qm-languages. I also get the collected data here to be able to add the number of translations to the panel title.

In the output() function, we generate the output. We first check if data is available. If not, we just output a notice. You may have recognized all these $this->before_*_output() and $this->after_*_out() functions. They are inherited from the parent class QM_Output_Html and saves us from writing some HTML markup ourselves.

The most important output is happening in the output_translation_table() function. This is a helper function that I have added. You might not need to have such a function, and you can also rename it so it best fits your use-case. I just named it similar to other functions in Query Monitor itself.

After generating the <thead> with some table column names, we print out the data for the translations. We are using some methods of the Translation objects from MultilingualPress to get the site ID, post ID, post title and permalink for the translated content.

Initializing the output class

Just as with the collector class, we have to initialize our HTML output class. We use another filter here:

function register_qm_output_mlp_translations( array $output ) {
	$collector = QM_Collectors::get( 'mlp_translations' );
	if ( $collector ) {
		$output['mlp_translations'] = new QM_Output_MLP_Translations( $collector );
	}

	return $output;
}

add_filter( 'qm/outputter/html', 'register_qm_output_mlp_translations', 50 );

With this last class implemented, we have all the parts we would need for our custom Query Monitor panel.

Bringing it all together

Now, one last step is missing. We still need to load all files. We could do it as simple as this:

if ( defined( 'QM_DISABLED' ) && constant( 'QM_DISABLED' ) ) {
	return;
}

add_action(
	'plugins_loaded',
	static function () {
		require_once __DIR__ . '/data/languages_mlp_translations.php';
		require_once __DIR__ . '/collectors/languages_mlp_translations.php';
		require_once __DIR__ . '/output/html/languages_mlp_translations.php';
	}
);

To avoid loading the classes if Query Monitor was disabled, we first check the QM_DISABLED constant. We then require or files with the three classes, which contain the filters to initialize them. In the implementation, I wanted to stick as closely to the Query Monitor code base, as possible. I would usually not have filters to initialize classes in the same file as the classes themselves.

The result

After doing all that, you probably all want to see the result of our work, right? Then let’s install that plugin real quick on my blog and show you the output for my last blog post:

Screenshot with Query Monitor opened, showing the new "Content Translations" panel, listing the German translation of the last blog post.

Pretty cool, right? Since my blog has only two languages, and it only shows one translation, this might not be super useful. But we could think about other information to show in this panel. Maybe the translation of all taxonomies used in the blog post. Or some important settings from MultilingualPress.

Conclusion

Adding a custom panel to Query Monitor can help you debug important information more easily. You might have a very different use-case for a custom Query Monitor panel. Or maybe you have even used it yourself in the past. Then please share a link to your solution, if it’s public.

If you have MultilingualPress running on your website as well, or just want to see the full code of this example, you can find it on Github where you can also download it as a plugin ZIP and test it.

Prevent hotlinking of premium fonts

You may have a website that is using a premium font. Since loading them from a CDN, maybe even with some external JavaScript, is not ideal for performance and privacy reasons. That’s why you probably want to host the fonts on your server. But many websites selling you fonts may have some license agreements like this:

Under this license, the font must be adequately secured to prevent unauthorized websites, beyond the licensee’s direct control, from accessing the font software for content display, including hotlinking or direct access via web server integration

Now, how can we do that? This highly depends on the web server and infrastructure you are using. Let’s take a look at some different apporaches.

Using a web application firewall like the Cloudflare WAF

In a project I had this need, it was not possible to change the server configuration. But the whole website was behind Cloudflare. In Cloudflare, there is a special Hotlinking Protection, but it can only prevent a few image types from being hotlinked.

After some tries, I came up with a rule that worked for web fonts as well. You can configure it with in the UI of the Cloudflare under “Security -> WAF”:

Screenshot of the UI to edit a Cloudflare WAF rule with the, which was named "Hotlink protection for fonts". The controls were used to create the rule that can also be found in code down below.

You can also alternatively use the “Edit expression” link and paste the following code into it:

(http.request.full_uri contains "/fonts/" and not http.referer contains "example.com" and not cf.client.bot and not http.user_agent contains "PTST")

This rule prevent hotlinking of any path that contains the folder /fonts/. This might be a bit too strict, since it could also prevent other (free) fonts or those stored in plugins. You might instead want to use a more specific folder, like wp-content/themes/your-theme-slug/assets/fonts/ in the “URI Full” field.

The other field you have to adjust is the “Referer” field. This is where you use your own domain name. It’s important to set it to “does not contain” (or similar), since we want to block requests, that “do not” come from your domain.

The “Known Bots” and “User Agent does not contain PTST” does prevent the rule from being used when your site is accessed by search engines or the performance tool webpagetest.org, so it does not interfere page visits by these services.

Finally, in the “Then take action…” we choose the “Block” action, which prevents the loading of the hotlinked font.

Using an nginx configuration

When you are using nginx as your web server, you can also block hotlinking to your premium fonts, using the ngx_http_referer_module, by adding the following location to your configuration:

location /fonts/ {
    valid_referers example.com www.example.com;
    if ($invalid_referer) {
        return 403;
    }
}

You can use static domain names or a regular expression, and some other values for the valid_referers, as described in the documentation.

Using an Apache configuration

If you are using Apache, which many shared hosting providers still use, you can add the following to the .htaccess file in the root folder:

<IfModule mod_rewrite.c>
  RewriteCond %{HTTP_REFERER} !^https?://(www\.)?example\.com [NC]
  RewriteCond %{REQUEST_URI} /fonts/
  RewriteRule . - [F,NC]
</IfModule>

This will also block any hotlinking from other domains. Consult the Apache documentation for other RewriteCond values.

Limitations

Both, the nginx and the Apache examples, do not handle search engine bots and performance tools. It would be possible to not block them, but this would be more complex to set up.

You should also be aware that all these solutions are not “secure”, as they don’t really prevent hotlinking. If someone wants to use premium fonts stored on your server for their websites, they could write “a proxy” and manipulate the HTTP_REFERER header so it looks like the requests are coming from your server. But I’d say that one of the measures shown in this blog post should be “adequately secure”, as stated in the license agreement. But since I can’t give you any legal advice, better ask back to the font vendor, if your measures are good enough for them.

Conclusion

Using a nice premium font can make your website stand out. Hosting them on your own server makes loading them faster and reduces privacy risks. But always read the license agreement, if you find some requirements like the prevention of hotlinking. Otherwise, you could get in real trouble.

Redirect attachment pages to media files

In a project I’m working on, I made an interesting discovery this week. The website had only two comments – it’s a WooCommerce shop, so comments are not really important. Those two comment however were made on an attachment page. The page had the title “contact-2” and was a header image. Most probably, someone searched for the “contact” page of the website and found this page with the comment form, thinking that this is the contact form.

What is an attachment page?

For those of you, who never came across an attachment page, let me quickly explain, what they are. Some of you might know, that every item you upload into the media library, a database entry is created. Those entries are in the wp_posts table with the post type attachment. So every media item it “a post”. And since the attachment post type is set to public, every media item has an attachment page.

In the case mentioned above, there was a file with the name “contact.jpg” uploaded. Since there was already a regular page with the title “contact”, WordPress appended the “-2” suffix. I have a blog post about this suffix and how to remove it. But what does that mean? If you access a URL like https://example.com/contact-2, you would get a page, that may be using the singular.php template of your theme, unless the theme has a better template for attachments.

On this page, you would see the media item, in the case of an image, you would see the full size. You can see an attachment page on the demo site for the Twenty Nineteen theme. This URL is using the attachment_id parameter, using the post ID of the attachment (media item). You could also use the p parameter, which then redirects. But depending on your permalink settings, you might also see a “nice URL” like the “contact-2” mentioned earlier.

Why you (don’t) want to have an attachment page

If you have a media heavy blog, and you want to allow commenting your media item, the attachment page might make sense. But for most pages, they would just “blow up” the pages of your website, and they can be in conflict with some “normal pages”.

Those attachment pages are also visible to search engines, and since they only use the file name as the title, you might end up with a page getting (more) traffic to those pages, and not to the normal pages you want to have traffic to. Fortunately, the attachment pages are not showing up in the sitemaps.

How to remove the attachment pages?

You can’t really “remove” them. You can make them “not public”, but there is a simpler way. If you set up a new WordPress site starting with WordPress 6.4, you can stop reading this blog post, since you already have a solution.

If your WordPress installation is older, you can use a new option that was introduced in WordPress 6.4, to redirect an attachment page URL to the media file URL.

Set the option with the WP-CLI

I usually use the WP-CLI for such tasks. If you also use it, you can run the following command:

wp option set wp_attachment_pages_enabled 0

A value of 0 for the wp_attachment_pages_enabled option disables the attachment pages, and the redirect_canonical() function would then redirect to the media file.

Using the option.php page

If you don’t use the WP-CLI or you can’t use it, you can also use a “hidden feature” in the WordPress Dashboard. Navigate to wp-admin/options.php, where you can view and edit options. Here you search for the option name, change the value from 1 to 0 and save the options “Save Changes” button at the end of the options page.

Using a SEO plugin

Some SEO also have an option to redirect the attachment pages (or deactivate them differently). I use Yoast SEO on most of my pages, and here you would find it as “Yoast SEO > Settings > Advanced > Media pages”. In this section, you toggle the “Enable media pages” off. If you instead want to use the pages, you can set some other settings here for attachment pages.

Conclusion

It’s easy to forget that attachment pages exist in WordPress. There were probably good reasons to have them in the past, but on a typical website, they are often not something you want to have traffic to. Fortunately, you can “disable” them without additional plugins now.

How to make a navigation menu more accessible by adding a label

After writing a blog post in the 24 days before Christmas, I took the month of January off to get some new energy and collect some more and other topics to write about. Since I’m currently preparing for an accessibility certification, I want to have some blog posts around these topics. Today, you will get the first one. But I might blog about different topics in between.

Setting a menu label in a Block Theme

When you have a website with only one navigation using the <nav> tag, you are probably safe. Still, it can make sense to have a (good) label for your main navigation menu. But as soon as you have two menus, you must add labels.

Many screen readers have the option to jump to “landmarks” on your site, so a visitor can use this to easily find the navigation menu(s) on your page. But they will usually only here how many items are in each menu, not what the menu is about.

The default navigation menu in Twenty Twenty-Five

When you start a new WordPress project using the default theme Twenty Twenty-Five, your navigation menu will only have the “Sample Page” as an entry, and it would have an aria-label attribute, but it is empty (all other attribute values are replaced with ..., since their content is not important in this code snippet):

<nav class="..." aria-label="" data-wp-interactive="..." data-wp-context="...">

The navigation menu has a name, though. It is simply “Navigation”. When you navigate to “Appearance > Editor > Navigation”, you should see something like this:

Screenshot of the Site Editor with the "Navigation" view opened showing the one "Navigation" with one entry "Sample Page".

When you click on the three dots next to the title, you can easily “Rename” the existing navigation. Let’s rename it to “Main navigation”. If we now refresh the frontend, we see … no change. Even after changing the title, the aria-label is empty.

Solution: change the title by duplicating the menu

I don’t really know why the change of the title does not add any aria-label, but I found a workaround. Instead of using “Rename”, you can use “Duplicate”. This will add a “(Copy)” suffix to the duplicated menu. Now you only have to use this new navigation menu at the position you want to have it. For example, you navigate to “Appearance > Editor > Patterns > Header” then, select the header and click on the navigation block. In the Block settings, you click on the three dots right of “Menu” and then select the new menu you have duplicated and renamed:

Screenshot of the "Navigation" block sidebar settings with the selection of the available menus opened.

If we now save the header with this new menu, and refresh the frontend, we finally have our aria-label:

<nav class="..." aria-label="Main Navigation" ... >

In the dropdown in the screenshot above, you can also see the “Create new Menu” option. If you create a new navigation here, it would get a name like “Navigation 2”. This would automatically be used as the label. But you would have to back to the Site Editor to change the label. It would be nice, if that would also be possible here.

Adding a label to a Classic Theme

If you are still using a Classic Theme, you have to manage the label differently. Even though WordPress supports labels since version 5.5, many themes still don’t use them. Or at least not with the new parameter. This is how the label is added in Twenty Twenty:

<nav aria-label="<?php esc_attr_e( 'Footer', 'twentytwenty' ); ?>" ... >

If you want to change the label here, you would need to edit the file – in a Child Theme. Not really ideal, but still possible. If you develop themes yourself, make sure to use the container_aria_label parameter of the wp_nav_menu() function.

Conclusion

Adding a label to each navigation menu can make a huge difference for the accessibility of your website. When you use a Block Theme, you can do all of that without touching any code directly in the Site Editor. And since I’ve showed you, that the automatic navigation menu names are not really ideal, you might want to check the labels of your website and give them a better label.

Debug submission errors in Contact Form 7

Note: This blog post was first published in German only on 24 January 2016. I’ve only translated it on 29 December 2024, updated some things in text and also created new screenshots. The solution presented in this blog post still works with current versions of Contact Form 7.

This week I received a report of an error on a website related to the very popular contact form plugin Contact Form 7. In this post, I’d like to briefly explain what exactly the error was (so you don’t make it too) and how to better debug errors with the plugin.

Finding the error

Most of the time you only get a message from someone saying something like “I can’t submit forms”. So you first must find out for yourself what exactly is not working with the form. Troubleshooting was not exactly easy, as the plugin does not provide much information about what went wrong in the event of an error. All you get is this message:

Read more →

Creating a dialog with native HTML

Here it is now, the last advent calendar blog post for 2024. Which also means, that many of you are celebrating Christmas today or tomorrow. So why do you read blog posts on such a day? 😁

The last topic for this edition will be about another HTML element you might not have known before. But you all know and hate cookie popups, right? Well, unless you are into the development of such tools. 😅

The dialog HTML element

When we “want” to create a popup, we would have used some kind of “modal library” in the past. But now, we have a native HTML element for this: the <dialog> element. It has some similarities with details element, I have blogged about four days ago. And if you visited the resources I’ve linked there, you might already have seen the element. This is how you would use it:

<dialog>
	Some Text inside the dialog.
</dialog>

If you put this code into your page, you would see … absolutely nothing. The element is not visible by default. You can add the open attribute to it, just as with the details element and then you would see this:

Chrome with open dev tools showing the properties of the open dialog element, that is centered on the page.

As you can see here, we get more than just a blank element with some text. The dialog has a border, some padding and is centered on the page, using an absolute position and auto margin. This would even mean, that it is displayed above any content that comes after it.

Opening and closing the dialog

A dialog that open by default is not really all that useful. Fortunately, we have some JavaScript functions to make it interactive. So let’s add a button that would open the dialog and a button inside of it, to close it:

Chrome showing an open dialog element. The dev tools are open and at the bottom of the HTML markup tree, a "#top-layer" is shown after the closing html tag. The dialog has a paragraph and an undordered list. The page behind the dialog has an "Open dialog" button and inside the dialog, there is a "Read it" button at the end.

This screenshot shown multiple things at once. First, we have the webpage with an “Open dialog” button. When clicked, it will open the dialog, which has some more HTML content this time. You may also notice, that the background of the website is gray. It is white, but when the dialog is open, there will be an overlay to the page. This can also be seen in the left with the ::backdrop pseudo-element. One last thing to recognize is the #top-layer after the HTML tree on the left. This is a special layer that covers the entire page and helps with accessibility.

When you close on the “Read it” button, the dialog will close. For the two buttons, we need some JavaScript to add this functionality:

const dialog = document.querySelector('dialog');
const openButton = document.querySelector('button#open-dialog');
const closeButton = document.querySelector('button#close-dialog');
openButton.addEventListener('click', () => {
	dialog.showModal();
});
closeButton.addEventListener('click', () => {
	dialog.close();
})

When you use the showModal() function, it does not really matter where the element is placed in the HTML tree. It will always open in the center of the page. If the content does not fit, a scrollbar will appear.

Alternative way to open the dialog

There is a second function to open a dialog. It is simply called show() and would open the dialog “in place” and not as a modal. In this case, it makes a difference where the dialog is placed. It will still show it with “position: absolute” over the content, but not vertically centered anymore. It will appear just where its HTML is placed in the DOM. There will also be no backdrop overlay and no #top-layer, so the behavior has some major differences.

Closing the modal without JavaScript

To open the dialog, you either need some JavaScript with one of the two functions to “show” it. Unless you add the open attribute to it, to have it open by default – I really wonder why the functions are not openModal() and open(), then the element is also using the open attribute.

To close the dialog, however, you might not need JavaScript. You can also have it closing itself, when you have a form inside of it:

<dialog open>
	<form method="dialog">
		Some text in the dialog.
		<button>OK</button>
	</form>
</dialog>

Do you see the special trick with the form? It’s using the value dialog for the method attribute. This will close the dialog, whenever the form is submitted with a button inside of it. So you could even just have an input and it will close on ENTER.

If the dialog was opened with showModal(), this modal can also be closed with the ESC key. So there are different ways to close it without any JavaScript.

Styling the element

The dialog itself can be styled like any other “container”. As shown earlier, it comes with some default border and padding. More interesting is probably the styling of the backdrop. Here is an example styling both:

dialog {
	border-radius: 5px;
	border: 3px solid red;
	top: 30px;

	&::backdrop {
		background: black;
		opacity: 0.7;
	}
}

We are changing the border of the dialog and also change the background of the backdrop. Ah, and by the way, what you see here is no Sass syntax. This is native nested CSS. If you haven’t seen this before, read the previous advent calendar blog post “Nested CSS without preprocessors“. Finally, this is how it would look like:

An open dialog with a changed "background" with lower transparency and a red border with a border-radius. The dev tools are open to show the HTML code and CSS of the dialog element.

Instead of the rgba() value for the background we now use a background-color in combination with an opcatity.

Getting even more out of the element

On the documentation page, you will fine many more examples on how to use forms inside the element, style it, interact with it, etc. But all of that would be too much for this blog post. The CSS-Trick blog post that covers the details element, also has some examples for the dialog element.

Conclusion

I hope I have saved the best topic for today. At least I found this element really fascinating and used it in one project. I might blog about this use-case. But that’s something for next year. This was my final advent calendar blog post and also the last one for this year, keeping my two weeks routine on blogging.

I hope you’ve learned some new things in the advent calendar blog posts, or in the others I have written this year. It was fun, but also a lot more work than I thought it would be. Have some great few days and see you all in 2025! 🎇

An easier dark mode

Four day ago, I wrote about “The prefers-color-scheme media query” and how to use it to create a dark mode for your browser. Today, I’m telling you: you might not even need it. Hm, but how do we get a dark mode then, you might ask? With a single CSS property instead of a media query.

The color-scheme property

All you need to use is one single property to tell the browser which color schemes you have. And this is how you can use it:

:root {
  color-scheme: light dark;
}

That’s it! You don’t need any more than that. Come back tomorrow for another blog post. 😁 Nah, there is more to it. But let’s see what this already does. I’ve created a page with some HTML and no CSS, other than the line above. This is how it looks like in my Chrome browser using a light mode:

Screenshot of some elements in light mode in Chrome: h1, p, fieldset, input, textarea, select and button.

And now we use the switch that I’ve shown you in the blog post “Simulate preferences in the browser” to switch to the dark mode. This is what you would get then:

Screenshot of some elements in dark mode in Chrome: h1, p, fieldset, input, textarea, select and button. The inputs and the buttons now have a background color.

While in the light mode, all elements had a white background, the form elements and the button now have a gray background. The placeholder text of the input however has a too low contrast now.

All of this we “got for free”. Just with a single line of CSS. But this design might not please you, so how can you change it, without using the media query? There are some special “functions” you can use.

The light-dark() function

Let’s take our source code from “The prefers-color-scheme media query” blog post to see how the color-scheme can be used to create a simple dark mode. This was our old code:

button {
	padding: 10px 20px;
	font-size: 2rem;
	background-color: #eee;
	border: 3px solid #333;
	border-radius: 10px;
	color: #333;
}
@media (prefers-color-scheme: dark) {
	button {
		background: #333;
		border-color: #333;
		color: #eee;
	}
}

Now, we take this code, and place every color into the new light-dark() function, that already has a good baseline support. We first set the light color and then the dark color. It is used like this:

button {
	padding: 10px 20px;
	font-size: 2rem;
	background-color: light-dark(#eee, #333);
	border: 3px solid light-dark(#333, #eee);
	border-radius: 10px;
	color: light-dark(#333, #eee);
}

Isn’t that a lot easier to read? When we view the page with a light color scheme preference, this is what we would see:

Chrome with open dev tools focused on a button. In the "Styles", the "light-dark()" function visualizes the two colors.

When you inspect the button, you can see the light-dark() function in the “Styles” and see/change the colors there. Now let’s view the same page with the dark color scheme preference:

Chrome with open dev tools focused on a button. In the "Styles", the "light-dark()" function visualizes the two colors. It simulates the dark mode.

That’s the basic usage of the color-scheme property and the light-dark() function. But the property can have different values.

Switching to a dark mode by default

If you want to use the dark mode as a default for your website and not use a light mode at all, just set the color-scheme like this:

:root {
	color-scheme: dark;
}

If you want to have a light mode by default, do the opposite, and use light as the only value. As soon as you have both values light and dark, the order is not important, you will have the behavior shown above.

Preventing the dark mode

As shown in the “Simulate preferences in the browser” blog post, Chrome has an option for an “Automatic dark mode”. This will use a dark mode, even if the color-scheme is set to light. But you can prevent a dark mode, by adding the only keyword as well:

:root {
	color-scheme: only light;
}

Now the browser will not use a dark mode. And sure enough, you can also use “only dark” to get the opposite.

Setting the property for some elements only

Some website designs have that use “different color schemes” for different element. Like a website that has a dark header, but a light footer. You might want to keep those colors, even if the modes can be switched. Then you can restrict the color-scheme for some elements, but allow both for other parts:

:root {
	color-scheme: light dark;
}
header {
	color-scheme: only dark;
}
.left {
	color-scheme: light;
}
.middle {
	color-scheme: light dark;
}
.right {
	color-scheme: dark;
}
footer {
	color-scheme: only light;
}

In this code snippet, we set both color schemes for the root element, but then change it for specific parts of the page. The header and footer are limited to just one color scheme. In the main section, we have three parts. The left and right only take one scheme, the one in the middle also takes two (we could leave property for the middle one out, as it would inherit the scheme from the root element, but for completeness, I left it here).

I’ve placed our button into each of these sections. Let’s see how this looks in Chrome with the different color scheme simulations:

With “prefers-color-scheme: light”

Chrome with "prefers-color-scheme: light" showing a dark button in the header and on the right, and light buttons on the left, in the middle and in the footer.

We can see here that our “color-scheme: dark” on the “.right” container has turned the button inside to the dark color-scheme.

With “prefers-color-scheme: dark”

Chrome with "prefers-color-scheme: dark" showing a dark button in the header, in the middle and on the right, and light buttons on the left and in the footer.

Switching to dark color scheme, the button in the middle has changed, but the other ones stayed the same. We also have a black background of the main element now, but the background colors of the header and footer stayed the same.

With “Automatic dark mode”

Chrome with "Automatic dark mode" showing a dark button in the header, in the middle and on the right, and a light button in the footer. The button on the left has a dark mode with a different border-color (darker).

Our header and footer still stay the same. But now also the left button has turned into a dark mode, since this part is not using the “only” keyword. The border color however is darker than in our defined CSS design.

Styling your dark mode even further

Since the light-dark() function can only be used for colors, you may still have to use the prefers-color-scheme media query to change other styles in your dark mode.

If you want to be even more creative, and learn deeply about dark mode option, I can recommend the “Dark Mode in CSS Guide” CSS-Trick blog posts, as well as the two pages on the color-scheme property and on the light-dark() function.

Conclusion

Adding a dark mode to your page and testing it has never been easier than with the methods described in the past blog posts. So maybe in those dark winter days, you can also adapt with the design of your website and try to create some nice dark color scheme.