Creating HLS content

MPEG-DASH is one way to create adaptive bitstream playback, but it’s not the only one. In this post, we’ll explore Apple’s HLS (HTML Live Streaming) a direct competitor for MPEG-DASH and see how to build a package ready for Video On Demand.

I will use FFMPEG to encode the video as required in the HLS specification. This is the same tool that I’ve used to create videos with other codecs.

HLS Creation: Single Rendition

At WWDC 2017, Apple announced support for HEVC (H265), letting you work with both types of streams in the same playlist.
The technologies to implement HEVC in HLS is significantly different than what we discuss here so I’ll probably do a separate post on it rather than confuse matters. I’m also banking on the fact that H264 has better support in both Apple and non-Apple devices.

HLS works differently than DASH with different formats and parameters. We will use FFMPEG command line tool to create the HLS video. It provides a lot of tools and supports the creation of the necessary files out of the box.

HLS works with Transport Stream segments and has much tighter control over the characteristics of the streams (or renditions in HLS parlance).

Encoding a single rendition of your video looks like this for an H264 product.

# Create the directory to store the files
mkdir serenity
# run the command
ffmpeg -i serenity.mp4 \
-vf scale=w=640:h=360:force_original_aspect_ratio=decrease \
-c:a aac -ar 48000 -b:a 96k \
-c:v h264 \
-profile:v main \
-crf 20 \
-g 48 -keyint_min 48 \
-sc_threshold 0 \
-b:v 800k -maxrate 856k -bufsize 1200k \
-hls_time 4 \
-hls_playlist_type vod \
-hls_segment_filename serenity/360p_%03d.ts \

The explanation for each of the parameters in the example are described in the following list:

  • -i serenity.mp4 – set serenity.mkv as input file
  • -vf "scale=w=1280:h=720:force_original_aspect_ratio=decrease" – scale video to maximum possible within 1280×720 while preserving aspect ratio
  • -c:a aac -ar 48000 -b:a 96k – set audio codec to AAC with sampling of 48kHz and bitrate of 128k
  • -c:v h264 – set video codec to be H264 which is the standard codec of HLS segments
  • -profile:v main – set H264 profile to main – this means support in modern devices
  • -crf 20 – Constant Rate Factor, high level factor for overall quality
  • -g 48 -keyint_min 48 – create key frame (I-frame) every 48 frames (~2 seconds) – will later affect correct slicing of segments and alignment of renditions
  • -sc_threshold 0 – don’t create key frames on scene change – only according to -g
  • -b:v 800k -maxrate 856k -bufsize 1200k – limit video bitrate, these are rendition specific and depends on your content type
  • -hls_time 4 – segment target duration in seconds – the actual length is constrained by key frames
  • -hls_playlist_type vod – adds the #EXT-X-PLAYLIST-TYPE:VOD tag and keeps all segments in the playlist
  • -hls_segment_filename beach/360p_%03d.ts – explicitly define segments files’ names
  • serenity/360p.m3u8 – path of the playlist file – also tells ffmpeg to output HLS (.m3u8)

Running the code above will produce a set of files:

  • One or more transport stream segments with a .ts extension. In this case, it produced 34 segments for the video I chose
  • The playlist with a .m3u8 format

The playlist for the video looks like this:


HLS Creation: Multiple Renditions And Master Playlist

All the work we did in the previous section was for one rendition. You will definitely want more than one to acommoadte both your low-end, low-bandwidth users as well as those who are watching in the latest 4k iMac on fiber connections. We can create multiple renditions and let the player decide what’s the best one to play at any given time of the playback.

This command will create 4 versions of the video along with a playlist for each:

  • 360p
  • 480p
  • 720p
  • 1080p
ffmpeg -hide_banner -y -i serenity.mp4 \
  -vf scale=w=640:h=360:force_original_aspect_ratio=decrease \
  -c:a aac -ar 48000 -c:v h264 -profile:v main \
  -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod  -b:v 800k -maxrate 856k \
  -bufsize 1200k -b:a 96k -hls_segment_filename serenity/360p_%03d.ts serenity/360p.m3u8 \
  -vf scale=w=842:h=480:force_original_aspect_ratio=decrease \
  -c:a aac -ar 48000 -c:v h264 -profile:v main \
  -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 1400k -maxrate 1498k \
  -bufsize 2100k -b:a 128k -hls_segment_filename serenity/480p_%03d.ts serenity/480p.m3u8 \
  -vf scale=w=1280:h=720:force_original_aspect_ratio=decrease \
  -c:a aac -ar 48000 -c:v h264 -profile:v main \
  -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 2800k -maxrate 2996k \
  -bufsize 4200k -b:a 128k -hls_segment_filename serenity/720p_%03d.ts serenity/720p.m3u8 \
  -vf scale=w=1920:h=1080:force_original_aspect_ratio=decrease \
  -c:a aac -ar 48000 -c:v h264 -profile:v main \
  -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 \
   -hls_playlist_type vod -b:v 5000k -maxrate 5350k \
  -bufsize 7500k -b:a 192k -hls_segment_filename serenity/1080p_%03d.ts serenity/1080p.m3u8

This created the media but there’s no way for the player to know what’s available. To fix this we create a master playlist that tells the player what renditions are available for the video.


Note that this playlist doesn’t include the stream transport segments. The master list only tells the player where to look for the corresponding renditions.

Yes, this is cumbersome to do by hand and, when also working with DASH packages, it can be hard to manage.

There are different tools to automate the generation of the HSL Renditions and playlists.

Newer versions of Google’s Shaka Packager support creating HLS segments and playlists. They also support creating HLS and DASH content with a single command (although this appears to only with single MP4 files).

Assuming that you’ve already downloaded/compiled the packager, the command to generate DASH and HSL look like this:

packager \
  in=h264_baseline_360p_600.mp4,stream=audio,output=audio.mp4,playlist_name=audio.m3u8,hls_group_id=audio,hls_name=ENGLISH \
  in=h264_baseline_360p_600.mp4,stream=video,output=h264_360p.mp4,playlist_name=h264_360p.m3u8,iframe_playlist_name=h264_360p_iframe.m3u8 \
  in=h264_main_480p_1000.mp4,stream=video,output=h264_480p.mp4,playlist_name=h264_480p.m3u8,iframe_playlist_name=h264_480p_iframe.m3u8 \
  in=h264_main_720p_3000.mp4,stream=video,output=h264_720p.mp4,playlist_name=h264_720p.m3u8,iframe_playlist_name=h264_720p_iframe.m3u8 \
  in=h264_main_1080p_6000.mp4,stream=video,output=h264_1080p.mp4,playlist_name=h264_1080p.m3u8,iframe_playlist_name=h264_1080p_iframe.m3u8 \
  --hls_master_playlist_output h264_master.m3u8 \
  --mpd_output h264.mpd

The result produces the following files:

├── audio.m3u8
├── audio.mp4
├── h264.mpd
├── h264_1080p.m3u8
├── h264_1080p.mp4
├── h264_1080p_iframe.m3u8
├── h264_360p.m3u8
├── h264_360p.mp4
├── h264_360p_iframe.m3u8
├── h264_480p.m3u8
├── h264_480p.mp4
├── h264_480p_iframe.m3u8
├── h264_720p.m3u8
├── h264_720p.mp4
├── h264_720p_iframe.m3u8
├── h264_baseline_360p_600.mp4
├── h264_main_1080p_6000.mp4
├── h264_main_480p_1000.mp4
├── h264_main_720p_3000.mp4
└── h264_master.m3u8

