Skip to content


Obsidian Repos

all the repos ive built over the years for obsidian

a collection of projects ive written for obsidian over the years. i don't like the direction of the app and its community as of late, so i probably wont be making any new stuff in an official capacity, and if i do, it'll just be for me/local fixes/features. but here's what i've done, hope its helpful to you!
Character Image
<LEDA> check out these projects!
GitHub - bramses/obsidian-audio-alchemiser: Alchemize boring text notes into exciting audio notes!
Alchemize boring text notes into exciting audio notes! - GitHub - bramses/obsidian-audio-alchemiser: Alchemize boring text notes into exciting audio notes!
GitHub - bramses/eleven-labs-obsidian: eleven labs integration w obsidian
eleven labs integration w obsidian. Contribute to bramses/eleven-labs-obsidian development by creating an account on GitHub.
GitHub - bramses/youtube-transcript-obsidian: Transcribe a Youtube video’s captions with timestamps into Obsidian MD format
Transcribe a Youtube video&#39;s captions with timestamps into Obsidian MD format - GitHub - bramses/youtube-transcript-obsidian: Transcribe a Youtube video&#39;s captions with timestamps into Obsi…
GitHub - bramses/summarize-with-gpt3-obsidian: Summarize text with GPT-3 in Obsidian (progressive summarization)
Summarize text with GPT-3 in Obsidian (progressive summarization) - GitHub - bramses/summarize-with-gpt3-obsidian: Summarize text with GPT-3 in Obsidian (progressive summarization)
GitHub - bramses/obsidian-stack-overflow: Fetch Stack Oveflow answers and copy paste them directly into Obsidian
Fetch Stack Oveflow answers and copy paste them directly into Obsidian - GitHub - bramses/obsidian-stack-overflow: Fetch Stack Oveflow answers and copy paste them directly into Obsidian


GitHub - bramses/stenography-obsidian
Contribute to bramses/stenography-obsidian development by creating an account on GitHub.
Stenography - Product Information, Latest Updates, and Reviews 2024 | Product Hunt
Stenography combines state of the art AI technology (OpenAI Codex) and code parsing techniques to create a smart codebase documenter. Stenography is designed to to fit the natural dev flow and do what dev tools have always promised us: decrease cognitive load!


GitHub - bramses/chatgpt-md: A (nearly) seamless integration of ChatGPT into Obsidian.
A (nearly) seamless integration of ChatGPT into Obsidian. - GitHub - bramses/chatgpt-md: A (nearly) seamless integration of ChatGPT into Obsidian.

Reddit Post

Bramses Highly Opinionated Vault

GitHub - bramses/bramses-highly-opinionated-vault-2023: A highly opinionated, fully featured Obsidian vault that can get you from Zero to Zettelkasten lickety split!
A highly opinionated, fully featured Obsidian vault that can get you from Zero to Zettelkasten lickety split! - GitHub - bramses/bramses-highly-opinionated-vault-2023: A highly opinionated, fully f…

Reddit Post

Ghost Publish Fork

GitHub - bramses/obsidian-ghost-publish: Write on Obsidian. Publish to Ghost with a single click.
Write on Obsidian. Publish to Ghost with a single click. - GitHub - bramses/obsidian-ghost-publish: Write on Obsidian. Publish to Ghost with a single click.
How to Upload an Audio Companion to Ghost From iOS Voice Memos and Obsidian
How can audio locally linked to content uploaded to Ghost servers?

June 20 2023

another banger alfred workflow


The Universal Tool
There is an old adage that “when you have a hammer, everything begins to look like a nail”


Another Alfred banger. This time I'm appending to my weekly note > day of the week > events | thoughts | todo heading. It's basically the best thing ever to an organization and journaling nerd like me.

Example of thoughts pushed today (look at the time stamp!)
example of result of weekly push.png

append to weekly 1.png
append to weekly 2.png
append to weekly 3.png


import sys
import os
import datetime
import argparse
from pathlib import Path

# <- how this is made

append_to = os.getenv("append_to")
query = os.getenv("value")

# Get the day of the week
now =
day_of_week = now.strftime('%A').lower()
time_string = now.strftime('%H:%M:%S')

# Define the path of the Weekly_Folder
Weekly_Folder = Path("{YOUR_WEEKLY_FOLDER_PATH}")

# Get the most recent file in the Weekly_Folder
most_recent_file = max(Weekly_Folder.glob('*'), key=os.path.getctime)

# Define the section to append to based on the input category
if append_to == "todo":
    section = "### todo/" + day_of_week
    query = "- [ ] (" + time_string + ") "  + query
elif append_to == "events":
    section = "#### events/" + day_of_week
    query = "(" + time_string + ") " + query
