Appearance
Blog and article support guide
Diagnostic and troubleshooting reference for blog listing and article reading experience — featured-article hero, tag-chip filter, author chips, reading time, list/grid toggle, author bio, social share, related articles, audio playback, BlogPosting JSON-LD structured data, and microdata.
For merchant setup and settings configuration, see the Blog guide.
FAQ
Q1: Featured-article hero is not showing on the blog listing
Check in order:
Is
show_featured_heroON? Theme editor → blog template section settings →show_featured_hero. Default is ON; if a previous merchant turned it OFF, re-enable it.Is this the first page? The featured hero is suppressed on page 2+ (
paginate.current_page == 1guard). Navigate to/blogs/news(page 1) — the hero should appear there.Is a tag filter active? The hero is suppressed on tag-archive pages (
/blogs/<handle>/tagged/<tag>) — the hero would incorrectly show an article that may not match the active tag. Navigate to the main blog URL (no tag) to see the hero.Does the blog have at least one published article? The hero uses
blog.articles.first— if no articles are published, the hero section is empty and rendered invisible.Is the article image set? The hero shows a Celtic knot SVG placeholder if no article image is set. The hero renders regardless — it does not require an image.
Q2: Author chip shows initials only, not the avatar photo
The author avatar comes from a shop metafield: shop.metafields.author.<handle>_avatar. The initials circle is the fallback when this metafield is absent or empty.
Check:
- Is the metafield definition created? Shopify admin → Settings → Custom data → Shop → look for
authornamespace definitions. - Is the metafield value set for this author's handle? The handle is the author name lowercased with spaces replaced by underscores (e.g., "Aoife Murphy" →
aoife_murphy). In Shopify admin → Settings → Custom data → Shop →author.aoife_murphy_avatar→ verify a file is selected. - Is the Shopify Files URL accessible? The metafield type is
file_reference— the image must be uploaded to Shopify admin → Content → Files first. - Open DevTools → Elements → find
.blog-author-chip__avatar— does it have ansrcattribute? Ifsrcis absent, the metafield lookup returned blank.
Q3: Tag chips not showing on the blog listing
Tag chips are driven by blog.all_tags. They only appear if at least one published article in the blog has tags.
Check:
The chip row renders automatically whenever any published article has tags. Verify at least one article has tags assigned (Shopify admin → Blog posts → select article → Tags field).
- Open Shopify admin → Blog posts → filter by your blog → look for any article with tags assigned. If no articles have tags,
blog.all_tagsis empty and the chip row is hidden. - The "All" chip always appears if any tags exist. If only the "All" chip shows, your articles have tags but Liquid is reading
blog.all_tagscorrectly — the individual tag chips should follow.
Q4: Audio player not appearing on the article
Check:
- Is
enable_audio_playbackON? Theme editor → article template section settings →enable_audio_playback. Default is ON. - Does the browser support
speechSynthesis? The audio player is hidden on unsupported browsers (feature-detected). Run the console diagnostic snippet → look forspeechSynthesis: true. Iffalse, the browser does not support Web Speech API (unusual — most modern browsers support it; check if an extension or policy is blocking it). - Is there an
<article-audio>element in the DOM? Open DevTools → Elements → search forarticle-audio. If absent, the section setting is OFF or the JS bundle did not load.
Q5: Audio plays but the reading-position highlight does not move
The highlight tracks SpeechSynthesisUtterance boundary events. Potential issues:
prefers-reduced-motionis ON. Under reduced-motion, the solid highlight is replaced with a wavy underline (CSStext-decoration). This is correct accessibility behavior — the underline still moves, but more subtly.- The
speechSynthesis.speakingflag is true but boundary events are not firing. This is a browser bug — some older Chromium versions emit noboundaryevents. Workaround: update the browser. The theme cannot work around missing browser events. - Article content is in a non-paragraph element. The highlight targets
<p>tags in.article__content. If the article content uses unusual markup, the highlight may skip elements.
Q6: Audio does not play on iOS Safari
iOS Safari enforces a user-gesture lock on audio/speech. The <article-audio> Custom Element handles this:
- First visit: a "Tap to listen" prompt replaces the Play button until the first user click.
- After the user taps Play,
speechSynthesis.speak()is called within the click handler — this satisfies iOS's gesture requirement. - If playback still doesn't start: verify the shopper has not turned off "Speak Screen" or "Speak Selection" in iOS accessibility settings (these affect SpeechSynthesis API availability).
Q7: BlogPosting JSON-LD not validating in Rich Results Test
If you see validation errors for the article's structured data:
Check for Liquid errors in the
<script>block. Open DevTools → Sources → search forBlogPosting. If the JSON block containsLiquid error, the JSON is invalid and no parser will read it. Common cause: article has no featured image (see next step).Missing article image: If
article.imageis blank, the"image"field is omitted (this is valid per schema.org —imageis recommended, not required for BlogPosting). If you seeLiquid error: invalid url inputinstead of a clean omission, you may be on an older version of the Uisce theme. Ensure you are running the latest version.Verify
@typeis"BlogPosting"(not"Article"). Theme 06.7-03 upgraded the type. If you still see"Article", the json-ld.liquid snippet may not have been deployed to your store.Run the JSON-LD inspection command from the curl cheat-sheet to check all fields programmatically.
Password-protected store: Google Rich Results Test URL mode cannot access password-protected dev stores. Use HTML-paste mode:
curl -s -b cookies.txt {article_url} > /tmp/article.html→ paste contents into the "Code" tab athttps://search.google.com/test/rich-results.
Q8: Social share rail not appearing on desktop
The sticky social share rail appears on desktop (≥ 1024 px viewport width) at the inline-end (right) edge of the content column. Check:
- Is the viewport ≥ 1024 px? The rail is hidden on mobile via CSS. Resize the window or use DevTools responsive mode.
- Are any share platforms enabled? If all
show_share_*settings are OFF, the rail renders empty and the container collapses. Re-enable at least one platform. - RTL stores: In right-to-left languages, the rail appears on the left (logical
inset-inline-end= physical left in RTL). This is correct behavior. - Run the diagnostic snippet →
shareToastfield confirms the share-related DOM is present.
Q9: "Copy link" toast doesn't appear / clipboard doesn't work
The copy-link button uses navigator.clipboard.writeText() with a fallback to document.execCommand('copy'). Common issues:
- HTTP vs HTTPS:
navigator.clipboardrequires HTTPS (orlocalhost). Dev stores use HTTPS, so this should not apply. - Browser permissions: In some browsers, clipboard access requires explicit permission. The fallback
execCommand('copy')bypasses this but may not work in all iframes. - Toast element: Run the diagnostic snippet →
shareToast: trueconfirms[data-share-toast]is in the DOM. Iffalse, the share snippet did not render.
Q10: Related articles showing the wrong articles
Related articles use a cascade: same-tag articles (3 most recent, excluding current) → same-blog most-recent (3, excluding current) → hidden if < 2 candidates.
To influence which articles appear:
- Tag consistently. Articles with the same tags are related to each other. Use broad topic tags (not per-article unique tags) to maximize related-article connections.
- If all related articles come from "same-blog most-recent", your articles have no tags or no overlapping tags — this is correct behavior.
- If the related block is hidden, you have fewer than 2 articles in the blog (excluding the current one) — publish more articles.
Decision tree: audio playback
Article audio player not working
│
├─ `enable_audio_playback` ON in theme editor?
│ No → enable it
│ Yes → continue
│
├─ `<article-audio>` in DOM? (DevTools Elements search)
│ No → JS bundle not loaded → check browser console errors
│ Yes → continue
│
├─ `speechSynthesis` in window? (see diagnostic snippet)
│ No → browser does not support Web Speech API
│ Workaround: none; feature hidden by design on unsupported browsers
│ Yes → continue
│
├─ iOS Safari?
│ Yes → first user gesture required
│ → "Tap to listen" prompt should appear; tap it
│ → If still no audio: check iOS Settings → Accessibility → Spoken Content
│ No → continue
│
├─ Click Play button → audio starts?
│ No → check voicesCount in diagnostic (speechSynthesis.getVoices().length)
│ 0 voices → browser hasn't loaded voice list yet → try after page settles
│ > 0 voices → check console for SpeechSynthesisUtterance errors
│ Yes → check highlight (Q5)Console diagnostic snippet
Paste into browser DevTools Console on any article page. Read-only, adds zero theme code.
js
(() => {
const r = {
audioCE: !!customElements.get('article-audio'),
speechSynthesis: 'speechSynthesis' in window,
voicesCount: 'speechSynthesis' in window ? speechSynthesis.getVoices().length : null,
clipboard: !!navigator.clipboard,
audioEl: !!document.querySelector('article-audio'),
audioState: document.querySelector('article-audio [data-audio-status]')?.textContent?.trim() ?? null,
shareToast: !!document.querySelector('[data-share-toast]'),
relatedCount: document.querySelectorAll('.article-related__card').length,
authorBio: !!document.querySelector('.article-author-bio'),
jsonLdType: (() => {
try {
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
for (const s of scripts) {
const d = JSON.parse(s.textContent);
if (d['@type'] === 'BlogPosting') return 'BlogPosting (correct)';
if (d['@type'] === 'Article') return 'Article (old — check 06.7-03 deploy)';
}
return 'BlogPosting not found';
} catch {
return 'JSON parse error in ld+json block';
}
})(),
};
console.table(r);
})();What to look for:
| Field | Expected value | What it means if wrong |
|---|---|---|
audioCE | true | false → article-audio.js not loaded |
speechSynthesis | true | false → browser does not support Web Speech API |
voicesCount | Integer > 0 | 0 → voice list not loaded yet; try again after 1s |
audioEl | true | false → enable_audio_playback OFF or CE not mounted |
audioState | null or "stopped" on load | Shows current play/pause/stop state |
shareToast | true | false → article-social-share snippet not rendered |
relatedCount | 2 or 3 | 0 → related-articles block hidden (< 2 candidates) |
authorBio | true if show_author_bio=ON AND metafields set | false → author bio OFF or no metafields |
jsonLdType | "BlogPosting (correct)" | "Article (old)" → 06.7-03 not deployed; "JSON parse error" → Liquid error in ld+json block |
Curl cheat-sheet
Replace {store} with your storefront domain and {password} with the storefront password (dev stores only). For production stores, skip the cookie step.
Capture auth cookie (dev stores)
bash
curl -s -c cookies.txt https://{store}/password -o /tmp/pw.html
CSRF=$(grep -o 'name="authenticity_token" value="[^"]*"' /tmp/pw.html | head -1 | sed 's/.*value="//;s/"//')
curl -s -c cookies.txt -b cookies.txt \
--data-urlencode "form_type=storefront_password" \
--data-urlencode "authenticity_token=${CSRF}" \
--data-urlencode "password={password}" \
-L "https://{store}/password" -o /dev/null
# Now add -b cookies.txt to all curl commands belowVerify RSS alternate link in article head
bash
curl -s -b cookies.txt https://{store}/blogs/news/{article-handle} | \
grep -i 'rel="alternate".*rss\|application/rss\|application/atom'Expected: <link rel="alternate" type="application/atom+xml" ... href="/blogs/news.atom" />. If absent, check that snippets/meta-tags.liquid has the RSS alternate link injection (Plan 06.7-04 feature).
Verify microdata removal (no article-body itemscope)
bash
curl -s -b cookies.txt https://{store}/blogs/news/{article-handle} | \
grep -c 'itemscope\|itemprop\|itemtype'Expected: 1 (breadcrumb microdata only). If > 1, the article body still has microdata attributes — check that Plan 06.7-03 was deployed (microdata reconciliation).
Inspect JSON-LD blocks
bash
curl -s -b cookies.txt https://{store}/blogs/news/{article-handle} | \
python3 -c "
import re, json, sys
html = sys.stdin.read()
blocks = re.findall(r'<script[^>]*type=[\"'\''']application/ld\+json[\"'\'''][^>]*>(.*?)</script>', html, re.DOTALL)
print(f'{len(blocks)} JSON-LD blocks found')
for i, b in enumerate(blocks):
b = b.strip()
if 'Liquid error' in b:
print(f'Block {i+1}: LIQUID ERROR - {[l for l in b.splitlines() if \"Liquid error\" in l][:1]}')
else:
try:
d = json.loads(b)
t = d.get('@type', '?')
print(f'Block {i+1}: @type={t}', end='')
if t == 'BlogPosting':
print(f'; mainEntityOfPage={bool(d.get(\"mainEntityOfPage\"))}; articleSection={bool(d.get(\"articleSection\"))}; image={\"ImageObject\" if isinstance(d.get(\"image\"), dict) else (\"absent\" if d.get(\"image\") is None else \"plain\")}', end='')
print()
except Exception as e:
print(f'Block {i+1}: parse error: {e}')
"Expected output:
4 JSON-LD blocks found
Block 1: @type=Organization
Block 2: @type=WebSite
Block 3: @type=BreadcrumbList
Block 4: @type=BlogPosting; mainEntityOfPage=True; articleSection=True; image=ImageObject (or absent if no featured image)Verify RSS feed content
bash
curl -s -b cookies.txt https://{store}/blogs/news.atom | head -30Expected: Atom XML with <feed> root element, <title>, <entry> elements per article. If the feed returns HTML (redirect to /password), the auth cookie is not being passed correctly.
Check reading time rendering
bash
curl -s -b cookies.txt https://{store}/blogs/news/{article-handle} | \
grep -i "min read\|min de lectura\|reading_time"Expected: a localized "N min read" string in the article meta line. If absent, show_reading_time is OFF or the locale key blog.article_meta_reading_time is missing.
Google Rich Results Test — HTML paste mode (dev stores)
bash
# Fetch article HTML with auth
curl -s -b cookies.txt https://{store}/blogs/news/{article-handle} > /tmp/article.html
# Validate structured data locally
python3 -c "
import re, json
with open('/tmp/article.html') as f:
html = f.read()
blocks = re.findall(r'<script[^>]*type=[\"\'']application/ld\+json[\"\''][^>]*>(.*?)</script>', html, re.DOTALL)
for b in blocks:
b = b.strip()
if 'BlogPosting' in b:
if 'Liquid error' in b:
print('FAIL: Liquid error in JSON-LD block')
else:
try:
d = json.loads(b)
errors = []
if d.get('@type') != 'BlogPosting': errors.append('Wrong @type')
if not d.get('mainEntityOfPage'): errors.append('Missing mainEntityOfPage')
if not d.get('headline'): errors.append('Missing headline')
if not d.get('datePublished'): errors.append('Missing datePublished')
if not d.get('author'): errors.append('Missing author')
if not d.get('publisher'): errors.append('Missing publisher')
if errors:
print('FAIL:', ', '.join(errors))
else:
print('PASS: BlogPosting valid')
print(' image:', 'ImageObject' if isinstance(d.get('image'), dict) else 'absent (articles without featured image — valid)')
except Exception as e:
print('PARSE ERROR:', e)
break
"To use Google's actual validator in HTML-paste mode:
- Run the curl command above to save the article HTML
- Visit
https://search.google.com/test/rich-results - Click the "Code" tab
- Paste the contents of
/tmp/article.html - Click "Test code"
Known limitations
Web Speech API voice availability
speechSynthesis.getVoices() returns an empty array synchronously on page load — voices are loaded asynchronously. The audio player waits for the voiceschanged event before enabling the Play button. On very slow connections, this may take 1–2 seconds. This is a browser behavior, not a theme bug.
Web Speech API and content chunking
Long articles are split into multiple SpeechSynthesisUtterance objects (one per paragraph) to work around the Web Speech API's 32,768 character limit per utterance. The reading-position highlight tracks across utterance boundaries. On some browsers, there may be a brief pause between paragraphs — this is expected.
Social share — platforms and intent URLs
All share buttons use direct intent URLs (no JavaScript library, no tracking pixel):
| Platform | URL pattern |
|---|---|
| X (Twitter) | https://x.com/intent/tweet?url={url}&text={title} |
https://www.linkedin.com/shareArticle?mini=true&url={url}&title={title} | |
https://www.facebook.com/sharer/sharer.php?u={url} | |
https://pinterest.com/pin/create/button/?url={url}&description={title} | |
https://api.whatsapp.com/send?text={title} {url} | |
mailto:?subject={title}&body={url} | |
| Copy link | navigator.clipboard.writeText(window.location.href) |
Platform intent URLs are opened in a new tab. Copy link uses the Clipboard API with a 2-second toast notification.
Need more help?
- File a GitHub issue with the console-snippet output + browser/OS + preset name.
- See the Blog guide for settings documentation.
Related docs
- Blog guide — configuration walkthrough
- Search support — predictive search, Storefront API token, typed tabs
- Product page support
- Collection page support
- Support form — submit a ticket if the above does not resolve your issue