Leveraging Eleventy’s custom data file format feature for simpler game scoring format
Code in this blog post was written with versions: eleventy: 3.1.2, Node: 23.3.0

Let’s talk about an Eleventy feature I didn’t know existed until yesterday and how it makes it easier for non-technical users to add data: Custom Data File Formats.
A quick story time to set the context: last weekend, our local Pokémon TCG gaming group started a Progression Series and we wanted an easy way to track scores across different weeks of gaming. So I said I’ll build a website and when I’m about to build a website, you know I’m reaching for Eleventy.
I wanted a way to add individual match scores to the website easily, preferably without having to access a computer, and have the website update all the different parts: individual matchup data, ranking table for current set and ranking table across all sets.
Custom Data File Format
I decided to go with the following format:
PlayerA:PlayerB:ScoreA:ScoreB
For each TCG set we open and play with, I add a new file into
_data/
folder. The first one was Scarlet
and Violet base set so I created a file
svi.score
and added our matches to it.
The .score
part is important here! It
defines our custom format so we can use Eleventy to parse it into a data
format we can use in the templates.
eleventyConfig.addDataExtension("score", (contents, filePath) => {
const matchData = contents.split("\n");
const ranking = {};
const matches = [];
matchData.forEach((match) => {
let [home, away, homeScore, awayScore] = match.split(":");
matches.push([home, away, homeScore, awayScore]);
if (!ranking[home]) {
ranking[home] = { games: 0, wins: 0, losses: 0 };
}
if (!ranking[away]) {
ranking[away] = { games: 0, wins: 0, losses: 0 };
}
let isHomeWin = parseInt(homeScore) > parseInt(awayScore);
ranking[home].games += 1;
ranking[home].wins += isHomeWin ? 1 : 0;
ranking[home].losses += isHomeWin ? 0 : 1;
ranking[away].games += 1;
ranking[away].wins += isHomeWin ? 0 : 1;
ranking[away].losses += isHomeWin ? 1 : 0;
});
const rankingTable = Object.entries(ranking).map(([playerName, stats]) => {
return {
playerName,
stats,
};
});
rankingTable.sort((a, b) => b.stats.wins - a.stats.wins);
return { table: rankingTable, matches };
});
Let’s break it down!
eleventyConfig.addDataExtension("score", (contents, filePath) => {
Using addDataExtension
, we define that
any file in _data/
that ends with file
extension .score
is processed by this
function. Its contents are read into a single string
contents
.
In my format, there’s one match per line so I split by line breaks (”\n"
) and for each line, split it by our custom format delimeter
":"
.
I then populate two thins: ranking
for
players’ scores and matches
for
individual matchups.
I also have a _data/series.json
file
which defines which series we have:
[
{
"name": "Scarlet and Violet",
"id": "svi",
"ongoing": true
}
]
The id field points to a score file to let our system know which scores are for which series.
Dynamically accessing global data?
I still have a problem though. I was able to come up with a workaround I’ll share here but if anyone knows a better way, please get in touch!
In my template, I want to loop over the series with {% for serie in series %}
and then dynamically access a global data object based on the
serie.id
. However, I haven’t been able
to figure out how to do quite that so I built a bit of a workaround by adding
them into a custom collection.
eleventyConfig.addCollection("seriesScores", (collection) => {
const all = collection.getAll();
const data = all.find((t) => t.inputPath === "./index.njk").data;
const series = {};
data.series.forEach((serie) => {
series[serie.id] = data[serie.id];
});
return series;
});
Here, I get all the stuff Eleventy knows about. It contains an object for each
template and global data is stored within each of those. So I pick one of
them, in my case the one for index.njk
,
loop over my known series to know which global data objects contain series
scores and then store and return the data accordingly.
Now, in my template, I can do
{% for serie in series %}
{% for player in collections.seriesScores[serie.id].table %}
// do stuff
{% endfor %}
{% endfor %}
I hope to discover a better way to do this. Something that would let me skip the extra collection creation.
User experience is much better now
When someone finishes a game and report it in Discord, I can open the
repository in GitHub with my mobile, add an entry to the
svi.score
file and commit it and the
website will be updated.
While I’m comfortable editing JSON or Javascript files for data (as I do in many of my other projects like our meetup websites), for this one I wanted to explore something that would allow non-developers to add matchup data in a simple format.
I think this format is as close as you can get to it while keeping the data parsing simple and robust.
If something above resonated with you, let's start a discussion about it! Email me at juhamattisantala at gmail dot com and share your thoughts. In 2025, I want to have more deeper discussions with people from around the world and I'd love if you'd be part of that.