React + Hugo

In order to render a React component in the Hugo framework, we need first a way to have custom JS for a certain blog post. Let’s start with that.

Adding Javascript to a post in Hugo

There are different approaches to this. I’ve used one where I can have a specific JS file for any given blog post.

  1. Find the single.html file in the layouts directory in your Hugo theme. (this could be in default or in posts or articles folder, in my case it’s themes/jane/layouts/articles/single.html).

  2. Go to the part where you render the content with {{ .Content }} and after it, add:

    {{- $jsFile := .Title | anchorize -}}
    {{- $pathJS := (printf "%s%s%s" "js/posts/" $jsFile ".js" ) -}}
    
      {{- if (fileExists (printf "%s%s" "static/" $pathJS)) -}}
         <script type="text/javascript" src="{{ $pathJS | absURL }}"></script>
      {{- end -}}
      
    
  3. In your root site directory, create a directory under static/js/ called posts.

  4. Create a new file with the name of a blog post slug, like this-is-a-post.js if you have a post called “This is a post”.

  5. Inside that file you can now add some JS code like:

    alert('From JS');

What this is doing is trying to find a JS file whose name is the title in the URL form and lives in the static/posts/... directory. This approach could be extended to support multiple files per post too but let’s keep it simple to focus on how to add React.

React component without JSX in a post in Hugo

Now that we have a file where we can put any Javascript code, we can change our alert from before to something in React like:

'use strict';

const e = React.createElement;

class LikeButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = { liked: false };
  }

  render() {
    if (this.state.liked) {
      return 'You liked this.';
    }

    return e(
      'button',
      { onClick: () => this.setState({ liked: true }) },
      'Like'
    );
  }
}

const domContainer = document.querySelector('#root');
ReactDOM.render(e(LikeButton), domContainer);

And then add an element to render this component in the DOM. If you’re using Hugo 0.60 or a more recent version, you need to create a shortcode in your theme to render HTML directly first.

Go to your theme folder / layouts / shortcodes, and create a new file called raw-html.html. The content should be:

{{ .Inner }}

Now, in the article you’re writing add somewhere:

{{< raw-html >}}
<div id="root"></div>
{{</ raw-html >}}

You’ll also need to load React and there are several ways to do this depending on how efficient you want to be. I think the simplest approach is to have a new part of the template that will iterate over a collection of JS files you want to add.

To do so, go to the header file in your theme and add:

{{- if and .IsPage .Params.jsFiles -}}
{{- range .Params.jsFiles -}}

<script src="{{ . }}"></script>

{{- end -}}
{{- end -}}

I assume we’ll use external JS so we don’t have to host them ourselves but we could modify this approach to do it too.

And now, in your post where you want to use React, you can add at the top:

------
....
jsFiles:
- "https://unpkg.com/react@16/umd/react.production.min.js"
- "https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"
...
------

In this fashion, for every article we’ll have the chance to load any JS files we want from a CDN.

But still this code will probably look odd to you if you’re used to JSX since we’re not using any of that syntax there. The problem is that the JSX syntax is not part of ES6 or anything like that so the browser by default doesn’t recognize it as valid. That’s why we had to use the createElement call. Let’s see how we can fix that.

React component with JSX in a post in Hugo

In order to solve the problem with JSX and being able to use it too, we need to include Babel.

  1. Add this line to the included scripts in the header:
https://unpkg.com/babel-standalone@6/babel.min.js
  1. In the single.html file, change the code we added to:

    {{- $jsFileName := .Title | anchorize -}}
    {{- $pathWithoutExtension := (printf "%s%s" "js/posts/" $jsFileName)  -}}
    {{- $pathJS := (printf "%s%s" $pathWithoutExtension ".js" ) -}}
    {{- $pathJSX := (printf "%s%s" $pathWithoutExtension ".jsx" ) -}}
    
    {{- if (fileExists (printf "%s%s" "static/" $pathJS)) -}}
    
      <script src="{{ $pathJS | absURL }}"></script>
    
    {{- else if (fileExists (printf "%s%s" "static/" $pathJSX)) -}}
    
      <script type="text/babel" src="{{ $pathJSX | absURL }}"></script>
    
    {{- end -}}

I’m sure there’s a better way to concatenate strings in Hugo but that’s what I use for now. 🤷‍♂️

  1. And now you can use JSX freely in the title-of-the-article file, but with jsx as its extension. For example title-of-the-article.jsx:
'use strict';

class LikeButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = { liked: false };
  }

  render() {
    if (this.state.liked) {
      return 'You liked this.';
    }

    return (
      <button onClick={() => this.setState({ liked: true })}>
        Like
      </button>
    );
  }
}

ReactDOM.render(
  <LikeButton />,
  document.getElementById('root')
);

Here’s a live example:

Approach for more complex React code

I’m aware this is not the ideal scenario to work with complex components hierarchies in React but I can use this to show small examples of components in React without having to bundle anything on my side.

For a more complex setup, we’d probably need a full tool and bundling process and a way to include the results files in the articles. Maybe in the future if I need it I’ll try to make it work with Hugo. 👋🙂