The Gatsby SSG era having come to a shuddering halt for me, due to upgrade calisthenics that didn’t quite land (and the now un-buildable-under-any-circumstances sharp module at the heart of such things), I was casting about for the next framework that would let me cobble together notes in markdown, for this dinky website, AND THEN WALK AWAY… unconcerned with bundling pipelines and the manifesting of css-tinged html.
I watched a youtube video on the wonders of contentlayer and nextjs13. I would link to it, but I don’t want anyone else to fall prey to its seductive promises. It was truly cool. I may even have swooned a little. This contentlayer thing was going to take my little markdown fences that I’d put up, between me and most things that might pass as managing and updating a website… and then turn those little fences into a gynormous 12 foot concrete wall, with spikes on top: PERFECT ABSTRACTION.
I’ll be honest, I implemented it BEFORE I EVEN LOOKED INTO ANYTHING.
‘Cept it didn’t work: My markdown taxonomy is designed to reduce friction for creating posts. I don’t even name my posts, lol. I mean I kind of do, as a sort of afterthought… but not in a file-system-cognizant way. So Contentlayer expected files like My First Post.md
, and instead it got a rather terse 2020/p1234.md
. It didn’t matter that the info it needed to infer names/titles of posts etc was in frontmatter: it didn’t seem to expose said frontmatter at build-time at all, unless I’m missing something. I had to write code to retro-infix all that, and things were still broken. Anyhoo: All of this was made moot by the singular, totally missed fact that the dude who created this lovely thing got inundated with tasks from their actual daytime job, and couldn’t develop it anymore, so it’s basically dead in the water. I fell in love with something that was dead in the water, y’all…
Nextjs is pretty chest-thumpy about also being a static site generator, so I figured ah heck, put the shiny new already-dead thing down and just go with the relative terra cognita of nextjs. After wading in a bit I realized I would still need a utility to snort all my own markdown files and hold on to the metadata before sneezing it all over the build process, because nextjs too was similarly (heck - identically) presumptious about how markdown content showed up in the file system.
I got it to grudgingly accept my taxonomy but even after several permutations of remark and rehype plugins, it still would not spit out emojis and syntax highlighting the way I wanted (pretty sure I’m the one who muffed something up, not nextjs)… and in any case, images were hosed: nextjs required images to be placed in the public / static folder so that you’re effectively abs-pathing your way to them throughout your markdown content. Or, you could always switch to .mdx
files proper, and use <Image>
components. Or, you could frolic amongst the branches of the intervening AST trees, replacing nodes according to your exotic use cases. 😑.
Narrator: She did frolic in the ASTs, and even transmogrified a few nodes, and then scampered right back down to earth, thank you very much… because TIME (taps wrist) waits for no lass… and this had time sink written all over it.
In the end I decided that if you - a web framework of great renown - claim to process markdown, then you should be able to process an arbitrary folder chock-full of entirely valid markdown files. You shouldn’t be asking me to restructure that markdown. The whole point of folks choosing markdown as their abstraction of choice at all, is PRECISELY because of it’s writing-only concerns. I do the writing, YOU do the finagling, framework; YOU do the finagling! Will stick to nextjs for other types of projects but maybe not SSGs. At least not ported SSGs with mildly-idiosyncratic taxonomies.
Altogether now, in your best Etta James: 🎶 Aaat Laaaaast!! This thing builds out my pages and it just does it out of the box! Type-able content collections? Ka-Pow! Emojis? Boom! Syntax highlighting? Bam! Pure .md
files with locally referenced images and no extra <Image />
component sugars? Ka-Blammo!
Though, that thing that Gatsby did out of the box, i.e. fully, properly dealing with images in markdown, replete with blur-in while loading and auto-resizing for different media output dimensions, which none of the contenders discussed thus far could/would (seemingly) do, is sorely missed. Sniff. :-(
Had to generally peg images to a 100%
width for safety; I shouldn’t wonder there’s some bizarre results hidden in these pages, lol 🤣 Oh well!
So after wholeheartedy embracing .astro
files and their syntax, this is my final line in the sand between the writing side of the equation and the web page generation side:
import type { CollectionEntry } from 'astro:content'
import kebabCase from 'lodash.kebabcase'
export type LabNote = CollectionEntry<'lab-notes'>
type GenericPathParams = {
params: {
slug: string
}
}
type SinglePagePathParams = {
params: {
year: string
slug: string
}
props: {
doc: LabNote
}
}
export const latestAtTop = (aDocsList: LabNote[]) =>
aDocsList.sort((a, b) => (b.data.date as string).localeCompare(a.data.date as string))
interface LabNotesHelper {
getSinglePagePathParams: () => SinglePagePathParams[]
getCategoryPagePathParams: () => GenericPathParams[]
getCategorySlugFilter: (catSlug: string) => (doc: LabNote) => boolean | undefined
getTagPagePathParams: () => GenericPathParams[]
getTagSlugFilter: (tagSlug: string) => (doc: LabNote) => boolean | undefined
}
let instance: LabNotesHelper
const tagMap = new Map<string, string[]>()
const categoryMap = new Map<string, string[]>()
const labNotesHelper = (docs: LabNote[]): LabNotesHelper => {
// memoize
if (!instance) {
docs.forEach((doc) => {
const { tags, category } = doc.data
// handle category
const catSlug = kebabCase(category)
const catDocIds = categoryMap.get(catSlug)
if (catDocIds) {
categoryMap.set(catSlug, [...catDocIds, doc.id])
} else {
categoryMap.set(catSlug, [doc.id])
}
// handle tags
tags.forEach((tag) => {
const tagSlug = kebabCase(tag)
const tagDocIds = tagMap.get(tagSlug)
if (tagDocIds) {
tagMap.set(tagSlug, [...tagDocIds, doc.id])
} else {
tagMap.set(tagSlug, [doc.id])
}
})
})
// for building each lab note page (single page view):
const getSinglePagePathParams = () =>
docs.map((doc) => ({
params: {
year: doc.slug.split('/')[0],
slug: kebabCase(doc.data.title),
},
props: {
doc,
},
}))
// for building each category listing page:
const getCategoryPagePathParams = () =>
Array.from(categoryMap.keys()).map((catSlug) => ({
params: {
slug: catSlug,
},
}))
const getCategorySlugFilter = (catSlug: string) => (doc: LabNote) =>
categoryMap.get(catSlug)?.includes(doc.id)
// for building each tag listing page:
const getTagPagePathParams = () =>
Array.from(tagMap.keys()).map((tagSlug) => ({
params: {
slug: tagSlug,
},
}))
const getTagSlugFilter = (tagSlug: string) => (doc: LabNote) =>
tagMap.get(tagSlug)?.includes(doc.id)
instance = {
getSinglePagePathParams,
getCategoryPagePathParams,
getCategorySlugFilter,
getTagPagePathParams,
getTagSlugFilter,
}
}
return instance
}
export default labNotesHelper
☝ This way, the category, tags and single page astro files are kept relatively clutter-free, and I deal with my taxonomy in one place, and views don’t have to care about anything except spitting out content. Astro’s getStaticPaths()
method does all the rest.
I will likely iterate on this solution, but for now this is what works.
All things considered, Astro feels a lot like a vuejs and nextjs layer cake, served on a contentlayer plate, with Gatsby sprinkles on top. I kinda like it. This is going to be my new Gatsby then, for the next little while.