Using .RenderString to expand shortcodes

My site has markdown files with shortcodes for embedding photos. I have two versions of the same photo shortcode, one for HTML output, and one that inserts informative placeholders where the images should go for a custom plaintext output format. For my plaintext output, I want Hugo to expand those shortcodes, but leave the markdown as-is.

For example, if my markdown file contains:

## Forest Photos

Here is a *nice* photo:

{{< photo src="photo.jpg" alt="A photograph of a tree." caption="An interesting tree I found in the forest." >}}

My HTML output, via {{ .Content }} in my HTML template, renders as intended:

<h2>
  Forest Photos
</h2>
<p>
  Here is a <em>nice</em> photo:
</p>
<figure class="photo">
  <img src="photo.jpg" alt="A photograph of a tree." height="200" width="300" />
  <figcaption>
    An interesting tree I found in the forest.
  </figcaption>
</figure>

My markdown source is already nicely formatted. For my plaintext output, my first instinct was to use {{ .RawContent }}, but that leaves the shortcode as-is:

## Forest Photos

Here is a *nice* photo:

{{< photo src="photo.jpg" alt="A photograph of a tree." caption="An interesting tree I found in the forest." >}}

If I use {{ .Plain }}, my shortcode works, but my markdown is flattened and stripped of semantic meaning:

Forest Photos
Here is a nice photo:
|--PHOTO---------------------------------------------->

   A photograph of a tree.

   Caption:
    "An interesting tree I found in the forest."

   URL:
   <http://example.com/content/photo.jpg>

<--END-----------------------------------------------||

In an earlier thread, bep suggested using {{ .RawContent | .RenderString }} to expand the shortcode, but that also converts the markdown to HTML, making it functionally equivalent to {{ .Content }}.

The .RenderString docs mention an optional markup argument, used to specify a content format to convert to HTML. One of those content formats listed is HTML. I thought perhaps if I passed in my markdown and called it HTML, .RenderString would leave it alone but expand the shortcodes as desired.

{{ $opt := dict "display" "block" "markup" "html" }}
{{ .RawContent | .RenderString $opt }}

This resulted in a build error:

Error: Error building site: failed to render pages: render of "page" failed: "/Users/tmcltr/Developer/myproject/layouts/_default/single.plaintext.txt:8:17": execute of template failed: template: _default/single.plaintext.txt:8:17: executing "_default/single.plaintext.txt" at <.RenderString>: error calling RenderString: "/Users/tmcltr/Developer/myproject/content/a-section/a-post/index.md:1:1": no content renderer found for markup "markdown"

The content file mentioned there doesn’t seem to be the problem. no content renderer found for markup "markdown" appears to be what it says when it doesn’t recognize the specified content format.

I tried a few different variations. Instead of html, I tried HTML, htm, HTM, md, and foo, but the build error was unchanged. I tried markdown and goldmark, both of which allowed the build to complete without error. But of course my markdown was rendered into HTML.

I guess I have two questions:

  1. Is this a bug? Is HTML not a valid content format to send through .RenderString?
  2. Is there no way to expand shortcodes without converting to HTML?

I don’t know of any way to accomplish your objective.

If you want to retain the markdown, you must use .RawContent.

If you want to render a shortcode, you must use:

  • .Content, OR
  • .Plain, OR
  • .RawContent | .Page.RenderString

You can’t have it both ways. There is no method to retain the markdown AND render a shortcode.

OK, here’s an ugly hack. We can take advantage of the fact that Hugo renders shortcodes within fenced code blocks, but does not render markdown within fenced code blocks.

content/post/test.md

This is **bold** text.

```bash
declare a=1
```

{{< foo >}}

site config

[outputs]
page = ['HTML','MARKDOWN']

layouts/_default/single.md

{{- $c := printf "~~~\n%s~~~" .RawContent | .Page.RenderString }}
{{- $lines := split $c "\n" }}
{{- range $k, $_ := $lines }}
{{- if and $k (ne $k (sub (len $lines) 1)) }}
{{ . }}
{{- end -}}
{{- end -}}

This will fall apart if your markdown contains fenced code blocks, and it will problably fall apart in other scenarios as well. I wouldn’t do it.


Edit: I replaced the bacticks with tildes. That addresses the situation when markdown contains code blocks fenced with backticks.

1 Like

That’s brilliant! This appears to work well. Thanks very much!

So if you’ll help me understand this: the magic appears to happening largely in that first line – wrapping the whole .RawContent in tildes so that it renders as one big code block in order to get the behavior we want from .RenderString.

Then you’re splitting at newline characters, and writing it out line by line, omitting the first and last line where the <pre></pre> and <code></code> tags are.

Is that accurate? That second bit where you split at newlines and iterate over each line seems strange to me, but I suppose it’s probably better than doing a replaceRE on those <pre>/<code> tags. Or is there more to it than that?

Thanks again, I really appreciate your help.

1 Like

Yes

I know that the first and last line need to vanish, but I don’t know what the content will be, so replaceRE seemed risky.

I prefer “ugly and fragile”, but thanks just the same. :slightly_smiling_face:

Also, I recommend you include a link to this topic as a comment in your code. Otherwise, when someone looks at your code in the future, there’s going to be some serious head scratching.

One more change. We need to unescape HTML in the output.

markdown

This is **bold** text.

```html
<div class="bar">
  <h2>Something</h2>
</div>
```

{{< foo >}}

template

{{- $c := printf "~~~\n%s~~~" .RawContent | .Page.RenderString | htmlUnescape }}
{{- $lines := split $c "\n" }}
{{- range $k, $_ := $lines }}
{{- if and $k (ne $k (sub (len $lines) 1)) }}
{{ . }}
{{- end -}}
{{- end -}}

Haha, yes, comments are important. I’m also hard-wrapping lines at 67 characters, so my template looks like this:

{{- /*
  There's a lot going on here.
  
  (See <https://discourse.gohugo.io/t/using-renderstring-to-expand-shortcodes/40994/5> for details.)

  For this plain text version, we want to expand our shortcodes, but keep the original Markdown formatting. Hugo doesn't make that easy.
*/ -}}

{{- /*
  First we wrap the entire .RawContent in tildes so that .RenderString treats it as one big code block. Inside a code block, Hugo will leave the markdown alone, but expand shortcodes like we want.
*/ -}}
{{- $c := (printf "~~~\n%s~~~" .RawContent) | .Page.RenderString | htmlUnescape }}

{{- /* We also want to hard wrap lines at 67 characters. */ -}}
{{- $c = delimit (findRE `.{1,67}[[:space:]]|\S{67}` ( $c )) "\n" -}}

{{- /* Now we split at newlines and print each line, omitting the first and last line to remove the lingering <pre> and <code> tags. */ -}}
{{- $lines := split $c "\n" }}
{{- range $k, $_ := $lines }}
{{- if and $k (ne $k (sub (len $lines) 1)) }}
{{ . }}
{{- end -}}
{{- end }}
1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.