elif append_to == "thoughts":
    section = "#### thoughts/" + day_of_week
    query = "(" + time_string + ") " + query
    sys.stderr.write("Invalid category. Please choose either 'todo', 'events', or 'thoughts'.")

# Read the file
with open(most_recent_file, 'r+') as file:
    lines = file.readlines()
    # Find the section and append the string_to_append
    start_append = False
    for i, line in enumerate(lines):
        if line.strip() == section:
            # Start appending after the section
            start_append = True
        elif start_append and (line.startswith("###") or line.startswith("####")):
            # Stop appending if we find another heading at the same or higher level
            lines.insert(i, query + '\n\n')
        if start_append:
            # If the section was the last one, append at the end of the file
            lines.append(query + '\n\n')
            print(f"Could not find the section: {section}")

    # Write the modified lines back to the file

sys.stderr.write(f"Successfully appended string to the {append_to} section on {day_of_week}.")

Obsidian Weekly Template

## Commitments

### todo/sunday

#### events/sunday

#### thoughts/sunday

### todo/monday

#### events/monday

#### thoughts/monday

### todo/tuesday

#### events/tuesday

#### thoughts/tuesday

### todo/wednesday

#### events/wednesday

#### thoughts/wednesday

### todo/thursday

#### events/thursday

#### thoughts/thursday

### todo/friday

#### events/friday

#### thoughts/friday

### todo/saturday

#### events/saturday

#### thoughts/saturday is a reader-supported published Zettelkasten. Both free and paid subscriptions are available. If you want to support my work, the best way is by taking out a paid subscription.

A Chat with ChatGPT: Obsidian App Ideas for Fashion and Style Management

What creative ways can Obsidian be used to help manage fashion and style?

system_commands: ['I am a generalist that is not afraid to make radical associations between distinct topics by using creative leaps. I write in short, quippy sentences that has a conversational tone.']
temperature: 0.9
top_p: 1
max_tokens: 512
presence_penalty: 1
frequency_penalty: 1
stream: true
stop: null
n: 1
model: gpt-3.5-turbo

write app ideas for:

use obsidian to dress interesting and flood into daily physical life


  1. "Outfit Vibes": Obsidian syncs your mood with your wardrobe, suggesting outfits based on your emotions & daily activities. Funky feeling? Get groovy threads!
  2. "Wear-Share": Connect clothing-swap circles in the Obsidian community! Share fashion finds, reduce waste, and upcycle amazing styles.
  3. "FitFab Fusion": Combine fitness tracking with style suggestions for a chic workout look that translates seamlessly into everyday wear.
  4. "Fashion Forecast": Weather-based outfit inspo! Look rad while being rain-ready or sizzling under the sun!
  5. "#OOTD Challenge": Gamify getting dressed – compete in daily challenges to unlock rewards & strut in unique apparel picked by fellow users.
  6. “Style Swap”: Bored of your closet? Swipe through user-generated looks and swap item for item – reinvent without feeling guilty about shopping sprees!
  7. “Eco Chic”: Recommendations for sustainable brands that'll make you feel good wearing their eco-friendly pieces every day! Go green, stay stylish.
  8. “Accessory Central”: Up-level any mundane outfit by adding some pizzazz with recommended statement accessories from local artisans!
  9. “Closet Curator”: Organize chaos! Snap pictures of clothes & let AI categorize items – making those early mornings less about fussing over outfits and more about rocking them.
  10. ”Fashion Alert”: Stay updated on current trends so you can tweak existing outfits rather than snagging new ones; saving money has never looked so fresh before!


i think 5 could fit the ethos of obsidian daily note well. barring the other users part, how could obsidian be used to create an ootd?


"ObsidianOOTD": Obsidian becomes your personal stylist! Here's how it could work:

  1. Create a "Wardrobe Vault": Use Obsidian to create individual notes for each item in your closet with photos, descriptions, and tags (e.g., color, style, occasion). Organize these notes under folders based on clothing type (tops, bottoms, dresses).
  2. Daily Notes Mood Board: Every morning, add an "OOTD Inspiration" section to your daily note. Include any preferences or specific items you wish to incorporate into the day's outfit.
  3. Outfit Template: Set up a customizable template that outlines the basics of any OOTD – top/bottom pairing or dress; shoes; outerwear; accessories.
  4. Tag-Based Suggestions: Using tags from your wardrobe vault & daily inspiration section as filters, Obsidian suggests suitable pieces for each component of the template.
  5. Randomized Looks: Feeling bold? Enable a randomization feature that mixes & matches tagged items for unique combinations - who said fashion can't be spontaneous!
  6. Reflect & Archive: Keep track of outfits worn by adding pictures and comments about what worked or didn't work in each ensemble – future you would appreciate those style tidbits!
  7. Weekly/Monthly Recap: Analyze patterns in outfits over time through visualizations generated using graphs view – spot trends or reinvent forgotten gems in your closet.

