Overview
I’ve had voice search capability on my photography website for several years. When I decided to revamp the website with a new WordPress theme, I discovered that the HTML5 Web Speech API had changed and my old code no longer worked. A little research showed me the correct way to get speech recognition working again. Problem solved, right?
Well, not right. In my implementation, I have placeholder text that says, Search (type or speak) …
, plus a microphone icon to give users a place to tap or click for voice search. But what about browsers that don’t support the Web Speech API? Which, , is all of them except for Chrome for desktop and Android. In other words, my placeholder and icon are instructing visitors to try a feature that won’t work for just over half of them. Can you say, anti-pattern
?
Clearly, I needed to serve the alternate search feature only to supported browsers. The 1st hurdle is feature detection. Plenty has been written about why you shouldn’t use user agent strings to accomplish this. Currently, the only reliable method is feature detection via JavaScript.
Okay, I’ve got feature detection, now what do I do with it? My first thought was to inject a class name in the DOM via jQuery and then run a PHP filter to render the appropriate form.
Except that will not work. PHP operates server-side, so by the time any JavaScript gets around to changing the DOM, all the PHP has already run. If only there were some way to get JavaScript to run a PHP action after it has loaded. Well, there is; it’s called Asynchronous JavaScript and XML
(AJAX).
Dependencies
Before you start, there are a couple of dependencies to get everthing working. If you want to use a custom icon in your input, you’ll want to download FontAwesome. I recommend you embed your script via their CDN and render your icons via their SVG + JavaScript method.
Since we’ll be writing our JavaScript AJAX call in jQuery, you’ll need jQuery.js as well. jQuery comes standard with WordPress, so it should already be loading in your theme, but if it isn’t, you can either download it, install it on your server, and then link to it in your theme template, or you can link to it directly from a CDN.
Plugin, or Child Theme?
There are 3 ways you can go about this:
- Write a plugin
- Create your own theme from scratch
- Modify a child theme
This tutorial uses the last method. If you’re writing a plugin, you’ll still have to modify the functions.php
in your theme or child theme. If you aren’t writing your own theme, you must make the changes to functions.php
in a child theme, because if you make them in a parent theme they will be overwritten when it is updated.
If you don’t know what a child theme is or how to make 1, Google it or read the Codex. Done? Okay, let’s get started.
Create the Script
Open a new file and save it as speech-input.js
. Save it in your project. Where you put it depends on your theme’s directory structure. Your child theme doesn’t need all of the files from the parent theme, but to keep things organized, you should use the same structure it uses. Here’s an example of how you might organize your files:
theme-folder/ ├── css/ │ ├── forms.css │ ├── fontawesome.css | └── fa-svg-with-js.css ├── include/ │ ├── some-php-stuff.php ├── js/ │ ├── speech-input.js │ ├── speech-input.min.js │ ├── jquery.js │ ├── jquery.min.js │ ├── fontawesome.js │ ├── fontawesome.min.js │ ├── fa-solid.js │ └── fa-solid.min.js ├── functions.php ├── custom.css └── style.css
Note the other files based on our dependencies. If you’re loading them from a CDN (recommended), you won’t need these unless you implement them as a fallback.
Register, Enqueue, & Localize the Script
Open your child theme’s functions.php
. Add the following function somewhere after the opening <?php
tag:
/*
* Enqueue and localize AJAX scripts for speech recognition
*/
add_action( 'wp_print_scripts', 'my_search_ajax_enqueue' );
function my_search_ajax_enqueue() {
if ( ! is_admin() ) {
$protocol = isset( $_SERVER['HTTPS'] ) ? 'https://' : 'http://';
wp_register_script( "my-search-ajax-script", get_stylesheet_directory_uri() . '/js/speech-input.js', array( 'jquery' ) );
$params = array( 'ajaxurl' => admin_url( 'admin-ajax.php', $protocol), );
wp_localize_script( 'my-search-ajax-script', 'myVoiceSearch', $params );
wp_enqueue_script( 'my-search-ajax-script' );
};
}
Let’s break that down, line by line:
add_action( 'wp_print_scripts', 'my_search_ajax_enqueue' );
This is the standard action to enqueue and print a script or style in a theme.
function my_search_ajax_enqueue() {
The function referenced by wp_print_scripts()
.
if ( ! is_admin() ) {
Load the script only on the front-end.
$protocol = isset( $_SERVER['HTTPS'] ) ? 'https://' : 'http://';
Create a variable, $protocol
, for callback later.
wp_register_script( "my-search-ajax-script", get_stylesheet_directory_uri() . '/js/speech-input.js', array( 'jquery' ) );
Registers the script. If building a theme from scratch, use get_template_directory_uri()
. In either case update the path to the file to reflect your directory structure.
$params = array( 'ajaxurl' => admin_url( 'admin-ajax.php', $protocol), );
Another variable, for use in the next line.
wp_localize_script( 'my-search-ajax-script', 'myVoiceSearch', $params );
In WordPress, you don’t call your AJAX script directly. All calls are routed through /wp-admin/admin-ajax.php
. If we expand the variables $protocol
and $params
, this code looks like:
wp_localize_script(
'my-search-ajax-script',
'myVoiceSearch',
array(
'ajaxurl' => admin_url (
'admin-ajax.php',
isset(
$_SERVER['HTTPS']
) ? 'https://' : 'http://'
),
)
);
1st, we name the script to be localized, reusing our handle from the previous lines, my-search-ajax-script
. Then we specify a handle, myVoiceSearch
, to be used in our AJAX call. Hyphens are not allowed in the handle; use camelCase or underscores. ajaxurl
is the handle pointing to /wp-admin/admin-ajax.php
. isset […]
tells the function to localize the script over both HTTP and HTTPS connections.
wp_enqueue_script( 'my-search-ajax-script' );
Lastly, we enqueue the script the WordPress way.
That’s it for functions.php
for now — we’ll add some more code in a bit. Save the file and upload it to your child theme’s root. Now fire up your browser and check the source code — you should see a link to speech-input.js
in your document <head>
.
Enable HTML5 Web Speech API
Open speech-input.js
and paste the following code:
// Add HTML5 Web Speech API
function startDictation () {
if (window.hasOwnProperty('webkitSpeechRecognition')) {
var recognition = new webkitSpeechRecognition()
recognition.continuous = false // Wait for dictation to end before parsing
recognition.interimResults = false // Don't generate interim results
recognition.lang = 'en-US' // English
recognition.start() // Init
recognition.onresult = function (event) {
document.getElementById('transcript').value = event.results[0][0].transcript // Parse results & change the value of the search input to match
recognition.stop() // Stop parsing speech
document.getElementById('my-voice-search').submit() // Submit the search form
}
recognition.onerror = function (event) {
recognition.stop() // Stop on error
}
}
}
Save the form and exit. Note the IDs my-voice-search
and transcript
; later we’ll give our <form>
element the former and our <input>
the latter.
Create the Forms
Open functions.php
and paste this code below the my_search_ajax_enqueue()
function. Remember to replace https://mysite.com
with the URL for your website!
/**
*
* Generate custom search form
*
**/
function my_voice() {
$form = '<form id="my-voice-search" itemscope itemprop="potentialAction" itemtype="https://schema.org/SearchAction" method="get" action="https://mysite.com/">
<div class="speech">
<label class="search-form-label screen-reader-text" for="s">Search (type or speak) …</label><input type="text" name="s" id="transcript" placeholder="Search (type or speak) …" />
<span id="search-mic" onclick="startDictation()"><i class="fas fa-microphone-alt"></i></span>
</div>
</form>';
echo $form;
wp_die();
}
add_action('wp_ajax_my_voice', 'my_voice');
add_action('wp_ajax_nopriv_my_voice', 'my_voice');
function my_novoice() {
$form = '<form id="my-search" itemscope itemprop="potentialAction" itemtype="https://schema.org/SearchAction" method="get" action="https://mysite.com/">
<div class="no-speech">
<label class="search-form-label screen-reader-text" for="s">Search …</label><input type="text" name="s" placeholder="Search …" />
<span id="search-search"><i class="fas fa-search"></i></span>
</div>
</form>';
echo $form;
wp_die();
}
add_action('wp_ajax_my_novoice', 'my_novoice');
add_action('wp_ajax_nopriv_my_novoice', 'my_novoice');
This code contains 2 actions: the 1st creates a form with speech-recognition capability and the 2nd creates a regular search form. wp_ajax_nopriv_my_voice
and wp_ajax_nopriv_my_novoice
are needed so the call will work for un-authenticated users. Note we end both functions with wp_die();
. This is required when calling the function via AJAX to terminate the process immediately and return a proper response.
Later, when we write the AJAX scripts, we will reference the function names my_voice
and my_novoice
.
The forms have different placeholder texts and icons appropriate for their type. Here are some minimal styles for the form; adjust as needed and add to your child theme’s style.css
or custom.css
.
.speech {
width: 30rem;
padding: 0;
margin: 0;
}
.speech input {
width: 24rem;
display: inline-block;
height: 3rem;
}
.speech span#search-mic,
.no-speech span#search-search {
font-size: 2.1rem;
display: inline-block;
margin: -3rem;
}
input#transcript {
font-size: 1.4rem;
}
Preparing the HTML
Our AJAX script is going to insert the forms into an element we specify, and replace whatever is already there. If your theme already has a default search form in it, that’s what you’ll want to target. Open your website, fire up your favorite developer tool or inspector, and examine the source code. Identify the class or ID of the form wrapper. As long as the only thing inside the wrapper is the form, it’s safe to target that element. For instance, you might have:
<div id="my-theme-search-wrap">
<form class="search-form">[…]</form>
</div>
In this case, our AJAX will hook the ID my-theme-search-wrap
and replace the existing form with 1 of our custom forms. If your form doesn’t have a wrapper but your theme has a file called searchform.php
, you can copy it to your child theme and edit it to wrap the form. If it doesn’t, you’ll have to edit your functions.php
to filter and alter the default search form. Here’s a pretty good article on how to filter the default form. Ensure you place the filter above the other actions we added to functions.php
!
The Good Stuff: AJAX
Finally, let’s create the AJAX. Open speech-input.js
and add this code:
//-------------------------------------------------*/
// Speech Recognition
//-------------------------------------------------*/
jQuery(document).ready(function ($) { // Runs when the document has finished loading
$(function () {
var ajaxData // Variable used below
if (('SpeechRecognition' in window) || ('webkitSpeechRecognition' in window)) { // Is the Speech Recognition API supported by the browser? Checks with and without vendor prefix.
ajaxData = {
action: 'my_voice'
} // Call the my_voice() function if speech recognition supported
} else {
ajaxData = {
action: 'my_novoice'
} // Otherwise call the my_novoice function
};
var xhr = new XMLHttpRequest() // Send an HTTP request so we can get 1 of the forms
var ajaxCall = $.ajax({ // Start of our AJAX. The ajaxCall variable is below in the "done" and "fail" callbacks
type: 'POST',
url: mpbVoiceSearch.ajaxurl, // Matches the handle mpbVoiceSearch from wp_localize_script() in functions.php.
dataType: 'html',
data: ajaxData // The data we are retrieving, either 'action": "my_voice"' or 'action: "my_novoice"'
})
ajaxCall.done(function (data) { // "success" deprecated; use "done" instead
$('#my-theme-search-wrap').html(data) // replace the contents of #my-theme-search-wrap with the retrieved data, i.e., our custom form
})
ajaxCall.fail(function (data, xhr, ajaxOptions, thrownError) { // "error" deprecated, use "fail" instead
var errorMsg = 'Oh, bother. Your AJAX request failed with a status code of ' + xhr.responseText + '.' // Generate an error message if the AJAX call fails to retrieve any data
console.log(errorMsg) // Log the error to the console for debugging
})
})
});
(window, jQuery, window.Window_Ready)
Save the file and upload. Open a browser to see if it works; in Chrome for desktop or Android, you should see the shiny new speech-capable form. In all other browsers, you should see the alternate form. See the comments to see how this code works, and check the console in your developer tools or web inspector to trouble-shoot.
One last thing to note: many articles on AJAX in WordPress state that you should use a WordPress nonce for security. This is true for AJAX calls for logged-in users on the back-dend, but for calls that only run on the front-end, nonces offer no additional security and may cause some problems.
Leave a Reply