Example code in a blog that actually runs
With a code block in an article, you never know whether the code actually runs. For Writing robust Web Components I wanted fourteen examples people can try in the browser. The code in the text should be exactly the code that runs there.
Copying the code into the article and into a second file goes wrong the moment you change one of them. Embedding CodePen works, but it brings a third party and an <iframe> along, and the code in the article is still a copy.
I have one file per example instead. Eleventy turns it into the runnable page and the code block in the article, with a link to that page.
One file per example
An example is a .njk file in the examples/ folder of the post. The front matter holds title, instructions and expected output, below it comes the code, without <html> and <body>.
---
heading: What the console box mirrors
steps:
- Open the console and compare it with the "Console output" box.
expected:
- a normal line
---
<script>
console.log('a normal line');
</script>Eleventy renders every file in that folder as a page on its own, no pagination or data list needed. It just has to know these are example pages and not blog posts. A data file in the folder tells it:
// examples/examples.11tydata.js
import { exampleData } from '../../../_11ty/examples.js';
export default exampleData({
base: '/11ty-runnable-examples/examples/',
article: '/11ty-runnable-examples/'
});exampleData() is a small helper of mine. It sets the layout, derives permalink and number from the file name, and keeps the pages out of feeds, sitemap and search. It stores the code without front matter as source, so the layout can show it below the example.
The layout is an ordinary Eleventy layout. Instructions and expected output on top, then a box that mirrors console.log and errors, then the example, and the source last. The example sits directly in the page without an <iframe> and runs with the page's CSS. If you want to isolate it from that, use <iframe srcdoc> and give up the page layout. This is the example from above:
<script>
console.log('a normal line');
console.log('several', 'values', 42, null);
setTimeout(() => {
throw new Error('an uncaught error, shown as well');
}, 0);
</script>The code block comes from a shortcode
In the text, a call stands in for the code:
{% example "01-mirror" %}The shortcode reads the file, makes a code block from it and appends the link:
eleventyConfig.addAsyncShortcode('example', async function (id, region, lang = 'html') {
const dir = path.join(path.dirname(this.page.inputPath), 'examples');
const code = readExample(path.join(dir, `${id}.njk`));
const block =
'```' + lang + '\n' + (region ? excerpt(code, region) : withoutMarkers(code)) + '\n```';
// base: the URL of the example pages, from the data file
return `${block}\n\n[Run example →](${base}${id}/)`;
});It returns Markdown, and markdown-it does the rest, syntax highlighting included. If an example is missing, the build fails. An article with a dead reference shouldn't go online in the first place.
Showing only an excerpt
Often the article needs just a few lines, but the page needs the whole example. For that I mark a region in the example:
<button id="count" type="button">Count</button>
<script>
let clicks = 0;
// #region handler
document.getElementById('count').addEventListener('click', () => {
clicks += 1;
console.log('clicks:', clicks);
});
// #endregion
</script>{% example "02-region", "handler", "js" %} shows only these four lines in the article. The page still runs everything:
document.getElementById('count').addEventListener('click', () => {
clicks += 1;
console.log('clicks:', clicks);
});The markers are comments (//, /* */ or <!-- -->) and don't show up in the result. A typo in the region name fails the build too, so the wrong code doesn't end up in an article.
The examples in Writing robust Web Components run that way. The links below the code blocks lead to the pages, and the code there is the one from the text.
A new post needs only an examples/ folder and a data file for this. Maybe that means I'll write more tutorials now. I have the means for it ready, at any rate.