With ObsidianOOTD unleashed within its ecosystem of versatile capabilities – even choosing clothes shall become an everyday adventure!


can you create md templaes for 3 (put them in code blocks)


Sure! Here are Markdown templates for the 3 components mentioned in step 3.

Outfit Template:

# Outfit of the Day

## Top:
- Item name: 
- Color: 
- Description:

## Bottom:
- Item name: 
- Color: 
- Description:

## Dress (Alternative to Top/Bottom):
- Item name:
- Color:
- Description:

## Shoes:
- Item name: 
- Color: 

## Outerwear (if needed):

Accessory Categories |
--- | ---
Necklaces | Hats / Headbands  
Earrings | Sunglasses             
Bracelets | Scarves / Shawls              
Rings | Belts                         
Watches | Bags                            

Daily Notes Mood Board Section Template:

Here's an example template you can add to your daily notes.

#OOTD Inspiration
My mood today is "{{obsidian_prompt=Mood}}"

Preferred colors for today are "{{obsidian_prompt=Color(s)}}"
Special occasion or event? If yes, what event? "{{obsidian_prompt=Event}}"
Weather consideration(s): "{{obsidian_prompt=Weather Condition(s) }}"
Any specific item I'd like to wear? "{{obsidian_prompt=Clothing Piece}}"
Accessories theme/preferences (bold, minimalistic...)? "{{obsidian_prompt=Accessories}}"

Simply adjust the obsidian prompt sections as per your requirement. In case you don't use prompts and want a simple one -

#OOTD Inspiration

My mood today is **____**
Preferred colors for today are **____**
Special occasion or event? If yes, what event? **____**╗", "format": ["text"]]("favorite_color")
Weather consideration(s): **___ **
Any specific item I'd like to wear?
Accessories theme/preferences (bold, minimalistic...)?

You can customize these templates further according to your preferences and integrate them seamlessly into Obsidian. Happy styling!


how could i use 2 and 5 to suggest random good looking outfits to wear with pictures and rate them at the end of the day


You can integrate steps 2 and 5 to create a process for suggesting random outfits in Obsidian. Here's how:

  1. Create an Outfit Matcher Note: Set up a new note that will be used as your central hub for generating outfit suggestions.


# Daily Random Outfit Picker

## Mood: {{mood}}
## Preferred colors: {{colors}}
  1. Tag-based matching: In this step, add queries or links to filter items from your wardrobe vault based on the mood, colors, or other preferences you've mentioned in your daily notes.

Example (you may need to adjust query parameters depending on how you tagged & organized wardrobe items):

