Wrap <h2> elements in sections

Hi guys,

I am currently focusing on the following idea. For my ghost blog (https://bitcoin-2go.de) I’d like to establish the use of semantic html-tags such as within my articles.

Therefore, I’d like to wrap all <h2> tags within an article with a <section> element.


<h2 id="first-h2-in-article">First H2 in Article</h2>

Now I know, that Ghost automatically adds the “id” property for all h2-tags. However, I was not able to determine where exactly this happens.

I’d be happy to get any ideas and hints where exactly the “id” property is added. With that knowledge, I could wrap the <h2> in addition with <section> tags.

Thanks in advance and a great day

It’s not a simple change. If you were going to do it the first place to look would be here Koenig/mobiledoc-html-renderer.js at main · TryGhost/Koenig · GitHub.

However, what is the reasoning behind the change? What are you hoping to gain from wrapping parts of the article content with a section?

Thank you Kevin for your answer.

Actually, I was hoping for better accessibility resulting to a more structured blog post helping Google to understand and index my site better. Eventually, this should be one of the factors improving my results in Google SERPs.

However, current literature is ambigous about the impact of using semantic html-tags for SEO. If it would have been easy to edit, I’d have just given it a try.

Maybe you can share your opinion about the impact of using semantic html tags for SEO?

The <section> tag wouldn’t do anything for you. It’s a generic tag meant to be used as a last resort when there’s no better tag for marking a section of content as unrelated to the rest of the article.

Ghost already renders html with as much semantic meaning as possible and outside of those basic document structure semantics the SEO benefits are extremely minimal to non-existent unless you’re trying to mark very specific things like navigation and so on. Any specific areas like that would typically be outside of Ghost’s content area and so controllable through your theme, or created using a html card in the editor where you have full control over any semantic html.


Thank you Kevin, really appreciate the fast and competent response. Have a great day!

@Kevin I am using this thread here for further discussion since my current topic is closely related to h2-sections.

I am using scrollnav.com in order to create a Table of Content (ToC). Scrollnav adds the id to each <h2> unless it already exists.

Since the Koenig html renderer already applys this functionality, scrollnav does not need to add further ids.

However, my content is written in German and therefore we take usage of the so-called “Umlaute”. More specifically, our authors use characters such as “ä”, “ö” or “ü” for their headlines.

As a result <h2>Was bedeutet VPN überhaupt</h2> becomes <h2 id="was-bedeutet-vpn-%C3%BCberhaupt">Was bedeutet VPN überhaupt</h2>.

This situation now produces in combination with scrollnav.js the following errors:

Uncaught DOMException: Failed to execute 'querySelector' on 'Document':

Actually, the solution would not be too difficult, I guess. We would need to modify your mobiledoc-html-renderer.js in the slugify() function. In concrete:

const slugify = function (inputString, {ghostVersion = '4.0'} = {}) {
    const version = semver.coerce(ghostVersion);

    if (semver.satisfies(version, '<4.x')) {
        // backwards compatible slugs used in Ghost 2.x to 3.x mobiledoc
        return inputString.replace(/[<>&"?]/g, '')
            .replace(/[^\w]/g, '-')
            .replace(/-{2,}/g, '-')
    } else {
        // news slugs introduced in 4.0
        // allows all chars except symbols but will urlEncode everything
        // produces %-encoded chars in src but browsers show real chars in status bar and url bar
        return encodeURIComponent(inputString.trim()
            .replace(/[\][!"#$%&'()*+,./:;<=>?@\\^_{|}~]/g, '')
            .replace(/\s+/g, '-')
            .replace(/^-|-{2,}|-$/g, '')
            .replace(/\u00e4/g, "ae")
            .replace(/\u00fc/g, "ue")
            .replace(/\u00f6/g, "oe")

Since I think this is the solution, there are several opportunities.

First, we could adjust the code in Github.
Second, I could adjust the code for my setup which would be my desired solution. However, I was not able finding the package in node_modules.

Could you help me out for this specific case and tell me where I can make the concrete adjustments?

Best regards

The output from the renderer is correct for use in URLs. It sounds like scrollnav.com has a bug? What exactly is it you are proposing to change?

Sorry, I’d missed that part. The issue with applying exceptions like this is that it’s the start of a continuous process of adding more and more exceptions for different languages and character sets.

Url-encoded data such as %C3%BC is completely valid in the id attribute and will be shown by the browser to users as ü. Are you sure that is what is causing the problem? The error doesn’t look directly related and I can’t reproduce with the scrollnav.js demos on CodePen.

This is the part I am proposing to change. I actually think the problem arises with %-encoded chars since they can not be fetched by document.querySelector().

Therefore, I’d suggest we replace the Umlaute.



Current result produced by slugify: /%C3%BCberhaupt/
Result after applying my modification: /ueberhaupt/

I still think %C3%BC is causing the problem with the querySelector and scrollnav.

Probably, you can’t reproduce the error with CodePen since scrollnav applies their own id-values. If you check the source-code, they actually enumerate all headings in the following pattern:

<h2 id="scroll-nav__1">This is my first heading</h2>
<h2 id="scroll-nav__2">This is my second heading<h2>

Since the Koenig mobile html renderer already creates <h2>, scrollnav does not modificate them anymore.

How could I manually adjust the mobiledoc-html-renderer.js? The question may sound a bit stupid but I was not able figuring out where exactly the file location is.

Edit: But meanwhile I’d check for other possible sources of error.

I edited the CodePen to add ids to the html with url-encoded text and Scrollnav.js worked fine - that’s why I was saying I couldn’t reproduce.

How could I manually adjust the mobiledoc-html-renderer.js ? The question may sound a bit stupid but I was not able figuring out where exactly the file location is.

You’d need to go into your Ghost install dir then change it in versions/current/node_modules/@tryghost/kg-mobiledoc-html-renderer/lib/mobiledoc-html-renderer.js then re-apply the changes every time you upgrade Ghost.

1 Like

I will check all the settings tomorrow after a couple hours of sleep. Really appreciate your fast help :slight_smile:

If you can share the rest of this error message it would help pinpoint what selector is incorrect

Uncaught DOMException: Failed to execute 'querySelector' on 'Document':

Okay so here we go. All occuring error messages are directly related to the headings containing “Umlaute.”

Basically I receive two error messages:

Uncaught DOMException: Failed to execute 'querySelector' on 'Document': '#was-bedeutet-vpn-%C3%BCberhaupt' is not a valid selector.


Uncaught DOMException: Failed to execute 'querySelector' on 'Element': '[data-sn-section=was-bedeutet-vpn-%C3%BCberhaupt]' is not a valid selector.

Now the part that really confuses me is the following:

As you can see the, the <a href=""></a> as well as [data-sn-section=was-bedeutet-vpn-%C3%BCberhaupt] equal the actual id of the heading.

Therefore, I can navigate to the corresponding heading by clicking on the table of content. However, the highlight function does not work and the error is thrown…

I have made the adjustments to versions/current/node_modules/@tryghost/kg-mobiledoc-html-renderer/lib/mobiledoc-html-renderer.js and now everything works like a charme.

If you don’t want to keep making that change each time you upgrade Ghost you could also fix the bug in Scrollnav.js.

In your scrollnav.min.js file, find and replace the following



1 Like