Skip to main content

Sitemap

The sitemap is generated dynamically through the Fat-Free Framework and includes xhtml:links for alternate language information.

To make the sitemap more readable, I include an XSL stylesheet based on one from GitHub.

I provide alternate language (hreflang) information only in the sitemap. Previously, I had also included hreflang links in the page head sections, but the major search engines, Google, Yendex, and Bing support hreflang links in the sitemap, so there is no point in using both (wastes bandwidth to include the links on every page). The Chinese search engine, Baidu, focuses on Chinese websites (sites where the primary language for the domain is Chinese, not just sub-folders), so they’re not a concern. The value of a Chinese hreflang is so the other search engines know there is a Chinese site available (i.e. there are pages with the lang attribute on the html tag set to zh-CN).

x-default – I haven’t designated one page as the x-default version as x-default appears to be used to designate a page with multiple languages or a language selection page.

One complication for creating XML files is some servers are configured to use short tags – <? in place of <?php. This causes the server to misinterpret the first part of an <?xml tag as being for PHP rather than XML. Some servers will accept the command, php_flag short_open_tag on, in the .htaccess file, and others (like this site’s server) want short_open_tag = Off added to a Custom php.ini section in the Environment section.

Fonts

I use system fonts for displaying most of the text (I use a system monospaced font for code samples). System fonts are the fonts the operating system uses for displaying text, so they work well with the device and users are generally comfortable with the font. System fonts are specified by using the system-ui font-family value. The main advantage for using system fonts is the fonts are already present on the device, so there is no waiting for an external font to download.

San Serif font

