HTML5 slides with Reveal.js, Markdown and Docker

Presentation slides. They can be a great tool for communicating ideas to others. I was looking for a way to create slide decks where I can specify the content as plain text and have it separate to the rendering. I’m sure this is somehow possible with PowerPoint or OpenOffice, but I didn’t bother to look for it. This post shows how I specify the content in a Markdown file and let it render by Reveal.js which I packed into a Docker image.

Change history:
Date Change description
2018-03-16 The first release
2018-03-16 Fixed wording for “3 highlighted empty lines” and a reference.
2018-03-19 Docker is NOT needed. Add an update note to the section.

Encapsulate Reveal.js with Docker

Updated on Mar 19, 2018

Mistakes were made. By me. Big time. Docker is NOT needed to render the Markdown file later. I’ve misinterpreted the README of Reveal.js and didn’t double-check it. What you really need is a web server, which means you can do $ python -m SimpleHTTPServer locally (in the directory where your index.html is) or use GitHub Pages [4] without any additional work. I’ll keep this section to not change the history, but be aware that it is not needed. Thanks to Chris H. for pointing that out.

Reveal.js is capable of using Markdown files [1] and render them as HTML5 slides. The downside of Reveal.js is, that it needs a lot of things installed to work and I didn’t want to have that on my laptop simply to show fancy slides. So I decided to encapsulate it with Docker.

Create a file named Dockerfile to specify the image’s content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
FROM ubuntu:16.04

RUN apt-get update && apt-get install -y \
  git \
  nodejs \
  nodejs-legacy \
  npm \
