Option to shift headings?

I’ve had a look around and haven’t had any luck finding this out.

Markdown articles will sometimes contain <h1> elements that may clash with the page being rendered (where an <h1> for the article section would be used for the post title, for instance).

Is there an option or setting to force all headings in a post to start at a certain level (that is, all headings in a post now begin at <h2>, even if they were created as <h1> / # elements)?

1 Like

I described doing exactly this in another Hugo Forum Thread: How I Extended Markdown for Hugo

I do a number of other changes to Markdown rendering in that post, so let me break out just the part you need.

We will create a partial template called contentShiftHeadings.html with the following code:

{{/* get the Markdown without front matter  */}} 
{{ $s := .RawContent }}

{{/* the regex of start of line `^` doesn't work. (GoLang has a subset of regex)
     Need to specify newlines via `\n` 
     Need to ensure  newlines at start and end of content */}} 
{{ $s :=  delimit (slice "\n" $s "\n") "" }}

{{/* search for any Markdown headings `#` at start of line  
      and add another `#` to start of line. NOTE: we could increment by more `#` here. */}} 
{{ $s :=  $s | replaceRE "\n(#+) " "\n#$1 " }}

{{/* render the Markdown to HTML
     declare it to be safe HTML and return */}} 
{{ $s :=  $s | markdownify }}
{{ $s :=  $s | safeHTML }}
{{ $s }}

In all templates replace {{ .Content }} with {{ partial "contentShiftHeadings.html" . }} You may want to refer to the documentation about Partial Templates

This works very well for me.

But there are potential problems:

  1. No shortcodes in the content. RawContent bypasses rendering shortcodes and I didn’t find a way to tigger shortcode rendering later. This doesn’t affect me, but I would prefer not to take away features.

  2. I force safeHTML. I want to sanitize the HTML first, but there is no function for this. There is an issue filed: https://github.com/spf13/hugo/issues/1457

  3. I am not sure how much of an impact this has on Hugo’s site building speed, or if I need to configure caching partial templates.

Hope this helps

1 Like

@patdavid Sounds like @leejoramo has put some thought into his solution.

I don’t know your full use case, but have you considered scripting this against the content files themselves rather than relying on Hugo to do it every single time you build your site? Is there a reason you need # in your content when likely ## is more semantic?

I can’t speak for @patdavid but I have a several reasons for shifting headings.

  1. I feel the default should be that individual Markdown pages should be able to stand alone and content rooted at #

  2. It can depend on context. Are you viewing at a single page or something composed of multiple pages: Sections, Chapter, or Book or a home page that lists the entire content of the last 10 postings.

  3. A number of Markdown editor tools treat heading structures as Tables of Content for navigation or provide outliner functionality. It is better to work with pages rooted at #

  4. Allow writers to not worry about these technical details which can be automated.

  5. I write almost every thing in Markdown. It might end out posted to a blog, exported to MS Word or PDF, copied and pasted into an email, made into a Presentation. Consistent use of starting with # makes it easier to build workflows to wherever the content gets sent.

In the past I have used Pelican with this python markdown add-on: https://github.com/cprieto/mdx_downheader

2 Likes

I use the HTML5 outline algorithm for this.

Given a document like

---
title: bald eagle
---

# Actually not bald

…

# Cooler than turkeys

…

I’d have it generate a page that uses sectioning elements like article and section like so:

<!DOCTYPE html>
<html lang="en-US">
<head>
    <meta charset="UTF-8" />
    <title>allbirds.local</title>
</head>
<body>
    <h1>allbirds.local — your local guide for all birds!</h1>
    <article>
        <h1>bald eagle</h1>
        <section>
            <h1>Actually not bald</h1>
            …
        
            <h1>Cooler than turkeys</h1>
            …
        </section>
    </article>
</body>
</html>

And conformance checkers (but not, alas, anything designed for blind people — nobody’s put a fire under the people who make VoiceOver and JAWS yet) would realize the outline looks like this:

- allbirds.local — your local guide for all birds!
    - bald eagle
        - Actually not bald
        - Cooler than turkeys
1 Like

Sorry for my late response, was busy working on other things for the site.

Yes, all of the reasons as enumerated by @leejoramo are reasons I also asked my question in the first place.

In particular, authors shouldn’t have to worry about this. Just let them write valid, stand-alone markdown posts. In this case it’s perfectly natural for them to start headings at # in their doc. Or, they are cognizant that the title may be the <h1> and as such start all of their markdown headings at ##.

I happened to use Pelican for building www.gimp.org, and it actually has an option to set a base header level for rendered documents. This is pretty handy for the reasons listed above.

I suppose this could be done with simple regex - where/how would I inject this into my site build when parsing .md?

1 Like

I wrote up https://github.com/gohugoio/hugo/issues/6373 to try to do this in hugo itself. It should be possible, as blackfriday supports it

1 Like

Hi, I know this is an ancient topic, but since issue 6373 went nowhere, this is the next best thing I could found.

Thank you so much for this extremely insightful code. I have made a couple of changes to get it work with shortcodes and (layout-specific) render hooks.

{{/* get the Markdown without front matter  */}} 
{{- $s := .content -}}

{{- /* the regex of start of line `^` doesn't work. (GoLang has a subset of regex)
     Need to specify newlines via `\n` 
     Need to ensure  newlines at start and end of content */ -}}
{{- $s  =  delimit (slice "\n" $s "\n") "" -}}

{{- /* search for any Markdown headings `#` at start of line
      and add another `#` to start of line. NOTE: we could increment by more `#` here. */ -}}
{{- $s  =  $s | replaceRE `\n(#+) ` `#$1 ` -}}

{{- /* render the Markdown to HTML
     declare it to be safe HTML and return */ -}}
{{- $s  =  $s | .page.RenderString -}}
{{ $s }}

And in place of {{ .Content }}, call the partial with {{ partial "contentShiftHeadings.html" (dict "page" . "content" .RawContent) }}