03

January 1, 2025

How this blog works, again, and my migration to Obsidian

I've been using Notion since 2018, and it's been an incredible note taking app, however small things kept adding up that prompted me to look for alternatives. I think the main reason being Notion's shift to workplace features, which makes sense for them as a company, but it did make the single-user knowledge base functionality start to feel bloated and overly complex.

Migration to Obsidian

After consulting with friends and doing my own research (and a bit of procrastinating), I settled on Obsidian as my new note-taking app. There are a couple of main reasons I made the switch:

Of course, not everything went smoothly. Several hurdles I came across included:

For the actual migration, I used the Importer plugin, which did a lot of the heavy lifting, however after it had completed it's task, I was left with thousands of markdown files in the same structure as my Notion workspace, and thousands more images all in the root directory! Here's an image of what the graph view looked like in Obsidian right after the import:

imported_graph.png

It took a couple of months to slowly go through and archive old notes into a preservable format (lots of moving images around), deleting hundreds of images that had been imported from pages I clipped with the Notion web clipper, and updating my workflows and processes to fit with Obsidian and the plugins I has available.

A lot of that involved migrating from how I used Notion databases previously to strategies as simple as folder structure, to tags, to the Kanban plugin to make boards that I can use to track the progress of tasks.

Recipe Database

I spent a large chunk of time recreating my recipe database using the Dataview plugin, Meta Bind plugin and a custom CSS stylesheet. This is how it currently looks:

recipe_database.jpg

Below is the code for that page, and you can see how I've set it up with meta bind filters, and dataview js that takes all the recipes and after filtering/sorting, generates the cards for each recipe.

1Filter `INPUT[inlineSelect(defaultValue(all), option(all, All recipes), option(breakfast, 🍳 Breakfast), option(lunch, 🍔 Lunch), option(dinner, 🍝 Dinner), option(sweets, 🍬 Sweets), option(cookies, 🍪 Cookies), option(icing, 🎂 Icing), option(vegan, 🌿 Vegan), option(gluten-free, 🌾 Gluten-free)):filter]` Sort `INPUT[inlineSelect(defaultValue(difficulty_asc), option(difficulty_asc, ⬆️ Difficulty), option(difficulty_desc, ⬇️ Difficulty), option(time_asc, ⬆️ Time), option(time_desc, ⬇️ Time), option(alphabetical_asc, ⬆️ Alphabetical), option(alphabetical_desc, ⬇️ Alphabetical), option(updated_asc, ⬆️ Last updated), option(updated_desc, ⬇️ Last updated)):sort]`
2
3//```dataviewjs
4const currentPage = dv.current()
5const filter = currentPage.filter ?? 'all'
6const sort = currentPage.sort?.split('_') ?? ['difficulty', 'asc']
7
8const query = `"Recipes"${filter === 'all' ? '' : `and #${filter}`}`
9
10const recipes = dv.pages(query)
11	.sort(p => {
12		if (sort[0] === 'difficulty') return p.difficulty
13		if (sort[0] === 'time') return sumTime(p)
14		if (sort[0] === 'alphabetical') return p.file.name
15		if (sort[0] === 'updated') return p.file.mtime.ts
16	}, sort[1])
17	.values
18	.flatMap(p => {
19		if (p.file.name === 'Recipes') return []
20
21		const firstImage = p.file.outlinks.values.find(link => link.type === 'file' && link.embed && !link.path.endsWith('.md'))?.path
22
23		return [{
24			name: p.file.name,
25			path: p.file.path,
26			tags: p.tags,
27			difficulty: p.difficulty,
28			time: sumTime(p),
29			image: firstImage,
30		}]
31	})
32
33const root = dv.el('div', '', { cls: 'recipe-grid' })
34root.innerHTML = ''
35
36for (const recipe of recipes) {
37	const card = root.createEl('a', { cls: 'internal-link', attr: { href: recipe.path } })
38
39	if (recipe.image) {
40		card.createEl('img', {
41			cls: 'recipe-image',
42			attr: { src: app.vault.getResourcePath(dv.fileLink(recipe.image)) }
43		})
44	} else {
45		card.createEl('div', { cls: 'recipe-image', text: '🍽️' })
46	}
47
48	card.createEl('h2', { text: recipe.name })
49
50	const duration = dv.duration(`${recipe.time} minutes`)
51		?.rescale()
52		?.toHuman({ unitDisplay: 'narrow', listStyle: 'narrow', type: 'unit' })
53
54	card.createEl('span', {
55		text: `${recipe.difficulty} ${duration}`,
56		cls: 'recipe-info',
57	})
58
59	const tags = card.createEl('div', { cls: 'recipe-tags' })
60	for (const tag of recipe.tags) {
61		tags.createEl('a', { text: tag, cls: 'tag', attr: { href: `#${tag}` } })
62	}
63}
64
65// Take a list of properties and sum all the time values
66function sumTime(props) {
67	return Object.entries(props).flatMap(([k, v]) => {
68		if (!k.endsWith(' time') || Number.isNaN(v) || v <= 0) return []
69		return [v]
70	}).reduce((total, time) => total + time, 0)
71}
72//```
73
74// code fence commented above to fix markdown formatting for this blog post

Of course, this just creates the elements, which I then apply this custom CSS snippet to in order to achieve the grid in the screenshot above:

1.recipe-grid {
2  display: grid;
3  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
4  gap: 1em .5em;
5
6  & > a {
7    text-decoration: none !important;
8    color: inherit !important;
9
10    .recipe-image {
11      border-top-left-radius: .3em;
12      border-top-right-radius: .3em;
13      width: 100%;
14      aspect-ratio: 4 / 3;
15      object-fit: cover;
16      display: block;
17      background-color: var(--background-secondary);
18    }
19
20    div.recipe-image {
21      display: flex;
22      justify-content: center;
23      align-items: center;
24      font-size: 2rem;
25    }
26
27    h2 {
28      font-size: 1rem;
29      font-weight: 500;
30      margin: 0;
31      margin-top: .5rem;
32    }
33
34    .recipe-info {
35      font-size: .9rem;
36      color: var(--text-muted);
37    }
38
39    .recipe-tags {
40      font-size: .8rem;
41      display: flex;
42      flex-wrap: wrap;
43      gap: .1rem;
44      margin-top: .3rem;
45    }
46  }
47}

I'm planning to create a dedicated public-facing website to view these recipes from my Obsidian, as the old Notion link is now unmaintained. Stay tuned for that!

This Blog

When I was using Notion, I decided that it would be a fun idea to set up my blog so each post lived as a page in a Notion database, and I wrote some javascript code to take the content of these pages and render them in my website. I wrote a post about the setup!

Of course, I had to come up with a different solution after migrating to Obsidian. Thankfully I already had experience rendering markdown in React with projects like https://benkoder.com, and the ECU wiki. The updated code now fetches markdown files (and images) from a public S3 bucket and renders them as HTML on the server side with Next.js.

I use the official sync plugin for Obsidian so I don't have to worry about a complex custom setup, but I also now have the Remotely Save plugin set up to sync just my blog files to an S3 bucket that I use just for my portfolio site. This means all I have to do is edit the blog pages on any device, and my website will be updated in about an hour (I've set the Next.js cache revalidation to 1 hour).

I also keep all the images used for my website in my Obsidian vault (which gets synced to the S3 bucket), including my projects, design work, and the favicons for my friends sites.