Site details

Things that make the site better:

  • Sitemap generation
  • Fonts
  • Social networking support
  • Scroll to anchor
  • Scroll-to-top button
  • Prefers reduced motion
  • Dark mode
  • Overlay image slider
  • Image management
  • Fixed hero image
  • Privacy, IP addresses, and Google analytics
  • Error response pages
  • Syntax highlighter
  • Language flags
  • Firefox flashs of unstyled content

Sitemap

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

Including the xhtml:link in the sitemap causes some browsers to think the page is not an xml document. That results in displaying the sitemap as html, similar to:

http://sbf-testing.byethost7.com/en/ 2018-06-15 http://sbf-testing.byethost7.com/en/bootstrap 2018-06-15 http://sbf-testing.byethost7.com/en/fatfree 2018-06-15

While the sitemap does validate (on Google), to make the information more readable, I added an XSL stylesheet based on one from GitHub.

I include alternate language (hreflang) information in the sitemap as well as in the page head section. The major search engines, such as Google, Yendex, and Bing support both methods. Baidu, the Chinese search engine, focuses on Chinese websites, so they’re not a concern so long as 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.

Fonts

I use system fonts for displaying most of the text (I use a 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. There is an additional advantage in that 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 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

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 anchors

For page anchors:

  • Clicking a link for a same-page anchor triggers a 1-second scroll to the new position, rather than the page jumping to the new position (unless the user has set prefers-reduced-motion to reduce, in which case, the page will jump to the new position).
  • Anchors are generally assigned to sections (there’s only one case that isn’t). Sections are styled with 4rem padding on the top and bottom (the padding is the same so that the section content is centered vertically, which is important if the section has a background color, as is done on the home page).
  • The 4rem padding at the top is to clear the fixed navbar at the top of the page. When the page is first loaded, the navbar is 74px high (4.625rem). After the page has been scrolled more than 100px, a class, .small-logo is added to the navbar that reduces the size of the logo, reducing the height of the navbar to 58px (3.625rem). Reducing the size of the navbar allows the anchor text to be seen without excessively increasing the spacing between sections.
    • The top sections in the detail pages have a 6rem (for mobile screens) or 7rem padding at the top to clear the full-height navbar.
    • In one case, the destination is within a section. I added the destination ID slightly above the desired text in a list item.
    • I tried styling the sections with a 3rem border-top and a -3rem margin-top, but with that styling on Chrome, the page would jump when I set the focus to the section.
  • Using a link within a page is not recorded in the browser history. Using the browser’s back button will return to the previous page, rather than the previous position on the same page.
    • It is possible to push the current address hash onto the browser’s history, but having that capability seems to make it more confusing to navigate the site.
  • After scrolling to a position on the same page, focus is set on the destination element to support keyboard users.
    • I style the section:focus outline-color to transparent so the section won’t have a line around it on Chrome browsers.

More information is available in the js-functions JavaScript file.

Scroll-to-top button

I looked at several scroll-to-top buttons from GitHub and other sites, but they all had one problem: the button went all the way to the bottom, over the footer. If the button is a similar color to the footer, the button would no longer be easy to see.

My solution is to monitor the distance from the top of the page (B) plus the height of the window (C) compared to the total height of the page (A) minus the height of the footer (D).

Graphic showing the calculation for when to adjust the position of the scroll-to-top button

Button position calculation

The button position is based on:
  • A – Total height
    • document.document­Element­.scrollHeight
  • B – Scroll top
    • window.pageYOffset
  • C – Window height
    • window.innerHeight
  • D – Footer height
    • footer = doc.get­Elements­ByTag­Name­("footer")[0]
    • footer.clientHeight

Normally the button is positioned near the bottom of the screen. If (B + C) > (A - D), then the footer is showing and the button position is adjusted.

I monitor the distance by listening for scroll events. Scroll events are routed through a throttler function that calls the scroll handler function. The throttler function is also called by a resize event listener to adjust the button position if there’s a change in the window height, which happens on mobile displays when the URL bar is hidden.

More information is available in the js-functions JavaScript file.

After the page is scrolled back to the top, focus is applied to the page, 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 and Internet Explorer don’t require the extra step, but it doesn’t hurt.

I use a sticky footer setup with a large bottom margin on the body that matches the height of the footer (the body margin-bottom is set using JavaScript, and is updated every time the browser is resized).

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, most Bootstrap CSS transition effects are disabled.

For the animations controlled by jQuery functions, I set $.fx.off to true, which causes all animations to happen immediately.

if (typeof window.matchMedia === 'function') {
    var motionReduce = window.matchMedia("(prefers-reduced-motion: reduce)");

    if (motionReduce.matches === true) {
        $.fx.off = true;
    }
}

Note: Internet Explorer does not support reduced-motion.

Dark mode

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

The easiest way to implement dark mode would be to use CSS custom properties (variables) on the root, but as Internet Explorer does not support CSS custom properties, I used @media queries in the CSS file to override the default light colors.

body {
    color: $body-color;
    background-color: $body-bg;
}

@media (prefers-color-scheme: dark) {
    body {
        color: rgba(256,256,256,0.87);
        background-color: #12161a;
    }
}

Bootstrap dark mode — The beta1 version of Bootstrap 5.2.0, which was released in May of 2022, supports dark mode. As version 5 does not support Internet Explorer, making the switch will depend on how many people continue to use Internet Explorer.

Dark mode design

I've based my dark mode design on Google’s Material Design concept — start 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 (#12161a).

Dark mode switch

A number of websites provide a switch for users to switch between light and dark modes (with the page using local storage to remember the user’s selection). 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 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

Overlay image slider

I wanted an image slider that could show information laid over a graphic of the site’s structure.

I tried Bootstrap 4’s carousel component, but I wanted the carousel to pause when the mouse was over the carousel and to resume moving once the mouse moves out (no significant delay). I looked at a number of jQuery plugins and image sliders on GitHub, but a plugin seemed like overkill and none of the sliders on GitHub had all of the features I wanted. So, I made my own slider that:

  • Uses img tags for the overlay images rather than styling the overlay images as background images
  • Supports text with each image
  • Has Previous and Next buttons
  • Has a current image indicator / selector
  • Is able to cycle the overlay images in a continuous sequence (a number of sliders will quickly reverse through all of the images after the last image to get back to the first image)
  • Can pause on mouse-over and quickly resume (500ms) when the mouse leaves (several sliders restart the full delay count before continuing to the next image)
  • Is responsive

Image management

To improve page loading times, JEPG and PNG image files are compressed using an online compression application (tinypng.com). I do not run the WEBP files through the online compression site as it adds JPEG type artifacts to the image (the file may be smaller, but I don’t like the effect). I use GIMP for graphics work, which is free, but the JPEG & PNG image files do need to be compressed for use on a website.

Larger images that are used on multiple pages (e.g. the logo) are loaded as separate images. Smaller images that are used on multiple pages (e.g. the language flags) are embedded in the CSS file as Base64 background images.

I use the loading="lazy" attribute on the img tag to delay loading images that normally do not show when the page is first loaded.

<picture>
    <source srcset="../images/bootstrap-sass-architecture-dark.webp,
        ../images/bootstrap-sass-architecture-lg-dark.webp 2x"
        media="(prefers-color-scheme: dark)">
    <source srcset="../images/bootstrap-sass-architecture.webp,
        ../images/bootstrap-sass-architecture-lg.webp 2x" >
    <img class="scss-diagram mb-3" src="../images/bootstrap-sass-architecture.png"
        alt="Diagram of the Sass files used to construct the site CSS"
        width="538" height="372" loading="lazy">
</picture>

Fixed hero image

Having a large image (a hero image) that stays fixed while the content scrolls over it is a popular front page design.

A more ambitious concept is to use a parallax setup that has multiple images whose positions are controlled by translate3d, but I find those types of displays distracting for a business website.

The problem with a hero image is the browser displays an empty space (just the background color) while the image is loading and a large image can take a while to download. Even worse, if it’s styled as a CSS background image (which seems to be the most common method for hero images), browsers will request it later in the load cycle, increasing the time the empty space is shown.

To reduce the delay for showing the image, I:

  • Reduced the image file size:
    • Use a smaller image – sites with hero images that I looked at used a 1920px by 1080px JPEG image, which is the same size as a large monitor. To reduce the image file size, I use a 1626px by 835px image based on:
      • Average the screen width of a 1920px wide display and a 1366px wide display (after subtracting the scrollbar) ~ (1920-17 + 1366-17) / 2 = 1626px
      • Average the screen height of a 1080px high display and a 766px high display (after subtracting for the browser tabs, url bar, & bookmark bar) ~ (1080-88 + 768-88) / 2 = 835px
    • Lower JPEG quality – I lowered the JPEG chroma and quality settings

  • Embedded the image in the page:
    • Base64 – I converted the image to a base64 image and embedded the image in the page

Embedding a base64 image pretty much eliminates the flash of background color. It does make the page larger (in my case, the page, gzipped, went from 5.7kb to 70.3kb), but it eliminates downloading an image (67kb). Embedding the image does mean the image will be downloaded every time, but, since it’s unlikely that people will visit the home page of an informational website that many times, it seems like a reasonable trade-off.

I did try preloading the image in the head section, but embedding the image was faster.

One other option I looked at was to embed a small version of the image that would load with the page. The small image would display quickly (albeit blurry), and then, when the full image was loaded, the browser would transition from the small image to the full image. While the technique (which is used on this site and the method explained on this site) looks nice in some situations, I decided it was worth the page size penalty to get the full-size image there quickly.

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 simulate the image, rather than the actual image.

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 notice (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 banner
        $("#eu-cookie").slideDown();
        $(".detail-first-section").addClass("cookie-notice-margin");
        $("#btn-cookie-accept").on("click", function() {
            var date = new Date();
            // gaScript is defined in the head
            date.setTime(date.getTime() + 31536000000); // one year
            document.cookie = "eu-cookie-notice=true; expires=" 
                + date.toGMTString() + ";path=/";
            $("#eu-cookie").slideUp();
            $(".detail-first-section").removeClass("cookie-notice-margin");
            
            // Enable tracking
            document.head.appendChild(gaScript);
        });
        $("#btn-cookie-opt-out").on("click", function() {
            try {
                window.sessionStorage.setItem("eu-opt-out", "true");
            } catch (error) {
                console.error(error);
            }
            $("#eu-cookie").slideUp();
            $(".detail-first-section").removeClass("cookie-notice-margin");
        });
    }
}

