Speech Synthesis API: you talk to the computer

Speech Recognition only works in Chrome and Opera. Firefox says it supports it but it doesn’t work and returns a very cryptic error message.

The second part of the Speech Synthesis API is recognition. Where in the prior post we used the Speech Synthesis API to have the browser talk to use when there was a problem on the form; in this post we’ll use a [demo from Mozilla](demo from Mozilla) to illustrate a potential use of speech recognition to make the browser change the background color of the page based on what the user speaks in to a microphone.

Because we’re using the user’s microphone the page/application must explicitly ask for permission and it must be granted before any of this code will work. This permission can be revoked at any time.

The HTML is simple. A place holder paragraph to hold any hints we provide the user and another paragraph to hold the output of the processes.

<h1>Speech color changer</h1>

<p class="hints"></p>
    <p class="output"><em>...diagnostic messages</em></p>

The first portion of the script creates a JSGF Grammar for the elements we want to recognize. JSGF is an old Sun Microsystems W3C submission that was used as the basis for the W3C Voice Browser Working Group (closed on October, 2015).

This will create the vocabulary that the rest of the script will recognize.

var colors = [ 'aqua' , 'azure' , 'beige', 'bisque', 'black', 'blue', 'brown', 
'chocolate', 'coral', 'crimson', 'cyan', 'fuchsia', 'ghostwhite', 'gold', 
'goldenrod', 'gray', 'green', 'indigo', 'ivory', 'khaki', 'lavender', 'lime', 
'linen', 'magenta', 'maroon', 'moccasin', 'navy', 'olive', 'orange', 'orchid',
'peru', 'pink', 'plum', 'purple', 'red', 'salmon', 'sienna', 'silver', 'snow',
'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'white', 'yellow'];
var grammar = '#JSGF V1.0; grammar colors; public <color> = ' + colors.join(' | ') + ' ;'
// You can optionally log the grammar to console to see what it looks like

We next setup the speech recognition engine. The three variables: SpeechRecognition, SpeechGrammarList and SpeechRecognitionEvent have two possible values, either an unprefixed version (not supported anywhere) or the webkit prefixed version (supported by Chrome and Opera). It’s always a good idea to future proof your code; I suspect that when Firefox finally supports the API it will be unprefixed.

var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition;
var SpeechGrammarList = SpeechGrammarList || webkitSpeechGrammarList;
var SpeechRecognitionEvent = SpeechRecognitionEvent || webkitSpeechRecognitionEvent;

The script then does assignments. First it associates variables with the speech recognition engine we set up above. Next the script configures the engine with the grammar created earlier and the attributes for the recognition engine to work. It also create placeholders for the HTML elements that will hold messages to the user and will actually change the background color.

var recognition = new SpeechRecognition();
var speechRecognitionList = new SpeechGrammarList();
speechRecognitionList.addFromString(grammar, 1);
recognition.grammars = speechRecognitionList;
//recognition.continuous = false;
recognition.lang = 'en-US';
recognition.interimResults = false;
recognition.maxAlternatives = 1;

var diagnostic = document.querySelector('.output');
var bg = document.querySelector('html');
var hints = document.querySelector('.hints');

For each color that we make available to the user, we use the color as the background-color to give the user an additional cue regarding the colors he can choose from.

var colorHTML= '';
colors.forEach(function(v, i){
    console.log(v, i);
    colorHTML += '<span style="background-color:' + v + ';"> ' + v + ' </span>';

The script is almost ready to start. It adds a message to the .hint container and, when the user clicks anywhere on the page, it begins the recognition process.

hints.innerHTML = 'Tap/click then say a color to change the background color of the app. Try '+ colorHTML + '.';

document.body.onclick = function() {
    console.log('Ready to receive a color command.');

When the user speaks and the script gets a result the SpeechRecognitionResultList object contains SpeechRecognitionResult objects. It has a getter so it can be accessed like an array.

The last variable holds the SpeechRecognitionResult object at the last position.

Each SpeechRecognitionResult object contains SpeechRecognitionAlternative objects that contain individual results. The last element represents the latest result the script obtained and, using the array notation we get the transcript for first result ([0]) of the latest (last) response the client has stored.

The script will allso display the result it received (sometimes it’s funny to see what the recognition engine thinks you said), change the background color to the specified color and provide a confidence level percentage, the higher the number the most likely the user will get a correct answer and the color will change.

recognition.onresult = function(event) {
    var last = event.results.length - 1;
    var color = event.results[last][0].transcript;

    diagnostic.textContent = 'Result received: ' + color + '.';
    bg.style.backgroundColor = color;
    console.log('Confidence: ' + event.results[0][0].confidence);

The final part of the script handles additional events. When the script detects the end of speech it stops the recognition. When there is no match it will notify the users in the diagnostic element. Finally, if there is an error, we also report it to the user.

recognition.onspeechend = function() {

recognition.onnomatch = function(event) {
    diagnostic.textContent = "I didn't recognise that color.";

recognition.onerror = function(event) {
    diagnostic.textContent = 'Error occurred in recognition: ' + event.error;

Full recognition example

Speech Recognition only works in Chrome and Opera. Firefox says it supports it but it doesn’t work and returns a very criptic error message.

There is another way to work with speech recognition: dictation. Rather than work through the example that illustrates how we can use the recognition portion of the API to create a dictation application that you can then copy of email.

Code demo

All the code for this posts is available on Github

Speech Synthesis API: computer talks

Accidentally I discovered a new API that makes it easier to interact with your site/app using your voice. The Speech Synthesis API provide both ends of the computer conversation, the recognition to listen and the synthesis to speak.

Right now I’m more interested in the synthesis part and how we can include it as additional feedback on our sites and applications as an additional cue for user interaction.


The Speech Syntthesis API gives us a way to “speak” strings of text without having to record them. These ‘utterances’ (in API speak) can be further customized.

At the most basic the utterance is made of the following:

  • An instance of SpeechSynthesisUtterance
  • The text and language we want the voice spoken in
  • The instruction to actually speak the command using speechSynthesis.speak
var msg1 = new SpeechSynthesisUtterance();
msg1.text = "I'm sorry, Dave, I can't do that";
msg1.lang = 'en-US';


The example below changes the content and the language to es-cl (Spanish as spoken in Chile). The structure of the code is the same.

var msg2 = new SpeechSynthesisUtterance();
msg2.text = "Dicen que el tiempo guarda en las bastillas";
msg2.lang = 'es-cl';


Copy and past each example in your Dev Tools (I use Chrome’s and have tested in Chrome and Firefox) and notice how different the default voices are for each message and for each browser you test with.

We can further customize the utterance with additional parameters. The parameters are now

  • msg contains a new instance of SpeechSynthesisUtterance
  • voices contains an array of all the voices available to the user agent (browser in this case)
  • voice assigns a voice from the voices array to the instance of utterance we are working with
  • voiceURI specifies speech synthesis voice and the location of the speech synthesis service that the web application wishes to use
  • rate indicates how fast the text is spoken. 1 is the default rate supported by the speech synthesis engine or specific voice (which should correspond to a normal speaking rate). 2 is twice as fast, and 0.5 is half as fast
  • pitch specifies the speaking pitch for the utterance. It ranges between 0 and 2 inclusive, with 0 being the lowest pitch and 2 the highest pitch. 1 corresponds to the default pitch of the speech synthesis engine or specific voice

As before text holds the message we want the browser to speak, lang holds the language we want the browser to speak in and the speechSynthesis.speak command will actually make the browser speak our phrase.

var msg = new SpeechSynthesisUtterance();
var voices = window.speechSynthesis.getVoices();
// Note: some voices don't support altering params
msg.voice = voices[0]; 
msg.voiceURI = 'native';
msg.volume = 1; // 0 to 1
msg.rate = 1; // 0.1 to 10
msg.pitch = 2; //0 to 2
msg.text = 'Hello World';
msg.lang = 'en-US';


Putting speech synthesis into action: why and where would we use this?

The most obvious place for me to use speech synthesis is as additional cues and messages to end-users when there is an error and problem. We’ll define three different functions to encapsulate the errors messages we want to “talk” to the user about.

For this example we’ll use the following HTML code:

<form id="form">
    <legend>Basic User Information</legend>
    <label for="username">Username</label>
    <input id="username" type="text" placeholder="User Name">
    <label for="password">Password</label>
    <input id="password" type="password" placeholder="password">

For the sake of the demo I’m only interested in the input fields and not in having a fully functional working form.

I will break the Javascript portion of the demo in two parts. The first part defines the Speech Recognition portion of the script which is very similar to the examples we’ve already discussed.

// Setup the Username Empty Error function
function speakUsernameEmptyError() {
  let msg1 = new SpeechSynthesisUtterance();

  msg1.text = "The Username field can not be empty";
  msg1.lang = 'en-US';


// Setup the Password Empty Error function
function speakPasswordEmptyError() {
  let msg2 = new SpeechSynthesisUtterance();
  msg2.text = "The Password field can not be empty";
  msg2.lang = 'en-US';


The second part of the script assigns blur event listeners to the input elements. Inside each event handler the code checks if the field is empty. If it is the code adds a 1px red border around it and plays the appropriate utterance we crafted earlier. If the field is not empty, either because the user entered a value before moving out of the field or at a later time, we set the border to a 1-pixel solid black color.

// Assign variables to hold the elements
let username = document.getElementById('username');
let password = document.getElementById('password');

// Add blur event listener to the username field
username.addEventListener('blur', function() {
  // If the field is empty
  if (username.value.length <= 0) {
    // Put a 1 pixel red border on the input field
    username.style.border = '1px solid red';
    // Speak the error as specified in the
    // speakUsernameEmptyError function
  } else {
    username.style.border = '1px solid black';

// Add blur event listener to the password field
password.addEventListener('blur', function() {
  // If the field is empty
  if (password.value.length <= 0) {
    // Put a 1 pixel red border on the input field
    password.style.border = '1px solid red';
    // Speak the error as specified in the
    // speakPasswordEmptyError function
  } else {
    password.style.border = '1px solid black';

The functions and event listeners are very basic and could stand some additional work, particularly in the validation area.


Converting Markdown to Slides

If you’ve seen some of my earlier posts about Markdown you know that I love the flexibility of writing Markdown and then generate other formats. Using my starter kit I can generate HTML and PDF from the same Markdown source.

I found out a project to convert Markdown to Google Slides using a modified Markdwon parser and the Slides API to generate complete presentations.

In this essay I’ll look at tree aspects of this process:

  • How to run the tool inside a Gulp build process
  • The md2gslides specific syntax for different types of slides
  • The code for some of the parser functionality to generate these types of content

We could use code from Literate CSS to build both the narrative and the presentation for a given content. In the future we may want to use our own custom parser so we write less raw HTML in the Markdown files.

Running the tool inside a build script

I’ll use the same tools from the starter project to add the slide functionality. We don’t need to add any plugins for the code to work.

The task is simple. It takes all the Markdown files from the src/slides directory and run the md2gslides utility to convert them to Google Slides.

// Build Google Slides
gulp.task('build-slides', () => {
  let options = {
    // default = false, true means don't emit error event
    continueOnError: false, 
    // default = false, true means stdout is written to file.contents
    pipeStdout: false, 
  let reportOptions = {
    // default = true, false means don't write err
    err: true, 
    // default = true, false means don't write stderr
    stderr: true, 
    // default = true, false means don't write stdout
    stdout: true 
  return gulp.src('./src/slides/*.md')
  .pipe($.exec('md2gslides --style github <%= file.path %> ', options))


Each slide is typically represented by a header, followed by zero or more block elements. The tool uses a modified markdown parser to generate the content.

Begin a new slide with a horizontal rule (---). The separator is optional on the first slide.

The following examples show how to create slides of various layouts:

Title slide


    # This is a title slide
    ## Your name here
Title Slide

Section title slides


    # This is a section title
Section title slide

Section title & body slides


    # Section title & body slide

    ## This is a subtitle

    This is the body
Section title & body slide

Title & body slides


    # Title & body slide

    This is the slide body.
Title & body slide

Main point slide

Add {.big} to the title to make a slide with one big point


    # This is the main point {.big}
Main point slide

Big number slide

Use {.big} on a header in combination with a body too.


    # 100% {.big}

    This is the body
Big number slide

Two column slides

Separate columns with {.column}. The marker must appear
on its own line with a blank both before and after.


    # Two column layout

    This is the left column


    This is the right column
Two Column Slide


Inline images

Images can be placed on slides using image tags. Multiple images
can be included. Mulitple images in a single paragraph are arranged in columns,
mutiple paragraphs arranged as rows.

Note: Images are currently scaled and centered to fit the
slide template.


    # Slides can have images

Image Slide

Background images

Set the background image of a slide by adding {.background} to
the end of an image URL.


    # Slides can have background images

Slide with background image


Include YouTube videos with a modified image tag.


    # Slides can have videos

Slide with video

Speaker notes

Include speaker notes for a slide using HTML comments. Text inside
the comments may include markdown for formatting, though only text
formatting is allowed. Videos, images, and tables are ignored inside
speaker notes.


    # Slide title


    These are speaker notes.


Basic formatting rules are allowed, including:

  • Bold
  • Italics
  • Code
  • Strikethrough
  • Hyperlinks
  • Ordered lists
  • Unordered lists

The following markdown illustrates a few common styles.

**Bold**, *italics*, and ~~strikethrough~~ may be used.

Ordered lists:
1. Item 1
1. Item 2
  1. Item 2.1

Unordered lists:
* Item 1
* Item 2
  * Item 2.1

Additionally, a subset of inline HTML tags are supported for styling.

  • <span>
  • <sup>
  • <sub>
  • <em>
  • <i>
  • <strong>
  • <b>

Supported CSS styles for use with <span> elements:

  • color
  • background-color
  • font-weight: bold
  • font-style: italic
  • text-decoration: underline
  • text-decoration: line-through
  • font-family
  • font-variant: small-caps


Use Github style emoji in your text using
the :emoji:.

The following example inserts emoji in the header and body of the slide.

### I :heart: cats


Code blocks

Both indented and fenced code blocks are supported, with syntax highlighting.

The following example renders highlighted code.

### Hello World

console.log('Hello world');

To change the syntax highlight theme specify the --style <theme> option on the
command line. All highlight.js themes
are supported. For example, to use the github theme

$ md2gslides slides.md --style github


Tables are supported via
GFM syntax.

Note: Including tables and other block elements on the same slide may produce poor results with
overlapping elements. Either avoid or manually adjust the layout after generating the slides.

The following generates a 2×5 table on the slide.

### Top pets in the United States

Animal | Number
Fish   | 142 million
Cats   | 88 million
Dogs   | 75 million
Birds  | 16 million

More information

Is this the only way to automate creation of Google Slides? No, it isn’t. Google provides an API that allows developers to programmatically create presentations, slides and slide content. The G Suite Dev Show provides tutorials in addition the tutorials and examples in the API website.

Local apps versus hosted tools

Rachel Andrew’s It’s more than just the words triggered a reflection that I want to share with you.

Does anyone remember when the only way to get a site online was to get your school (if you were so lucky) to provide you space for your web presence. Then came Geocities and other ‘mass’ host providers and we saw the birth and proliferation of the “weird” web… either one was the place where most of us began to play with the web.

Example Geocities Pages

Then some of us moved to private commercial hosting. I went through IO.com mostly because I could get free access to Steve Jackson’s game at the time. The page below is an experiment from that era

Author’s home page circa 1995 via the Wayback machine

It evolved from there both in terms of hosting (where it went to the school’s server, to IO and then to the WELL) and in terms of content, the more I learned the better I was able to present the content both in terms of HTML structure, CSS presentation and Javascript behaviors.

As time passed we’ve been able to host our content in third party applications and it became easier:

  • We can upload images to Flickr or Google Photos
  • We can host our written content in WordPress blogs, AWS or Google Cloud
  • Video can be hosted in Youtube or Vimeo
  • Code samples can be placed in JSbin or Codepen

But, as Rachel points out on her post, we loose flexibility for convenience. We loose the flexibility of creating our own designs and surrender to the way other people want us to create our content.

In the next sections i’ll explore some of the issues involved in self-hosting versus third party tools and why i keep open spaces to play with technology regardless of where it goes…

Before we start: CORS, What it is and how it works

Before jumping into hosting locally versus using third party hosted apps I’ll take a little detour and talk about CORS and how it works. CORS is essential for cross origin work and some of the APIs will only work with resources from the same origin unless CORS is enabled.

A cross-origin request is one where the resource is located in a different server than the one making the request. For example the following example makes a cross-origin request from server a to server b.

<!-- This is a normal link pointing to a resource on same server -->
<a href="resource.html">local resource on server a</a>
<!-- This link is placed on server a -->
<a href="http://www.b.com/resource.html">Link to resource on server b</a>

For security reasons, browsers restrict cross-origin HTTP requests initiated from within scripts. For example, XMLHttpRequest and Fetch follow the same-origin policy. So, a web application using XMLHttpRequest or Fetch could only make HTTP requests to its own domain. To improve web applications, developers asked browser vendors to allow cross-domain requests.

Another example of how CORS requests work. Taken from MDN

CORS (Cross-Origin Resource Sharing) provides a way for servers to indicate who can access their resources. The only thing necessary for CORS to work is a header on a server’s response to indicate to indicate they they can serve resources across domains. The header is:

Access-Control-Allow-Origin", "*"

How you configure the server to send the CORS header will depend on txhe server you’re working with. This information was taken from enable-cors.com


Adding the header with express is fairly simple. We create a an app.use block that adds the headers before passing to the next step in the chain.

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
             "Origin, X-Requested-With, Content-Type, Accept");

app.get('/', function(req, res, next) {
  // Handle the get for this route

app.post('/', function(req, res, next) {
 // Handle the post for this route

Apache config or .htaccess

Apache’s configuration style can be used in three different places:

  • Default configuration
  • Directory / Virtual Host configuration
  • .htaccess configuration

To enable CORS we use mod_headers to set the header we want to, something like this:

Header set Access-Control-Allow-Origin "*"

Although mod-headers is installed by default on newer Apache configurations, please make sure it’s enabled on your system:

a2enmod headers

Unless you added the header to an .htaccess file you must restart the server for the changes to take effect. Th wayyou do it depends on your syste. It’s either:

apachectl -t

sudo service apache2 reloadx


apachectl -t

apachectl -k graceful


Based on Michiel Kalkman nginx cors open configuration we can see that an nginx configuration is a little more commplicated. We’ve added more than just the CORS header for each of the methods we want to work with (GET, POST, and OPTIONS).

Test in your server setup before deploying to a production environment.

location / {
  if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' '*';
    # Om nom nom cookies
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    # Custom headers and headers various browsers **should** be OK 
    # with but aren't
    add_header 'Access-Control-Allow-Headers' 
    # Tell client that this pre-flight info is valid for 20 days
    add_header 'Access-Control-Max-Age' 1728000;
    add_header 'Content-Type' 'text/plain charset=UTF-8';
    add_header 'Content-Length' 0;
    return 204;
  if ($request_method = 'POST') {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,
  if ($request_method = 'GET') {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 

Going with third party hosting

I want to start by saying that I’ve got nothing against third party hosting in general or the companies i will mention in this section. Most of the time the tools and their use are ideal for a user’s skill level or for the audience that a project is designed for.

The flip side is that we limit ourselves to the way the companies want us to work and the technology and licenses we can use in our projects.

Two of my biggest concerns have always been:

  • What happens if a service goes down and how do we migrate the content from the service to a new offering
  • How much will it cost (both time and money) to move from one service to another.

Some of my experiences using other hosted services include the ones below.


Take the WordPress hosting service (WordPress.com) for example.

It is free of charge and it allows to serve content within minutes of setting it up with a wide variety of themes that are available both free of charge and paid. there are a variety of plugins that enhance the site’s functionality.

The customization is what I consider the weakest aspect of WordPress.com. While I understand the need to keep a hosting environment working well for everyone I find it restricting that you can’t upload your own themes or themes that are provided outside WordPress.

In short unless you can find the perfect theme or one that is easily customizable to meet your goals there is no real way to get your theme to run in the hosted WordPress.com.

Google Photos

Google Photos images are stored in the cloud and indexed by Google using their technologies available to the Google platform. However there is no obvious way to retrieve a URL to link to the images from outside the Photos platform. There is also no obvious way to set up CORS.


Codepen is one of those awesome all-in-one code tool. It allows you create HTML, CSS and Javascript and their variations along with different CSS and Javascript libraries and frameworks. It’s an awesome too for collaborative development, pair programming and live coding presentations at conferences and other events.

When I first started using it I chose it because of its simplicity and, back then, because it had features that were unique to the platform… and for the longest time they worked well.

Embed preview/creation dialogue

Over time the sheer number of embeds that I use made Codepen use prohibitive in terms of performance. The Javascript code for the emebeds is not shared across multiple embeds and I’ve never been certain on how (if) they are cached by the browser. The more distinct elements that we need to fetch from the network the higher the potential for our application to become unresponsive or sluggish.

Hosting locally

In the previous section we discussed some of the shortcomings of using hosted versions of some popular applications. Now I’ll discuss some of my issues when working with the same or similar applications hosted in your own server that you have complete (or almost complete) control of the application and the hosting environment.

This is both a blessing and a curse.

It is a blessing because you can do a lot of things that a hosted service doesn’t allow you to do and you don’t have to worry about the maintenance of the backend. I can run most non-java and web-based applications as my server storage space allows. At some point I had Drupal, Joomla and WordPress installed and running simultaneously on my host.

It is a curse because the hosting provider may severely limit how you run your server…. Most modern VPS and dedicated virtual servers come bundled with cPanel or Plesk and that means that you’re forced to do things the way the control panel wants you to do it.

For most people this may be ok. I’ve got a different opinion. I grew up as a techie in an environment where everything was installed by hand and I miss being able to do manual installations and configurations without having to learn how Plesk wants me to do it.


I host 2 blogs with WordPress, my personal blog started 10 years ago with 1 and 1 and later moved to Media Temple where it’s lived since. My professional / technical blog has been living in Media Temple since I started it in 2010.

Some of the things I particularly like about hosting WordPress on my own:

  • I can run the SVN version of WordPress which means I can run my development environment with the latest WordPress code
  • It gives me the ability to install and run any theme or plugin I want or need and I can debug them with whatever tools I need

Some of the downsides:

  • You must know you way around a Unix system both through command line and GUI interfaces in Unix or OS X. There’s probably a way to automate the process with a GUI but I choose to do it manually. It works better and it’s what I’m used to it
  • Unless you’re working directly on the server and Vi (the editor) is your friend you must have a way to transfer the files to the server for your production system. Either through SFTP, FTP or some other way to transfer files

Google Cloud

It is technically possible to serve static HTTP sites from Google Cloud Storage buckets. The information to create the static site in the Google Cloud Storage documentation site under Hosting a Static Website.

As the site warns this will only work with HTTP sites (shame Google Cloud) but they provide these alternatives to deliver content through HTTPS.

COnsidering that most technologies now require HTTPS to work this makes Google Cloud a less appealing alternative for hosting experimental content.

Adding AMP to a WordPress blog


I love AMP as a technology. As restrictive as it is I think it’s so for a reason. We’ve let the web grow fat and we need a way to work around that until technologies like Promised IndexedDB, ServiceWorkers and other client-side caching and application like functionality are fully implemented across browsers.

I will never implement an AMP parser by hand and fortunately I don’t have to 🙂

Automattic, the company behind wordpress.com and WordPress VIP has created an AMP plugin that I use in all my WordPress blogs.

Once the plugin is installed and activated it will dynamically generate an AMP version for all my posts. If you have pretty permalinks enabled you can view the amp content by appending amp to the URL. For example, if we want to view the AMP version of this post: https://publishing-project.rivendellweb.net/introducing-vtt/ you’d visit: https://publishing-project.rivendellweb.net/introducing-vtt/amp.

If you need to pretty permalinks ThemeLab provides instructions on how to setup pretty permalinks in WordPress

If you do not have pretty permalinks enabled, you can do the same thing by appending ?amp=1, i.e. http://example.com/2016/01/01/amp-on/?amp=1

The important thing to notice is that enabling the plugin doesn’t provide a way to automate the linking of the AMP content to make it viewable in the blog. I’ll take care of it in the next section.

Customizing a WordPress template

As we saw earlier creating the AMP content is easy. The plugin provides a good default AMP content is good enough to share but will not modify the theme to show a link to the AMP version of the post… if you know that adding amp will show you the AMP content you’re good but not many people know that or care it to remember such information.

Instead we’ll customize a WordPress template to add a link to the post’s AMP version of the content currently displayed.

I am using a child theme of Twenty Seventeen, the default theme for WordPress as of WordPress 4.7, so I had to copy the files to the child theme directory. The file I’m modifying is located at: wp-content/themes/twentyseventeen-child/template-parts/post/content.php

What I want to do is the following: If the user is viewing a single post we’ll add a paragraph with the class ‘amp-link’ and the content that tells the user that the post is also available as an AMP page with a link to the AMP version of the post.

Note that I’ve only extracted the header element that surrounds the changes I want to make. This is not the full template.

<header class="entry-header">
    if ( 'post' === get_post_type() ) :
      echo '<div class="entry-meta">';
        if ( is_single() ) :
        else :
          echo twentyseventeen_time_link();
        echo '</div><!-- .entry-meta -->';

    if ( is_single() ) {
      the_title( '<h1 class="entry-title">', '</h1>' );?>
      <p class='amp-link'>Also available in <a href='<?php the_permalink(); ?>/amp'>AMP</a></p>
    <?php } else {
        the_title( '<h2 class="entry-title"><a href="' . 
        esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
</header><!-- .entry-header -->

The amp-link css is simple, it makes the font 80% of the main body font size and aligns the text to the right.

.amp-link {
    font-size: 80%;

That’s all folks. With these customization AMP content is now available to users without having to go through search engines or any other third party. It’s the little things like this that make me really love WordPress as a platform.