Skip to main content

Creating connections with music and technology

Building a personal digital music library with Eleventy and APIs

Back in December 2024, while I was doing the usual end-of-year contemplation, Spotify's annual Wrapped feature arrived, ringing a bell in my mind. I realised my scattershot listening habits on Spotify and the snapshot Wrapped presents failed to capture my changing tastes over time or evoke feelings and memories in the same way as a tangible music collection. Music is a fundamental part of my life—I've always been a fan. My earliest memories are rooted in music, exploring my parents' vinyl collection and watching Top of the Pops, fascinated by the sounds and imagery. And later, making the rounds of record shops, building my collection, and plastering my teenage bedroom walls with band artwork, portraits, and memorabilia. In a funny sort of way, music was my gateway into design.

An image from the music video; song lyrics relate to the article's theme.
Always on My Mind by Pet Shop Boys

At the same time I was thinking about this, I was also feeling generally uninspired about the tech industry’s current direction and its impact on the world—especially compared to earlier visions of the internet. I want to direct my online thinking and doing toward something I believe in. My music collection seemed to be one of the areas I could meaningfully apply technology to connect with something I'm passionate about and learn something new in the process. I've previously written about cultivating a digital garden to achieve these aims. One of the concepts I came across while researching the idea was the Memex, proposed by Vannevar Bush in his 1945 article As We May Think.

Illustration of a Memex device, showing a desk with built-in screens, microfilm readers, and a mechanical system for retrieving and linking information.
Theoretical Memex (memory index) device

Bush envisioned a device that would act as a personal knowledge repository, capable of storing and organising books, records, and other information while automatically creating connections between them—essentially serving as a personalised library system. This concept seemed useful for applying to one of the challenges with physical media collections like music and books: as collections grow larger, they become increasingly difficult to organise, navigate, and use to retrieve the ideas and emotions contained within them.

Eleventy

To turn this concept into a working application that would help me reconnect with my music collection, I turned to Eleventy, a static site generator. I had a basic working knowledge of Eleventy before developing the latest iteration of this website in January 2025—learning more was the motivating factor in choosing it. Fortunately, it turned out to be well-suited to this type of task and has an active, supportive community around it.

The rest of this document outlines my approach, decisions made along the way, challenges encountered and solutions found, including some code examples. Rather than a technical how-to guide, think of it as a living document of release notes tracking the progress and evolution of ideas.

Version 1: Proof of concept

So, in summary, here's where I stood at the outset: Spotify Wrapped galvanised me into action and Bush's Memex concept signposted the direction I wanted to move in.

A photograph of the authors CDs and vinyl records in storage boxes.
My music collection

Things I know:

  • I have approximately 500 records—CDs from 1991–2015 and vinyl from 2017–present—that I want to organise meaningfully. I don't have purchase dates for most of the collection.
  • I want to build my own system, incorporating automation using tools and services that align with my values, learning something through the process.

Assumptions:

  • APIs can provide the data and be used to create the automated connections needed for a personalised library system.
  • I can master Eleventy sufficiently to make it serve my needs.

Library classification

Libraries employ classification systems to organise materials on shelves and in catalogues and indexes. Each item gets a call number indicating its location within the system. In record shops, you encounter a faceted classification system that separates vinyl from CDs and organises records by genre (like Rock and Pop), then sorts them alphabetically by artist.

 A card catalogue in the University of Graz Library. Source: https://www.newworldencyclopedia.org/ Vinyl records, CDs, and cassette tapes displayed in a record shop. Source: https://unsplash.com/
Library classification systems

The record industry uses its own system of identifiers called ISRC (International Standard Recording Code), but these aren't readily accessible. Instead, barcodes and label catalogue numbers printed on sleeves and discs—which identify specific versions of releases—can be used to connect physical collections with APIs and create a digital library organised similarly to record shops.

API comparison

There are a range of APIs that provide metadata to help organise and catalogue a library of music releases. The question is which one to choose. I picked several and tried to compare the relative benefits of each:

DiscogsMusicBrainzSpotify
Physical media focusExcellentGoodLimited
Data completenessExtensive physical release data, high accuracy for vinyl/CDHigh for basic metadata, community-maintainedComprehensive for streaming content, limited physical media data
API rate limits60 requests/minute authenticated1 request/sec for anonymous, 4/sec authenticatedSeveral tiers based on API quota
Community contributionModerated submissionsOpen editingClosed system
CostFree for non-commercial useFree, open sourceFree tier available

