Programming

Add BeyondWords to Shopify

Female listening to earbuds
Publish Insights 30 April 2022

Start Talking

Earbuds are changing the way people interact with the internet. When you are ready for a robust text-to-speech solution, consider BeyondWords. [1] Though a free option is available, with prolific writing, you will quickly upgrade to a subscription plan.

If you have a considerable amount of existing pages to convert, consider investing $75 or more per month.

The breadth of features and options within BeyondWords is too vast to consider here. Instead, I will focus on two methods of getting audio into your website.

BeyondWords Audio Text-to-Speech

  1. Manual text-to-speech. With this option, you can choose numerous voices. It is ideal for stories with several people speaking. It is rewarding, but time consuming, to paste text into paragraph fields, edit out unwanted phrases, and select a voice from associated dropdown menus.
  2. Automated text-to-speech. Using RSS, it is possible for BeyondWords to create audio files as you publish articles. If you edit an article, the audio updates soon thereafter. These default to one voice for the title and another for the rest of the text.

You can even use a combination of manual and automated audio on the same site. This is particularly useful if you have multiple blogs or stories that benefit from voice changes within the main text.

Basic BeyondWords installation instructions are simpler than what’s proposed on this page. But parsing and centralizing code injection will benefit you in long run—especially if you should have to pivot providers.

Shopify RSS Feed

The standard Shopify RSS feed is robust, non-editable, and works with BeyondWords. With the appropriate iFrame code on each blog article page, you can connect the atom feed for automated text-to-speech audio.

This is fine if your articles include subheads and basic text. But you might have redundant blockquotes, superscript footnotes, table data, inline ads, image captions, or other page elements you prefer to exclude from the audio. In this case, it is best to parse and optimize the text within a custom RSS feed.

A prior article described a method of embedding the audio controls on desired pages without editing them individually. My valiant efforts to develop a solution with a friendly interface for modifying settings failed. With a user interface, settings can be validated with menus and checkboxes.

This custom RSS feed is a hack that requires settings to be typed into the code. It conforms to BeyondWords specifications but does not pass official RSS validation due to a couple of issues (HTML text and lack of recognized RSS origin). [2]

The goal is to minimize needless text (which can be distracting in audio and costly to convert), not format pristine HTML. It is inspired by code from Jason Bowman for a MailChimp RSS feed. [3]

Modify Liquid Code

Shopify is an e-commerce platform that supports blogging. Merchants build custom websites based on themes. Each theme adheres to similar underlying structure based on programming code called Liquid. [4]

Before making modifications to your Shopify theme, duplicate it. Within Admin, under the lefthand navigation, choose Online Store: Themes. Click the menu for the active theme, Actions: Duplicate. Work on the duplicate and preview before activating when done.

Caution: Modifying Liquid code may invalidate your theme for future updates by the developer. Anything you add will require manual migration to a different theme, with possible incompatibility.

Now you access the Liquid programming code with Actions: Edit code. Beneath the area called Templates, click Add a new template. Choose the liquid instead of JSON option. Then replace all default code with the custom code on this page:

