Sharing notes with Hermes and Agora
This project is in its infancy state and I’m sharing my early experiments with hopes that my ideas spark discussions, improvement ideas or inspires someone else to build similar things for themselves. The plugin is not currently installable anywhere but if you want, you can take the code below and build your own. I might publish it at some point in Obsidian plugin directory if it reaches a good enough state.
The concept of sharing individual notes with people is a topic that comes up constantly in my discussions with friends who also take notes. It’s a built-in feature to many commercial SaaS notetaking tools like Notion and Apple Notes but if you’re using something more local and private like Obsidian or Emacs, there’s no built-in option because that would require there to be a centralised server.
While I already have a setup of publishing a subset of my notes in Garden of Learning from Obsidian, I consider that a more permanent place and notes being available to everyone to browse.
What I want — and ended up building a first prototype of — is as follows:
- Write my note in Obsidian
- Run a command from within Obsidian
- The currently open note gets copied into a project and published on a website
- The URL is copied to my clipboard to be shared with a friend
These pages and URLs will be short-lived by nature. They are meant to be shared privately in discussions and not meant to be linked to permanently. There’s no promise that the page the URL points to will exist permanently.
I have one example note up at https://share.hamatti.org/notes/584178d8/ to demo it.
That’s exactly what I’m building. The project is still in its early phases so I don’t have a ready made, installable system to share yet because it needs way more polish to be useful to someone else and not rely on my internal project and folder structure and deployment setup as it does now.
Since naming projects is a big reason to even build projects, after a long careful pondering on a sleepless night, I decided to call the website Agora and the plugin Hermes. I don’t know if naming a project that spreads my ideas after the “messenger of gods” is narcissist or grandiose but that’s what I decided to go with.
Technical overview
Hermes - Obsidian plugin + shell script
I’ve never built an Obsidian plugin before so the first hurdle was to figure out how those are built. Lucky for me, they are built with Javascript so at least I had a familiar language to work with.
Obsidian has documentation for how to build plugins but I found it either lacking or hard to find stuff so a lot was just trial and error to get myself into a working prototype. I started with the sample plugin code and started removing bits and pieces until I had a minimal setup that still ran.
I went through multiple iterations of the design as I was trying to figure out the system. I’m not expecting this plugin at this point to be generally usable but I’m trying my best to separate it from the website project.
Here’s my current plugin code (main.ts):
import {
Notice,
Plugin,
Editor,
MarkdownFileInfo,
MarkdownView,
} from 'obsidian';
import {
DEFAULT_SETTINGS,
HermesSettings,
HermesSettingsTab,
} from './settings.js';
import { exec, ExecException, spawn } from 'child_process';
import crypto from 'crypto';
function handleExecOutput(
error: ExecException | null,
stdout: string,
stderr: string,
) {
if (error) {
console.log({ error });
new Notice(`ERROR in Hermes: ${error}`);
}
console.log({ stdout });
}
function handleCommand(
ctx: MarkdownFileInfo | MarkdownView,
hermes: Hermes,
): boolean {
const { projectPath, websiteURL, folder } = hermes.settings;
if (projectPath === '') {
new Notice('Project path is not defined. Aborting...');
return false;
}
if (websiteURL === '') {
new Notice('Website URL is not defined. Aborting...');
return false;
}
const file = ctx.file;
if (!file) {
new Notice('No file open to share');
return false;
}
// @ts-ignore
const path = `${file.vault.adapter.basePath}/${file.path}`;
const filename = crypto.randomUUID().split('-')[0];
const outpath = `${projectPath}/${folder}/${filename}.md`;
exec(`${projectPath}/bin/execute "${path}" "${outpath}"`, handleExecOutput);
const shareURL = `${websiteURL}/${folder}/${filename}`;
let proc = spawn('pbcopy');
proc.stdin.write(`${shareURL}`);
proc.stdin.end();
new Notice(shareURL);
return true;
}
export default class Hermes extends Plugin {
settings!: HermesSettings;
async onload() {
await this.loadSettings();
const hermes = this;
this.addCommand({
id: 'hermes-share-note',
name: 'Share note',
editorCallback(
editor: Editor,
ctx: MarkdownFileInfo | MarkdownView,
) {
handleCommand(ctx, hermes);
},
});
this.addSettingTab(new HermesSettingsTab(this.app, this));
}
onunload() {}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
(await this.loadData()) as Partial<HermesSettings>,
);
}
async saveSettings() {
await this.saveData(this.settings);
}
}
and its accompanying settings code
import { App, PluginSettingTab, Setting, TextComponent } from 'obsidian';
import Hermes from './main.js';
export interface HermesSettings {
projectPath: string;
websiteURL: string;
folder: string;
}
export const DEFAULT_SETTINGS: HermesSettings = {
projectPath: '',
websiteURL: '',
folder: '',
};
export class HermesSettingsTab extends PluginSettingTab {
plugin: Hermes;
constructor(app: App, plugin: Hermes) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Project path')
.setDesc('Absolute path to your website project')
.addText((text: TextComponent) =>
text
.setPlaceholder('/path/to/project')
.setValue(this.plugin.settings.projectPath)
.onChange(async (value: string) => {
this.plugin.settings.projectPath = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Folder')
.setDesc('Where your notes go')
.addText((text: TextComponent) =>
text
.setPlaceholder('notes')
.setValue(this.plugin.settings.folder)
.onChange(async (value: string) => {
this.plugin.settings.folder = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName('Base URL')
.setDesc('Where your website lives')
.addText((text: TextComponent) =>
text
.setPlaceholder('https://example.com')
.setValue(this.plugin.settings.websiteURL)
.onChange(async (value: string) => {
this.plugin.settings.websiteURL = value;
await this.plugin.saveSettings();
}),
);
}
}
When you run the command Hermes Share Note, it runs a
bin/execute command that lives in the
website project and is eventually responsible for deciding what to do when
Hermes runs. Hermes is agnostic of everything, it’s simply a
messenger (of gods).
Agora - share.hamatti.org
Agora is an 11ty website project with a
bin/execute shell script that Hermes
activates. All the 11ty project does is that it turns Markdown files in
notes/ into a webpage.
The execute script (mine is in written in zsh but the system is intentionally designed so that you can build it with any language you want) then copies the file to Agora as defined in Hermes’ settings. It then does a few things (it’s very WIP still as I’m figuring stuff out):
-
It finds all
![[image.webp|alt]]image references, copies the files toassets/notes/and replaces the links with standard markdown of
new_file_name=$(basename "$2")
folder_name=$(print ${new_file_name:t:r})
cd /Users/juhis/code/share.hamatti.org/
# Replace image references
## Find all images for copying
images=( $(grep -E "\!\[\[(.*)\]\]" "$2" | sed -r 's/!\[\[(.*)\]\]/\1/g' | cut -d"|" -f1) )
## If found, create a folder in assets
if (( ${#images[@]} != 0 )); then
mkdir -p "assets/notes/$folder_name"
fi
for image in "${images[@]}"; do
cp "/Users/juhis/Documents/Juhisland/999 - Assets/$image" "assets/notes/$folder_name/$image"
done
## Replace links to standard markdown
sed -ri '' "s/!\[\[(.*)\|(.*)\]\]//g" "$2"
sed -ri '' "s/!\[\[(.*)\]\]//g" "$2"
# Remove Wikilinks
sed -ri '' "s/\[\[(.*)\]\]/\1/g" "$2"
# # Add it to git and publish
git add "notes/$new_file_name"
if (( ${#images[@]} != 0 )); then
git add "assets/notes/$folder_name"
fi
git commit -m "New note: $original_file_name -> $new_file_name"
git push origin main
If something above resonated with you, let's start a discussion about it! Email me at juhis@hamatti.org and share your thoughts . This year, I want to have more deeper discussions with people from around the world and I'd love if you'd be part of that.