Discogs appears to best fit my needs and aligns with one of my overarching principles for this project: using services that strike a balance between public good and commercial interests.

Selecting metadata

Taking one of my favourite records as an example: Brian Eno's Another Green World. Using the barcode printed on the sleeve (0602557703887), I can search Discogs and see the resulting URL reveals its release ID (11176407). With this ID, I can fetch information about this specific release at the command line:

curl https://api.discogs.com/releases/11176407 --user-agent "FooBarApp/3.0"

The API returns a wealth of information in JSON format. From this data, I can cherry-pick the essential key/value pairs to display and organise releases in my collection according to format, genre, release year, and artist. Here's a simplified version of the response showing these fields:

{
    "year": 2017,
    "uri": "https://www.discogs.com/release/11176407-Eno-Another-Green-World",
    "formats": [
        {
            "name": "Vinyl",
            "descriptions": [
                "LP",
                "Album",
                "Reissue",
                "Remastered"
            ],
            "text": "180 gram"
        }
    ],
    "videos": [
        {
            "uri": "https://www.youtube.com/watch?v=bNwhtnaoVZU"
        }
    ],
    "genres": [
        "Electronic"
    ],
    "tracklist": [
        {
            "position": "A1",
            "title": "Sky Saw",
            "duration": "3:25"
        }
    ]
}

Release dates

Release dates are a bit of a minefield. While they reliably follow the ISO 8601 format (YYYY-MM-DD), entries can contain partial dates (YYYY or YYYY-MM), and release dates vary between regions like the US and UK. Since my vinyl collection consists mainly of reissues, the dates returned by the API reflect when the reissue was published rather than original release dates. Some degree of imprecision doesn't bother me—this isn't meant to be an encyclopedia. What matters most is having at least the original release year—it allows me to access personal memories by consulting my library and playing records from specific years.

Global Data Files

Before the internet, online databases, and standardised cataloguing protocols, libraries relied on a physical library catalogue system. This used index cards to record key details about each item, making it easy to find and retrieve materials. Following this principle, I use a Global Data File to organise my collection—each entry functions like an index card, with the release ID serving as its call number.

{
  "artist": "Brian Eno",
  "title": "Another Green World",
  "format": "Vinyl",
  "release_id": 11176407,
  "first_released": "1975-11-14",
  "favourite": true
},

In a separate Global Data File, I configure the Fetch plugin to manage data requests. The plugin caches data locally to avoid bombarding the API with requests for assets including JSON, HTML, images, videos, etc. Walking through the code, here's how I fetch data from the Discogs API:

  • Import Node packages to manage environment variables, handle API requests, and perform asynchronous file operations
  • Set up Discogs API token and user agent from environment variables for authentication
import "dotenv/config";
import EleventyFetch from "@11ty/eleventy-fetch";
import { promises as fs } from 'fs';
const DISCOGS_TOKEN = process.env.DISCOGS_TOKEN;
const DISCOGS_USER_AGENT = process.env.USER_AGENT;

Firstly, add a 1-second delay between API calls to respect Discogs' rate limits, and cache responses locally for 24 hours.

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function fetchWithRateLimit(url) {
  await delay(1000);
  return EleventyFetch(url, {
    duration: "1d",
    type: "json",
    fetchOptions: {
      headers: {
        'Authorization': `Discogs token=${DISCOGS_TOKEN}`,
        'User-Agent': DISCOGS_USER_AGENT,
      },
    }
  });
}

Next, read the library catalogue, which contains index cards with details about each item.

export default async function () {
  try {
    const localData = await fs.readFile('_data/musicCollection.json', 'utf8');
    const myCollection = JSON.parse(localData);
    const releases = await Promise.all(myCollection.map(fetchReleaseDetails));
    return { releases };
  } catch (error) {
    console.error('Error processing music collection:', error);
    return { releases: [] };
  }
}

Finally, details are fetched from the Discogs API using the release IDs, transforming everything into a consistent format that can be used in collections and templates.