{% layout none %}{% comment %}
/*
 * Shopify custom blog feed for BeyondWords text-to-speech
 *
 * Copyright (c) 2022-2023 Kevin RRW (mail@clinicalposters.com)
 * https://clinicalposters.com/blogs/insights/shopify-beyondwords
 * Licensed under the MIT license:
 * https://www.opensource.org/licenses/mit-license.php
 *
 * Result may not pass validator.schema.org test.
 * It is specifically for BeyondWords.com
 * Access with blog URL followed by " ?view=feed " (no quotes)
 */
{% endcomment %}
{% capture feed_settings %}
  {% comment %} // Default author email address. // {% endcomment %}
 {%- assign author_email = '' -%}
  {% comment %} // How many articles to show. Target site may also set limit. // {% endcomment %}
 {%- assign article_limit = 6 -%}
  {% comment %} // Optionally include description // {% endcomment %}
 {%- assign hide_description = false -%}
  {% comment %} // Escape text instead of CDATA. BeyondWords accepts either. // {% endcomment %}
 {%- assign use_escape = true -%}
  {% comment %} // Remove existing CDATA tags. Shopify doesn't add any. // {% endcomment %}
 {%- assign remove_cdata = false -%}
  {% comment %} // Forces content to plain text. BeyondWords prefers HTML. // {% endcomment %}
 {%- assign no_html = false -%}
  {% comment %} // Force content to plain text if HTML might break feed // {% endcomment %}
 {%- assign no_html = false -%}
  {% comment %} // Optionally truncate content for very long posts // {% endcomment %}
 {%- assign truncate_content = false -%}
  {% comment %} // For long content, how many words should we trucate to? // {% endcomment %}
 {%- assign truncate_amount = 1000 -%}
  {% comment %} // Skip articles containing specific tag // {% endcomment %}
  {%- assign skip_tag = 'no-reader' -%}
  {% comment %} // Suppress blockquotes, superscript, tables, and <h6> headers // {% endcomment %}
 {%- assign suppress_common = true -%}
  {% comment %} // Convert one thru ten to 1 thru 10 to reduce character count // {% endcomment %}
 {%- assign use_numerals = false -%}
  {% comment %} // Parse for homograph pronunciation hints // {% endcomment %}
 {%- assign use_homographs = false -%}
  {% comment %} // Adjust the image size (not used in BeyondWords audio) // {% endcomment %}
 {%- assign image_size = '600x600' -%}
  {% comment %} // Do not alter CDATA opening/closing tags // {% endcomment %}
 {%- assign cdata1 = '<![CDATA[' -%}{%- assign cdata2 = ']]>' -%}
{% endcapture %}{% capture headers %}<?xml version="1.0" encoding="UTF-8"?>
 <channel>
 <feed xml:lang="en" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns="http://purl.org/rss/1.0/"> <channel>
 <link rel="self" type="text/html" href="{{ canonical_url | append: '?view=feed' }}"/>
 <title>{{ shop.name }} - {{ blog.title }}</title>
 <description>{{ blog.metafields.global.description_tag }}</description>
 <lastBuildDate>{{ blog.articles.first.created_at | date: "%Y-%m-%dT%H:%M:%S%Z" }}</lastBuildDate>
 <generator>Shopify {%- if use_escape and use_html == false -%}{{ headers | escape }}{%- else -%}{{ headers }}{%- endif -%}
 {% for article in blog.articles limit:article_limit %}
  {%- assign authorEmail = 'info@' | append: shop.url | remove: 'https://' | remove: 'www.' -%}  {%- if shop.url contains 'myshopify.com' -%}{%- assign authorEmail = shop.name | append: '-author@gmail.com' -%}{%- endif -%}
  {%- unless article.tags contains skip_tag -%}
    {% comment %} // Strip non-breaking spaces and soft hyphens // {% endcomment %}
  {%- assign articleContent = article.content | replace: '&nbsp;', ' ' | remove: '&shy;' | remove: '­' | replace:'<br>','<br />' | remove: '<span class="Apple-converted-space"> </span>' | replace: ' U.S.A.', ' USA' | replace: 'U.S.A. ', 'USA ' | replace: ' U.S.', ' US' | replace: 'U.S. ', 'US ' -%}

    {% comment %} // String parsing based on settings // {% endcomment %}
  {%- if suppress_common -%}
    {%- assign articleContent = articleContent | replace: '<blockquote>', '<blockquote class="pullquote">' | replace: '<pre>', '<aside><pre>' | replace: '</pre>', '</pre></aside>' -%}
    {%- assign articleContent = articleContent | replace: '<sup>', '<aside><sup>' | replace: '</sup>', '</sup></aside>' | replace: '<table', '<aside><table' | replace: '</table>', '</table></aside><p>View chart</p>' -%}
    {%- assign articleContent = articleContent | replace: '<h6', '<aside><h6' | replace: '</h6>', '</h6></aside>' | replace: '<s>', '<s class="definition">' | replace: 'class="caption"', 'class="definition"' -%}
    {%- assign articleContent = articleContent | replace: '<aside>', '<aside><span style="display: none;">' | replace: '>/aside>', '</span></aside>' -%}
  {%- endif -%}
  {%- if use_numerals -%}
    {%- assign articleContent = articleContent | replace: ' one ', ' 1 ' | replace: ' no 1 ', ' no one ' | replace: ' No 1 ', ' No one ' | replace: ' two ', ' 2 ' | replace: ' three ', ' 3 ' | replace: ' four ', ' 4 ' | replace: ' five ', ' 5 ' -%}
    {%- assign articleContent = articleContent | replace: ' six ', ' 6 ' | replace: ' seven ', ' 7 ' | replace: ' eight ', ' 8 ' | replace: ' nine ', ' 9 ' | replace: ' 911.', ' 9 1 1.' | replace: ' 911?', ' 9 1 1?' | replace: ' ten ', ' 10 ' -%}
  {%- endif -%}
  {%- if use_homographs -%}
    {%- assign articleContent = articleContent | replace: 'bow<!--bo-->', 'bo' | replace: 'Bow<!--bo-->', 'Bo' | replace: 'bow<!--bough-->', 'bough' | replace: 'content<!--cuntint-->', 'cuntint' | replace: 'lives<!--lyves-->', 'lyves' | replace: 'read<!--red-->', 'red' | replace: 'read<!--reed-->', 'reed' | replace: 'Read<!--reed-->', 'Reed' -%}
    {%- assign articleContent = articleContent | replace: 'resume<!--rezoom-->', 'rezoom' | replace: 'resume<!--rezumay-->', 'rezumay' | replace: 'résumé<!--rezumay-->', 'rezumay' | replace: 'résumé', 'rezumay' | replace: 'present<!--preezent-->', 'preezent' | replace: 'presents<!--preezents-->', 'preezents' -%}
    {%- assign articleContent = articleContent | replace: 'tear<!--teer-->', 'teer' | replace: 'tears<!--teers-->', 'teers' | replace: 'Tears<!--teers-->', 'Teers' | replace: 'tear<!--tare-->', 'tare' | replace: 'tears<!--tares-->', 'tares' | replace: 'Tears<!--tares-->', 'Tares' -%}
  {%- endif -%}
  {% capture items %}
  <item>
    <title>{%- if use_escape -%}{{ article.title }}{%- else -%}{{ article.title | prepend: cdata1 | append: cdata2 }}{%- endif -%}</title>
    <link>{{ shop.url | append: article.url }}</link>
    <author>{{ author_email | default: authorEmail }}{{ shop.name | prepend: ' (' | append: ')' }}</author>
    {%- if article.excerpt_or_content != blank -%}
      <description>{%- unless hide_description -%}
      {%- assign articleExcerpt = article.excerpt_or_content | replace: '&nbsp;', ' ' | remove: '&shy;' | remove: '­' | replace:'<br>','<br />' | remove: '<span class="Apple-converted-space"> </span>' -%}
      {%- if use_escape -%}{{ articleExcerpt }}
      {%- else -%}{{ articleExcerpt | strip_html | prepend: cdata1 | append: cdata2 }}
      {%- endif -%}{%- endunless -%}</description>
    {%- endif -%}
    <pubDate>{{ article.updated_at | date: "%Y-%m-%dT%H:%M:%S%Z" }}</pubDate>
    <guid>{{ shop.url | append: article.url }}</guid>
    <enclosure url="http:{{ article.image | img_url: image_size }}" length="0" type="image/jpg" />
    <content>{% if remove_cdata %}{% assign articleContent = articleContent | remove: cdata1 | remove: cdata2 %}{% endif %}
    {%- if remove_cdata -%}{% assign articleContent = articleContent | remove: cdata1 | remove: cdata2 %}{%- endif -%}
    {%- if truncate_content -%}{% assign articleContent = articleContent | strip_html | truncatewords: truncate_amount %}
      {%- if use_escape -%}{{ articleContent }}{%- else -%}{{ articleContent | prepend: cdata1 | append: cdata2 }}{%- endif -%}
    {%- elsif no_html -%}{{ articleContent | strip_html | prepend: cdata1 | append: cdata2 }}
    {%- elsif use_escape -%}{{ articleContent }}
    {%- else -%}{{ articleContent | prepend: cdata1 | append: cdata2 }}
    {%- endif -%}
    </content>
  </item>
  {% endcapture %}
  {%- if use_escape and use_html == false -%}{{ items | escape }}{%- else -%}{{ items }}{%- endif -%}
  {%- endunless -%}
 {% endfor %}
 {% capture footers %} </channel>