And these can be used to play either DASH or HLS video. We’ll tackle playback next.


I know how to use Shaka Player to play DASH content. It should be possible to use the same player to play both DASH and HLS content although it shouldn’t be necessary to play both, the player scripts should take care of cross-platform issues.

Another option is to use Video.js with the HSL plugin and the DASH if you need to serve both formats on the same page.

The basic functionality looks like this:

<video  id=example-video
        width=600 height=300
        class="video-js vjs-default-skin"

  Put the lines below at the bottom of the page,
  right before the closing body tag
<script src="video.js"></script>
<script src="videojs-contrib-hls.min.js"></script>
var player = videojs('example-video');;

I’m tracking issue 1365 in the Video.js HSL Plugin Repository to track the issue of content not playing in Chrome.

I’m also researching if I can combine DASH and HLS in a single player for either Shaka Player or Video.js.

Encoding Suggestions

Like DASH, HLS allows for multiple renditions of content at different resolutions. What these resolutions are will depend on your content and your target audience.

Each row presents to bitrate values, one for low motion content like conference presentations and other activities where there is little motion. The other is for the opposite type of videos like sports and other high motion activities. The audio bitrate remains constant for both low and high motion videos.

The table is taken from Peer 5’s Creating A Production Ready Multi Bitrate HLS VOD stream

Quality Resolution bitrate – low motion bitrate – high motion audio bitrate
240p 426×240 400k 600k 64k
360p 640×360 700k 900k 96k
480p 854×480 1250k 1600k 128k
HD 720p 1280×720 2500k 3200k 128k
HD 720p 60fps 1280×720 3500k 4400k 128k
Full HD 1080p 1920×1080 4500k 5300k 192k
Full HD 1080p 60fps 1920×1080 5800k 7400k 192k
4k 3840×2160 14000k 18200 192k
4k 60fps 3840×2160 23000k 29500k 192k


The difference between visual and document order

One of the things that I’ve always been curious about, and that didn’t become an issue until I started working with accessibility, is the difference between the logical or document order of your content and the visual order it’s displayed in.

This is what we do when working with Media Queries to adapt layouts to different form factors, we change the visual order of the content without altering the order of the content in the document or the semantics that we’ve assigned to elements in the content.

Example of an accessibility tree from Google Web Fundamentals
Example of an accessibility tree from Google Web Fundamentals

In addition to the DOM, the browser generates an accessibility tree based on a subset of the DOM and passes that accessibility tree to assistive technology devices like screen readers. This is why the document order and the ability to work without CSS are important: Screen readers don’t care about the visual order of your document or what CSS you styled the content with. It cares that the semantics are appropriate and it reads the content as is, not as you meant to create it.

So, when we talk about document order I worry about accessibility. When we talk about the visual order I talk about device accommodations and media queries adaptations.

Useful Links

Font stacks and other explorations on typography on the web, Part 2

Writing Modes and World Languages

Another thing that affects typography is the writing direction of your text. Either because the language requires it or because you’re experimenting with new technologies like Jen Simmons does in this example from Layout Land Youtube Channel

It leverages CSS Grid, Transforms, and other modern technologies to create really impressive layouts.

Going back to writing modes. Different languages use different writing modes. Some things to consider:

  • Most Latin and Cyrillic languages run the text from left to right and top to bottom
  • Arabic and Hebrew run the text from right to left and top to bottom
  • Japanese is a special case
    • Japanese can run the text from right to left, top to bottom (tategaki (縦書き) style)
    • Japanese can also run from top to bottom and left to right (yokogaki (横書き) style)
    • Both writing modes for Japanese can be used in the same page

There are other languages and considerations but you get the idea.

But you can also use it for creative typographical effects that require CSS, not images or CSS translate, to accomplish the goal. A good, and subtle example is the “Rise To Success” text in the pen below:

See the Pen Writing Mode Demo — Article Subheadlines by Jen Simmons (@jensimmons) on CodePen.

If you look at the CSS code in the embedded pen, you’ll see the following code:

