these notes concern ways of importing markdown from external files into a page built with eleventy, primarily to coalesce multiple markdown notes into a single web page. if you want to use eleventy like a normal person (1 markdown file = 1 blog post) and you’re just getting started with eleventy then you probably just want to write a layout for your markdown files.
collections
if each markdown file is treated by eleventy as a page (it’s not in a directory that doesn’t get built like _includes) then markdown files can be accessed using collections.
benefits:
- can be used in for loops
- already preconfigured – don’t need to mess about with plugins and filters to use it
- if want to return a subset, can use filters
cons:
so far i’ve only figured out how to use it with for loopslol literally two days after writing this i finally figured out how to write filters that take a collection as an argument then returns something else :’) see the next heading for an exampleif you want to render an individual file, can hack it using a for loop with only an if statement inside with some unique condition. i image if u have a lot of files in the collection though it might get inefficientsee the writing a filter to render a specific markdown file without front matter for rendering a specific file that doesn’t use collections
how i use it on my crafts page:
{% for fo in collections.crafts %}
<section>
<input class="modal-toggle" id="modal-{{ fo.page.fileSlug | slugify }}" type="checkbox" />
<label class="modal-open" for="modal-{{ fo.page.fileSlug | slugify }}">
<div class="img-box">
<img src="{{ fo.data.image }}" alt="{{ fo.data.title }}" />
<div class="transparent-box">
<h2 class="caption">{{ fo.data.title }}</h2>
</div>
</div>
</label>
<div class="modal">
<label class="modal-close" for="modal-{{ fo.page.fileSlug | slugify }}"></label>
<div class="popup">
{{ fo.content }}
</div>
</div>
</section>
{% endfor %}
{{ fo.content }}
is the body of the file, converted to html. {{ fo.data.image }}
and {{ fo.data.title }}
correspond to a property in the YAML front matter. {{ fo.page.fileSlug | slugify }}
is the filename without extension with the slugify filter. this makes it so i can generate a unique id for each project that is valid (ex. periods removed, spaces converted to dashes). for example, i can name a markdown file cardigan no. 9.md
, and it’ll generate the id modal-cardigan-no-9
.
if you don’t want the file to get their own page, can skip writing to the output folder by adding permalink: false
to the front matter of the markdown file. you can also set it for a whole folder with a directory data file. if, for example, you keep all markdown files used in the generation of /crafts/index.html
inside the folder /crafts/projects/
, you can create /crafts/projects/projects.json
that looks like {"permalink": false}
.
using filters with collections
instead of just returning a subset of a collection as i initially thought, you can also use filters to manipulate the data stored in the collection’s front matter. when i was trying to add a tag system to my crafts page, i had a problem where i wanted to automatically generate the following from the yaml front matter:
- the checkbox you click to show a finished object of a given craft
- the css that handles showing and hiding objects based on which checkboxes are toggled
for each value of a given key, i need to add these two parts to my code. optionally i wanted to extract the number of times each value is used and show that in the checkbox label. after much trial and tribulation i wrote the following filter to accomplish this:
// filter to extract a given yaml key from a collection, then return an array
// of objects with the key name and the count of times it appears. array can
// be sorted by count or alphabetically
eleventyConfig.addFilter("extractKeyWithCount", function (collection, key = "categories", sortBy = "count") {
// initialize an empty object to store counts of each value
let counts = {};
// loop through each item in the collection
collection.forEach(item => {
// if the item has a value for the specified key
if (item.data[key]) {
// if the value is already an array, use that, otherwise convert it to an array
let values = Array.isArray(item.data[key])
? item.data[key]
: [item.data[key]];
// loop through each value in the array of values
values.forEach(val => {
// trim the value and add it to the counts object
let name = String(val).trim();
counts[name] = (counts[name] || 0) + 1;
});
}
});
// convert the counts object to an array of objects
let result = Object.entries(counts).map(([name, count]) => ({
name,
count
}));
// sort the results array
if (sortBy === "alpha") {
result.sort((a, b) => a.name.localeCompare(b.name, "en", { sensitivity: "base" }));
} else if (sortBy === "count") {
result.sort((a, b) => b.count - a.count);
}
return result;
});
the filter applies to a collection, and takes the following arguments:
- the yaml key (defaults to categories)
- the method of sorting, either
count
for the number of appearances from most to least, andalpha
for alphabetically (defaults to count)
then it returns an array of object, where
name
is the name of the valuecount
is the number of times the value appears in the given collection
while trying to figure all this out a wrote an initially simplified version of my craft page that somehow ended up becoming its own much more complicated page (oops). i turned it into a free to use image gallery template, where generating the tag system is accomplished by using the above filter twice in the eleventy template file.
to generate the <label>
elements that you click:
{% assign categoriesSorted = collections.image-gallery | extractKeyWithCount: "categories", "count" %}
<form>{% for category in categoriesSorted %}
<label><input type="checkbox" name="{{ category.name }}">#{{ category.name }} ({{ category.count }})</label>{% endfor %}
</form>
for my demo, this returns
<form>
<label><input type="checkbox" name="day">#day (5)</label>
<label><input type="checkbox" name="night">#night (5)</label>
<label><input type="checkbox" name="thailand">#thailand (3)</label>
<label><input type="checkbox" name="vietnam">#vietnam (3)</label>
<label><input type="checkbox" name="indonesia">#indonesia (1)</label>
<label><input type="checkbox" name="china">#china (1)</label>
<label><input type="checkbox" name="japan">#japan (1)</label>
<label><input type="checkbox" name="taiwan">#taiwan (1)</label>
</form>
to generate the css that shows and hides images based on which tags/<label>
s are toggled:
{% assign categoriesAlpha = collections.image-gallery | extractKeyWithCount: "categories", "alpha" %}{% for category in categoriesAlpha %}
form:has([name="{{ category.name }}"]:checked)~#image-grid-container section:not([category~="{{ category.name }}"]){% unless forloop.last %}, {% endunless %}{% endfor %} {
display: none;
}
for my demo, this returns
form:has([name="china"]:checked)~#image-grid-container section:not([category~="china"]),
form:has([name="day"]:checked)~#image-grid-container section:not([category~="day"]),
form:has([name="indonesia"]:checked)~#image-grid-container section:not([category~="indonesia"]),
form:has([name="japan"]:checked)~#image-grid-container section:not([category~="japan"]),
form:has([name="night"]:checked)~#image-grid-container section:not([category~="night"]),
form:has([name="taiwan"]:checked)~#image-grid-container section:not([category~="taiwan"]),
form:has([name="thailand"]:checked)~#image-grid-container section:not([category~="thailand"]),
form:has([name="vietnam"]:checked)~#image-grid-container section:not([category~="vietnam"]) {
display: none;
}
adding a markdown filter to the eleventy config
you can configure the markdown library eleventy uses to render markdown into html if you want to add some extra features, which will apply anytime the library is used. then you can add your own filter to render a given templating variable. for example, i add markdown-it plugins to set attributes and use definition lists. then i write a filter that would render a variable from markdown into html. these parts of my config file look like this:
const markdownIt = require("markdown-it");
const markdownItAttrs = require("markdown-it-attrs");
const markdownItDeflist = require("markdown-it-deflist");
module.exports = function (eleventyConfig) {
// basic markdown setup
const md = markdownIt({
html: true,
breaks: true,
linkify: true,
typographer: true
}).use(markdownItAttrs)
.use(markdownItDeflist);
eleventyConfig.setLibrary("md", md);
// filters for rendering markdown
eleventyConfig.addFilter("markdownify", (content) => {
return md.render(content);
});
eleventyConfig.addFilter("markdownify-inline", (content) => {
return md.renderInline(content);
});
};
i use the filter markdownify-inline
with my changelog. i store entries in a json file, and i write them in markdown syntax as i find writing links in markdown to be easier than html. here’s the relevant part of my changelog template file:
{% for log in logs %}
<section class="box">
<div class="content">
<div class="date">{{ log[0] }}</div>
<div class="message">{{ log[1] | markdownify-inline }}</div>
</div>
</section>
{% endfor %}
if i didn’t add markdownify-inline
to {{ log[1] }}
, then links would appear like [title](url). that’s because eleventy just assumes it’s plaintext; it needs to be explicitly told to render that part of the text from markdown.
the render plugin
another option is to use eleventy’s built-in render plugin. a good option if you want to embed a chunk of markdown right into a file like
{% renderTemplate "md" %}
# I am a title
* I am a list
* I am a list
{% endrenderTemplate %}
and have it render as html. there’s also options to use it with variables and files. when i tried to use the renderFile filter however, it wouldn’t render just the body of the file but also the front matter. if you don’t use front matter in your markdown files this is probably a more straightforward method than the next solution.
writing a filter to render a specific markdown file without front matter
on my language page, i prefer keeping the text of each tab as a markdown file on my computer. unlike my crafts page, their locations in the html file aren’t generated programmatically; rather i want to tell eleventy which file to add where. while i could use the hack solution i mentioned using collections, i ideally wanted to be able to keep the files in an _includes folder, and i also wanted to have yaml front matter in my files. after longer than i care to admit i figured out how to write a function that takes a file and returns the body without the front matter, converted to html. add this to the top of .eleventy.js
next to the other imported packages:
// necessary npm packages
const fs = require("fs");
const matter = require("gray-matter");
then add the following function inside of module.exports = function (eleventyConfig) {}
// shortcode to include rendered content of a specific input file
eleventyConfig.addShortcode("renderExternalMarkdown", function (filePath) {
// read the file and parse with gray-matter
const fileContent = fs.readFileSync(filePath, "utf8");
const parsedFile = matter(fileContent);
// return the content without the front matter, converted to html
return md.render(parsedFile.content);
});
now on my language page, i can use the following code to render my entry on studying thai:
{% renderExternalMarkdown "language/_includes/thai.md" %}