How I Use Eleventy

This website is based on the static site generator Eleventy, the template language Liquid, and the markup language Markdown.

Contents

Working on the Website

Before making changes to the website’s layout and content, start Eleventy and the development server:

npx @11ty/eleventy --ignore-initial --incremental --serve

Publishing the Website

When the changes (e. g., adding new pages, editing pages, modifying the layout) are done, call a shell script publish.sh to upload the changed pages to the web server using FTP:

#!/usr/bin/env bash

USER='<user name>'
PASSWORD='<password>'
HOST='example.org'
LOCAL_DIR="$(pwd)/_site/"   # End slash to prevent creation of dir on server.
REMOTE_DIR='/'
NUM_PARALLEL_CONNS=8

# lftp's `mirror` command supports an option `--delete` to delete files that
# are not present in the source.  However, as this project does not have its
# designated directory on the server, this option is not used for safety
# reasons.
echo "Starting upload at $(date)."
lftp -u "$USER","$PASSWORD" $HOST <<EOF
#set ftp:ssl-allow true
#set ftp:ssl-force true
#set ftp:ssl-protect-data true
#set ssl:verify-certificate no
mirror --parallel=$NUM_PARALLEL_CONNS --verbose -R "$LOCAL_DIR" "$REMOTE_DIR";
exit
EOF
echo "Upload finished at $(date)."

The FTP client LFTP will only upload files that do not yet exist on the web server or are newer than the files on the web server. It can be installed on Linux Mint:

sudo apt install lftp

The correct file permissions are important for this approach to work, as lftp will retain the permissions of the transferred files. Read permissions have to be set for other so the uploaded files can be accessed via HTTP.

Theme Development

Automatic Figures for Fenced Code Blocks

Modify markdown-it’s fenced code renderer to enclose the rendered pre element in a figure element:

import markdownIt from 'markdown-it';

export default function (eleventyConfig) {
  …
  const mdSetup = markdownIt('commonmark', { html: true });
  const proxy = (tokens, idx, options, env, self) =>
    self.renderToken(tokens, idx, options)
  ;
  const defaultFenceRenderer = mdSetup.renderer.rules.fence || proxy;
  mdSetup.renderer.rules.fence = (tokens, idx, options, env, self) => {
    return `<figure class="listing">
  ${defaultFenceRenderer(tokens, idx, options, env, self)}
</figure>\n`;
  };
  …
}

Preserve Text Formatting in TOC Entries

The markdown-it plugin markdown-it-table-of-contents strips Markdown formatting from table of contents entries by default. Text formatting can be preserved by providing a custom implementation for getTokensText that renders the entry’s tokens:

import markdownIt from 'markdown-it';
import markdownItAnchor from 'markdown-it-anchor';
import markdownItTOC from 'markdown-it-table-of-contents';

export default function (eleventyConfig) {
  …
  eleventyConfig.amendLibrary('md', (mdLib) =>
    mdLib.use(
      markdownItTOC,
      {
        includeLevel: [2, 3],
        transformContainerOpen: () => {
          return '<h2 id="toc"><a href="#toc">Table of Contents</a></h2>';
        },
        transformContainerClose: () => { return ''; },

        // Allow markdown formatting in TOC entries.  The default implementation of
        // `getTokensText` removes any formatting.
        getTokensText: (tokens) => { return mdLib.renderer.render(tokens); },
      }
    )
  );
  …
}

Custom Containers in Markdown with Caption

The markdown-it plugin @mdit/plugin-container can be used to generate custom containers, such as figures, listings, and blockquotes. The example below creates a source code listing with a caption:

This is a paragraph.

:::listing This is a _very_ important code sample.
```
int main()
{
    return 42;
}
``` 
:::

This is a paragraph.

This markup is rendered to the following HTML:

<p>This is a paragraph.</p>
<figure class="listing">
  <pre><code>int main()
{
    return 42;
}</code></pre>
  <figcaption>This is a <em>very</em> important code sample.</figcaption>
</figure>
<p>This is a paragraph.</p>

The implementation renders figure elements with a figcaption element at the bottom of the figure. Unfortunately, the close renderer does not provide access to the caption available to the open renderer. Additionally, custom containers can be nested. Therefore we need to push the caption on a stack in the open renderer and pop it in the close renderer:

import markdownIt from 'markdown-it';
import { container } from '@mdit/plugin-container';

export default function (eleventyConfig) {
  …
  let figureCaptions = [];
  const figureOpenRender = (tokens, index, _options) => {
    const info = tokens[index].info.trim();
    const separatorPos = info.indexOf(' ');
    let figureType = '', figureCaption = '';
    if (separatorPos != -1) {
      figureType = info.substring(0, separatorPos).trim();
      figureCaption = info.substring(separatorPos + 1).trim();
    } else {
      figureType = info.trim();
    } 
    figureCaptions.push(figureCaption);
    return `<figure class="${figureType}">`;
  };
  const figureCloseRender = (tokens, index, _options) => {
    const figureCaption = figureCaptions.pop();
    const figcaption =
      figureCaption ?
      `<figcaption>${mdSetup.renderInline(figureCaption)}</figcaption>` : ''
    ;
    return `${figcaption}</figure>`;
  };
  eleventyConfig.amendLibrary('md', (mdLib) => mdLib.use(container, {
    name: 'listing',
    openRender: figureOpenRender,
    closeRender: figureCloseRender,
  }));
  eleventyConfig.amendLibrary('md', (mdLib) => mdLib.use(container, {
    name: 'figure',
    openRender: figureOpenRender,
    closeRender: figureCloseRender,
  }));
  …
}

Create a List of Direct Child Pages

This website uses index pages that provide a list of direct child pages. To do so, it makes use of eleventyNavigation together with keyparent relations set up in the front matter. The Liquid code below compiles the list of child pages. Title and description are taken from each page’s front matter, the filter md renders the Markdown markup to HTML:

{%- assign navPages = collections.all | eleventyNavigation: eleventyNavigation.key -%}
{%- if navPages.size > 0 %}
<dl class="index">
  {%- for child in navPages %}
  <dt><a href="{{ child.url | url }}">{{ child.data.title | md }}</a></dt>
  <dd><p>{{ child.data.description | md }}</p></dd>
  {%- endfor %}
</dl>
{% endif -%}

The listing below shows a solution based on a WebC component that renders a list of the current page’s subpages. The file subpages-index.webc has to be put into the _components directory which is located in the project’s root directory:

<script webc:setup>
const currentKey = getCollectionItem(collections.all).data.eleventyNavigation.key;
const subpages = eleventyNavigation(collections.all, currentKey);
</script>
<dl webc:if="subpages.length > 0">
  <x webc:for="page of subpages" webc:nokeep>
  <dt><a :href="url(page.url)" @html="md(page.data.title)"></a></dt>
  <dd><p @html="md(page.data.description)"></p></dt>
  </x>
</dl>

The eleventyNavigation front matter data entry is shadowed by the eleventyNavigation filter, therefore the current page is retrieved from the collection to gain access to the front matter data. In order to render the dt element together with the dd element for each subpage, they have to be enclosed by an arbitrary placeholder element that holds the webc:for attribute.

The subpages-index element can be added to a layout or template:

<subpages-index></subpages-index>

How To Write Markdown Code