</rss>
 {% endcapture %}
 {%- if use_escape and use_html == false -%}{{ footers | escape }}{%- else -%}{{ footers }}{%- endif -%}

Give special consideration to the options above in red. Specify a default author email on line 16. Otherwise a default will be info@[your shop name] unless your shop URL includes ‘myshopify’. Then it will be [your shop name]-author@gmail.com.

Beyond Parsing

BeyondWords recognizes various classes to suppress content. Wrapping text within <aside> elements is quite effective. CSS classes .pullquote, .callout, and .definition reportedly can markup text for suppression.

I manually edited my blog articles to suppress references at the bottom of each page by wrapping them within <aside> elements. But modifying other CSS classes within body copy is more tedious. This is why a parsed RSS feed is helpful. Shopify lacks the power of Grep string modifications. Instead, we must rely on replace or remove Liquid commands.

The default number of recent articles is set to a half dozen. You can increase it to 25, 100, or whatever you like. But BeyondWords allots a limited number of ASCII characters each month. So for your initial test, you should try just 2 articles.

Settings within the above code include an option to suppress <blockquote>, <pre>, <sup>, <s>, <table>, and <h6> content within attributes. (You may add more replacements or remove some if required. For example, [square brackets] are good pronunciation substitutions for <em> and </em> italic entities.) You can also specify an article.tag that excludes corresponding posts from your RSS feed. (Replace no-reader.)