body {
    font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
Font System
system-ui Default system font
Segoe UI Windows
Roboto Android
Helvetica Older Apple versions
Arial Older Windows
sans-serif generic fallback

Monospaced font

body {
    font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
        "Roboto Mono", "Courier New", monospace;
}
Font System
ui-monospace Default system font (Apple only)
Menlo Older Apple devices
Monaco Older Apple devices
"Cascadia Mono" Newer Windows
"Segoe UI Mono" Windows
"Roboto Mono" Android
"Courier New" Older Windows
monospace generic fallback

Google Noto fonts

I previously used Google fonts, but as an EU court (see below for more information) has ruled against sharing users’ IP addresses with third-parties (such as Google fonts), I stopped.

It is possible to self-host fonts from Google. Fonts, such as the Noto family of fonts, which includes fonts for Chinese, Japanese, and Korean (CJK) characters, are available under a SIL International Open Font License. The problem is the stylesheets. Many of the fonts have been subsetted (broken into a number of files, referenced by character Unicode values). The separate font files and the stylesheets with the Unicode-range make it possible for a browser to download just the font files needed for a webpage. Those font files and stylesheets are only available from Google’s font API (they are not included under the Open Font License).

Social networking

Social networking sites and apps check how a page should be referenced if someone shares a link to the page.

Open Graph

Most social networking apps support Open Graph:

  • og:type – website
  • og:title – page title with the company name
  • og:description – description of the page content
  • og:url – URL for the current page
  • og:site_name – the name of the site
  • og:image – image when displaying a link to this page (keeps messaging apps from grabbing the logo to use as the representative image for the site) – recommended size is 1200px by 630px, but should look good when scaled down to 250px by 128px
  • og:image – second image, 400px by 400px, for applications that prefer a square image

Twitter Card

Twitter has their own set of tags:

  • twitter:card – defines the type of card (Twitter has four types, Summary, Summary with large image, Player card, and App card – Summary is the best type for a business website)
  • twitter:description – description of the page content
  • twitter:title – title for the page
  • twitter:image – Twitter includes a 144x144 image when displaying a tweet that references the page
  • twitter:site – Twitter address for the company (@twittername) – this is optional

Note: I’m using the page title and description variables ({{ @siteOwner }} – {{ @pageTitle }} & {{ @pageDescription }}) for the social network / twitter title and description. If a different title and/or description is desired, it would be easy to add separate variables to the page controller.

Scroll-to-top button

I provide a button in the lower-right portion of the page that is displayed after the page has scrolled past the header section. If the button is clicked or tapped, it will scroll the page back to the top.

After the page scrolls back to the top, focus is applied to the page using JavaScript, which enables a keyboard user to navigate the page from the beginning.

doc.documentElement.tabIndex = 0;
doc.documentElement.focus();

I include the tabIndex line for the documentElement as it’s needed for Chrome. Firefox doesn’t require the extra step, but it doesn’t hurt.

Prefers reduced motion

Prefers reduced motion is a system setting (for Windows, it is the Show animations in Windows setting and for Mac, it is the Reduce Motion setting) to aid people who are afflicted with vestibular disorders and could experience problems with animated content. When reduced motion is enabled, CSS transition/animation effects are disabled or overridden.

Dark mode

More than 80% of smartphone users, according to one website, use dark mode, and these users expect websites to have a dark theme. While 80% seems high, it is clear that sites do need to consider having two modes, light and dark.

I’ve implemented dark mode by using CSS custom properties (variables). CSS custom properties wasn’t an option before as Internet Explorer did not support them. Now that Microsoft is no longer supporting Internet Explorer, there is less of a need to fully support the older browser. For more details on how I’m implementing dark mode, please see the Dark mode section on the Bootstrap 5 details page.

Dark mode design

I've based my dark mode design on Google’s Material Design concept — starting from a dark gray background (#121212) and use increasingly lighter shades of gray for secondary surfaces (e.g. dropdown menus). While Material Design suggests pure grays, I’ve gone with a more steel-blue color (#16191c).

Color scheme

I include a meta element in the head that specifies the color scheme that tells the browser how to style scrollbars to match the page display.

<meta name="color-scheme" content="light dark">

Dark mode switch

A number of websites (including Bootstrap) provide a switch for users to switch between light and dark modes (with the page using local storage to remember the user’s selection for their next visit). I don’t think a switch is needed unless a page has the potential for users to visit multiple times and the users need to interact with the page (not just read information). The user has already made their preference for light or dark mode known, through the setting in the OS.

Dark mode favicon

Favicons are important for a website as they’re used for identifying the site on the:

  • Page tab
  • Favorites menubar & dropdown
  • Address bar

If there are more than ten or fifteen tabs open in a browser, the title in the tab becomes shorter, making the favicon the primary way to visually locate a tab.

In light mode, the color for tabs (active, idle, or hover) and for bookmark lists runs from white (#ffffff) to a light-gray (#d6d6d6). In dark mode, the colors run from #444547 to #1c1b22. Designing a favicon that works well against light and dark backgrounds and matches with the site branding is not easy.

The original favicon for this site was a person wearing a hardhat (in keeping with the construction theme). The hardhat was black and the person was yellow with a black outline. This works with light backgrounds, but isn’t as good against dark backgrounds

Original favicon on different backgrounds

To have the icon more visible against light and dark backgrounds, I changed the outline from black (#000000) to gray (#808080) in the favicon.ico file.

Favicon using a gray outline for better visibility against dark backgrounds

For browsers that use an SVG favicon file (i.e., modern browsers except Safari), I provide a favicon.svg file that has two versions, one with a black outline and one with a white outline for light and dark mode.

SVG favicon showing better visibility against light and dark backgrounds

Touch icons

I include touch icons for Apple & Android mobile devices. While most users will likely not save the site to their home screen, I like having the option available (especially for company employees if it’s a business website).

Example of a touch icon for use on a mobile device

Images

I use a combination of WebP and, to reduce number of server requests, inline SVG and Base64 encoded PNG images. I do provide JPEG and PNG files as backup images for browsers that do not support WebP formats.

For creating images, I use GIMP, a free & open source image editor. When saving images in WebP format, I use lossless, 100% quality for graphic images, lossy, 80% quality for photographs to be displayed at standard resolution, and lossy, 50% quality for photographs to be displayed at high resolution (2X or greater pixel density).

I use a picture element that specifies the image file to use for light or dark mode and for standard and high resolutions.

<picture>
    <source srcset="../images/home-card-learn-dark.webp,
        ../images/home-card-learn-lg-dark.webp 2x"
        type="image/webp" media="(prefers-color-scheme: dark)">
    <source srcset="../images/home-card-learn.webp,
        ../images/home-card-learn-lg.webp 2x" type="image/webp">
    <img src="../images/home-card-learn.png"
        alt="Learning – computer generated graphic of three textbooks on programming"
        width="180" height="180" loading="lazy">
</picture>

SVG images are included either in the HTML file or in the CSS file. Some image files are included in the CSS file as Base64 files (e.g. the flag icons for the language selector). Embedding the image files makes them available as soon as the page is loaded.

Images that normally do not show when the page is first loaded have the loading attribute set to lazy.

Hero image

Having a large image (a hero image) is a popular front page design.

The problems with a hero image is the file size is large (hero images usually take up the most of the browser window) and it takes a while for the browser to recognize it needs an image and then to request the image from the server. While the image is being downloaded, the browser displays an empty space (just the background color). Even worse, if the image is a CSS background image (which seems to be the most common method for hero images) rather than an embedded image, browsers will request it later in the load cycle, increasing the time the empty space is shown.

To reduce the delay for showing an image:

  • Preload the image:
    • Use a preload link in the head section to download the appropriate image.
    • I preload the hero and logo images to avoid any delay in their being displayed.
  • Reduced the image file size:
    • WebP file types – WebP are smaller than PNG and JPEG images (AVIF files are even smaller, but Microsoft Edge did not, when I created this site in 2023, support the file type. Edge now supports AVIF files: Edge Version 121.0.2277.49: January 8, 2024).
    • Provide different image sizes – use the picture element to source different image sizes and formats:
      • Provide different images for smartphones, tablets, and laptop / monitors.
      • Provide different sizes based on generally available display sizes.
        • There is a tool, RespImageLint – Linter for Responsive Images that can be used to evaluate the images on a webpage, although you do need to use your own judgement as to what recommendations you should follow.

preload in the head gives the browser the available image files from which to select the file to load:

<link rel="preload" as="image" imagesrcset="
    ../images/Site-hero-360x480-80.webp 360w, 
    ../images/Site-hero-390x520-80.webp 390w, 
    ../images/Site-hero-720x960-50.webp 720w, 
    ../images/Site-hero-780x1040-50.webp 780w"
    imagesizes="100vw" media="(max-width: 575.98px)">

<link rel="preload" as="image" imagesrcset="
    ../images/Site-hero-768x614-80.webp 768w, 
    ../images/Site-hero-1024x819-80.webp 1024w, 
    ../images/Site-hero-1536x1229-50.webp 1536w, 
    ../images/Site-hero-2048x1638-50.webp 2048w"
    imagesizes="100vw" media="(min-width: 576px) and (max-width: 1199.98px)">

<link rel="preload" as="image" imagesrcset="
    ../images/Site-hero-1349x691-80.webp 1349w, 
    ../images/Site-hero-1423x729-80.webp 1423w, 
    ../images/Site-hero-1583x811-80.webp 1583w, 
    ../images/Site-hero-1903x975-80.webp 1903w,
    ../images/Site-hero-2543x1303-80.webp 2543w"
    imagesizes="100vw" media="(min-width: 1200px)">

And srcset lists the same images for the browser to select which image to display:

<picture>
<!-- monitors - no hi-density -->
<source srcset="
    ../images/Site-hero-1349x691-80.webp 1349w,
    ../images/Site-hero-1423x729-80.webp 1423w,
    ../images/Site-hero-1583x811-80.webp 1583w,
    ../images/Site-hero-1903x975-80.webp 1903w,
    ../images/Site-hero-2543x1303-80.webp 2543w" 
    sizes="100vw" type="image/webp" media="(min-width: 1200px)"
    width="1349" height="691">

<!-- tablets - 1x and 2x -->
<source srcset="
    ../images/Site-hero-768x614-80.webp 768w,
    ../images/Site-hero-1024x819-80.webp 1024w,
    ../images/Site-hero-1536x1229-50.webp 1536w,
    ../images/Site-hero-2048x1638-50.webp 2048w" sizes="100vw" type="image/webp" 
    media="(min-width: 576px)" width="768" height="614">

<!-- smart phones -->
<source srcset="
    ../images/Site-hero-360x480-80.webp 360w,
    ../images/Site-hero-390x520-80.webp 390w,
    ../images/Site-hero-720x960-50.webp 720w,
    ../images/Site-hero-780x1040-50.webp 780w" sizes="100vw" type="image/webp" 
    width="360" height="432">
<img src="../images/Site-hero-1024x525.jpg"
    alt="Worker(s) erecting steel for a new building"
    loading="eager" decoding="sync">
</picture>

The largest supported display size is 2560 x 1440. On a larger monitor, such as a 3440 x 1440, the hero image's width will be fixed at 2543px. The max width of navbar and footer content is set by the max-width of the Bootstrap container – 1320px.

Note: The hero image used on the demo site is used under a Creative Commons license. The image used for the GitHub files is a graphic to emulate the image.

Hero image and modals

When Bootstrap displays a modal, it disables the scrollbars for the page by adding overflow: hidden and padding-right to the body. The amount of padding depends on the browser.

This additional space can cause an image set to width: 100% to expand. If the is-fixed class is added to the image, Bootstrap adds padding-right to the image, which stops the image from changing size.

Privacy and tracking users

The European Union’s General Data Protection Regulation (GDPR) defines how internet users’ personal data is to be handled by organizations that have European offices as well as non-European organizations that target European users for free or paid goods or services.

You should consult with your legal advisor on whether or not you need a cookie notice, a privacy statement, and/or a way for users to opt-out of tracking.

IP address privacy

In 2016, the Court of Justice of the European Union held that, in certain circumstances, IP addresses are personal data.

This was followed in early 2022 by a decision from a court in Munich that linking to a third-party without prior consent, such as to Google to use Google fonts, violated the EU privacy law.

Based on the prohibition against sharing a user’s IP address, I have:

  • switched from using Google fonts to using system fonts;
  • switched from sourcing framework and library files (i.e. Bootstrap and jQuery JavaScript) from content delivery networks (i.e. cdnjs) to self-hosting the files;
  • and switched to downloading the JavaScript file for Google Analytics only if the user has agreed to accept tracking cookies.

Google analytics

I use Google Analytics (along with Google Search Console) to understand how my sites are being used. Previously, to enable users to opt-out of their data being sent to Google, I used Google’s ga-disable cookie method, but that method still required the browser to download a JavaScript file from Google to read the cookie that said whether the user had opted out or not. The browser was still sending the user’s IP address to Google in order to get the JavaScript file.

I switched from using Google’s ga-disable cookie method to using either my own cookie or a sessionStorage value. When someone visits the site for the first time, they are presented with a cookie notice where they can either accept cookies (and allow the use of Google Analytics) or they can opt-out.

If the visitor accepts the use of cookies (and the use of Google Analytics), I create a cookie, valid for one year, to record their choice. After that, when they visit the site, the page reads the cookie and does not display the cookie notice.

If the visitor rejects the use of cookies (and the use of Google Analytics), I store a value in sessionStorage to record their choice. After that, when the visitor views any other site page during the current browser session (the browser and the tab remain open), the pages will read the sessionStorage value and not display the cookie notice. If the visitor returns to the site in a different session, they will again be shown the cookie notice.

To make this work, I use three JavaScript code components to manage user tracking. The first is in the head section of the page:

var gaScript;
var googleAnalyticsID = "UA-11155712-3";
gaScript = document.createElement("script");
gaScript.async = true;
gaScript.src = "https://www.googletagmanager.com/gtag/js?id=UA-11155712-3";

// Until Google Tagmanager has been loaded, this code will have no effect
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-11155712-3', { "anonymize_ip": true });

if (document.cookie.indexOf("eu-cookie-notice=true") > -1) {
    // User has accepted the terms, so load analytics (not opted out).
    document.head.appendChild(gaScript);
}

This code creates a script tag to load the Google Tag Manager JavaScript, and, if the user has previously accepted cookies, the script tag is appended to the page head. This code also sets up a dataLayer array for Google tag information. If the user has or does accept cookies, the tracking information will be sent to Google. If the user rejects tracking, the Google JavaScript will not be downloaded and the tracking information will never be sent.

The second component is in the js-functions file and handles the cookie notice:

// Show the EU cookie modal (if it's the first time to the site or a new session)
if (document.cookie.indexOf("eu-cookie-notice") < 0) { // no cookie
    if (window.sessionStorage.getItem("eu-opt-out") !== "true") { // no session item
        //Cookie not found and no current session, so show the modal
        const options = {
            "backdrop": "static",
            "keyboard": false
        };

        // Don't show the modal on the terms page
        // Don't show the modal on the terms page
        if (typeof termsPage === "undefined") {
            const euModal = new Modal(document.getElementById("euCookieModal"),
                options);
            euModal.show();
        }

        document.getElementById("btn-cookie-accept")
            .addEventListener("click", function() {
            let date = new Date();
            date.setTime(date.getTime() + 31536000000);
            document.cookie = "eu-cookie-notice=true;
                expires=" + date.toGMTString() + ";path=/";
            euModal.hide();
        }, {
            passive: true
        });

        document.getElementById("btn-cookie-opt-out")
            .addEventListener("click", function() {
            try {
                window.sessionStorage.setItem("eu-opt-out", "true");
            } catch (error) {
                console.error(error);
            }
            euModal.hide();
        }, {
            passive: true
        });
    }
}

This code checks whether a visitor is new to the site, and if so, shows a modal with information about the site using cookies and provides two buttons, one for the user to accept cookies and the other for the user to opt-out of tracking.

Accepting cookies stores a cookie that the user has accepted, hides the notice, and triggers Google tracking for this visit. The cookie is good for one year, after which the user will need to reaccept cookies.

Opting out of cookies stores a session variable that the user has opted out and hides the notice.

The third JavaScript component is on the Terms of Use page, which includes privacy information and its own allow and opt-out buttons with JavaScript code:

/* similar button functions as on the EU Cookie modal */
document.getElementById("btn-cookie-accept").addEventListener("click", function() {
    try {
        let date = new Date();
        date.setTime(date.getTime() + 31536000000);
        document.cookie = "eu-cookie-notice=true;
            expires=" + date.toGMTString() + ";path=/";
        // Just in case the user had opted out, but has changed their mind.
        window.sessionStorage.removeItem("eu-opt-out");
        alert("Thank you for allowing us to learn how this site is being used");
    } catch (error) {
        console.error(error);
    }
}, {passive: true});

document.getElementById("btn-cookie-opt-out").addEventListener("click", function() {
    try {
        window.sessionStorage.setItem("eu-opt-out", "true");
        // Just in case the user previously accepted, but now wants to opt out,
        // delete the cookie
        document.cookie = "eu-cookie-notice=;
            expires= Thu, 01 Jan 1970 00:00:00 GMT; path=/";
        alert("We will not track how you use our site.");
    } catch (error) {
        console.error(error);
    }
}, {passive: true});

The opt-out button stores a session variable that the user has opted out. If the cookie notice is visible, it is hidden, and if there was a cookie, it is removed.

Tracking (and the associated cookies) are not applied to Internet Explorer.

Email obfuscation

Including an email address on a webpage runs the risk that email spammers could harvest the address and start sending spam and phishing emails. To make it a little more difficult for spammers to harvest an email address, I obfuscate the address by:

  • Encoding the email address using HTML entities – while a browser will convert HTML entities to their text equivalent (e.g. &#103;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109; converts to gmail.com), a spammer would need to include the conversion step in their process.
  • Assembling the email address using JavaScript – the spammer would need to visit a page on my site, process the page’s JavaScript, and then look for an email address. This method won’t stop a spammer, but it will require more work on their part.

To include an email address in the pages, the address is defined in the config.ini file and then stored in Fat-Free’s hive as HTML entities in the Controller.php file:

From config.ini
; address for displaying on the footer and for messages from the contact form
contactAddress = "contact1@yourdomain.com"

From Controller.php
function loadEmail($f3) {
    $email = preg_split('/[\s,]+/', $f3->get('contactAddresses'))[0];
    $email = preg_split('/[@]+/',$email);
    $f3->set('emailName',mb_encode_numericentity($email[0],
        array(0x000000, 0x10ffff, 0, 0xffffff), 'UTF-8'));
    $f3->set('emailHost',mb_encode_numericentity($email[1],
        array(0x000000, 0x10ffff, 0, 0xffffff), 'UTF-8'));
}

The email information is added to the page using a <script> tag that uses document.write to add the code to the page.

<span>
    <script>
        var name = "{{ @emailName | raw }}";
        var host = "gmail.com";
        var linktext;
        var subject = "?subject=Inquiry%20via%20Website";
        linktext = name + "@" + host + subject;
        document.write('<a class="email break-all"
            title="{{ @dictFooterContent3c | raw }}"
            href="' + 'mai' + 'lto:' + linktext + '">' + name + "@" + host + '</a>');
    </script>
</span>

I use var rather than const so the email address will show in Internet Explorer.

Contact page

HTML form

The HTML form uses the post method to send contact requests to the server (to the same URL as the contact page). Each of the fields are set to required="required", so the PHP controller should receive a full set of data.

The contact page does validate the email address, but only to check that the address has a username, an asperand (the @ character) and a domain name. The browser does not check that the domain name includes both a top level domain (e.g. .com) and a domain name.

JavaScript code on the contact page listens for the form submit event and uses the fetch() function to submit the form data and process the resulting JSON data from the server.

A Bootstrap alert is used to display the status from the server in response to submitting the contact form (the alert is displayed using the Bootstrap collapse function).

PHP controller

The Fat-Free routes.ini file includes a POST contact route for each language. The routes connect to the UtilitiesController’s contactPost function, which checks for three potential error conditions. Based on the results of the checks, the message is either sent via email to the site owner or rejected back to the user. For suspicious messages (potential hacking attempts), the IP and email addresses are recorded in a log file on the server.

The checks are:

  • $inputFieldsValid – All four fields (name, email address, subject, and message) exist and have some content.
    • The browser shouldn’t omit a field nor submit an empty field.
    • If this occurs, the message will be rejected with the explanation: “We experienced an unknown problem with submitting your message. Please send your request using our email address below.”
    • The user’s submitted email and IP address will be recorded in a log file.
  • $validEmailAddress – The email address is valid.
    • Uses Fat-Free’s email audit function, which uses PHP’s FILTER_VALIDATE_EMAIL to verify the address is properly formed and PHP’s getmxrr function to verify the hostname is valid (the function does not check whether the recipient’s name is valid, just the host name).
    • If the email address is invalid, the message will be rejected with the explanation: “Our server is not able to process your email address as entered.”
    • The user’s email address (after sanitizing) and IP address will be logged.
  • $goodInputData – The name and subject do not contain malicious content (e.g. html tags).
    • The name and subject are checked for the text strings 'content-type', 'bcc:', 'to:', 'cc:', 'href', 'src=', and '<'. If any of the listed strings are found, the strings are sanitized, the email message is sent with a flag in the subject line, and the email and IP addresses are logged.
    • The body of the message is not checked, but it is sanitized using the PHP function htmlspecialchars before being sent.

Fat-Free SMTP

Contact messages are sent using Fat-Free’s SMTP class to send the contact information using tls (ssl wouldn't work with my host’s current setup). The PHP mail function could also be used to email a contact request.

Logging unusual contact requests

As noted above, anytime there is something unusual with a contact request, the email address and the user’s IP address (retrieved using $f3->get('IP') function) are saved to a log file so there is a record of possible issues.

Logging the IP address does not go against the Privacy Terms as the terms state a user’s info can be recorded if they contact us. I just can't share it with someone else.

Error response pages

I created custom error pages for 404 Page not found and 500 server errors that fit with the theme of the site.

And just for fun, the 404 page includes an animation while the error page includes scrolling text (as in “I’m busy checking the code at that very moment.”)

Syntax highlighter

I’m using highlight.js to highlight the code examples. Highlight.js’s download page builds a JavaScript file specific to the types of code to be highlighted, and includes all of the available CSS files in the download file (I went with the GitHub style with a few changes).

The class names for the code types that I included in my build are:

  • php
  • apache
  • http
  • ini
  • css
  • xml
  • javascript

I did convert the Highlight.js CSS file to Sass so I could include it in the Sass build for the site.

Language flags

The websites I work with need two or more languages (at least English and Korean), which means a language switcher, and I wanted an indication near the top of the page that other languages are available (instead of in the footer).

The easiest way to indicate the current language is to display a country flag (but be careful about implying that Taiwan or Hong Kong are countries). The source I use for flags is GoSquare’s Flag Icon Set GitHub site. GoSquare makes a complete set of flags available under the MIT license.

I compressed the PNG files and converted them to Base64 so the images can be embedded in the CSS file as background images.

Internet Explorer support

There are people who still use Microsoft Internet Explorer. While Microsoft has started permanently disabling Internet Explorer on some versions of Windows 10, globally there are more than 45 million desktop systems running Windows 7 or Windows XP that may be using Internet Explorer.

While Bootstrap 5 does not support Internet Explorer, it is possible to provide some functionality:

  • IE Notice – at a minimum, let users know that the site does not support IE.
Our site does not support Microsoft Internet Explorer. We apologize for the inconvenience.
  • Add IE only CSS – I add a separate CSS file for IE that:
    • Shows content that is specific to IE (e.g. the IE notice)
    • Hides content that is not necessary or doesn’t work on IE
    • Adds or changes styles to work with IE
  • Add IE only JavaScript – I add a separate JS file, js/ie_overrides.js for IE that enables dropdowns to work for IE11.

Microsoft Edge’s Internet Explorer mode can be used to check pages.

Firefox flash of unstyled content

I use the Firefox browser as I like its separate search bar. Firefox was showing a flash of unstyled content (FOUC) if I loaded the page with an empty cache. To fix the problem, I applied the hack from this Stackoverflow answer at the end of the head section to resolve the issue:

<script>
    /*to prevent Firefox FOUC*/
    var FF_FOUC_FIX;
</script>