h2 {
  writing-mode: vertical-rl;
  float: left;
  margin: 1.5rem 0 0 -3.8rem;
  font-size: 1.8em;
  background: #96e5fb;
  padding: 8px 0;

It’s the writing-mode: vertical-rl; attribute that makes the text rotate while still allowing us to highlight it and keeping it in the document to be read by assistive technology.

UN Website in Eglish
UN Website in Arabic
United Nations Website in English (top) and Arabic (bottom). Notice how they are mirrors of each other.

Jen wrote an article for 24 ways in 2016, where she provides a thorough explanation of writing modes and how to make them work on your projects, today.

font-variant: Low Level Plumbing

CSS offers different levels of control over font features available from your font. The preferred way is to use individual font-variant-* attributes in a selector or use the shorthand font-variant.

Not all fonts provide all the features discussed in this section. As always research whether the chosen font or fonts have the features that you need.

This may be a good thing to include in your font specimen if you have one.

The code looks for the shorthand looks like this:

body {
  font-variant: common-ligatures annotations() slashed-zero;

The different values for the property are:

Specifies a normal font face; each of the longhand properties has an initial value of normal.

Sets the value of the font-variant-ligatures to none and the values of the other longhand property as normal, their initial value.
<common-lig-values>, <discretionary-lig-values>, <historical-lig-values>, <contextual-alt-values>
Specifies the keywords related to the font-variant-ligatures longhand property. The possible values are: common-ligatures, no-common-ligatures, discretionary-ligatures, no-discretionary-ligatures, historical-ligatures, no-historical-ligatures, contextual, and no-contextual.
stylistic(),  historical-forms, styleset(), character-variant(), swash(), ornaments(), annotation()
Specifies the keywords and functions related to the font-variant-alternates longhand property.
small-caps, all-small-caps, petite-caps, all-petite-caps, unicase, titling-caps
Specifies the keywords and functions related to the font-variant-caps longhand property.
<numeric-figure-values>, <numeric-spacing-values>, <numeric-fraction-values>, ordinal, slashed-zero
Specifies the keywords related to the font-variant-numeric longhand property. The possible values are:  lining-nums, oldstyle-nums, proportional-nums, tabular-nums, diagonal-fractions, stacked-fractions, ordinal, and slashed-zero.
<east-asian-variant-values>, <east-asian-width-values>, ruby
Specifies the keywords related to the font-variant-east-asian longhand property. The possible values are: jis78, jis83, jis90, jis04, simplified, traditional, full-width, proportional-width, ruby.

We can also use these variables individually. The individual names are:

The code using individual properties looks like this:

body {
  font-variant-ligatures: common-ligatures;
  font-variant-alternates: historical-forms;
  font-variant-numeric: slashed-zero;

.japanese {
  font-variant-east-asian: ruby full-width jis83;

.small {
  font-variant-caps: small-caps;

.sup {
  font-variant-position: sub;

.super {
  font-variant-position: super;

Performance: FontFace Observer and font-display

Whenever I need to make sure that a font has loaded before using it I work with Fontface Observer to load the fonts with a good fallback and timeouts.

The process consists of the following sections:

  • Font Loading
  • Font Use
  • Javascript Loader

We first define our fonts in CSS like normal. We use the same declarations as we do normally to load fonts.

/* Regular font */
@font-face {
  font-family: 'notosans';
  src: url('../fonts/notosans-regular.woff2') format('woff2'), url('../fonts/notosans-regular.woff')
      format('woff'), url('../fonts/notosans-regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
/* Bold font */
@font-face {
  font-family: 'notosans';
  src: url('../fonts/notosans-bold.woff2') format('woff2'), url('../fonts/notosans-bold.woff')
      format('woff'), url('../fonts/notosans-bold.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
/* Italic Font */
@font-face {
  font-family: 'notosans';
  src: url('../fonts/notosans-italic.woff2') format('woff2'), url('../fonts/notosans-italic.woff')
      format('woff'), url('../fonts/notosans-italic.ttf') format('truetype');
  font-weight: normal;
  font-style: italic;
/* bold-italic font */
@font-face {
  font-family: 'notosans';
  src: url('../fonts/notosans-bolditalic.woff2') format('woff2'), url('../fonts/notosans-bolditalic.woff')
      format('woff'), url('../fonts/notosans-bolditalic.ttf') format('truetype');
  font-weight: 700;
  font-style: italic;

Next, we prepare three versions of the default element styles. One for when there is no Javascript (body), one for when the fonts fail to load (.fonts-failed body) and one for when the fonts load successfully (.fonts-loaded body). Only one of these body declarations will be used for the page.

/* Default body style */
body {
  font-family: Verdana, sans-serif;
  font-size: 16px;
  line-height: 1.275;
  -webkit-text-decoration-skip: ink;
  -moz-text-decoration-skip: ink;
  -ms-text-decoration-skip: ink;
  text-decoration-skip: ink;

    This will match if the fonts failed to load.
    It is identical to the default but doesn't
    have to be
.fonts-failed body {
  font-family: Verdana, sans-serif;
  font-size: 16px;
  line-height: 1.375;
  -webkit-text-decoration-skip: ink;
  -moz-text-decoration-skip: ink;
  -ms-text-decoration-skip: ink;
  text-decoration-skip: ink;
    This will match when fonts load successfully
.fonts-loaded body {
  font-family: notosans-regular, verdana, sans-serif;
  font-size: 16px;
  line-height: 1.375;
  -webkit-text-decoration-skip: ink;
  -moz-text-decoration-skip: ink;
  -ms-text-decoration-skip: ink;
  text-decoration-skip: ink;

The final piece is the Javascript file that will actually load the fonts. This assumes that fontfaceobserver.js has already been loaded.

We first define a constant for each of the fonts we want to load. We use the same name but add a second attribute to the FontFaceObserver object to indicate additional information about the font (weight and style)

we assign document.documentElement to a variable that we will work with later in the script.

We add the class fonts-loading to document element as a temporary placeholder while we download the font.

Next, we use promise.all to create an array of promises with each font’s load method. Promise.all is an atomic function, either they will all succeed or they will all fail. This will help us make sure that all fonts are available.

If the fonts are successful the then branch is followed. This branch will remove the fonts-loading class and replace it with fonts-loaded. This is the CSS class that uses the web font we just downloaded and it will only be used if the fonts loaded successfully.

If the fonts fail to load the script follows the catch path. This path replaces fonts-loading with fonts-failed. This CSS class doesn’t use the web font and is essentially identical to the body element definition.

const sans = new FontFaceObserver('notosans', {
  weight: normal,
  style: normal
const italic = new FontFaceObserver('notosans', {
  weight: normal,
  style: 'italic'
const bold = new FontFaceObserver('notosans', {
  weight: 700,
  style: 'normal'
const bolditalic = new FontFaceObserver('notosans', {
  weight: 700,
  style: 'italic'

let html = document.documentElement;


Promise.all([sans.load(), bold.load(), italic.load() bolditalic.load()]).then(() => {
}).catch(() =>{

Yes, this is more work but think about it. You’re already loading the fonts and we could optimize the loader script to use only two elements (body and .fonts-loaded). The only new things we do is load fontfaceobserver.js and run our loader script.

Another thing we can add to @font-face declarations to speed up font loading resolution is the font-display rule. The rule tells browsers how would you like it to handle loading web fonts.

The possible values are:

  • auto: The default. Typical browser font loading behavior will take place. This behavior may be FOIT or FOIT with a relatively long invisibility period. This may change as browser vendors decide on better default behaviors
  • swap: Fallback text is immediately rendered in the next available system typeface in the font stack until the custom font loads, in which case the new typeface will be swapped in. This is what we want for stuff like body copy, where we want users to be able to read content immediately
  • block: Like FOIT, but the invisibility period persists indefinitely. Use this value any time blocking rendering of text for a potentially indefinite period of time would be preferable. It’s not very often that block would be preferable over any other value
  • fallback: A compromise between block and swap. There will be a very short period of time (100ms according to Google) that text styled with custom fonts will be invisible. The unstyled text will then appear if the custom font hasn’t loaded before the short blocking period has elapsed. Once the font loads, the text is styled appropriately. This is great when FOUT is undesirable, but accessibility is more important
  • optional: Operates like fallback in that the affected text will initially be invisible for a short period of time, and then transition to a fallback if font assets haven’t completed loading. The optional setting gives the browser freedom to decide whether or not a font should even be used, and this behavior depends on the user’s connection speed. If you use this setting you should anticipate custom fonts may possibly not load at all

So, depending on the importance of the font to the layout and ease of reading of the site you can play with the different values for font-display to see how it affects your site. Since you’re likely to have a high-speed connection and not throttle it, it’s important to test the site in your target devices.

Loading a font using @font-face and font-display looks like this:

@font-face {
  font-family: 'Ubuntu'; /* regular */
  src: url('Ubuntu-R-webfont.woff2') format('woff2'), url('Ubuntu-R-webfont.woff')
      format('woff'), url('Ubuntu-R-webfont.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;

I will not go into details on why testing on devices is important, I’ll just leave you, again, with Alex Russell’s video on web performance

One thing to keep in mind is that your fonts are subject to the web’s same origin policy. This means that unless you configure your server with universal CORS access or you serve fonts from a CDN like Google Fonts or Typekit they will not load across different origin.

HTTP2 and Preload

We can also tackle performance issues from the server side. I’m not talking about server-side rendering but to use HTTP/2.

HTTP2 allows several requests to use the same network connection, reducing the overhead of several individual requests significantly and makes inlining obsolete.

Browser support for HTTP/2 (and its predecessor SPDY) is excellent, so there’s no reason not to use HTTP/2.

We can preload resources from the server Using Apache as an example we preload assets when the browser loads index.html. We’re preloading both woff and woff2 fonts to make sure cover modern browsers that will support either version. If we must support older browsers we should also push the ttf version of the font.

<If "%{DOCUMENT_URI} == '/index.html'">
  H2PushResource add css/site.css
  H2PushResource add js/site.js
  H2PushResource add font/font.woff2
  H2PushResource add font/font.woff

We can also customize what resources we push-based in the URI of the resource. In the following example each time we match a URI we will load specific assets for that file and nothing else. We could also have a wildcard match that will load assets needed by all pages and use the system below for page specific assets.

<if "%{DOCUMENT_URI} == '/portfolio/index.html'">
  H2PushResource add /css/dist/critical-portfolio.css?01042017

<if "%{DOCUMENT_URI} == '/code/index.html'">
  H2PushResource add /css/dist/critical-code.css?01042017

Nginx also allows you to push resources to the browser. The same examples reworked for Nginx. The first one will preload a set of resources.

server {
  location = /index.html {
    http2_push /css/style.css;
    http2_push /js/main.js;
    http2_push font/font.woff2;
    http2_push font/font.woff;

And the second example pushing assets depending on the page we’re trying to access:

location = /portfolio/index.html {
  http2_push /css/dist/critical-portfolio.css?01042017;

location = /code/index.html {
  http2_push /css/dist/critical-code.css?01042017;

If you don’t have access to your server’s configuration, don’t want to depend on manually updating the cache busting string you can do the preload from the client side using link elements with the preload attribute.

<link rel="preload" href=""
  as="font" crossorigin type="font/woff2">
<link rel="preload" href=""
  as="font" crossorigin type="font/woff">
<link rel="preload" href=""
  as="style" crossorigin type="text/css">
<link rel="preload" href=""
  as="script" crossorigin type="text/javascript">

The attributes of the link are:

  • rel – the type of link it is. In this case, the value is preload
  • href – the URL to preload
  • as – the destination of the response. This means the browser can set the right headers and apply the correct CSP policies.
  • crossorigin – Optional. Indicates that the request should be a CORS request. The CORS request will be sent without credentials unless you add crossorigin="use-credentials" to the link
  • type – Optional. Allows the browser to ignore the preload if the provided MIME type is unsupported.

I discuss link preloading along with other HTTP2 resource pushing and preloading strategies in HTTP/2 Server Push, Link Preload And Resource Hints

Service Worker Support

Service Workers are the core of progressive web applications. They work as a reverse network proxy that intercepts requests for your site and performs actions based on its configuration. I’ve written about service workers on my blog before so I won’t go into detail.

I will use workbox.js version 3, currently in beta, to illustrate how to cache fonts. You will most definitely want to add additional routes and caching strategies for your site.

At the root of your site use the following snippet inside a script tag to register the service worker.

We test if the navigator object has a serviceWorker method. If it does it means that Service Workers are supported and we can register it. If it doesn’t then Service Workers are not supported and we bail accordingly.

Registering the Service Worker means that it’ll work for all pages under its scope but not above it (This is why we put the service worker at the root of the application).

if ('serviceWorker' in navigator) {
    .then(function(registration) {
        'Service Worker registration successful with scope: ',
    .catch(function(err) {
      console.log('Service Worker registration failed: ', err);

The actual Service Worker script is fairly simple.

We import workbox-sw, the core of our Service Worker.

We check if Workbox loaded successfully and if it does then we register a route matching all possible font types and create a custom cache using a cache-first strategy (check the cache and if the resource is not there then fetch it from the network).

The cache will store 10 fonts for 30 days (as indicated in maxEntries and maxAgeSeconds). If more than 10 fonts are added the oldest will be removed first.

  ' '

if (workbox) {
      cacheName: 'font-cache',
      plugins: [
        new workbox.expiration.Plugin({
          maxEntries: 10,
          maxAgeSeconds: 30 * 24 * 60 * 60
} else {
  console.log(`Boo! Workbox didn't load`);

Using a Service Worker to cache fonts using this method means that fonts will be loaded from cache in second and subsequent visits and when the browser is offline or connectivity is unreliable.

We could have precached the fonts but that would remove the possibility of customizing the cache. The size of fonts may also impact how long does it take to precache resources and the whole idea of precaching is to make the first load of the page work fast.

For more information, check Workbox 3 documentation.

Links and Resources


Some material is taken from MDN created by Mozilla Contributors and licensed under a Creative Commons Attribution-ShareAlike 2.5 Generic license.

Material taken from CSS-Tricks used according to their license.

Content in HTTP2 Push taken from Jake Archibald’s site (H2 Push is tougher than I thought), from Smashing Magazine (A Comprehensive Guide To HTTP/2 Server Push) and Filament Group’s site (Modernizing our Progressive Enhancement Delivery).

Content from The Elements of Typographic Style Applied to the Web by Richard Rutter used under a Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) License

Content from Google Web Fundamentals is licensed under a Creative Commons Attribution 3.0 License. Code samples are licensed under the Apache 2.0 License.

Font stacks and other explorations on typography on the web, Part 1

I have dealt with some of these topics before in different places but now want to weave a more coherent narrative around these topics and try to find a way how to tie them together so it makes sense.

What is a font-stack and why it matters

It wasn’t until I started reading blogs and books by Jason Pamental, Bram Stein, and Tim Brown among others that I realized that web typography is a lot more than choosing the right fonts.

The font stack is the set of fonts that we use for our site or application. The browser’s rendering engine will pick the first font on the stack that is installed and available.

To install the font we use CSS’s @font-face declaration like this:

@font-face {
  font-family: 'Ubuntu'; /* regular */
  src: url('Ubuntu-R-webfont.woff2') format('woff2'), url('Ubuntu-R-webfont.woff')
      format('woff'), url('Ubuntu-R-webfont.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
@font-face {
  font-family: 'Ubuntu'; /* italic */
  src: url('Ubuntu-RI-webfont.woff2') format('woff2'), url('Ubuntu-RI-webfont.woff')
      format('woff'), url('Ubuntu-RI-webfont.ttf') format('truetype');
  font-weight: normal;
  font-style: italic;
@font-face {
  font-family: 'Ubuntu'; /* bold */
  src: url('Ubuntu-B-webfont.woff2') format('woff2'), url('Ubuntu-B-webfont.woff')
      format('woff'), url('Ubuntu-B-webfont.ttf') format('truetype');
  font-weight: bold;
  font-style: normal;
@font-face {
  font-family: 'Ubuntu'; /* bold italic */
  src: url('Ubuntu-BI-webfont.woff2') format('woff2'), url('Ubuntu-BI-webfont.woff')
      format('woff'), url('Ubuntu-BI-webfont.ttf') format('truetype');
  font-weight: bold;
  font-style: italic;

To prevent faux bold and italics we load four fonts, one each for normal, italics, bold and bolditalics.

But in this @font-face demo we can already see one of the performance pitfalls of using web fonts: The size of the files.

Each of these font files can range from just a few tens of kilobytes to 400 kilobytes or more for a single weight of the font and 1.2 mega bytes for the four weights that we’re using.

So try to find smaller sizes or subset the fonts.

Subsetting fonts will make the files smaller by only adding the glyphs (character, punctuations and others) that are needed to render the page. You must be careful when doing this as any glyph missing will be rendered in a backup font, either one you designate or one of the defaults for serif, sanserif and monospace.

I’ve discussed subsetting fonts in two posts: subsetting fonts and more on font subsetting.

This is what a font stack looks like:;

/* Times New Roman-based stack */
font-family: Cambria, 'Hoefler Text', Utopia,
  'Liberation Serif' 'Nimbus Roman No9 L Regular', Times, 'Times New Roman', serif;

/* Modern Georgia-based serif stack */
font-family: Constantia, 'Lucida Bright', Lucidabright, 'Lucida Serif', Lucida,
  'DejaVu Serif', 'Bitstream Vera Serif', 'Liberation Serif', Georgia, serif;

/* Traditional Garamond-based serif stack */
font-family: 'Palatino Linotype', Palatino, Palladio, 'URW Palladio L',
  'Book Antiqua', Baskerville, 'Bookman Old Style', 'Bitstream Charter',
  'Nimbus Roman No9 L', Garamond, 'Apple Garamond', 'ITC Garamond Narrow',
  'New Century Schoolbook', 'Century Schoolbook', 'Century Schoolbook L',
  Georgia, serif;

/* Helvetica/Arial-based sans serif stack */
font-family: Frutiger, 'Frutiger Linotype', Univers, Calibri, 'Gill Sans',
  'Gill Sans MT', 'Myriad Pro', Myriad, 'DejaVu Sans Condensed',
  'Liberation Sans', 'Nimbus Sans L', Tahoma, Geneva, 'Helvetica Neue',
  Helvetica, Arial, sans-serif;

/* Verdana-based sans serif stack */
font-family: Corbel, 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans',
  'DejaVu Sans', 'Bitstream Vera Sans', 'Liberation Sans', Verdana,
  'Verdana Ref', sans-serif;

/* Monospace stack */
font-family: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console',
  'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono',
  'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;

These are fairly complex stacks and the closer you move to the right the more likley you are to find an equivalent until you get to the final font of the stack, one of five generic fonts: serif, sans-serif, cursive, fantasy, and monospace. The generic fonts are different than the other fonts in the stack, rather than trying match a specific font, they ask the browser to provide a default fallback.

According to the CSS Fonts Level 3 Specification generic fonts section:

All five generic font families must always result in at least one matched font face, for all CSS implementations. However, the generics may be composite faces (with different typefaces based on such things as the Unicode range of the character, the language of the containing element, user preferences and system settings, among others). They are also not guaranteed to always be different from each other.

User agents should provide reasonable default choices for the generic font families, which express the characteristics of each family as well as possible, within the limits allowed by the underlying technology. User agents are encouraged to allow users to select alternative choices for the generic fonts.

Making sure your fallback fonts work well

One problem with fallback fonts is that they may not match the web font they replace 100%… and this will definitely become a problem when working on how will the layout look in your fallback font and whether it’ll work at all or not.

Monica Dinculescu created a Font Style Matcher to work around this issue. We match the fonts as close as possible as show in the figure below and then copy the two CSS blocks.

Font Matcher Demo of Work In Progress
Font Matcher Demo of Work In Progress

What we get when we copy the CSS is one block for the fallback font (Georgia) and one for the web font (Merriweather). Since CSS will apply both sequences in order it’ll jump from Georgia to Merriweather but, since we took care that the fonts would match, the jarring change will be minimized.

font-family: Georgia
font-size: 16px
line-height: 1.6;

font-family: Merriweather
font-size: 16px
line-height: 1.6;

System Fonts: Use what’s given to you

OK, we have our font stack, we understand what it is and we can customize it, after a fashion, to reduce any jarring text change between fallback fonts and the web fonts we’ve downloaded.

Another way provide a better experience is to use system fonts; the same fonts that the Operating System uses on your machine. We use the following CSS to load system specific fonts across platforms, knowing that the CSS parser will stop when it hits the first match.

Also note that we use the long form, with different attributes for each of font-family, font-size and line-height to work around a bug that may cause the -apple-system font (used in Safari) to be considered a named prefixed and cause the rest of the declaration (fonts for all other systems) to be ignored.

html {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
    'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
    'Helvetica Neue', sans-serif;
  font-size: 16px;
  line-height: 1.2;

CSS fonts module Level 4 introduces a new generic font family. The system-ui generic font family will always map to the default user-interface font on all platforms.

There is no browser support for system-ui yet, when it arrives on browsers, you’ll be able to reduce the above font stack to a single item:

html {
  font-family: system-ui;

This is cool but what’s the use case for system fonts?

System fonts are best used for the UI of your application. This will give users a familiar feell by using the same fonts throughout the Operating System.

Variable Fonts: New Swiss Army Knife?

Opentype 1.8 introduced the concept of variable fonts. In these variable fonts we get multiple axes representing six predefined axes: weight, width, optical size, italic, slant; plus an unlimited numbers of custom axes to define your own properties in the font. This is all packaged together in one file rather than have multiple files to represent the different font faces we use.

If you have variable fonts avaialble you can use Axis Praxis a preview and experimentation tool. You can work with any of the available fonts or upload your own variable font to play with, all available axes will appear in the tool to play with.

One thing to notice about Variable Fonts is that there are only six pre-defined axes (duiscussed above) and no limit to the number of custom axes a font can have. This means that we can have a single font that works for body copy, heaadings, and text-decorations if needed.

For examples of what you can do with variable fonts check out Variable Fonts Demo and Explainer for work with early versions of variable fonts.

Another idea worth exploring is whether we can use a single variable font for our site. This will depend on what Axes the variable font makes available and how complex is the typography for your site. It should be possible but requires careful planning and font selection.

Choosing Fonts

Choosing your font stack is a tricky balancing act of many conflicting and, sometimes, contradictory priorities. We’ll look at what I consider the most important design and non-design considerations.

Design Considerations

These are some of the design consideratons that go into my font selection process.

If you think I missed anything, ping me on twitter (@elrond25)

Font Selection

Display faces are designed to be used large, such as in headlines and are usually less readable at smaller sizes and should not be used for body copy. These are called display fonts or faces.

Other typefaces are designed specifically to be used in large areas of smaller body copy. These are called text, body, or copy fonts.

Identifying fonts for your specific needs is the first step but then comes the big question: Serif or Sans-Serif?

Sans Serif Font
Serif font with serif lines show in red
Comparison of sans-serif font (left) and serif font showing serif lines in red (right)

I normally work with whatever font I think will work best for the project I’m working on either on its own if I’m using it for both headings and copy or paired with another font.

So which is more legible: serif or sans-serif typefaces? In print it appears that serif fonts are considered more legible. Serif fonts allow the eye to flow more easily over the text, improving reading speed and decreasing eye fatigue.

For the web the situation is somewhat different. We will never set as much text on the web as we would in a book or magazine and there are sans-serif fonts that work just as well as serif for the amount of text that we use on the web.

This is one subject that is debated whenever typography people talk about fonts – some people argue that serif typefaces make text harder to read in smaller screens. Others believe that there’s no difference. My position is to test the fonts and then have people read it.

Font Sizes

We need to make sure that the text is not too small or too large for the device users are viewing the content on. The default size in browsers is 16px. I like using that as my base and then working with Modular Scales.

A modular scale, like a musical scale, is a prearranged set of harmonious proportions.

Robert Bringhurst

I use Tim Brown and Scott Kellum’s modular scale builder where we can play with a base value (for example 16px, the default for most browsers) and a set of predefined ratios (I normally use the golden ratio, 1.618) to create a series of values that are ratios of the base number.

For the values of 16px and 1:1.618 ratios the scale looks like this:


So you can take these values and plug them (up or down) to your CSS. If the Golden Ratio doesn’t work you can experiment with different ratios available on the tool; they will each produce different values you can plug in to your stylesheets.

This is how I like to define the basic font size for the document. I rely on the fact that most browsers have a default font size of 16px; so I set the default value to 16px and then use em or rem units in other selectors to make sure that the sizing is relative to the parent element (when using em) and to the root element (with rem).

I’ve also chosen not to support IE6 and 7. If you do need to support those or older browsers, then Richard Rutter’s How to Size Text in CSS provides additional guidance for your work.

body {
  font-size: 16px;
  line-height: 1.2em;
.bodytext p {
  font-size: 1em;
.sidenote {
  font-size: 0.75em;


Sometimes we allow the lines of text to become to wide; that makes them harder to read as the eye looses sight of where the line ends and when should the user return to the left edge of the screen. The horizontal distance is referred to as measure (also called line length).

So what should the maximum width a text block be? Well, it all depends on the size of the font. The larger the font size, the longer the line can be; that said you don’t want to go beyond 80 characters or 40ems (since block width is easier to measure than counting characters). We can ensure the text grows no wider than our specified width using the max-width css attribute. It looks like this:

body {
  max-width: 40em;


Leading (pronounced “ledding”) is so called because, in mechanical presses, strips of lead are placed between lines of type to space the lines apart. CSS handles leading through the line-height property.

Line-height is unique among CSS properties in that it doesn’t require a unit attached to its value, like in the example below the line-height is 1.25 times the value of the font size (18px in this case):

p {
  font-family: 'Minion Pro', 'Minion Web', serif;
  font-size: 16px;
  line-height: 1.25;

I prefer to use the long hand syntax for my CSS and explictly write all attributes separately. You could write the declaration above as:

p {
  font: 16px/1.25 'Minion Pro', 'Minion Web', serif;

Spacing/Line Height

One of the most common typographic “mistakes” I see on the web today is improper type spacing. What I’m referring to here is instances where a block text isn’t given enough margin, subheads and correlated body text which aren’t visually grouped together, and so on. Proper spacing (combined with hierarchy) allows the reader to scan the text and access it at the desired points.

It seems to me that the relationship of paragraph spacing (additional spacing placed before or after a paragraph), the space around a block of type, and letter spacing can be related proportionally to the line height of a paragraph. Line height is defined as the vertical distance between lines of text. So for instance, if the line height of one paragraph is set to 2em and a paragraph with the same size text is set to 1.5em, the first paragraph will require more paragraph spacing and probably more margin around it.

Much of this is done by eye rather than an exact formula, but I do use a good rule of thumb when it comes to the relationship of paragraph spacing to line height. When working with web content we need to make sure that we do equal spacing on both the top and the bottom margins, otherwise the first and last paragraphs will look weird and your content will not flush to the top, as you probably intended.

p {
  line-height: 1.5;
  margin-top: 1.5em;
  margin-bottom: 1.5em;

We can modify the styles for the first and last paragraphs to remove the top or bottom margin as needed using :first-child and last-child pseudo-classes. The code look lie this.

p:first-child {
  margin-top: 0;

p:last-child {
  margin-bottom: 0;

The final part of spacing that I consider is paragraph indentation. I don’t intend the first paragraph, either I’m using a drop cap or want to follow convention and not indent.

The code uses and adjacent sibling selector to indent paragraphs that are next to each other. This will fail for the first and last paragraphs since there is no paragraph before/after it.

p {
  font-size: 1em;

p + p {
  text-indent: 2em;


It may sound obvious that good type contrast is essential for readability but we always try to push the boundaries of contrast.

We forget that even sighted users may have problems with our “clever” light gray text on white background design assuming that what’s good for us is good for everyone or that everyone will understand the message.

The Web Content Accessibility Guidelines have the following to say about color and contrast.

Don’t use color as the only way to procide information or eliciting a response (Rule 1.4.1)

Color is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element. (Level A)

Rule 1.4.1

Use high contrast between text and background color, with few exception indicated in the rule.

The visual presentation of text and images of text has a contrast ratio of at least 4.5:1, except for the following: (Level AA)

  • Large Text: Large-scale text and images of large-scale text have a contrast ratio of at least 3:1;
  • Incidental: Text or images of text that are part of an inactive user interface component, that are pure decoration, that are not visible to anyone, or that are part of a picture that contains significant other visual content, have no contrast requirement.
  • Logotypes: Text that is part of a logo or brand name has no minimum contrast requirement.

Rule 1.4.3

When planning your site’s color pallete (font, links, headers, additional text colors) you can use tools like Lea Verou’s Contrast Ratio tool.

The other aspect is a matter of cultural awareness. In addition to avoiding using color as the only way to convey meaning we have to be mindful about conveying the right meaning. Colors have different meanings for different cultures.

The table below (From Creating Culturally Customized Content) shows what different colors mean in 5 different countries with vastly different cultures. It is obvious you’re conveying a very different message to each of those audiences, even if they live in the same country.

COLOR USA China India Egypt Japan
Red Danger


Good fortune




Death Anger

Orange Confident




Sacred (the Color Saffron) Virtue




Yellow Coward




Celebration Mourning Grace

Green Spring







Eternal life
Blue Confident







Purple Royalty

Royalty Unhappiness Virtue Wealth
White Purity


Mourning Fun


Joy Purity

Black Funeral




High Quality
Evil Death


Same font for all text or combined?

Does the font you’re using work well for body and headings or will you have to combine it with another font that works better for headings and larger text. Not all fonts lend themselves to this dual usage so you’ll have to be careful in your research to test the font or fots work as intended.

If you use specimens, particularly those that live in the web, use them to test visually if your font or fonts work well for your design.

Media queries are your friend

When we work with media queries we can change every detail of our content, including text layout and attributes. This way when you change the layout for smaller screens. You may also consider adjusting size, leading and other typography items to better suit the smaller size.

Non Design Considerations

There are other considerations outside design that must be taken into account when selecting fonts… as much as I hate to admit it sometimes licensing or cost will drive me away from a font that, otherwise, would be awesome for the project.

These are some of the non-design things that come to mind.

Have you set your viewport correctly?

The viewport meta tag tells the browser how to render the page. This is required for responsive sites (and I’m assuming you’re working on responsive sites, right?!)

<meta name="viewport" content="width=device-width, initial-scale=1">

This will likely render the width of the page at the width of its own screen. So if that screen is 320px wide, the browser window will be 320px wide, rather than way zoomed out and showing 960px or whatever that device does by default.

Do not, I repeat, do not use this meta tag if your site is not responsive. The results are unpredictable but ugly. You’ve been warned.

Are you subsetting your fonts? Pros and cons.

One way to make your fonts files smaller is to subset them. When you subset a font you take out all the characters you’re not using for your content. Depending on the font this may reducen the file size significantly at the risk of having to redo the work every time you add content to your site.

I will not discuss subsetting in detail. I’ve written about it in Subsetting Fonts and More on Font Subsetting.

To load Roboto Regular with only the Basic Latin character range look like this.

@font-face {
  font-family: 'Ubuntu'; /* regular */
  src: url('Ubuntu-R-webfont.woff2') format('woff2'), url('Ubuntu-R-webfont.woff')
      format('woff'), url('Ubuntu-R-webfont.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  unicode-range: U+0020-007f;

One thing that you need to be mindfult of when/if you subset your fonts is to make sure that you keep all the characters you will need for your content to render with the chosen font.

In the case of English using the first 127 characters (equivalent to the ASCII encoding standard) this is usually not a problem. However, if you’re working with more than one language (like English and Spanish or English and Swedish) you will have to make sure that you subset the additional characters that you will need, like opening question (¿) and exclamation mark (¡) or some of the Diacritics and accents needed (like ñ in Spanish or ö in Swedish). These additional characters are distinct Unicode codepoints and need to be included in your subset for it to work.

Does the font have all the weights and styles that you need?

Related to the necessary glyphs are the necessary weights for each font that you use in your page. Ideally we’d use 4 files representing regular, bold, italic and bold italic. We want to avoid the browser creating “faux” synthetic styles that will adversely impact the way your content look.

In Say No to Faux Bold Alan Stearns shows what the impact of faux styles (bold in particular) can have in your content.

If you want more in depth information, check How to use @font-face to avoid faux-italic and bold browser styles. It goes deeper into the technical details than Alan’s article and it’s geared for a more technical audience.

Creating speciments of the fonts you use

An interesting idea is to start building specimen libraries for the fonts you have used in projects or plan to use in projects. If you have a good specimen template wil;l definitely help you in your design process.

In Real Web Type in Real Web Context Tim Brown states the neeed explicitly: “I need to know how my type renders on screens, in web browsers”.

Depending on your needs you could use the W3C’s element sampler and style the bare content with your styles and fonts.

Another option is to use Tim Brown’s [](Web Font Specimen) to create more advanced specimens. I’ve created specimens for fonts with open sources licenses at Font Specimen Archive

Have you tested how will your fallback fonts affect your design?

So far we’ve made the assumption that our fonts will load and everything works well out of the box. But this is not always the case and we shouldn’t assume it is.

This begs the question: How well will the fallback fonts work with your design

If the fallback has a higher or lower x-height or thicker strokes than your web font, the layout will look different. This is why you must test your stack during development and make sure that your web fonts match as close as possible with the fallbacks. Monica Dinculescu’s Font Style Matcher can help

Hosting locally or using a font service?

Are you hosting your fonts with a service like Google Fonts, Adobe Typekit or other font hosting providers or are you hosting your fonts in your own site?

If you choose to host fonts in your own site you will have to contend with network congestions and having your server become a bottleneck for your users. Yet it may be the only way to comply with some fonts restrictive licensing terms and conditions; as we’ll discuss in the next section be very careful about the licensing of your font.

These are some of the font services I’ve used in the past.

As with anything else on the web, your milleage may vary.

Do you have the right license to use the font on the web?

The last, and perhaps most important, non-design aspect of selecting and using fonts on the web is the licensing. Rightly or not some foundries are very restrictive in the terms of their license and some will release their fonts under open source licenses like the SIL OFL or Apache (seen in some early fonts from Google and other companies for whom liability may be an issue).

For most foundries, fonts are software users license rather than a product you can own. This has implications if you want to modify the fonts or want to convert them to other formats. Some foundries will go as far as block usage of Font Squirrel’s Webfont Generator with their fonts.

These problems with Foundries lead me to prefer comissioning fonts specifically for a project. If that is not possible my next option is to choose Open Source Fonts and, only if I have no choice, I will work with foundries based on past experience or expressed instructions from the client.

This is a step we often overlook but is essential to cover yourself from liability.

Structured Metadata in HTML

This post deals mostly with Google’s implementation of JSON-LD structured data.
While the effort was undertaken in 2011/2012 by all major search engines at the time (Google, Bing, Yahoo, and Yandex) not all of them have adopted a common platform. Bing doesn’t support JSON-LD, Yandex has its API behind email registration and I don’t know if Yahoo has any resources to validate JSON-LD for their search engine (particularly post-acquisition).
So, while in theory, this should work in all search engines, only Google provides examples and freely available validation tools.

Also note that this post only covers how to use JSON-LD with types to support search engine result discoverability. It does not discuss how to build APIs with JSON-LD.

One thing I’ve always found intriguing is how to markup data to make it appear like it does in Google’s search results. There are several different structured markup formats. 3 of the most widely used are Microdata, RDFa Lite, and JSON-LD. I’ve chosen to work with JSON-LD because Microdata is not supported across browsers (Webkit removed the feature because “the feature never gained any traction and was eventually removed to clean up the codebase”). Support for the Microdata API was also removed from Blink (Google Chrome). Removal of the feature from a browser also shows us a likely future for Microdata, which is less and less support.

What Is Linked Data

Before jumping straight to JSON-LD (JSON for Linked Data) it would help to understand what Linked Data is and how it relates to the web and other resources.

In computing, linked data (often capitalized as Linked Data) is a method of publishing structured data so that it can be interlinked and become more useful through semantic queries. It builds upon standard Web technologies such as HTTP, RDF, and URIs, but rather than using them to serve web pages for human readers, it extends them to share information in a way that can be read automatically by computers.

Tim Berners-Lee, director of the World Wide Web Consortium (W3C), coined the term in a 2006 design note about the Semantic Web project.[1]

Tim Berners-Lee outlined four principles of linked data in his Linked Data note of 2006, paraphrased along the following lines:

  1. Use URIs to identify things
  2. Use HTTP URIs so that these things can be found and dereferenced
  3. Provide useful information about the object a name identifies when it’s looked up, using open standards such as RDF, SPARQL, etc
  4. Refer to other things using their HTTP URI-based names when publishing data on the Web

What is JSON-LD?

JSON-LD is a lightweight Linked Data format. It is easy for humans to read and write. It is based on the already successful JSON format and provides a way to help JSON data interoperate at Web-scale.

Core Markup

In the following example, we can see some of the basic attributes of a JSON-LD document. I’m working primarily with context and @type

<script type="application/ld+json">
  "@context": "",
  "@type": "Book",
  "accessibilityAPI": "ARIA",
  "accessibilityControl": [
  "accessibilityFeature": [
  "accessibilityHazard": [
  "aggregateRating": {
    "@type": "AggregateRating",
    "reviewCount": "0"
  "bookFormat": "EBook/DAISY3",
  "copyrightHolder": {
    "@type": "Organization",
    "name": "Holt, Rinehart and Winston"
  "copyrightYear": "2007",
  "description": "NIMAC-sourced textbook",
  "genre": "Educational Materials",
  "inLanguage": "en-US",
  "isFamilyFriendly": "true",
  "isbn": "9780030426599",
  "name": "Holt Physical Science",
  "numberOfPages": "598",
  "publisher": {
    "@type": "Organization",
    "name": "Holt, Rinehart and Winston"

Basic JSON-LD concepts

This is a basic (and incomplete) overview of JSON-LD as I’ve used it in search engine optimization markup. It is not complete nor it’s meant to be. If you want more detailed information look at the latest syntax specification from the JSON for Linking Data Community Group.


In JSON-LD, a context is used to match terms in the document to Internationalized Resource Identifiers (IRIs), internationalized URIs/URLs.

The Web uses IRIs for unambiguous identification, a given IRI will match one and only one resource. The idea is that these terms mean something that may be of use to other developers and that it is useful to give them an unambiguous identifier. They will also help prevent collisions if more than one author decides to use the same name: the contexts will be different so they will not collide.

In this example, we are using as the context. All future work will be bound to it.

  "@context": "",
  "@type": "Book"


The type attribute tells us what type of resource it is in the given context. For example, using the following code:

  "@context": "",
  "@type": "Book"

Tells whoever is reading the JSON file that this is defining a book using the context from


The @language attribute tells JSON-LD what language to use for the specified resource. There are multiple ways to use it. In the first example, we set it up on the root context as the default language (unless it’s overwritten)

  "@context": "",
  "@type": "Book",
  "@language": "en"

You can also incorporate the attribute wherever you use the @type attribute, which will override the default for that particular resource only.

Square Brackets

Square brackets exist for situations where there are multiple values for an item property. In the example below, both accessibilityControl and accessibilityFeatures have multiple values, like saying there are multiple accessibility controls and features.

This is different than formal nesting, discussed in the next section.

<script type="application/ld+json">
  "@context": "",
  "@type": "Book",
  "accessibilityAPI": "ARIA",
  "accessibilityControl": [
  "accessibilityFeature": [


Where square brackets give you the option to nest multiple values for the same attribute nested elements use curly brackets {} to insert a completely different but related element.

Or a more complex example of nested elements taken from Google’s Fact Check Structured Data

<script type="application/ld+json">
  "@context": "",
  "@type": "ClaimReview",
  "datePublished": "2016-06-22",
  "url": "",
    "@type": "CreativeWork",
      "@type": "Organization",
      "name": "Square World Society",
      "sameAs": ""
    "datePublished": "2016-06-20"
  "claimReviewed": "The world is flat",
    "@type": "Organization",
    "name": " science watch"
    "@type": "Rating",
    "ratingValue": "1",
    "bestRating": "5",
    "worstRating": "1",
    "alternateName" : "False"
  • JSON-LD nesting checklist
    • Must use the item property (specific to the item type)
    • The value lives in curly braces
    • You MUST identify the item type of that property. The JSON-LD parser must know what type of object you nested
    • Attribute/value properties of the type must be included
    • Follow proper JSON syntax


The @id element gives a unique IRI/URI referencing the element it’s used in. Where there is more than one reference for the same resource (different formats for the same video, or different size for the same image) don’t use @id, just name the resource to follow the guidelines.

  "@context": "",
  "@type": "NewsArticle",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": ""

Commonly used types provides vocabularies for some common types of markup. This is a subset of the material supported by Google and it’s a good starting as it provides a comprehensive cross-list of all the material supported in a given type.

These type descriptions are not like Google’s Search Gallery in that they don’t provide full examples but a list of attributes that you can use for a given content type.

Tagging Examples

Now that we know what Linked Data is, some basics about how to write JSON-LD and where to find more information about it, let’s look at some examples of how Google recommends you write JSON-LD to work with its search engine. We’ll look at both structural and content markup examples.

You can find more fully realized examples in the Google Search Gallery


This example shows the markup for a news article. Notice how it breaks author and publisher into different types and how it can extend those types to get as rich as we need to.

  "@context": "",
  "@type": "NewsArticle",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": ""
  "headline": "Article headline",
  "image": [
  "datePublished": "2015-02-05T08:00:00+08:00",
  "dateModified": "2015-02-05T09:20:00+08:00",
  "author": {
    "@type": "Person",
    "name": "John Doe"
  "publisher": {
    "@type": "Organization",
    "name": "Google",
    "logo": {
      "@type": "ImageObject",
      "url": ""
  "description": "A most wonderful article"

Video Object

The video object describes a video that you may put up on your website as marketing collateral or in a Youtube channel to try to monetize and grow your audience.

  "@context": "",
  "@type": "VideoObject",
  "name": "Title",
  "description": "Video description",
  "thumbnailUrl": "",
  "uploadDate": "2015-02-05T08:00:00+08:00",
  "duration": "PT1M33S",
  "publisher": {
    "@type": "Organization",
    "name": "Example Publisher",
    "logo": {
      "@type": "ImageObject",
      "url": "",
      "width": 600,
      "height": 60
  "contentUrl": "",
  "embedUrl": "",
  "interactionCount": "2347"

Use the Force, Luke

I hate reinventing the wheel. This is where the Google Search Gallery comes in. They provide full markup examples of the most frequently used types.

The library also includes what they call enhancements like breadcrumbs, search boxes, and carousels. These may not be visible on the page that links to the JSON-LD but will allow new functionality in the SERP (Search Engine Results Page) where they appear.

Available examples from the gallery:

  • Content types
    • Article
    • Book
    • Course
    • Dataset
    • Event
    • Fact Check
    • Job Posting
    • Local Business
    • Music
    • Occupation
    • Paywalled Content
    • Podcast
    • Product
    • Recipe
    • Review
    • TV and Movie
    • Video
  • Enhancements
    • Breadcrumb
    • Corporate Contact
    • Carousel
    • Logo
    • Sitelinks Searchbox
    • Social Profile

Process to create JSON-LD data

So what does it take to create a JSON-LD document for a page? I’ve adapted this list from JSON-LD For Beginners. It has been slightly edited for style.

It’s important to note that the first two steps are planning what you will markup and why do you want to do it. Experimenting is OK but marking up content just because you can’t isn’t or at least it shouldn’t be.

  1. Mentally answer:
    • What do you want to mark up?
      • Goal: Determine that you can mark up the content with the vocabulary. Some things may make sense conceptually, but are not available within the vocabulary
    • Why do you want to mark it up?
      • Goal: Determine whether there is a business case for what you want to markup. If you’re not doing it for business reasons or as an experiment you shouldn’t mark content up just for the sake of marking it up
      • You want to mark up content that will help search engines understand the most vital information on your page and maximize your ability to demonstrate that you are the best resource for users
      • Look at Google’s resources on structured data, how they use them, and examples of different types of supported elements and how they recommend using them
  2. If you’re using a markup that Google explicitly supports (i.e., JSON-LD, Microdata or RDFa), open the specific documentation page and any relevant examples
    • Don’t feel like you have to create your markup from scratch. You should understand JSON-LD and the vocabulary
    • If Google or search engine results provide examples of the type of markup you want to use then use it!
  3. Open up the item type page
    • Especially when you’re starting off with, review the technical documentation page to get an idea of the item type; what it entails, and its properties. This can help you better understand an example or make it easier to create your own content
  4. Copy/paste the immutable elements
    • Copying and pasting the required elements saves time and mental energy. If you’re using an external example the immutable elements should already be present
    • Occasionally an examples may leave out the script tags. Please note that they are vital; Browsers will not parse JavaScript outside script tags or properly linked scripts
  5. Add desired item type you’re marking up as the value of @type:
  6. List item properties and values
    • This step doesn’t require syntax and is more of a mental organization exercise. Concentrate on what you want to markup — don’t sweat the details yet.
    • You may have ideas about what you want to mark up, but may not necessarily know if it’s possible within the vocabulary or how it’s nested
  7. Add JSON-LD syntax, nesting where required/appropriate
    • The step where you finish up your model, nest it, and put markup together
  8. Test with the Structured Data Testing Tool
    • The tool is your linter for grammar and syntax. Confirm that the elements and attributes you use are well formed and pass validation. When in doubt, check the Google documentation and the corresponding entry in
  9. Determine strategy for adding to the webpage
    • You must inline the JSON-LD data in a script tag in the head of the page
    • If you’re working with dynamic content or through a CMS, work with your dev team to add the script to the page

Review the Guidelines

Once you have the markup that you want to use and have validated it using Google’s validation tool, make sure to check their Structured Data General Guidelines, particularly their quality guidelines. This will save you time and potential headaches since Google does not guarantee that your structured data will show up in search results, even if your page is marked up correctly according to the Structured Data Testing Tool and this may be caused by a variety of reasons.

But if you’ve created good content and marked up your JSON-LD appropriately the search results should become a much better experience for the people using search engines.