Laurence Mercer

How to build a simple Wagtail Streamfield code block with highlight.js

By default, the formatting features displayed in Wagtail's RichTextField can be modified to include code, which then allows a user to enter inline code. However, there is no such default means of supporting code blocks. For example:

# base/models.py
class BasicPage(Page):
    pass

Existing solutions

Several solutions already exist to tackle this, including:

Another solution(?)

Given my preference for highlight.js, the obvious choice was to implement the solution described by the last of these links. However, whilst it is an extensive tutorial, I really only needed a simple solution which provided a custom Streamfield block that would allow a user to:

  • select the language for which syntax highlighting should be applied
  • (optionally) add a filename, to display at the top of a code block
  • add the code, to display in the body of the code block

How to

My solution was to:

  • create a custom Streamfield block model (CodeBlock)
  • create a custom template (code_block.html) which would be used to render the new block
  • edit my existing blog_page.html template to include the relevant CSS and JavaScript files required by highlight.js

For simplicity, I put the CodeBlock model and the code_block.html template in my existing, general-purpose base app.

The CodeBlock model

Notably, the CodeBlock model:

  • subclasses Wagtail's StructBlock (as per Wagtail's documentation)
  • defines a language field which is a ChoiceBlock whose choices are key-value pairs wherein each key matches the alias of a language supported by highlight.js
  • defines an optional filename field
  • defines a code field with a form_classname that can be used to target the field in the admin UI
  • defines a template to use when rendering the block
# base/models.py
from wagtail.blocks import CharBlock, ChoiceBlock, StructBlock, TextBlock


class CodeBlock(StructBlock):
    """
    A custom Wagtail Streamfield block for displaying code highlighted using
    highlight.js.
    """
    language = ChoiceBlock(
        choices=[
            ("bash", "Bash"),
            ("cs", "C#"),
            ("css", "CSS"),
            ("django", "Django"),
            ("dockerfile", "Dockerfile"),
            ("gdscript", "GDScript"),
            ("graphql", "GraphQL"),
            ("javascript", "JavaScript"),
            ("json", "JSON"),
            ("html", "HTML"),
            ("tex", "LaTeX"),
            ("less", "Less"),
            ("markdown", "Markdown"),
            ("nginx", "Nginx"),
            ("plaintext", "Plaintext"),
            ("postgresql", "PostgreSQL"),
            ("php", "PHP"),
            ("python", "Python"),
            ("python-repl", "Python REPL"),
            ("scss", "SCSS"),
            ("sql", "SQL"),
            ("twig", "Twig"),
            ("typescript", "TypeScript"),
            ("xml", "XML"),
            ("yml", "YAML"),
        ],
        required=False
    )
    filename = CharBlock(
        required=False
    )
    code = TextBlock(
        form_classname="code-block",
    )

    class Meta:
        template = "base/code_block.html"

The code_block.html template

Notably, the code_block.html template:

  • uses value to access the model's fields
  • renders the optional filename markup separately from the code
  • uses language-{{ value.language }} as a target for highlight.js
# base/templates/base/code_block.html
{% load wagtailcore_tags %}

{% if value.filename %}
<span class="filename"># {{ value.filename }}</span>
{% endif %}
<pre><code class="language-{{ value.language }}">{{ value.code }}</code></pre>

The blog_page.html template

Notably, the blog_page.html template:

# blog/templates/blog/blog_page.html (abridged)
{% extends "base.html" %}
{% load static wagtailcore_tags %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/a11y-dark.min.css' %}">
{% endblock extra_css %}
{% block extra_js %}
<script src="{% static 'js/highlight.min.js' %}" defer></script>
<script src="{% static 'js/code.js' %}" defer></script>
{% endblock %}

{% block content %}
{% include_block page.body %}
{% endblock %}

Code

For the complete website code, please see https://github.com/input/lm-wagtail.

Return to blog