async function fetchReleaseDetails(release) {
  if (!release.release_id) {
    console.error('No Discogs ID provided for release:', release.title);
    return release;
  }
  const releaseUrl = `https://api.discogs.com/releases/${release.release_id}`;
  try {
    const releaseDetails = await fetchWithRateLimit(releaseUrl);
    const uniqueFormats = new Set();
    return {
      ...release,
      year: releaseDetails.year,
      notes: releaseDetails.notes,
      released: releaseDetails.released,
      genres: releaseDetails.genres || [],
      uri: releaseDetails.uri,
      videos: releaseDetails.videos?.map(video => ({
        url: video.uri,
      })) || [],
      formats: (releaseDetails.formats || []).reduce((acc, format) => {
        if (!uniqueFormats.has(format.name)) {
          uniqueFormats.add(format.name);
          acc.push({
            name: format.name,
            descriptions: format.descriptions
          });
        }
        return acc;
      }, []),
      tracklist: (releaseDetails.tracklist || []).map(track => ({
        position: track.position,
        title: track.title,
        duration: track.duration
      }))
    };
  } catch (error) {
    console.error(`Error fetching details for ${release.title}:`, error);
    return release;
  }
}

Collections

The Collections API can be used to access and organise data. I use this feature to organise my music library in a variety of ways: grouping releases by artist, sorting by genre and format, organising by release year and creating related content connections. Collections are powerful because they let you transform and arrange content in ways that suit your specific needs.

For example, this collection organises music releases by year, handling various date formats and categorising releases by year. The parseDate function handles three date formats: year only (YYYY), year and month (YYYY-MM), and complete date (YYYY-MM-DD). When dates are incomplete, it defaults to the first day of the month or year.

const parseDate = (dateStr) => {
  if (!dateStr) return null;

  const parts = dateStr.split('-');
  // If only year is provided
  if (parts.length === 1) {
    return new Date(parts[0], 0, 1);
  }
  // If year and month are provided
  if (parts.length === 2) {
    return new Date(parts[0], parts[1] - 1, 1);
  }
  // If complete date is provided
  return new Date(parts[0], parts[1] - 1, parts[2]);
};

The releaseYears collection performs several tasks: it extracts years from the music data, prioritising first released date, removes duplicates, sorts chronologically, and groups releases by year.

eleventyConfig.addCollection("releaseYears", function (collectionApi) {
  const musicData = collectionApi.getAll()[0]?.data?.music;
  if (!musicData || !musicData.releases) {
    console.warn("Music data not found or invalid");
    return [];
  }

  const years = [...new Set(musicData.releases
    .map(release => {
      // Prefer first_released if available, otherwise use released
      const date = parseDate(release.first_released) || parseDate(release.released);
      return date ? date.getFullYear() : null;
    })
    .filter(year => year !== null)
  )].sort((a, b) => a - b);

  return years.map(year => ({
    year,
    releases: musicData.releases.filter(r => {
      // Prefer first_released if available, otherwise use released
      const date = parseDate(r.first_released) || parseDate(r.released);
      return date && date.getFullYear() === year;
    })
  }));
});

Filters

Eleventy provides built-in Filters to transform data within templates, you can also create custom filters. Here's a simple filter with a modest task—extracting just the year from a date string. I use it to create links to relevant year pages from within individual release pages.

eleventyConfig.addFilter("extractYear", (dateString) => {
  if (!dateString) return "";
  const parts = dateString.split('-');
  return parts[0];
});

Creating pages from data

Individual pages for artists, formats, genres, years, and releases are automatically generated using pagination to create multiple files from a single Nunjucks template. The following example shows the YAML front matter that generates individual release pages for each entry in my library.

---
eleventyNavigation:
  key: "{{ release.title }}"
  parent: Releases
eleventyComputed:
  title: "{{ release.title }} - {{ release.artist }} | Music Collection"
pagination:
  data: collections.releases
  size: 1
  alias: release
permalink: "/music-collection/releases/{{ release.title | slugify }}-{{ release.artist | slugify }}-{{ release.format | slugify }}.html"
--- 

And from within the template itself, filters are applied to collection data to format dates and generate links to relevant artist, year, format, and genre index pages.

