Skip to site navigation
Go home

Lee Reamsnyder

Make headings with links appear in Safari Reader View

2021 May 10 5 min read Published by Lee Reamsnyder Permalink

TL;DR:

If you’re doing links in headings like this…

<h2 id="introduction">
  <a href="#introduction">Introduction</a>
</h2>

…and they’re not showing up in Safari Reader View, try wrapping an extra element like a <span> around the text content:

<h2 id="introduction">
  <a href="#introduction">
    <span>Introduction</span>
  </a>
</h2>

The longer version

On my site here I recently started adding links to headers so you can link directly to a part of an article, like this link to the header right above here.

For my first pass, I followed this very sound advice from Amber Wilson. I strongly recommend reading the whole article because the process is important, but the final markup pattern was this:

<h2 id="introduction">Introduction</h2>
<a href="#introduction">
  <span aria-hidden="true">#</span>
  <span class="hidden">Section titled introduction</span>
</a>

The end result is a link after each heading with a clear description for screen readers, but only show a # (or whatever you want) visually. Makes sense!

So first pass ended up looking like this:

A screen capture of a heading with a “#” link to the side of the heading

A potential issue with this pattern is that if that markup ends somewhere that you don’t control the styling like an RSS feed or browser reader mode, those links will be visible in their entirety. Here’s an example heading in NetNewsWire:

Screen capture of a heading in the NetNewsWire RSS reader with visible duplicate text

Or in Safari’s Reader View with only “#” visible, which is a little weird:

Screen capture of a heading in Safari Reader View with only “#” visible

None of these are deal-breakers, but I started exploring alternatives.

Turns out after I initially implemented this, Amber updated her original article with an alternative markup pattern where you make the entire content of the heading a link, like so:

<h2 id="introduction">
  <a href="#introduction">Introduction</a>
</h2>

Again, it’s worth reading her reasoning and also this thread on Github, particularly this amazing roundup of the pros and cons of assorted header link patterns.

That has simpler markup, a larger click/tap target, no additional elements to worry about in other contexts, and the accessibility seems reasonable. The one drawback is you can’t put another link inside the heading, but I don’t really do that here, so that pattern seemed like a win.

One problem: here that is in Safari Reader View, or more precisely, isn’t there because now it doesn’t render those heading tags:

Screen capture of a heading with a link missing in Safari Reader View

Well that’s worse! It’s not just me as you can see that same pattern in action on MDN Web Docs or the The Web Almanac and those pages don’t have headings in Reader View either.

(I’m checking in Safari 14.0.3 on macOS 11.2.3, for what it’s worth.)

So… how do we fix it?

Safari’s Reader View is a black box, but there are little hints of how it works floating around.

The top Google hit for structuring content for it is Mandy Michael’s article. There’s a link there to a discussion with Apple developer Ricky Mondello who sheds some light on how it pulls content.

In short, to work with as many websites as possible, it uses a lot of fuzzy logic to try to match content and metadata, like these examples for pulling an author name:

But to work with lots of existing content, we’ll look for stuff like this, too:
`.byline, .article-byline, .entry-meta, .author-name, .byline-dateline, .article-author`

— Ricky Mondello (@rmondello) July 20, 2018

So I had that thought that if I made the headings look more content-ish with some classes, maybe that’d work? First pass with an extra <span> with a faux-content-y class of header:

<h2 id="introduction">
  <a href="#introduction">
    <span class="header">Introduction</span>
  </a>
</h2>

That worked! Although it isn’t a link, the heading content is back in Reader View:

Screen capture of Safari Reader View with the missing header back in place

I futzed around with it some more and it seems like the class on the span isn’t necessary. Just so long as the link has any other element around the text, it seems to show up in Reader View. So, the final pattern you want is:

<h2 id="introduction">
  <a href="#introduction">
    <span>Introduction</span>
  </a>
</h2>

I’m using remarkable for Markdown conversion, so here’s the plugin I threw together to do this automatically for my content:

// https://www.npmjs.com/package/slug
const slugify = require('slug')

function headerLinks(options) {
  const appliedOptions = {
    levels: [1, 2, 3, 4, 5, 6],
    anchorClassName: 'header-anchor',
    ...options,
  }

  return function (remarkable) {
    const originalOpen = remarkable.renderer.rules.heading_open
    remarkable.renderer.rules.heading_open = function (tokens, idx) {
      const { hLevel } = tokens[idx]

      // Only anchorize supported header levels
      // the extra span seems necessary for Safari reader mode
      // if you have links in headers, they disappear
      // but then having a span around the content seems to clue Safari back in to grab it
      // ¯\_(ツ)_/¯
      if (appliedOptions.levels.indexOf(hLevel) !== -1) {
        const { content } = tokens[idx + 1]
        const slug = slugify(content)
        const id = `${slug}`
        const href = `#${slug}`

        return `<h${hLevel} id="${id}"><a class="${appliedOptions.anchorClassName}" href="${href}"><span>`
      }

      return originalOpen(tokens, idx)
    }

    const originalClose = remarkable.renderer.rules.heading_close
    remarkable.renderer.rules.heading_close = function (tokens, idx) {
      const { hLevel } = tokens[idx]

      // Only anchorize supported header levels
      if (appliedOptions.levels.indexOf(hLevel) !== -1) {
        return `</span></a></h${hLevel}>`
      }

      return originalClose(tokens, idx)
    }
  }
}

And you’d use it in Remarkable like so:

import { Remarkable } from 'remarkable'
const renderer = new Remarkable().use(
  headerLinks({
    levels: [2, 3],
    anchorClassName: 'my-special-anchor-class',
  })
)

const html = renderer.render('## hello')

Note that this isn’t super robust because if you happen to have multiple headings with identical text content, you’ll have multiple identical IDs, but that isn’t a problem for me here.

← Older post A Bit More → Newer post Dynamic social media images with Puppeteer and Netlify on-demand builders

Menu

  • Home
  • Blog
  • Work
  • Contact
  • Archives
  • Feeds: RSS | JSON

Search

Elsewhere

  • GitHub
  • Instagram
  • Mastodon
  • Twitter
© Copyright 2006–2023, Lee James Reamsnyder
Back to top