### Tops:
!\[\[*tag:#Top #color:{colors} #style:{mood}*]]

### Bottoms:
!\[\[*tag:#Bottom #color:{colors} #style:{mood}*]]

Note: Replace {mood} and {colors} with your input from "Daily Notes Mood Board Section Template."

  1. Randomized Selection: Use the Dataview plugin or another similar method within Obsidian ecosystem to randomly pick one item each from tops and bottoms (or dresses).

Example using Dataview plugin:

const topItems = dv.pages("#Top").filter(p => p.file.tags.includes({{Color}}) && p.file.tags.includes({{Mood}}));
const bottomItems = dv.pages("#Bottom").filter(p => p.file.tags.includes({{Color}}) && p.file.tags.includes({{Mood}}));

  1. Embed pictures of these selected pieces into the daily OOTD section of your daily notes template so you get visuals before actually trying them on!
  2. After wearing the suggested outfit, use an additional section such as "OOTD Rating & Reflection" at the end of every day where you'll rate today's look out of 10 along with any thoughts about what worked well or could be improved upon.

Rating Template Example:

# OOTD Rating & Reflection

## Today's look rating (out of 10): 
- Score: 

## What I liked about today’s outfit:

## What I would change/do differently next time?

Over time by gathering more ratings and feedback in this manner - you can tweak Queries/Filters accordingly within step-2


Ok -- finally write some Dataview code for Reflect & Archive and Weekly/Monthly Recap


Sure! Here's some Dataview code for Reflect & Archive and Weekly/Monthly Recap:

Reflect & Archive:
First, let's create a template for the Rating & Reflection note to be used after each day.

# OOTD - {{date(today)}}

## Outfit Details
- Top: !\[\[{{top}}]]
- Bottom: !\[\[{{bottom}}]]
- Dress (if worn): !\[\[{{dress}}]]
- Shoes: !\[\[{{shoes}}]]

## OOTD Picture:
[Insert outfit picture here]

## Today's look rating (out of 10):
Rating : {{rating}}

## What I liked about today’s outfit:

## Changes or improvements next time?

Weekly/Monthly Recap Example:

Create a new note titled "OOTD Recaps" and use the following Dataview code inside to generate weekly/monthly reviews of your outfits based on tagged reflection notes. Replace {your_tag} with the tag you use for your Rating & Reflection notes.

For weekly recaps, add this snippet in your OOTD Recaps note:

const pastWeek = moment().subtract(7, 'days');
dv.table(["Date", "Top", "Bottom", "Shoes","Rating"], 
    .where(p => datetime(p.file.mtime).isSameOrAfter(pastWeek))
    .map(entry => {
      return [
        entry.Shoes.value.length > 0 ? `${}${entry.Shoes.suffix}` : '',

For monthly recaps, modify the previous weekly snippet as follows:

Replace moment().subtract(7, 'days'); with moment().subtract(1, 'month');.

Now you have both weekly/monthly recap templates set up in Obsidian using Dataview! Customize these according to your preferences and enjoy analyzing your fashion trends over time! is a reader-supported published Zettelkasten. Both free and paid subscriptions are available. If you want to support my work, the best way is by taking out a paid subscription.

How to Upload an Audio Companion to Ghost From iOS Voice Memos and Obsidian

How can audio locally linked to content uploaded to Ghost servers?

  1. Record voice memo
  2. AirDrop it to Mac
  3. Drag it into Obsidian Attachments folder
  4. Rename any special characters
  5. Publish using Obsidian Ghost plugin
  6. Manually upload audio on web dashboard
  7. Delete uploaded (2nd file)
  8. Done! Now the Obsidian audio is locally "linked" to the content uploaded to Ghost servers
  9. Note /content/media/${year}/${month} is coded to be taken care of with yaml keys ghost-images-month and ghost-images-year
const content = data.content.replace(
		(match: any, p1: string) => {
			if (
			) {
			} else if (
				p1.toLowerCase().includes(".m4a") ||
				p1.toLowerCase().includes(".mp3") ||
			) {
				let year;
				let month;
				if (frontmatter.imagesYear && frontmatter.imagesMonth) {
					year = frontmatter.imagesYear;
					month = frontmatter.imagesMonth;

					if (month < 10) {
						month = `0${month}`;
				} else {
					// get the year
					year = new Date().getFullYear();
					// get the month
					const monthNum = new Date().getMonth() + 1;
					month = monthNum.toString();
					if (monthNum < 10) {
						month = `0${monthNum}`;

				return `<div class="kg-card kg-audio-card">
				<div class="kg-audio-player-container"><audio src="${BASE_URL}/content/media/${year}/${month}/${p1
					.replace(/ /g, "-")
					)}" preload="metadata"></audio><div class="kg-audio-title">${p1
					.replace(".m4a", "")
					.replace(".wav", "")

	); is a reader-supported published Zettelkasten. Both free and paid subscriptions are available. If you want to support my work, the best way is by taking out a paid subscription.

Better Footnotes in Ghost with Obsidian

Because text is limited to the domain of one direction ^ftmvri, adding breaks in the tedium for the reader is critical for engagement. Footnotes do a great job of this.[1] In addition, footnotes play the important role of allowing the writer to convey information that is akin to world building (basically the way "meanwhile" is used on TV shows) without having to use other symbols like "--" or ";" to symbolize a change in pace.

This doesn't mean footnotes are easy -- for the writer or the reader.

In books, the reader has to make the conscious decision to stop reading to go to the bottom of the document. This runs the risk of disrupting flow or even worse, causing the reader to stop all together. Alternatively, the reader can read all the footnotes at once after finishing a piece, but this sucks too. Most often, readers will be tired after consuming a whole article/blog/chapter, and footnotes just feel like more work.

For the writer, there is a choice to be made about whether to put something in a footnote or find a way to fit that information into the overall word budget of the piece. Not at all pleasant!

So are we just shit out of luck?

Fear not friends, if you too have a footer fetish, there is a light at the end of the tunnel!

Thanks to the web, we can use CSS and JS to hide information that sits right in the middle[2] of the flow of the piece. Not only that, but we can put images in footnotes,[3] or even full quotes and links![4] And best yet, all the footnotes are hidden behind cute ellipsis, and don't take up real estate at the bottom of the post!

Enough preamble, let's add some footnotes!

In Obsidian

Firstly, in Obsidian you're going to want to install the Footnote Shortcut plugin. This will allow you to create footnotes with a single hotkey (I mapped mine to alt-shift-f).

Next, if you publish to Ghost[5] you're going to want to use the Obsidian Ghost Plugin. This plugin converts markdown to HTML with markdown-it.[6] Specifically, and this is important so pay attention(!) you are going to want to use my fork here: bramses/obsidian-ghost-publish. This fork has the code you will need to run the footnote logic[7] as well as including other nice updates (The Obsidian Ghost Plugin).

That's all you really need on the Obsidian side of things.[8] Let's take a look at what we need to do in Ghost.

In Ghost

The core of the footnote experience is littlefoot.js. It handles pretty much everything, so all we need to do is make it play nicely with Ghost.[9]

Most importantly, you need to be able to edit the theme in Ghost, specifically gulp.js and default.hbs, so if you don't have a custom theme going, this tutorial will be harder to complete. This tutorial will walk you through a deep dive of how to do this, but in short:

  1. Inside your theme, run npm install --save littlefoot
  2. add littlefoot to gulpfile.js, mine looks like the code below:
function js(done) {
            'node_modules/littlefoot/dist/littlefoot.js' //
        ], {sourcemaps: true}),
        dest('assets/built/', {sourcemaps: '.'}),
    ], handleError(done));

  1. in screen.css add the following line to the imports:
@import "../../node_modules/littlefoot/dist/littlefoot.css";
  1. In default.hbs add the following script to the footer, right above </body>:
    activateOnHover: true,
  }) // Pass any littlefoot settings here.
  1. Run yarn zip to zip your theme
  2. Upload the zip to your theme in Ghost

That's it! Congrats on your new, sexy footnotes![10]

If you like what you read, please hit that Subscribe button in the corner to join other thinkers in this community! I send out multiple think pieces or Maps of Content a week as well as many notes that can only be seen by subscribing to the RSS feed above.

  1. or do they?…ok yeah they do

  2. Everything, everything'll be just fine
    Everything, everything'll be alright

  4. Jimmy Eat World - The Middle (Official Music Video) - YouTube

  5. which I safely assume you do since you're reading this tutorial

  6. and the markdown-it-footnote plugin, of course!

  7. Ghost needs to upload footnotes as an HTML card or the API swallows the markup for some annoying reason

  8. One more thing. You probably want to install Linter and enable footnote re-indexing to make sure footnotes stay in order

  9. easier said than done…

  10. I hope we got off on the right foot!

Inbox Processing in Flow State with Alfred


Every time I want to get into flow mode, I feel like I need music to get going. Especially when processing Obsidian notes, which requires a clear mind to place things and occasionally jump into a deep writing exercise.


Create a Alfred workflow that does the following:

  1. Open Obsidian up to the BHOV Inbox note (/Computed/To Process)
  2. Use Alfred Spotify Mini to play a random song or a playlist
  3. Run ztl to activate

alfred spotify and obsidian.png

obsidian uri alfred.png


Use Obsidian Advanced URI to open a note (can be downloaded from Obsidian plugin store) and uses Spotify Mini Alfred plugin ( to play a song.

- set up Spotify Alfred
- Obsidian
- Obsidian Advanced URI plugin


Why I Migrated 300+ Posts From Obsidian Publish to Ghost


303 posts manually backfilled one by one from Obsidian Publish to Ghost, with great effort and mental + physical expense. But why?
303 issues ghost.png

The Goal of a Zettelkasten

For the uninitiated, a zettelkasten is a form of a personal knowledge management system (PKM). The Zettelkasten method prioritizes small pieces of information (i.e. the information that could fit on one flashcard) linked together in a web like format by using IDs.

These two simple features together create emergence, as different notes influence the system in unique ways (A is linked to B, B is linked to C, meaning it possible to get to C from A despite no direct link).

"Wholly new forms of encyclopedias will appear, ready-made with a mesh of associative trails running through them, ready to be dropped into the memex and there amplified."

In the 2020s, the Zettelkasten has come back into vogue thanks to software like Obsidian and Roam Research. Everyone from academics to business professionals drowning in information deluges have sought the refuge of systems that can scale flexibly without breaking under their own weight.

The New Native

The Zettelkasten, in my belief, is a pivotal piece in the future of information creation and consumption. The system allows for healthy consumption (easy to jump into the graph and easy to jump out), updating of stale information when it is revisited, and most importantly implicit dense forests of linking.

But there is a problem. How do we share this information on the web, the closest technology we have to a global communication layer, a platform that works on a Hypertext layer, but displays information linearly? (think of a Twitter feed, information is delivered in LTR and top to bottom) How can we create a graph like experience when information is hidden behind links?

Obsidian + Publish

This is where Obsidian comes in.

Obsidian is beautiful software. In fact, I like it so much, I wrote a 10,000+ word guide on how to use it to its full potential.

Obsidian offers an official plugin named Publish. Publish functions exactly how you might imagine. It publishes markdown notes from Obsidian onto the web.

Publish, much like Obsidian, is a solid piece of software. For a lightweight hosting solution for Obsidian notes, it is fast, reliable, and intuitive. It functionally operates as a CRUD uploader with options to change custom domains, upload custom JS and CSS files, etc.
obsidian publish settings.png

Unfortunately, the current version of Publish didn't fit the full laundry list of features I needed to make the Zettelkasten in my head a reality. But what are these features anyway? And why can't I live without them?

The Deal Breakers

I had a list of non negotiable elements I wanted to achieve in this build. I have some pretty strong opinions about the logos, ethos, and pathos of a working & published Zettelkasten, and I wasn't really willing to compromise on anything from this list.

It HAS To Be Fun

First and foremost, my cardinal belief is that writing and reading the Zettelkasten should be fun. I as the writer and you as the reader should feel ideally feel no friction moving from note to note.

Topics should merely be a slight change of context instead of a drastic pivot from one field of expertise to another. The non linear path should be a sandbox for the mind to discover concepts that are surprising -- akin to a pleasant midday walk through the garden of the mind.
"Oh, the hydrangeas look nice today! Or perhaps I'll spend time amongst the cherry blossoms. I think I'll take this tulip home with me."

If it is not fun to garden, if it is not fun to walk the garden -- then the whole enterprise should be abandoned from the outset.

Lazy Utils

In service of making writing an enjoyable and fun process, the work around publishing a piece should be minimized. My goals were as follows:

  • Spend minimal time on tedious dev work: this includes maintenance, required features, annoying work like targeting different browsers/optimizing for mobile, SEO, etc.
  • Do 100% of the writing process within Obsidian
  • One Button Sharing

All three of these allow for me to focus on the act of writing, while being to maintain a high quality visual and tactile experience for the reader as well.

Reader Engagement

Speaking of the reader, I wanted a blog that could engage with the audience on a level past just reading. For better or worse, social media has become the human bazaar of communication. This causes a lot of problems, since most people have a lot of emotional baggage attached to their social profiles, not to mention the patterns of addiction and harm caused from subtraction of context + hostile territory.

For years, I've been looking for a way to solve this problem. I think there are four reliable, solid tools to manage reader-author interactions on the web: self-hosting, email newsletters, RSS, and in-place comments. All of these technologies are tried and true, last years, and many have protocols built into the very fabric of the web.

Having access to all four greatly increases the longevity of a website, as well as the utility the owner can provide to the visitors.


Being able to get my hands dirty with the source code allows me to build spirit into the garden. As I evolve as a writer and developer, and as I learn new skills, customizability is paramount to creating a fun experience for the reader.

In fact, the need to have customization is what initially chased me away from Substack, I didn't like how limited I felt by the tools Substack offered (they don't even offer an API!).

Why (Not) Ghost?

Of the dealbreakers listed above Ghost offers a lot, but also misses a few things that I had to try my hand at building manually. From the above list, Ghost provides built-in:

  • Minimal dev work for features like fast mobile loading, image optimization, routing, etc. built-in
  • Comments built in
  • Full text search built in
  • RSS built in
  • Email newsletters built in
  • Admin dashboards built in
  • Admin and Content API built in
  • Mainstream CSS themes to start from built in
  • An official way to make money through Stripe built in (this is uber important for people who want to go full time with their work!)
  • Option to self host or host through Ghost's platform

Thanks to Ghost being open source, I don't (probably) have to worry about situations like the below happening (which it did, and because I didn't see the email, I was blindsided and lost access to all the newsletters I wrote on Revue -- like totally locked out)

The deal breakers that had yet to be built:

  • One button publish from Obsidian
  • Writing in Obsidian and converting Markdown to a published post -- seamless CRUD workflow
  • Elements of fun and exploration
  • Internal Hover Links

The Obsidian Ghost Plugin

The first and most important extension to Ghost's base offerings was to build an Obsidian plugin that would automatically interface with Ghost without leaving Obsidian.

One button publish was built off a great open source plugin by jaynguyens, which converts Obsidian markdown into HTML and uploads it directly to Ghost using the Ghost API. I forked it and added a number of features including:

  • automatically checks if slug exists; if so updates instead of creating a new post
  • converts [[ links into a.href and maintains canonical name
  • inserts image placeholder cards that match names of local files for easy discovery while uploading
  • removes title H1 from notes to prevent duplicate titles
  • auto sizes YouTube embeds and auto centers them in the middle of the page
  • opens automatically in preferred browser after uploading with the API

For example, here's the wikilink to a.href converter feature code:

// converting <a href="">link</a> to <a href="BASE_URL/id" class="link-previews">Internal Micro</a>for Ghost
	const content = data.content.replace(
		(match: any, p1: string) => {
			if (p1.toLowerCase().includes(".png") || p1.toLowerCase().includes(".jpg") || p1.toLowerCase().includes(".jpeg") || p1.toLowerCase().includes(".gif")) {
				try {
					// get the year
					const year = new Date().getFullYear();
					// get the month
					const monthNum = new Date().getMonth() + 1;
					let month = monthNum.toString();
					if (monthNum < 10) {
						month = `0${monthNum}`;

					return `<figure class="kg-card kg-image-card"><img src="${BASE_URL}/content/images/${year}/${month}/${p1.replace(/ /g, "-").replace(/%20/g, "-")}" alt="${BASE_URL}/content/images/${year}/${month}/${p1.replace(/ /g, "-").replace(/%20/g, "-")}"></img><figcaption>${p1}</figcaption></figure>`;
				} catch (err) {

			const [link, text] = p1.split("|");
			const [id, slug] = link.split("#");
			const url = `${BASE_URL}/${id}`;
			const linkText = text || slug || link;
			const linkHTML = `<a href="${url}">${linkText}</a>`;
			return linkHTML;

Fun and Exploration

To make the process of traversing the published Zettelkasten fun, I thought about what I liked about note discovery in Obsidian. The first thoughts that came to mind were tags and internal hover links.

All topics are available at the bottom of each post in addition to read later from items within the same tag. This gives the reader an option to laterally move within a tag or to jump back to the top level of another tag and explore there instead.

jump to a top level search -- a new tag with completely new ideas to explore
tags and topics ghost.png

or explore within the same tag
within same tag ghost 1.png
within same tag ghost 2.png

To implement these, I edited the code in my Ghost theme to show the posts I wanted to surface for users:

{{!-- --}}

{{#get "tags" include="count.posts" limit="all" as |topic|}}
    {{!-- <section class="gh-section"> --}}
    <div class="gh-read-next gh-canvas">
        <section class="gh-pagehead">
            <h4 class="gh-pagehead-title">Topics</h4>

        <div class="gh-topic">
            {{#foreach topic}}
                <a class="gh-topic-item" href="{{url}}">
                    <h3 class="gh-topic-name">{{name}}</h3>
                    <span class="gh-topic-count">
                        {{plural count.posts empty="0 issues" singular="% issue" plural="% issues"}}

Show Me What You GOT

For internal hover links, I used the Microlink API and code injection to create a block in the footer of my Ghost pages that would give all internal links on my site the ability to be hovered (if you are on desktop -- hover this link for an example may take a second to load! 202212181947)

<!-- --> <style> :root { --microlink-background-color: #1A1A1A; --microlink-color: white; --microlink-border-color: #666; --microlink-hover-border-color: #999; } </style>  <!-- load @microlink/hover-vanilla --> <script src=",npm/react-dom@16/umd/react-dom.production.min.js,npm/react-is@16/umd/react-is.production.min.js,npm/styled-components@5/dist/styled-components.min.js,npm/@microlink/mql@latest/dist/mql.min.js,npm/@microlink/hover-vanilla@latest/dist/microlink.min.js"></script>  <!-- intialize `microlinkHover` --> <script> document.addEventListener('DOMContentLoaded', function (event) { microlinkHover('.link-previews') }) </script>
<script defer>
    const BASE_PATH = ""
	const EXCLUDED_PATHS = ['/', '/about/']
    const EXCLUDED_PATTERNS = ['#/portal/', '/author/', '/tag/', '#fn']
	document.querySelectorAll("a").forEach((a) => {
            a.href.includes("") // only match internal links
           && EXCLUDED_PATHS.filter(path => a.href !== BASE_PATH + path).length == EXCLUDED_PATHS.length // skip exact paths
           && EXCLUDED_PATTERNS.filter(epattern => !a.href.includes(epattern)).length == EXCLUDED_PATTERNS.length // skip paths including a pattern
           && (!a.classList.contains("gh-card-link") && !a.classList.contains("gh-topic-item") && !a.classList.contains("gh-navigation-link"))) // homepage bug

Was it Worth It?

I had to add many features to Ghost to "catch up" with Publish, but the stuff I would've had to add to Publish (full text search, mainstream CSS, newsletter, comments, admin pages, Stripe integrations, etc.) would be nigh impossible, so I'm glad overall that I made the move.

The overall UX of Ghost is much more polished than Obsidian Publish currently (though that may one day change!).

If you want to have a blog that could pass as a high quality standalone blog, check out Ghost. The ease of connecting to Obsidian really sealed the deal for me, as I would be able to maintain my writing habits while gaining a new venue to publish in.

What's Next?

I want to continue to make this site more fun as I come across new ideas and techniques along my journey. I'm excited to play with new themes and new visual ways to tell stories.

Concretely, I'd like to add some form of Obsidian block support for Ghost. Being able to link within specific blocks within Obsidian pages is such a treat, it's hard to not have that on my website.

More pressingly, I'm using this time to write. At the top of this post I mentioned I manually uploaded 300+ posts one by one. This is true. What I failed to mention is that my #to-process tag (where all my zettels are sourced from) has over 4000 (oof) items to go through. Some of these will be quick, others will take days to fully process.

In other words, I have a lot of writing to do.

*me right after I rebuild my site from scratch*

If you read this far into this post, I'm gonna assume you're into Zettelkasten stuff. If you want to learn about the process of building a Zettelkasten by watching one be built from the bottom up, do be sure to subscribe!

Thank you for reading, and I hope to walk past you in the garden one day!

Edit 2023-02-03

A comment on Reddit from /u/johnlunney mentioned that it would be helpful to call out the features Publish is missing, so I'd like to carve a little space to do that here.
missing features.png

Here are some things that caused me to move away from Publish.

Limited Control Over Full Feel

Publish gives an option to upload a custom publish.css file, but unfortunately it is up to the user to find the correct CSS classes and reverse engineer them. For example, I added this feature on mobile to be able to swipe to open the sidebar, but I had to inspect the element itself and take a leap of faith that the CSS class I was toggling would control what I hoped. It worked, but the whole thing was honestly a shot in the dark since there is very minimal documentation floating about the web.

// swipe left on mobile to open menu
let touchstartX = 0;
let touchendX = 0;

function checkDirection() {
  // check if greater than 100px
  // if swipe right close menu
    if (touchendX < touchstartX && touchstartX - touchendX > 100) {

  if (touchendX > touchstartX && touchendX - touchstartX > 100) {

document.addEventListener("touchstart", (e) => {
  touchstartX = e.changedTouches[0].screenX;

document.addEventListener("touchend", (e) => {
  touchendX = e.changedTouches[0].screenX;

There are many other examples like this, but off the top of my head:

  • No control over the way the sidebar displays file order
  • No way to add onClick() events
  • Adding comments and other features is hard due to loading order (onLoad() didn't work since I think Publish is a SPA app?)
  • Hard to anticipate how publish.css/js changes will propagate to mobile
  • Unnecessarily large gutters for content
  • Full Text Search
  • Limited Space (4GB) for everything

Unfortunately all this adds up to a novel (read: kinda weird) UX for both the developer and readers to learn.

I think Zettelkastens across the web would be a lot more popular if SEO was better and they adhered to more mainstream principles.


The "gitignore" feature in Publish is awesome. It makes uploading changed and new files a breeze, as well as deleting them. Unfortunately, this also causes all the files in Obsidian to be tracked, which is pretty annoying.

To combat this, the Obsidian devs added an option to ignore folders/files. This works great…until you locally move the files. Then the whole game starts over again. This is pretty annoying if you uploaded something, changed its path and then need to do the whole upload process from scratch.

Limited Communication Around Updates

With the core team hard at work on the app of Obsidian, I felt at times Publish was passed over as a side offering instead of a main project. As I said earlier in the essay: this is fine if you need an immediate readonly hosted solution. But the second you want to scale the public offer, Publish can no longer provide utility, imo.

You can find more of my thoughts on whether or not Obsidian Publish makes for a remarkable venue here: 202301240135

Getting Dv and Templater to Freeze Lists into Html


const dv =["dataview"].api ;
const te = await dv.queryMarkdown(`TABLE resources as Resources WHERE  contains(projects, "Migration from 2022 to 2023")`)
tR += te.value

reference notes for v2

const dv = app.plugins.plugins["dataview"].api;  
const ingr = tp.frontmatter.ingredients;  
let sortedIngr = ingr.sort();  
var arr = [];  
	for (i=0; i < sortedIngr.length; i++) {  
	var name = sortedIngr[i];  
	var link = '<a href="' + name + '">' + name + '</a>'  
let dvList = dv.markdownList(arr);  
tR += dvList;  

v2 (did Not compile)

const dv =["dataview"].api ;
const projectsFM = tp.frontmatter.projects; // look for projects in frontmatter

const projectName = "Migration from 2022 to 2023"
const arr = [];

for (let i = 0; i < projectsFM.length; i++) {
	if (projectsFM[i] === projectName) { 
		arr.push(`<a href="${projectsFM[i]}">${projectsFM[i]}</a>`)

let dvList = dv.markdownList(arr);
tR += dvList;

v3 (works!)

(command: templater -> replace template in modal then remove backticks -- should probably be done only on archive)

const dv =["dataview"].api ;
const arr = await dv.queryMarkdown(`LIST where contains(projects, "Migration from 2022 to 2023")`)
tR += arr.value