<dl class="detail__meta">
  <dt>Artist</dt>
  <dd><a href="/music-collection/artists/{{ release.artist | slugify }}.html">{{ release.artist }}</a></dd>
  {% if release.released or release.first_released %}
  <dt>Release date</dt>
  <dd>
  {% if release.released %}
  {% if release.first_released and release.first_released !== release.released %}
  {{ release.released | readableDate }} (first released: <a href="/music-collection/years/{{ release.first_released | extractYear }}.html">{{ release.first_released | readableDate }}</a>)
  {% else %}
  <a href="/music-collection/years/{{ release.released | extractYear }}.html">{{ release.released | readableDate }}</a>
  {% endif %}
  {% else %}
  <a href="/music-collection/years/{{ release.first_released | extractYear }}.html">{{ release.first_released | readableDate }}</a>
  {% endif %}
  </dd>
  {% endif %}
  <dt>Format</dt>
  <dd>
    <a href="/music-collection/formats/{{ release.format | slugify }}.html">{{ release.format }}</a>
    {% for format in release.formats %}
    {% if format.name == release.format %}
    ({{ format.descriptions | join(", ") }})
    {% endif %}
    {% endfor %}
  </dd>
  {% if release.genres %}
  <dt>Genres</dt>
  <dd>{% for genre in release.genres %}
    <a href="/music-collection/genres/{{ genre | slugify }}.html">{{ genre }}</a>{% if not loop.last %}, {% endif %}
    {% endfor %}
  </dd>
  {% endif %}
</dl>

Images

Image quality from community-driven APIs like Discogs varies. What constitutes acceptable is subjective and personal. For me, the artwork is as intimately linked to my thoughts and feelings as the music itself. Using Another Green World as an example—you can see the primary image provided by the API displayed alongside the one I chose for my library. I've downloaded and hosted the API's image locally rather than hotlinking to it, since their version could change.

Cover image of Brian Eno's Another Green World album provided by the Discogs API, showing quality issues including a visible sticker and distracting reflection.High-quality cover image of Brian Eno's Another Green World, showing the distinctive abstract artwork without any stickers or reflections.
Image quality comparison: API vs personal choice

The API's version is marred by a sticker and, on closer inspection, has a distracting reflection on the cover. To take control over quality, I select and process my own images using ImageMagick installed with Homebrew. Within templates, I use the Eleventy Image plugin to automatically optimise images and the built-in slugify filter to construct paths to images.

Data cascade

In Eleventy, data from multiple sources merges through a process called the Data Cascade before templates are rendered. Using the library metaphor, Eleventy acts as a librarian—it consults the global data files (library catalogue) and uses the release ID (call number) to fetch data from the API, organise everything, and render templates when I run npx @11ty/eleventy at the command line. All this happens quickly, as Eleventy's report after building my site demonstrates: Wrote 1,310 files in 43.67 seconds (33.3ms each, v3.0.0).

A montage of screenshots showcasing the digital music collection optimised for small screens in a grid layout.
Templates rendered by Eleventy

Retrospective

What have I learned? The concepts borrowed from Bush and libraries are worth pursuing—technology can be used to breathe new life into classic ideas.

The learning and development process has been enjoyable, rather than just a means to an end. Eleventy feels almost magical—there's no need for databases or complex frameworks. Instead, I can work with familiar web technologies (HTML, CSS & JS) and closely related concepts and tools.

If I can use Eleventy to build a time machine—unlocking personal memories and feelings by revisiting any specific format, genre, year, or artist in my library and playing these records—I wonder what else I can build?

The proof of concept has triggered new ideas that provide momentum to continue development, including:

  • Enriching API data with personal touches: highlighting standout tracks, tracking favourites over time, and adding personal reviews and memories connected to specific times, places, people, and events
  • Enhancing views with data visualisations (think Information is Beautiful)
  • Improving data requests and error handling
  • Refining image processing workflows
  • Adding search functionality

Looking back at my original motivation—disenchantment with streaming services and the tech industry's current trajectory—and comparing it to my experiences with this project brings to mind diving. Given the choice between diving into a stream or an ocean, who wouldn't choose the ocean?

A photograph of a figure diving into a mirror-like ocean surface, in this context used to symbolise the journey of exploration and discovery.
Image from inside cover of Wish You Were Here
© Pink Floyd Music

Acknowledgements

Several members of the Eleventy community have published valuable resources that helped me get started:

Andy Bell
Original author of Learn Eleventy from Scratch (now maintained by Uncenter).
Stephanie Eckles
Author of 11ty.Rocks.
Sia Karamalegos
Organiser of the Eleventy Meetup.
Zach Leatherman
Creator/maintainer of Eleventy.
Bob Monsour
Author of The 11ty Bundle website and newsletter.

Footnote

If you've read this far and found it useful, that makes me happy. If you'd like to offer advice on how I could improve my approach, please feel free—you can figure out how to get in touch with me easily enough. ↘