Centralize Javascript Option

The BeyondWords player code includes a remote script link enabling audio to play anywhere. I recommend loading their script within the Theme template and including only their iFrame code on article pages.

This eliminates the need to load the script multiple times while browsing your website. You can optionally, choose to isolate the script below and add it near the bottom of the header area within the Theme template.

{%- if template == 'article' -%}
 <script src="https://proxy.beyondwords.io/npm/@beyondwords/audio-player@latest/dist/module/iframe-helper.js" type="text/javascript" defer="defer"></script>
{%- endif -%}

To place the iFrame code on each page, either paste it directly within the code on the article.liquid page or pair this code with the prior tip for controlling where and on which blogs to insert the BeyondWords player.

Ready to Activate

Navigate to the desired blog on your site and append it with ?view=feed. This is the link you select within the BeyondWords Importer Settings field. Click the Test button to validate.

BeyondWords Importer Settings

Find Your Voice

Human voices can be deep, high-pitched, sultry, commanding, or irritating. BeyondWords offers hundreds of voices with different dialects across global regions. Choose one that conveys the intent of your articles or, for a fee, create a custom one modeled after your own voice.

No-Print Bonus

Another piece of bonus code is in harmony with a prior post about printable page elements. Suppress printing of the iFrame player by adding the following to the bottom of your CSS file or within the .no-print code in that prior post, append  ,iframe after button.

@media print { iframe { display: none !important; } }

Choose Your RSS

A prior article discussed how to enable RSS for specific blogs on your website. After adding this alternate blog to your website (and testing), you can include an option in your blog page settings. Here, you can choose which RRS feed to serve.

First edit the schema at the bottom of the blog-template under the Sections area within your Shopify theme. The code below adds a menu option.

{
 "type": "select",
 "id": "rss_type",
 "label": "RSS type",
 "info": "Be cetain custom code exists before selecting",
 "default": ".atom",
 "options": [
{
 "value": ".atom",
 "label": "Standard"},
{
 "value": "?view=feed",
 "label": "Custom"
}
 ]
},

Next, search for the word “atom” within the code above it. It might look something like <a href="{{ blog.url }}.atom" or <a href="{{ blog.url | append: '.atom' }}. You can replace it with:

<a href="{{ blog.url | append: section.settings.rss_type }}".

Read next article

'Writer typing on keyboard'
'Female reading digital tablet'