&& rm -rf /var/lib/apt/lists/*

RUN git clone https://github.com/hakimel/reveal.js.git

RUN cd reveal.js/ && npm install

ADD ./index.html /reveal.js/index.html

EXPOSE 8000

ENTRYPOINT ["npm", "start", "--prefix", "/reveal.js/"]

nodejs Reveal.js is a node.js application, that’s why we need it. If you don’t install nodejs-legacy, you’ll see this error message:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[...]
sh: 1: node: not found
[...]
npm ERR! Linux 3.10.0-693.17.1.el7.x86_64
npm ERR! argv "/usr/bin/nodejs" "/usr/bin/npm" "install"
npm ERR! node v4.2.6
npm ERR! npm  v3.5.2
npm ERR! file sh
npm ERR! code ELIFECYCLE
npm ERR! errno ENOENT
npm ERR! syscall spawn

npm ERR! node-sass@4.7.2 install: `node scripts/install.js`
npm ERR! spawn ENOENT
npm ERR!
npm ERR! Failed at the node-sass@4.7.2 install script 'node scripts/install.js'.
npm ERR! Make sure you have the latest version of node.js and npm installed.

npm is the package manager for node.js applications, which is needed so that the dependencies of Reveal.js get installed. The index.html file gets explained in the next section. The ENTRYPOINT starts the web server of Reveal.js when a container based on that image is started.

The slide deck main page

Create the file index.html, which will be used by reveal.js to serve when the HTTP server starts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

        <title>Markus Slides</title>

        <link rel="stylesheet" href="css/reveal.css">
        <link rel="stylesheet" href="css/theme/black.css">

        <!-- Theme used for syntax highlighting of code -->
        <link rel="stylesheet" href="lib/css/zenburn.css">

        <!-- Printing and PDF exports -->
        <script>
            var link = document.createElement( 'link' );
            link.rel = 'stylesheet';
            link.type = 'text/css';
            link.href = window.location.search.match( /print-pdf/gi ) ? 'css/print/pdf.css' : 'css/print/paper.css';
            document.getElementsByTagName( 'head' )[0].appendChild( link );
        </script>
    </head>
    <body>
        <div class="reveal">
            <div class="slides">
                <section data-markdown="content/index.md"
                         data-separator="^\n\n\n"
                         data-separator-vertical="^\n\n"
                         data-separator-notes="^Note:"
                         data-charset="iso-8859-15">
                </section>
            </div>
        </div>

        <script src="lib/js/head.min.js"></script>
        <script src="js/reveal.js"></script>

        <script>
            // More info about config & dependencies:
            // - https://github.com/hakimel/reveal.js#configuration
            // - https://github.com/hakimel/reveal.js#dependencies
            Reveal.initialize({
                    dependencies: [
                            { src: 'plugin/markdown/marked.js' },
                            { src: 'plugin/markdown/markdown.js' },
                            { src: 'plugin/notes/notes.js', async: true },
                            { src: 'plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } }
                    ]
            });
        </script>
    </body>
</html>

I basically copied the index.html which comes with Reveal.js and changed only the highlighted lines.

Note

Please note that the links are URIs and not operating system file paths.

In detail:

  • data-markdown="content/index.md specifies the path to the Markdown file. This will become important later when we start the container and do a bind mount from the host file system.
  • data-separator="^\n\n\n" tells Reveal.js to interpret 3 empty lines in the index.md content file as the beginning of a new slide.
  • data-separator-vertical="^\n\n" creates a new vertical slide, which is a nice feature of Reveal.js, which, for example, let’s you add backup slides closer to the content.
  • data-separator-notes="^Note:" let’s you add speaker notes which get shown in a separate browser window when opened.
  • data-charset="iso-8859-15" make it work for Western European languages.

Let’s create the content linked to with content/index.md in the next section.

Specify the content in Markdown

Create the file index.md which specifies our content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Example with Code

first slide



## Python Code

second slide

```python
def do_something():
    action = "did something"
    print(action)
    return 0
```

Note:
This will only display in the notes window.



## Bullet List

third slide

* this
* and that
* and other things



# Image

fourth slide

![Example image](content/example.drawio.svg)

As specified with ^\n\n\n", 3 empty lines signal the beginning of a new slide. The speaker notes get added with Note:. The last highlighted lines links to an image. This example image example.drawio.svg (not shown in this post) is locally in the very same directory where example.md resides in. The link begins with content/ though, because this the bind mount directory I will declare when running the container later.

Build the Docker image

We have the Dockerfile and all things which need to be part of the image specified, now we need to build the image.

Create the file build.sh to build the image. This needs to be executed in the same directory as the Dockerfile:

1
2
3
#!/usr/bin/env bash

docker build --rm --tag markus:revealjs .

I tagged the image with markus:revealjs to reference it easier later on.

Build the image with:

1
$ ./build.sh

Note

You don’t need to save those commands in bash scripts like I did here. It’s simply a thing I do when I have a hard time to remember the exact CLI command. It’s other benefit is, that I can commit that file into git (or any other VCS).

Run the container with a bind mount

Create the file run.sh to run the Docker container. The different options are explained below this:

1
2
3
4
5
6
7
8
#!/usr/bin/env bash

docker run \
-d \
-v $(pwd):/reveal.js/content:Z \
-p 50000:8000 \
--name slides \
markus:revealjs
  • docker run: run a Docker image.
  • -d: let it run in the background as a daemon. I had trouble exiting the process when running it as foreground process, that’s why I let it run in the background.
  • -v $(pwd):/reveal.js/content:Z: do a bind mount of the current directory on the host ($(pwd)) to the directory /reveal.js/content within the running container and consider the SELinux context (Z) so that Reveal.js is allowed to read the files in that directory.
  • -p 50000:8000: use the container’s exposed port 8000 and bind it the the hosts port 50000 (I’ve arbitrarily chosen).
  • --name slides: gives the container the name slides, which makes it easier to reference to.
  • markus:revealjs: use the image with that tag (we’ve set when building the image in the previous section).

Run the container and watch your slides with:

1
2
$ ./run.sh
$ google-chrome localhost:50000

This will open the index.html which renders your Markdown content with Reveal.js. The slide overview is opened with Esc.

Markdown content rendered with |rs| in the browser.

Clean up the container

After everyone is thoroughly impressed by your slides, the container needs to be stopped and removed.

Create the file clean.sh which helps with the cleanup:

1
2
3
#!/usr/bin/env bash

docker stop slides && docker rm slides

We use the name slides we specified when starting the container in the previous section.

Start the cleanup with:

1
$ ./clean.sh

Re-use the container for different presentations

My initial thought was to re-use this container for different presentations where I only need to store the content in different files, all named index.md but in different directories. My local directory structure looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ tree
.
|-- docker
|   |-- build.sh
|   |-- clean.sh
|   |-- Dockerfile
|   |-- index.html
|   `-- run.sh
`-- slides
    |-- topic-about-that
    |   |-- index.md
    |   `-- run.sh
    `-- topic-about-this
        |-- example.drawio.svg
        |-- index.md
        `-- run.sh

Depending on which presentation I want to show, I switch into that directory and call ./run.sh to let the bind mount of Docker make the index.md available inside the container.

Conclusion & Outlook

The way I encapsulated Reveal.js here in a Docker container lets me use it without polluting my local laptop with things I rarely need.

The image I created is big (~700MB), which doesn’t cause any trouble because I have enough disk space, but I was still wondering if I could get that smaller with an alpine [2] or nodejs [3] base image instead of the ubuntu image.

To be honest, my initial goal was to have static HTML generated, which I can push to github pages or something similar. I couldn’t figure out if this was possible with Reveal.js.

The beautiful transitions between the slides only keep being beautiful if there is no lag, like in some screen sharing solutions. I’m going to use it in a face-to-face meeting in the next weeks (without any screen sharing), maybe it will work well for me in that scenario.