This code checks whether a visitor is new to the site, and if so, shows a banner across the top of the page with information about the site using cookies, with one button for the user to accept cookies and another 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 notice includes a link to the Terms of Use page, which includes privacy information and its own opt-out button. This is the third JavaScript component:

<button type="button" class="btn btn-dark center opt-out" onclick="gaOptOut()">
    Opt-out of Cookies & Analytics</button>
               
function gaOptOut() {
    try {
        window.sessionStorage.setItem("eu-opt-out", "true");
    } catch (error) {
        console.error(error);
    }
    document.cookie = "eu-cookie-notice=; expires=Thu, 01 Jan 1970 00:00:01 GMT;
        path=/";
    $("#eu-cookie").slideUp();
    $(".detail-first-section").removeClass("cookie-notice-margin");
}

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.

The head template includes code to conditionally load a polyfill for the string.trim JavaScript function. IE 8 and older versions of Internet Explorer don’t support the trim function, which is needed for the Google tag manager. Loading the polyfill prevents any errors.

<script>
// Polyfill for old Internet Explorers
if (!String.prototype.trim) {
    String.prototype.trim = function () {
        return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
    };
}
</script>

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).

Note: I use highlight.js Version 9 as that version is compatible with Internet Explorer 9, 10, & 11. Newer versions of highlight.js work only with modern browsers.

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 CSS file 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.

Firefox flashs 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>

Firefox was also displaying the alt text for the logo when the page was loaded for the first time (again, an empty cache). I fixed that by setting the text color to transparent:

.navbar-brand img {
    color: transparent;
}