On October 27, 2011, a friend sent an email to eight people he knew with the subject line “Shameless Indulgence.” The pitch was simple: meet monthly, watch a film, eat well, talk too long. The kind of idea that sounds like it’ll last three months and then quietly stop when everyone gets busy.
It didn’t stop. Fourteen years later, we’ve watched 91 films together, hosted by the same nine people, with a running diary that stretches back to the Bush administration. Somewhere along the way it became the kind of thing nobody would let lapse.
For most of that time, the record-keeping was ad hoc. Letterboxd lists for the catalog. A spreadsheet for tracking whose pick was whose. Emails for coordination, including elaborate pre-screening hints sent before the host’s film was revealed. It worked, but it was fragmented — the watch history lived in one place, the member records in another, the hints in nobody’s inbox but the sender’s.
The fragmentation started to bother me more than it should have. So I built a proper home for it.
What SIFS Is
SIFS (Shameless Indulgence Film Series) is a static site built with Hugo that serves as the club’s archive, catalog, and stats dashboard. It has:
- A film list with all 91 picks, with position badges, sortable and filterable by member, decade, and search
- A full watch diary going back to 2011, grouped by year and month
- A stats dashboard with charts: watches per year, runtime distribution, top directors, watches by day of week, films by member
- Member profiles for all nine of us, with individual pick histories and taste profiles
- Individual film pages with poster, metadata, full watch history, and pre-screening hints extracted from the email archive
The site is hosted on Netlify, built from a git repository, and deployed automatically on every push. There’s no database, no server, no CMS. The entire thing is YAML files and Hugo templates.
The Data Model
The heart of the site is data/films.yaml — a 3,000-line file with an entry for every film, populated from a Letterboxd export and enriched with metadata from The Movie Database (TMDB) API. Each entry has: title, year, director, runtime, genres, tagline, cast, poster path, list position, the member who picked it, and a watches array with one entry per time we’ve screened it.
Each watch entry has the date, which members attended, any pre-screening hints that were sent beforehand, and a favorite moment — a one-liner that usually captures some aspect of how the screening went.
I chose YAML over a database or headless CMS deliberately. Everything is in git, diffs are readable, and there’s no service to keep running. The downside is that films.yaml is 3,000 lines and editing it by hand requires care. For a project maintained by one person that changes a few times a month, that’s the right tradeoff — but it’s a tradeoff, not a freebie.
The import was a Python script (import_letterboxd.py) that reads a Letterboxd CSV, makes TMDB API calls for metadata, and builds the YAML. Letterboxd’s export format changed between versions — the v7 format adds a metadata preamble before the column headers, which broke the original CSV parser. The fix was to scan forward until the actual headers are found rather than assuming row zero is always the header. Small problem, but the kind of thing that only surfaces when you’ve got real data in front of you.
Hugo and the Stub File Problem
Hugo can build pages from content files, and it can use data files, but it can’t natively generate pages from data files. If you have 91 films in a YAML file and want 91 film detail pages, you need 91 content files.
The workaround I used: stub files. Each film gets a minimal .md file in content/films/ with nothing but frontmatter — a film_id field that matches the key in films.yaml. The layout template uses Hugo’s where filter to find the matching data entry and renders from there. The stub files are four lines each and require no manual maintenance; adding a new film means adding an entry to films.yaml and running a one-liner to generate the stub.
content/films/the-thing.md → film_id: the-thing
data/films.yaml → the-thing: { title: "The Thing", ... }
The same pattern handles member profiles: nine stub files in content/members/, nine entries in members.yaml.
Client-Side Filtering
The list page has real-time search and filtering — by member, by decade, by title or director or cast. All of it runs client-side, without any page reloads or build-time permutation pages.
Static sites don’t get server-side filtering for free. The options were: pre-build a page for every permutation of filters, or do it in the browser. With 91 films filterable by 9 members, 7 decades, and freetext search, permutation pages would have been dozens of static files for a problem the browser can solve with data it already has. The choice was easy.
Hugo serializes filter data into HTML attributes (data-tags, data-decade, data-director, data-cast) and the film grid’s JavaScript reads those on input events. No framework, no build step — the only dependency is Chart.js via CDN for the stats charts. Charts on the stats page use Chart.js via CDN.
The stats page has an “as of” year selector: a dropdown that lets you see how the metrics stood in any prior year. That took some care to get right. Watch history is cumulative, so you can’t just swap in pre-built data for each year — you have to maintain the full dataset in the browser and refilter every metric to include only watches on or before the end of the selected year. It’s more computation per interaction, but it means the feature works correctly for every year without any build-time cost.
Extracting the Hints
The part I was most pleased with was rescuing the pre-screening hints.
Before every screening, the hosting member sends out a set of hints — cryptic clues about the film they’ve picked. They’ve been doing this since roughly 2012. The hints are in emails, which means they were in a .mbox archive that I’d exported at some point and then ignored.
A Python script parsed the archive, matched emails to films by sender name and approximate date, extracted the hint text, stripped out logistics (parking info, arrival times, RSVP notes), and cleaned up encoding artifacts — Windows smart quotes that had survived as Unicode replacement characters. The result was 33 hint sets across 41 target films, added directly to the watches entries in films.yaml.
For films that had hints in the archive, the film detail page now shows the full set with each hint on its own line. It’s a surprisingly good record of a longstanding ritual that would otherwise have been invisible to the site.
GitHub Actions for Mobile Editing
The YAML-as-database approach is great for durability and version control, but it means adding a new film requires opening a terminal, running a script, and pushing a commit. That’s fine on a laptop. It’s friction when someone wants to log a new addition from their phone.
The solution is three GitHub Actions workflows:
- Add Film — takes a Letterboxd URL or TMDB ID, runs
add.pyto fetch metadata and updatefilms.yaml, creates the stub, commits, and pushes - Add Hints — takes a film slug and hint text, appends to the correct watch entry
- Add Favorite Moment — same, for the one-liner favorite moment field
All three can be triggered from GitHub’s web UI with no code required. Multi-line inputs (hints especially) were tricky: substituting them directly into shell conditionals caused syntax errors. The fix was passing all workflow inputs through an env: block rather than inline substitution, which solves both the parsing bug and prevents shell injection.
What TV Shows Taught Me About Assumptions
For thirteen years I assumed SIFS only watched movies. Then I went through the Letterboxd list carefully and found Police Squad! — the 1982 TV series, not The Naked Gun, which is the film adaptation. We watched the original show.
TMDB uses a completely different API endpoint for TV shows, with different field names (name vs title, first_air_date vs release_date). The import script assumed everything was a film. Fixing it meant detecting the TMDB type on response and normalizing TV data to match the schema downstream. Not complicated, but the kind of thing you only discover when you’re treating your own archive seriously enough to be accurate about what’s in it.
What I’d Do Differently
The YAML-as-database approach was right for launch, but films.yaml is already 3,000 lines and it’s going to keep growing. At some point hand-editing becomes error-prone enough to warrant validation — a JSON Schema check that runs before commit and catches missing required fields or malformed entries. I haven’t added that yet. I should.
Hugo’s inability to natively generate pages from data files is the main structural irritant. The stub pattern works, but it’s a workaround for a limitation that shouldn’t exist. If I were starting fresh, I’d evaluate Eleventy first — it handles data-driven pages more naturally. Hugo is fast and the templating is capable, but the stub pattern is the kind of thing I’d rather not explain to someone trying to contribute.
The hints extraction was the most satisfying part of the project, and I wish I’d done it sooner. There are films in the catalog where I know hints were sent but they’re not in the mbox archive I had. Some of those are gone — the earlier years are patchier than the recent ones. More systematic archiving from the beginning would have preserved them, but you don’t know you’re building an archive until you’re far enough in to care about what you’ve lost.
Where It Is Now
The site has the full 91-film catalog, 200+ watch diary entries, 9 member profiles, 33 hint sets from the email archive, and all the charts and filters working. The GitHub Actions workflows are live and I’ve already used the Add Film workflow to log a new pick (Twelve Monkeys, added last week).
The design is a dark theme that leans into the GitHub color palette — not because it’s the most original aesthetic, but because it renders well on phones and laptops equally, loads fast, and doesn’t distract from the content. The films are the thing.
For a project that started as “let’s replace a Letterboxd list,” it ended up being a good excuse to think carefully about what we’ve actually built over fourteen years.
The code is on GitHub if you’re curious about the Hugo stub pattern or the hints extraction approach.