Bascule has a new client - a new website to generate, besides this humble blog. For a number of months, my Dad has been giving a series of presentations on classical music to local members of the U3A. Hours and days of research are put into each programme, some tackling a particular composer, others taking a theme as inspiration. These presentations have been well received, but are rather ephemeral, and so my father and I have decided to put them online.
The simplest solution, of course, would be to find a nice Wordpress template, and paste his notes in. But where's the fun in that? I have a ready-made website generator in Bascule, and while it's not stable enough for my Dad to use, it's not a lot of work to convert his notes into corresponding Markdown source files.
The new website is called The Right Notes - the title is in reference to a famous Morecambe and Wise sketch with pianist and composer André Previn. It's still very much a work-in-progress, but I'm pleased that I've been able to use Bascule to build it.
Spotify Links
A website about music needs a way of letting the reader listen to the music. Dad's programmes are generally collated in Spotify, though sometimes he links to YouTube too. So how can we create links to Spotify songs? Spotify provides an embedding API (similar to YouTube's) of the format:
<iframe src="https://open.spotify.com/embed/track/2BB5WatVyed1cHbFsJN15A" width="100%" height="80" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
I decided to write a Flexmark extension (based on the AbstractMediaLink class) to generate this iframe
fragment from just the spotify URI. This allows for a fairly simple syntax in the Markdown file:
!S[Play Maskerade: Overture … 4:12](spotify:track:2BB5WatVyed1cHbFsJN15A)
Because Bascule is very flexible about what is put in the Markdown front matter, it's a simple thing to add the Spotify URI for the entire playlist, too. Any yaml
key which Bascule does not recognise is added to the model as an attribute: playlist:7cfKH6ExfL01NtV4j7VrKy
is accessible in the model as {{attributes.playlist}}
I haven't written a Flexmark extension for the playlist; it's included in the handlebars template instead:
{{#if attributes.playlist}}
<iframe src="https://open.spotify.com/embed/playlist/{{attributes.playlist}}" min-width="300px"
width="100%" height="380" frameborder="0" allowtransparency="true"
allow="encrypted-media"></iframe>
<p><em>{{ title }}</em> playlist</p>
{{/if}}
This is working pretty well - to get the full benefit, you need to have logged into your Spotify account in the web browser, otherwise you can only hear the first 30 seconds of a song. I will be adding YouTube video links alongside the Spotify embed.
Spotify plugin doesn't belong in bascule?
It didn't take me too long to write the Spotify extension to Bascule. But it felt wrong to include it in the core of Bascule, as Spotify links are pretty niche. I could add it to bascule-lib, but again, it didn't really belong there. I wondered if I could allow users to write their own extensions, in Java or Kotlin, and plug them in. I'd already done some work with loading different generator
classes at run time, with three are enabled by default: IndexPageGenerator
, PostNavigationGenerator
and MultiTaxonomyNavigationGenerator
(more on that last one, below). So I had some idea how to do it. Users can now specify additional extensions in their project yaml
file:
generators: [IndexPageGenerator, PostNavigationGenerator, MultiTaxonomyNavigationGenerator]
extensions: [ org.liamjd.bascule.flexmark.spotify.SpotifyExtension ]
The compiled code must sit in a folder called plugins
in the project folder. A note on the language used: Generators create new types of pages, as Bascule will only generate HTML for each Markdown source file. If the generators
block is left out of the project yaml
file, Bascule will assume the three listed above (IndexPageGenerator, PostNavigationGenerator, MultiTaxonomyNavigationGenerator
). Extensions are applied when parsing individual Markdown files, and allow you to parse new and custom Markdown syntaxes, such as the !S[]()
Spotify code above.
I may need to revise this language: plugins, extensions, generators - it's all a little unclear.
Tagging and Filtering
Bascule has always supported the idea of tagging a page, and generating navigation and lists around these tags. This web page is tagged bascule
, and clicking on the word bascule
in the top-right of the page will take you to a list of all pages with that tag. I haven't seen this functionality on other static site generators.
But for my Dad's music website, I wondered if I could extend it to support different types of tagging? Each type, or category of tag, would have its own listing page. For the music website, pages could be tagged with both genres
(romantic, baroque, impressionist, etc) and composers
(Bach, Mahler, Beethoven and so on). This will lead to new ways of navigating around the site. Getting this to work, however, required quite a lot of changes to the tagging model in Bascule. Firstly, we allow the user to define the tagging
set in the project yaml
file:
tagging: [composers,genres]
Then these new tagging categories can be used on the yaml
frontispiece for each post:
composers: [Erich Wolfgang Korngold,Brahms,Gustav Mahler,Tchaikovsky,Edvard Grieg,Verdi,Franz Liszt,Shostakovich,Schubert,Bob Dylan,Dvorak]
genres: [poetry,opera]
It took me several iterations to get this working, especially in generating the tag listing pages (such as posts marked bascule). I'm still not entirely sure it's working correctly, and I've some seen some off-by-one counting errors which confuse me. I've also written a pretty gnarly bit of Kotlin, I don't fully understand:
val groupedList = tList.groupBy { it.label }.values.map { it.reduce { acc, item -> Tag(category = item.category, label = item.label, url = item.url, postCount = item.postCount + 1, hasPosts = item.postCount > 1) } }
This chain of filters and maps gathers up all the tags applied to all the posts in the project, groups them by category (genre, composer), and then groups and sums the count of each tag in each category - counting all the posts with Mahler in the composer
category, for instance. An earlier version of this would have had conflicts if a tag was shared across different categories: imagine a product listing website, selling shoes of various colours, and pens of various colours. I need to distinguish between blue pens and blue shoes, as a listing of all blue things wouldn't necessarily be that useful. My code will distinguish between the different uses of blue in this example:
footwear: [blue,green]
writing: [blue,black]
It's a contrived example, I admit! In the music website for my Dad, who is to say there isn't a composer called Bob Romantic, or a genre simply called Bach for music inspired by Bach himself?
It's nearly there. I did break the generation of this website, which does not take advantage of this multi-tagging functionality. Creating my Dad's website has allowed me to revisit some of the assumptions I have made in writing Bascule, and I think it will be a better tool at the end.