I use Eleventy for this site. The documents I write in Djot are Syncthing’d to my fileserver and the webserver this site is hosted on, which runs a simple SystemD service that just starts up 11ty’s dev server. It’s reverse-proxied via Nginx. I tried to set it up on a subdirectory of my main site, but getting the websockets required by the auto-reload script to work was just too much of a pain to be worth it.
Here’s my Eleventy config. It’s largely inherited from Una’s.
import handlebarsPlugin from '@11ty/eleventy-plugin-handlebars';
import syntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight';
import fs from 'node:fs/promises';
import process from 'node:process';
import path from 'node:path';
import djot from '@djot/djot';
import {parse as jik} from '@bgotink/kdl/json';
import { DateTime } from 'luxon';
export default function (cfg) {
// plugins
cfg.addPlugin(syntaxHighlight);
cfg.addPlugin(handlebarsPlugin);
cfg.setInputDirectory("/opt/garden/source/");
// Relative to input directory
cfg.setIncludesDirectory("_includes");
// Keys are relative to project root; values are relative to the output folder
cfg.addPassthroughCopy({'source/static': 'static'});
// Additional things to watch, relative to project root
cfg.addWatchTarget("source/_include/");
cfg.addWatchTarget("source/eleventy.config.js");
// Additional template format for djot
cfg.addTemplateFormats('dj');
// experiment
cfg.setFrontMatterParsingOptions({
engines: {
kdl: (str) => jik('- {\n'+str+'\n}'),
}
});
function renderDjot(c, highlight) {
let general = (node, renderer) => {
if (node.attributes && node.attributes.class === 'noscript') {
return '<noscript>'+renderer.renderChildren(node)+'</noscript>';
}
if (node.attributes && node.attributes.datetime) {
return '<time datetime="'+node.attributes.datetime+'">'+renderer.renderChildren(node)+'</time>';
}
return renderer.renderAstNodeDefault(node);
};
return djot.renderHTML(djot.parse(c), {
overrides: {
div: (node, renderer) => {
let clz = node.attributes ? node.attributes.class : '';
if (clz === "figure" || clz === "figcaption" || clz == "details" || clz == "summary") {
return `<${clz}>${renderer.renderChildren(node)}</${clz}>`;
}
return general(node, renderer);
},
span: general,
code_block: (node, renderer) => {
if (node.lang) {
return highlight(node.lang, node.text);
}
return renderer.renderAstNodeDefault(node);
},
verbatim: (node, renderer) => {
if (node.attributes && node.attributes.lang) {
return highlight(node.attributes.lang, node.text).replace(/<pre class="[^"]+">/, '').replace(/<\/pre>$/, '');
}
return renderer.renderAstNodeDefault(node);
},
symb: (node, renderer) => {
return '<span class="symbol symbol-'+node.alias+'"></span>';
},
link: (node, renderer) => {
let clz = node.attributes?.class;
let dest;
if (node.reference && !node.destination) {
dest = renderer.references[node.reference]?.destination;
} else {
dest = node.destination;
}
if (clz === "pronounce") {
return `<a class="pronounce" href="#"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z"/></svg>${renderer.renderChildren(node)} <audio src="${dest}"/></a>`;
}
if (dest?.indexOf('data:image/svg+xml;utf8,') === 0) {
return decodeURIComponent(dest.substring(24));
}
return renderer.renderAstNodeDefault(node);
}
}
});
}
cfg.addExtension('dj', {
key: 'njk',
compile: async function(inputContent, inputPath) {
return async (data) => {
let highlight = this.config.javascriptFunctions.highlight;
let noscript = (node, renderer) => {
if (node.attributes && node.attributes.class === 'noscript') {
return '<noscript>'+renderer.renderChildren(node)+'</noscript>';
}
return renderer.renderAstNodeDefault(node);
};
return renderDjot(await this.defaultRenderer(data), this.config.javascriptFunctions.highlight);
};
}
});
cfg.addExtension('djraw', {
compile: async function(inputContent, inputPath) {
return async (data) => {
return renderDjot(inputContent, this.config.javascriptFunctions.highlight);
};
}
});
cfg.addFilter('keys', function(value) {
return Object.keys(value);
});
cfg.addFilter('keysExceptAll', function(value) {
return Object.keys(value).filter(k => k !== 'all');
});
cfg.addFilter('sortByActualModifiedTime', function(value) {
let arr = [...value];
arr.sort((a, b) => {
return b.data.mtime - a.data.mtime;
});
return arr;
});
cfg.addFilter('toISO', async function(value) {
while (typeof value === 'function') {
value = await value(this);
}
return value ? new Date(value).toISOString() : '??';
});
cfg.addFilter('mtime', async function(value) {
return value.eleventyComputed.mtime(value);
});
cfg.addFilter('templateSyntaxToHighlight', function(value) {
if (value === 'dj') return 'markdown'; // close enough!
if (value === 'njk,md') return 'markdown';
if (value === 'liquid,md') return 'markdown';
if (value === 'njk') return 'jinja2';
return value;
});
cfg.addFilter('basename', function(value) {
return path.basename(value);
});
cfg.addFilter("postDate", dateObj => {
var dateTimeObj;
if (typeof(dateObj) === "object") {
dateTimeObj = DateTime.fromJSDate(dateObj);
} else if (typeof(dateObj) === "number") {
dateTimeObj = DateTime.fromMillis(dateObj);
} else {
return "";
}
dateTimeObj.setZone("America/Chicago");
return dateTimeObj.toLocaleString({
month: "short",
day: "2-digit",
year: "numeric"
});
})
cfg.addGlobalData("layout", "default.njk");
cfg.addGlobalData("eleventyComputed.mtime", () => { return async (data) => {
if (!data.page || !data.page.inputPath) return 0;
let s = await fs.stat(data.page.inputPath);
return new Date(s.mtimeMs);
}});
cfg.addGlobalData("eleventyComputed.srcCode", () => { return async (data) => {
return (await fs.readFile(data.page.inputPath)).toString('utf8').replace(/data:([^\/]+)\/[^;]+;(base64,[a-zA-Z0-9+/=]+|utf8,[^\s]+)/g, '8< snip: raw $1 data');
}});
cfg.addGlobalData("eleventyComputed.wip", () => { return async (data) => {
return data.classes && (data.classes === "wip" || data.classes.indexOf('wip') >= 0)
}});
cfg.addGlobalData("eleventyComputed.blog", () => { return async (data) => {
return data.classes && (data.classes === "blog" || data.classes.indexOf('blog') >= 0)
}});
return {
markdownTemplateEngine: 'njk',
htmlTemplateEngine: 'njk',
};
}