In the third part of this series I described how to squeeze more performance out of your site by optimizing the static media files. Today I'll talk about putting the finishing touches which make your site more polished and professional. They mostly have to do with how your site interacts with search engines and social networks but there's also a little bit of performance to be found in there. I think information about polishing your site is the best way to send off this year.

Site building calisthenics

If you've implemented all recommendations thus far you're about 90% of the way to a site that performs well, ranks well and is easy to maintain. This section is about how to eke out that extra performance without going to silly lengths.

What are "silly lengths" I hear you asking wearily, having seen that some of the recommendations here are not quite what we'd call straightforward. Well, there are people who are used to working on massive sites, at least in terms of volume of page views served, who will recommend actions that make sense in the context of the sites they build but not in the context of the medium to small (realistically, extremely small) sites that most of the people reading this article tend to build. Here are some of the recommendations I've seen and which make sense in some contexts only but are presented as an axiomatic One True Way:

  • Remove all whitespace by using template overrides which use PHP string concatenations for all HTML. Most of you think this is a joke. It does make sense when you're serving hundreds of thousands of page views every day over a public cloud that charges you by volume of data served. Turning a 300KB page into a 299KB page has an appreciable financial impact when it's times three hundred thousand page views a day. It doesn't make sense at all when your site has a few hundred to low thousand page views per day. More so considering what you lose: you have no syntax highlighting, it's notoriously hard to understand what the heck is going on and good luck finding that missing closing DIV that screws up your template.
  • Using "minimal" CSS frameworks and overriding the output of every single extension you install on your site. This makes sense in large sites with a large budget since it not only reduces the total amount of CSS you serve but it ensures absolute consistency of everything displayed on the site. It is also notoriously hard to implement because you are trying to override EVERYTHING first- and third-party and in many cases you need to "hack core" (modify some extensions' PHP files) to change or remove hard-coded CSS classes or hard-coded HTML. Implementation is a mountain to climb and takes forever. Updates are a nightmare. You only do that if you absolutely have to. Apart from the biggest sites, which wouldn't be using Joomla anyway, I don't know anyone else for whom it would make business sense to do that.
  • Override all extensions on the site to not load any CSS and JS files, using only combined, minified and pre-compressed files you've deployed with the site. Yeah, well, as above. It's a titanic task that you don't even consider unless there's an absolutely good reason for it.
  • Progressive web applications (PWA). The main benefit of a PWA is that your content is available off-line. The downside is that it gets substantially more complicated building a site like that, especially when you venture outside core content. For some sites it does make sense. For example, if you're building an education resource implementing it as a PWA may be doing God's work since you allow a student to lurk outside an establishment with an open WiFi network, essentially download the material they are interested in and go back home to study without degrading their experience. For most sites, however, it doesn't make sense or is an actively asshole thing to do. For example, I don't want your brand's site to loiter in my browser cache forever, especially if I have a low end device with limited storage I could put to much better use. So, you know, if you decide to go the PWA way make sure there's actually a need for it instead of "it's the new hot thing and all the cool kids are doing it".

Instead of elaborate and often unrealistic suggestions like that I'll focus on the simpler stuff that you are far more likely to use on the Joomla sites you build.

PS: Please don't write angry comments about how all these techniques make sense on every site because, you know, they don't. You may have the privilege of working for a few, select clients every year that pay well and have the time for you to implement these elaborate techniques, hand over the site and not look back. That's your privilege, not the reality most people face. Most of the people out there work on tight deadlines, even tighter budgets, daily shifting goals and do the grunt work of maintaining sites day in and day out.

OpenGraph and Twitter Cards

If you've ever typed a URL into a social network such as Facebook (and its other web properties such as Instagram, Messenger and WhatsApp) or Twitter you might have seen something interesting happening. You get some sort of a card with an image and a short description of the content of the page. You may even get links to the content author or publisher on that social network. How does that magic work? The answer, my friend, is NOT blowing in the wind, it's meta tags. Namely, the OpenGraph and Twitter Card meta tags.

There are two ways to go about it: use a third party extension; or edit your template and make template overrides.

The first way is arguably much simpler. You will first need to use the Phoca Site Plugin to add the necessary OpenGraph namespace to your HTML element. Edit the site's options and set the “HTML XMLNS Attributes” option to:


Then use the Phoca Open Graph Content Plugin to set up your OpenGraph and Twitter Card tags for your content. Please note that there are two plugins, one called Phoca Open Graph Plugin (that's the one you need) and Phoca Open Graph System Plugin.

The downside to using plugins is that, well, it's more code that needs to be loaded on your site and you don't really need them if you can edit your template. The first change is obviously changing the root HTML element of your template to include the namespace for OpenGraph, e.g.:

<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>" prefix="og:">

The astute reader may notice that we're using prefix instead of an XML namespace. This is the recommended way that works on newer browsers, i.e. Internet Explorer 10 and later.

Then you need to create template overrides for your categories and articles display which include the necessary OpenGraph and Twitter Card tags. For example, an article template override (templates/MY_TEMPLATE/html/com_content/article/default.php) might read something like:

$imagesRegistry = new \Joomla\Registry\Registry($this->item->images ?? '{}');
$imageIntro = $imagesRegistry->get('image_intro', null);
$imageFull = $imagesRegistry->get('image_fulltext', $imageIntro);
$canonicalURL = Route::_(ContentHelperRoute::getArticleRoute($this->item->id), true, Route::TLS_IGNORE, true);

$doc->setMetaData('og:type', 'blog');
$doc->setMetaData('og:title', $this->item->title);
$doc->setMetaData('og:description', $doc->getDescription());
$doc->setMetaData('og:site_name', $app->get('sitename'));
$doc->setMetaData('og:url', $canonicalURL);
$doc->setMetaData('og:image', $imageFull);
$doc->setMetaData('twitter:card', 'summary_large_image');
$doc->setMetaData('twitter:site', '@your_twitter_handle');
$doc->setMetaData('twitter:creator', '@your_twitter_handle');
$doc->setMetaData('twitter:description', $doc->getDescription());
$doc->setMetaData('twitter:title', $this->item->title);

A category blog template override would likewise need code similar to the following:

$canonicalURL = Route::_(ContentHelperRoute::getCategoryRoute($category->id), true, Route::TLS_IGNORE, true);

$doc->setMetaData('og:type', 'blog');
$doc->setMetaData('og:title', $this->params->get('page_title', $category->title));
$doc->setMetaData('og:description', $doc->getDescription());
$doc->setMetaData('og:site_name', $app->get('sitename'));
$doc->setMetaData('og:url', $canonicalURL);
$doc->setMetaData('og:image', $category->params->get('image'));
$doc->setMetaData('twitter:card', 'summary_large_image');
$doc->setMetaData('twitter:site', '@your_twitter_handle');
$doc->setMetaData('twitter:creator', '@your_twitter_handle');

Using template overrides obviously works best if you are already intent on making your own template, therefore creating copious amounts of template overrides anyway. If unsure, go with the third plugins method.

DNS prefetch and external resource preloading

Sometime earlier in the series I said that browsers don't magically know which files they need to request when they are loading the page, they start loading files as they figure out they need them or you pushed them to the browser with HTTP/2 Push. I lied somewhat. You can give some hints to the browser so it starts preparing things while it is still downloading the first few bytes of your HTML content using the DNS prefetch and preload link tags. But I think I'm getting ahead of myself.

DNS prefetch

When I described how a browser retrieves CSS, JavaScript and image files necessary to render the page I used a simplistic model where the browser sends a request to "your server". The unspoken assumption was that "your server" refers to the same server and domain name as the one it is loading the HTML content from. On most Joomla sites this is indeed the case. However, your site may be using an external CDN to host some media files, include CSS from Google Fonts or a similar resource, load JavaScript from an external service and so on and so forth. These are all resources hosted on a domain name different to the one hosting your site and delivering its HTML content. Your browser can't magically connect to these external domain names; domain names need to be resolved to an IP address first by means of a DNS query.

DNS queries can be slow, especially on high latency connections such as satellite, cellular, or just shoddy shared WiFi. This adds a delay to loading that resource which can vary from annoying (large layout shifts) to downright problematic (a non-deferred, non-asynchronous JavaScript inclusion is causing the page to stall loading).

Using DNS prefetching you can give a heads up to the browser, essentially telling it that it will need to resolve these domain names in the course of rendering the page. The browser can do the DNS queries while it's twiddling its thumbs waiting for a blocking JavaScript or CSS file to finally load, saving the user some time in the later parts of rendering the page.

Another good reason to provide the DNS prefetch hint is when you know that a user interaction will cause a resource to be loaded from an external site. For example, if you know that clicking a Like button will cause a connection to a social network's domain name it makes sense to tell the browser to resolve the domain name ahead of time to make that button appear more responsive to the user's interaction.

There are two ways to do that: using a third party extension or by editing your template.

You can (ab)use the HeadTag plugin by Michael Richey to insert the necessary meta tags to your site. Use the User Group Tags tab, add a rule for the Public user group, since this is the user group that applies to both guests and logged in users, and select the Custom Tag type. The “Script/Style Declaration or Custom Tag” to enter is one or more DNS prefetch lines. For example:

<link rel="dns-prefetch" href="">
<link rel="dns-prefetch" href="">
<link rel="dns-prefetch" href="">
<link rel="dns-prefetch" href="">

Note that the href attribute is the root of the domain. There's no path.

If you are modifying your template you can use PHP code like the following before you start outputting the HEAD element in the HTML document:

/** @var \Joomla\CMS\Application\SiteApplication $app */
$app = JFactory::getApplication();
/** @var \Joomla\CMS\Document\HtmlDocument $doc */
$doc = $app->getDocument();
$doc->addCustomTag('<link rel="dns-prefetch" href=""> ');
$doc->addCustomTag('<link rel="dns-prefetch" href=""> ');
$doc->addCustomTag('<link rel="dns-prefetch" href=""> ');
$doc->addCustomTag('<link rel="dns-prefetch" href=""> ');


Having the browser do the DNS query ahead of time is great. If you, however, know that the browser will most definitely need to connect to the external site to pull resource used to render the page – such as CSS, JavaScript, images or font files – it makes even more sense to tell the browser to open an HTTP connection to the external server and keep it at the ready.

This works the exact same way as the DNS prefetch except that instead of rel="dns-prefetch" you use rel="preconnect". The practical use case I've found for this is Google Fonts where you definitely know that the browser will need to connect to to retrieve the actual font file from. I am also planning on using it on our business site's video page to initiate a connection to the CDN where the video and banner frame image are retrieved from, reducing the time it takes to start playing the video by just a smidge.

Resource preloading

In some cases the file(s) that need to be loaded are not immediately obvious to the browser by just parsing the HTML of your page. Some resources may be referenced from a CSS or JavaScript file. When that happens the browser is, like, "oh great, let me add yet another file to my to-do list". This can have an adverse effect to your site's performance because that file may be important e.g. a font file or image that will cause a layout shift, or an externally hosted JSON or JavaScript file which will only become apparent it's necessary when the browser finishes parsing and executing another piece of JavaScript. The latter is very common when including JavaScript code from third party services.

Again, you can give a heads up to your browser by telling it that it will need to eventually retrieve such and such file. By giving it the hint very early in the HEAD section of the HTML document the browser can optimise the retrieval of that file i.e. start downloading it in the background while its main thread is busy waiting for and parsing any blocking CSS and JavaScript on the page. 

Just like DNS prefetching, resource preloading works by putting special LINK elements in the HEAD of the site.

If you're using a plugin to do that, the LINK elements look like this:

<link rel="preload" href="" as="font" crossorigin="anonymous">

Likewise, when editing your template's index.php file it will look like this:

$doc->addCustomTag(sprintf('<link rel="preload" href="/%smedia/jui/fonts/IcoMoon.woff" as="font" crossorigin="anonymous">',Joomla\CMS\Uri\Uri::root(true)));

It's important to note that unlike the DNS prefetch you are giving a full URL to a file in the href attribute. Moreover, the as attribute is mandatory and tells the browser the usage intent of that file e.g. font, image, video etc. The crossorigin attribute is essentially mandatory for modern browsers and instructs them how to handle request authentiaction. Setting it to anonymous sends no cookies, HTTP authentication or client-side SSL certificates and is always allowed. Setting it to use-credentials will pass cookies, HTTP authentication or client-side SSL certificates but is subject to the cross origin resource policy of the site which may effectively prevent the request from going through, nullifying your effort at hinting the browser.

Swap fonts

This is an important feature when you are using custom fonts. By default, the browser will try to guess whether it should display any text at all while your custom font has not yet been loaded and parsed. If your CSS font-face does not include a fallback system font the default behaviour is "block" which means that your browser will not display any text at all before it retrieves the custom font you specified. Ever visited a site which displayed sod all text forever and you were frustrated staring at a mostly white page with a few image elements here and there until poof! all text was suddenly there? Yeah, that's why.

Luckily, you can tell the browser to do something different using the font-display CSS attribute. Setting it to swap means that the browser will be impatient and fall back to the default font while it's waiting for the custom font to download. When the custom font downloads, even if it's after several minutes, it will re-render the page with the new font. If you set it to fallback the browser will be likewise impatient but if the custom font doesn't load within seconds it will stick with the fallback system font and ignore the custom font altogether.

If you are using Google Fonts it is enough to replace /css? with /css2? in the URL and appending &display=swap. For example, loading Noto Sans becomes:

<link href="" rel="stylesheet"> 

The best thing to do is to set up good fallback fonts and set the font-display to swap. For icon fonts you should use block so what renders is a blank space while the icon font is loading.

Speaking of font fallbacks, you should include system fonts as your fallbacks. For example, sans serif fonts can have a fallback of

BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", Helvetica, Arial, sans-serif

which covers newer and older Apple devices, Windows 7 to 10, Android and legacy systems, preferring the default user interface sans serif font on each platform. The idea is that the user will see a familiar font instead of the typically uglier default sans-serif fallback. On Apple devices, for example, this defaults to the San Francisco system font instead of the Helvetica font.

Site icons (favicons)

Back in the old days when the dinosaurs roamed the earth and 800x600 pixels was considered "high resolution" –that would be around 2001– there were 16x16 pixel favicons, originally intended to present a site logo in the user's bookmarked favourite sites. Fast forward two decades and these icons are no longer 16 pixel square or limited to bookmarks.

Favicons are of course still used for bookmarks and in site tabs, just like they did 20 years ago. They are also used to display touch icons on smartphones and tablets when you create a shortcut for the site in your home screen. Moreover, they are used by third party services to pull a logo for your site. If you didn't know about this use check out this third party CodePen.

To make matters more complicated, there are several sizes necessitated by different generations of smartphones, tablets, browsers and operating systems. You can find a great reference for all of these at

If we were to come up with a shortlist that supports every device, browser and operating system from 2013 to 2020 inclusive we'd come up with the following:

  • An ICO file containing 16x16, 24x24, 32x32, 48x48 and 64x64 pixels.
  • Seven PNG files with the sizes 32x32, 128x128, 152x152, 167x167, 180x180, 192x192 and 196x196 pixels.
  • An 144x144 pixel PNG and a background colour for the Windows Modern UI site tiles which are deprecated but not removed yet (and very much used in Windows 7 which people still use for whatever reason).

Adding them to Joomla requires editing the index.php file of the template and putting this before outputting the HEAD of the HTML document. Assuming that your files are stored in images/logos/favicon we have the following code:

$doc = JFactory::getApplication()->getDocument();
$baseUrl = JUri::base();

// Good old 16px square favicon
$doc->addFavicon($baseUrl . 'images/logos/favicon/favicon-16.ico');
// Default fallback: 152x152 pixels
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-152.png', 'apple-touch-icon-precomposed', 'rel');
// Windows Modern UI tile
$doc->setMetaData('msapplication-TileColor', '#d43431');
$doc->setMetaData('msapplication-TileImage', $baseUrl . 'images/logos/favicon/favicon-144.png');
// All other sizes
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-196.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "196x196"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-192.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "192x192"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-180.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "180x180"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-167.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "167x167"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-152.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "152x152"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-128.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "128x128"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-32.png', 'icon', 'rel', ['sizes' => "32x32"]);

The obvious problem is that you need to remember to make all these changes which can be impractical. You can automate this somewhat by uploading only two files in your images/logos/favicon folder:

  • favicon.ico which you still need to generate yourself but can get away by only including the legacy 16x16px icon
  • favicon.png a 512x512 pixel transparent PNG with the high quality version of your logo

You can then use this substantially more complicated code to automatically generate all necessary sizes:

* Generate favicons.
* You need to upload two files to the $basePath folder, by default images/logos/favicon :
* - favicon.ico which includes 16x16, 24x24, 32x32, 48x48 and 64x64 pixel images.
* - favicon.png a 512x512 pixel transparent PNG
* @param string $basePath Path with your favicons relative to your site's root
* @param int[] $favIconSizes List of sizes to generate e.g. [32, 57, 152], see
* @param string|null $tileColor HTML hex color for the IE Modern UI tile icon, null to skip generating it
* @param int $defaultSize Fallback favicon size when the browser doesn't request a specific dimension
$faviconGenerator = function (string $basePath, array $favIconSizes, ?string $tileColor, int $defaultSize = 152) use ($doc) {
$ensureSize = function (int $size) use ($basePath) {
$filePath = JPATH_SITE . '/' . $basePath . '/favicon-' . $size . '.png';

if (!file_exists($filePath))
(new \Joomla\CMS\Image\Image(dirname($filePath) . '/favicon.jpg'))
->resize($size, $size)
->toFile($filePath, IMAGETYPE_PNG, ['quality' => 9]);
catch (\Exception $e)

return basename($filePath);

$baseUrl = Uri::base(false) . $basePath . '/';

// Fallback .ICO file
$doc->addFavicon($baseUrl . 'favicon.ico?' . $doc->getMediaVersion());
// Default favicon
$doc->addHeadLink($baseUrl . $ensureSize($defaultSize) . '?' . $doc->getMediaVersion(), 'apple-touch-icon-precomposed', 'rel');
// Internet Explorer Modern UI (formerly Metro) tile icon, classic Edge
if (!is_null($tileColor))
$doc->setMetaData('msapplication-TileColor', $tileColor);
$doc->setMetaData('msapplication-TileImage', $baseUrl . $ensureSize(144) . '?' . $doc->getMediaVersion());
// All other favicons
foreach ($favIconSizes as $size)
$doc->addHeadLink($baseUrl . $ensureSize($size) . '?' . $doc->getMediaVersion(), 'apple-touch-icon-precomposed', 'rel', ['sizes' => "{$size}x{$size}"]);
$faviconGenerator('images/logos/favicon', [
// Favicons size reference:
32, // Default fallback for most desktop browsers.
// 57, // Deprecated: Standard iOS home screen (iPod Touch, iPhone first generation to 3G), old Android
// 72, // Deprecated: First- and second-generation iPad
// 76, // Deprecated: iPad home screen icon
// 96, // Deprecated: GoogleTV icon
// 114, // Deprecated: iPhone retina touch icon with iOS <= 6
// 120, // Deprecated: iPhone retina touch icon with iOS >= 7
128, // Chrome Web Store icon & Small Windows 8 Star Screen Icon
// 144, // Deprecated: IE10 Metro tile for pinned site, iPad Retina with iOS <=> 6
152, // iPad Retina with iOS >= 7
167, // iPad Retina with iOS >= 10 (in practice iOS will still use 152×152)
180, // iPhone Retina
192, // Google Developer Web App Manifest Recommendation
// 195, // Deprecated: Opera Speed Dial icon (Not working in Opera 15 and later)
196, // Chrome for Android home screen icon
// 228, // Deprecated: Opera Coast icon
], '#d43431');

This code may not yield the best results under all circumstances. Downsized logos may appear a bit blurry. Whenever possible generate the PNGs yourself from a vector source and manually tune the PNG files' size with pngcrush or a similar tool.

Analytics and cookie notices

Since you're browsing the web like the rest of us you must be already deeply frustrated by the cookie banners that litter every corner of the web. In most cases the only applicable cookies which are not mandatory are those used by analytics services such as Google Analytics. If your site needs to show a cookie banner just because you are using analytics stop and think: do you really need the features provided by Google Analytics or similar services?

In most cases you do not need those advanced features. If all you need is basic analytics about how many people visited your site, whether they came from organic search or a referral link and which country they come from you may have to look no further than your hosting control panel which most likely already offers AWstats or a similar solution. These work by parsing the web server access logs and require no cookies, therefore no cookie banners on your site.

Another solution is to use a self hosted analytics service such as Matomo (formerly called Piwik). It offers most of the features you'd get with Google Analytics but since it's a first party solution that's GDPR and CCPA compliant you don't even need to show a cookie banner. It can even import data from Google Analytics so you have continuity of your data if that's important to your business use case.

By removing the need for a cookie banner you make your site easier to navigate and far less frustrating to your users. Knowing that you can do that without sacrificing the data you need to make business decisions and without forcing your users to share their data with a third party that makes money off selling personalised ads (Google is primarily an ad network, if you hadn't already noticed) is empowering.

Dark Mode

Most likely your site's design only comes in one colour scheme and chances are it is a "light" theme, i.e. a bright background with dark text. This has been the case for most web sites the past two decades and is consistent with what most operating systems used to only offer as an option.

For some users this colour scheme is inaccessible and using inverted screen colours is simply not a very good solution; all colours are inverted, including illustrations and images, making for a jarring experience. Also offering a dark colour scheme for your site will make it more accessible. It will also make your site more appealing to people who just prefer dark colour schemes over light ones – a good point to consider if your site's target audience is geeks and gamers.

I have already written about what is Dark Mode, why and how to use it and I have recently delivered a presentation on the subject of Dark Mode. I will simply link to these resource instead of repeating myself in what is already a really long article.


I started working on this article series back in July 2020 and I intended on recommending AMP (Accelerated Mobile Pages) for your site. I am not so sure anymore.

If you didn't already know, AMP is pretty much a subset of HTML with some quirks that make it faster to load on mobile devices. It is much more restrictive than a regular responsive site. The appeal of the AMP standard is that Google will prioritise AMP content in search result carousels when a user makes a search using Google's application on a smartphone.

The downside is that AMP is very much Google-specific and unsupported outside the Google ecosystem. Moreover, people clicking on search results pointing to your site's AMP pages will see in their browser address bar instead of your site. I feel that these are major disadvantages which isolate you from your audience.

The fact that a member of the AMP Advisory Committee recently quit because he felt that Google had pretty much hijacked the entire AMP project for its own gain and that the Texas Attorney General has filed an anti-trust lawsuit against Google which would end AMP's preferential treatment in the search carousel make me wonder if there is any point in introducing AMP to your site. From my experience, the integration is all but straightforward if you want to create your own custom template for your AMP pages.

Finally, based on the analytics I had collected on my own site, AMP doesn't really bring in as much traffic as it's supposed to. Most of my traffic comes from direct links  from other places, organic search leading to my regular responsive template and social media. AMP was barely a blip in the radar of traffic sources.

So, instead of telling you how to integrate AMP on your site I will tell you that you probably shouldn't. If you are, however, hell bent on implementing it there are solutions for Joomla. I have used wbAMP but I have to warn you that creating a custom template is quite complicated. I am inclined towards removing AMP from my site in a future iteration – very likely when I redo my site in Joomla 4.0 once that version of Joomla is officially released.

To be continued

Come tomorrow for a New Year's special and last part of this series, Part V: Content Quality.

No comments