Obsidian Kirby Sync
Nach über zehn Jahren mit Kirby als CMS fürs Bloggen habe ich jetzt endlich einen Workflow, der mir wirklich gefällt.
Kirby ist ein tolles CMS, um ein Blog zu betreiben. Daten werden in Textdateien gespeichert, Texte können in Markdown geschrieben werden. Das Panel – Kirbys Verwaltungsoberfläche – lässt sich bis ins kleinste Detail individualisieren und jede Seite kann ihr individuelles Set an Feldern haben.
Das macht es möglich, das eigene Blog auf die eigenen Bedürfnisse anzupassen und Dinge so zu gestalten, dass sie das Bloggen besonders einfach machen.
Aber so sehr ich die Oberfläche mag und mir coole Features und Plug-ins dafür gebaut habe, einen großen Haken hatte das ganze Set-up all die ganzen Jahre: Ich schreibe meine Texte nicht direkt im CMS, sondern in einem Markdown-Editor.
Das war mal VS Code, mal iA Writer, dann Ulysses, wieder iA Writer und zwischendurch immer mal wieder Obsidian. Alle hatten ein Problem gemeinsam: Sobald ein Text in Kirby war, liefen die Kirby-Version und die Version im Editor auseinander.
Wer kennt das nicht: Ein Text ist online und beim direkten Blick auf die Seite fällt dann doch noch ein Fehler auf. Ein Link ist nicht korrekt gesetzt oder irgendwo ein Vertippper. Also korrigiere ich diesen Fehler schnell im Panel, damit die Änderung sofort online ist.
Anfangs, als die Motivation noch groß war, korrigierte ich diese Fehler auch noch in meinem Dokument im Markdown-Editor. Aber um ehrlich zu sein, irgendwann hatte ich darauf keinen Bock mehr, schob es nach hinten und ließ es schließlich ganz sein.
Die Texte auf der Webseite sind dann also in einer anderen Form, als sie im Editor sind.
Das wäre alles kein Problem, wenn ich die Texte nicht gerne auch eben im Editor hätte, der für mich auch eine Art Datenbank ist. Ich bin großer Freund des Zettelkastens. Ich halte es auch für sinnvoll, Texte immer irgendwie als Datei lokal liegen zu haben, anstatt nur online.
Das erste Problem ist also die Synchronität zwischen beiden Welten.
Das zweite Problem ist der Zustand beider Dokumente. Neben dem Text gibt es noch zahlreiche Metadaten, wie Tags, ein Datum und den eigentlichen Zustand, also ob ein Dokument ein Entwurf, nicht gelistet oder publiziert ist. Einige Editoren haben dafür Lösungen parat.
Die Markdown-Editoren
Ich habe zwar anfangs VS Code zum Schreiben von Markdown verwendet, aber bin irgendwann davon ab. VS Code ist nun mal ein Code-Editor und viele Funktionen, die ich von einem Texteditor erwarte, sind nicht enthalten, ließen sich vielleicht nachrüsten, haben dann aber umgekehrt eigentlich auch nichts in einem Code-Editor zu suchen. Ich bin also schnell wieder zu anderen Apps gewechselt.
iA Writer
iA Writer ist einer der besten Editoren da draußen. Er ist minimal gestaltet und bietet dennoch alle Funktionen, die man sich beim Schreiben wünscht. Neben dem hübschen (und kaum vorhandenen) Interface haben mir besonders die Grammatikhelfer gefallen. Ich benutze iA Writer seit der ersten Version und dieses Feature war eines meiner meistgenutzten. Ich kann mir verschiedene Wortarten anzeigen lassen und auf diese Weise schnell Füllwörter und unnötige Adjektive ausfindig machen.
Hier sieht man iA Writers Stilprüfung:
Und so bunt kann es aussehen, wenn man alle Wortarten hervorheben lässt:
Alles lässt sich im Detail ein- und ausschalten:
Ganz besonders toll ist aber die Möglichkeit, Texte über verschiedene Dienste/APIs direkt aus iA Writer heraus zu veröffentlichen:
Wie man sieht, sind hier die populärsten Schnittstellen vertreten. Besonders interessant ist hier sicherlich Micropub, weil der Standard nicht auf ein bestimmtes CMS zugeschnitten ist. Für meinen Fall war das allerdings nur eingeschränkt nützlich.
Sebastian hat ein ganz großartiges Plug-in dafür geschrieben, welches es mir auch möglich machte, über mein Smartphone kurze Posts als Notizen zu veröffentlichen – und eben meine Texte aus iA Writer heraus zu veröffentlichen.
Das Plug-in wird leider nicht weiterentwickelt. Es lief zwar immer noch mit den letzten Kirby 4 Versionen, wurde mir dann aber doch etwas zu riskant; eine nicht gewartete Schnittstelle zum eigenen CMS ist langfristig keine gute Idee.
Außerdem gibt es auch hier ein wesentliches Problem: Die Kommunikation führt nur in eine Richtung, vom Editor ins Blog, aber nicht wieder zurück. Es ist eben nur zum Veröffentlichen gedacht.
Wenn es um das Verwalten meiner Texte geht, bin ich mit iA Writer bedauerlicherweise auch nie so richtig warm geworden. Die Datei/Ordner-Ansicht empfand ich nie als übersichtlich genug, gerade wenn es darum geht, dass Dokumente auch in Verbindung miteinander stehen und in verschiedene "Rubriken" einsortiert werden sollen.
Ulysses
Ulysses ist hier etwas besser aufgestellt. Neben Ordnern lassen sich auch noch Projekte anlegen, sodass sich verschiedene Inhalte gut gruppieren lassen. Das hilft mir ganz gut, weil ich nicht nur für mein eigenes Blog Texte schreibe und für jedes Blog ein eigenes Projekt anlegen kann.
Auch Ulysses kommt mit einer eigenen Stil- und Grammatikprüfung, die ganz hervorragend funktioniert; wenn auch nicht so plakativ wie bei iA Writer. Dafür sind die Empfehlungen hier häufig etwas ausgefeilter und umfangreicher, meist werden mehrere Vorschläge gemacht, wie man etwas besser eleganter formulieren könnte.
Offensichtlich ist die Oberfläche ebenfalls recht minimalistisch, wenn auch lange nicht so minimal wie bei iA Writer. Generell empfinde ich iA Writer immer als etwas "runder", mag vor allem den iA Writer Font besonders gerne.
Auch bei Ulysses lassen sich Texte direkt veröffentlichen. Wie zu sehen ist, sind auch hier alle gängigen Kanäle vertreten. Jedoch leider kein Micropub, stattdessen aber eine direkte Schnittstelle zu Micro.blog. Ein Feature-Request vor ein paar Jahren konnte hier bisher leider nichts bewegen.
Auch hier bleibt allerdings das Problem des fehlenden Rückkanals. Texte können zwar veröffentlicht, aber nicht wieder zurück in den Editor geholt werden.
Obsidian
Die Eierlegende Wollmilchsau.
Obsidian ist Open Source und kostenlos. Entsprechend weit verbreitet ist es. Es gibt zahlreiche Themes und mindestens ebenso viele Plug-ins, um den Editor zu erweitern.
Der Lieblingssport einiger User besteht darin, Obsidian mit Plug-ins und selbstgebauten Konstrukten so weit zu tunen, dass es wie ein Projektmanagementsystem funktioniert; daraus dann endlose Videoreihen bei YouTube zu machen und sich schließlich darüber zu beschweren, wie kompliziert Obsidian ist und zu erklären, dass man jetzt zu Notion wechselt.
Keine Panik. Obsidian kann so sein, dazu muss man sich aber schon anstrengen. Nach der Installation ist Obsidian erst einmal ein Texteditor, mit ein paar grundlegenden, hilfreichen Funktionen.
Der Griff zu einem Theme hat meinerseits dennoch nicht lange gedauert, weil ich unbedingt etwas Minimales haben wollte. Ich verwende Obsidian Boom mit einigen zusätzlichen Einstellungen, damit alles schön clean aussieht:
Weil ich den Font von iA Writer so toll finde, habe ich ihn mir heruntergeladen und installiert. Er ist frei verfügbar und eingeschränkt nutzbar.
Von Haus aus hat Obsidian keine besonderen Stil- oder Grammatik-Features. Das lässt sich über ein Plug-in nachrüsten. Das funktioniert dann ähnlich gut wie bei Ulysses:
Hier verwende ich das LanguageTool Integration Plug-in. Leider wird es nicht mehr aktiv weiterentwickelt, ich hoffe, es bleibt uns noch eine Weile erhalten, oder dass sich jemand findet, um die Entwicklung wieder aufzunehmen.
Ich habe außerdem eines der Focus-Plug-ins im Einsatz, welches sämtliche UI-Elemente ausblendet und damit eine ähnlich minimale Darstellung herstellt, wie iA Writer und Ulysses.
Plug-ins zum Synchronisieren von Texten und Einstellungen gibt es einige. Die meisten davon sind allerdings eher Backup-Syncs oder zum Synchronisieren zwischen verschiedenen Rechnern und Mobilgeräten gedacht. Hier habe ich mich vor einiger Zeit dazu entschieden, Obsidian Sync zu nutzen. Das scheint mir am stabilsten zu laufen und ich kann die Entwickler auf diese Weise etwas unterstützen. Die Versions-Historie hat mir bereits einmal den Hintern gerettet, als ich mit einem eigenen Plug-in herumprobiert habe; war den Kauf also schon wert.
Damit kommen wir zum wesentlichen Thema: dem Veröffentlichen von Texten. Auch hier gibt es einige Plug-ins, aber nichts, was mit Kirby funktionieren würde. Allerdings durfte ich freudig feststellen, dass Obsidian-Plug-ins in TypeScript geschrieben werden, etwas, womit ich mein Geld verdiene, also gut beherrsche.
Umso weniger konnte ich mich dann beherrschen und habe losgelegt, ein eigenes Plug-in zu schreiben.
Obsidian und Kirby verbinden
Mein Problem habe ich ja nun häufig genug benannt, mir fehlt die Möglichkeit, Texte zwischen Kirby und meinem Editor synchron zu halten. Das betrifft den Text selbst, aber auch dessen Metadaten.
Um das herzustellen, brauche ich jeweils ein Plug-in für Kirby und eines für Obsidian. Das Kirby-Plug-in muss eine Schnittstelle bereitstellen, die mir das Auslesen und Senden von Daten an Kirby ermöglicht. Das Obsidian-Plug-in muss diese Anfragen ausführen und mit den Ergebnissen arbeiten, also etwa meine lokalen Dateien aktualisieren.
Ich bin das Ganze experimentell in einem extra dafür erstellen Obsidian Vault angegangen. Ich wollte ein wenig herumspielen und dabei keine Angst vor Datenverlust haben müssen.
Dabei hat sich herausgestellt, dass ich vier Endpunkte brauche, die folgende Aufgaben erfüllen:
- Daten in Kirby aktualisieren
- Daten in Obsidian aktualisieren
- Eine neue Seite in Kirby anlegen
- Eine neue Seite in Obsidian anlegen
Im Grunde werden damit drei Fälle abgedeckt:
- Ich habe einen neuen Text in Obsidian erstellt und will diesen nun in Kirby veröffentlichen
- Ich habe bereits einen Text in Kirby, den ich bisher nicht lokal habe und möchten diesen Text in Obsidian "herunterladen"
- Ich habe den Text sowohl in Obsidian als auch in Kirby und will eines von beidem aktualisieren
Das Obsidian-Plug-in
Wie schon erwähnt, werden Obsidian-Plug-ins mit TypeScript geschrieben, was mir sehr liegt, weil ich damit mein Geld verdiene. Ich habe mich also ran gesetzt und erst einmal geschaut, wie das in Obsidian alles so funktioniert. Glücklicherweise ist das alles recht naheliegend und mithilfe von Ollama in meiner IDE kam ich ohne viel Grundwissen über die Obsidian API schnell voran.
Zunächst musste ich mir überlegen, was ich synchronisieren möchte. Natürlich den Text selbst, aber auch eine Handvoll, Metadaten. Das Problem hier sind die unterschiedlichen Formate, in denen Metadaten abgelegt werden. Wären diese identisch, bräuchte ich gar keine Plug-ins, sondern könnte vermutlich einfach direkt mit den Dateien arbeiten.
Obsidian legt Metadaten im Frontmatter Format ab. So sieht das gerade beim Schreiben für diesen Artikel aus:
---
type: text
aliases:
date: 2024-12-05
channel: blog
status: draft
sync: false
slug:
title:
intro:
tags:
---
Die Darstellung in Obsidian sieht dann wie folgt aus:
Kirby hingegen legt die Metadaten in einem leicht abweichenden Format ab:
Title:
----
Intro:
----
Text:
----
Date:
----
Tags:
Wie man sieht, unterscheiden sich beide leicht in ihrem Format und den Inhalten. Während Metadaten wie das Datum und Tags bei beiden direkt in die Textdatei geschrieben werden, identifiziert Kirby den Status der Seite (draft
, unlisted
, listed
) über den Ordner, legt diese Information also nicht direkt in der Datei ab.
Die meiste Übersetzungsarbeit übernimmt Kirby für mich. Da ich in der API alle Kirby-Klassen und -Methoden nutzen kann, muss ich mir um das Format keine Gedanken machen. Ich schicke lediglich alle Metadaten und den Text als separate Datensätze mit.
Ich habe bereits ein Kirby-Plug-in namens "Internal API" in dem ich diverse Endpunkte bereitstelle, um Kleinkram zu machen oder auf dem Mac Informationen anzuzeigen. Das Plug-in konnte ich also einfach erweitern. Letztendlich handelt es sich um ein Plug-in, welches ein paar Routen bereitstellt.
Dank des relativ simplen Anwendungsfalls, konnte ich hier den klassischen CRUD1 Weg gehen. Wobei eigentlich nur CRU, weil ich das Löschen bewusst weggelassen habe. So sehen die Endpunkte aus:
[
'pattern' => 'ENDPOINT/(:any)/(:any)/(:any)',
'method' => 'VERB',
'action' => function ($channel, $folder) {
$request = kirby()->request();
$requestData = $request->data();
$requestHeaders = $request->headers();
if ($requestHeaders['Authorization'] !== 'Bearer ' . option('mauricerenck.obsidian.token')) {
return new Response('Unauthorized', 'text/plain', 401);
}
// DO STUFF
return new Response(json_encode($data), 'application/json');
},
],
Diese Route gibt es dreimal, dabei unterscheidet sich jeweils das Verb:
- Create ->
'method' => 'POST'
- Read ->
'method' => 'GET'
- Update ->
'method' => 'PUT'
Der ENDPOINT
heißt in Wirklichkeit natürlich auch anders.
Ich hätte hier eine Route wählen können, die alle drei Fälle abfängt, habe mich aber dagegen entschieden und mich auf etwas Code-Duplizierung eingelassen, um alles etwas klarer getrennt und letztendlich übersichtlicher zu gestalten.
Wie man sieht, wird jeder Request durch einen Token abgesichert. Jede Anfrage muss den richtigen Token im Header mitschicken, sonst wird sie abgelehnt. Damit sichere ich die Endpunkte ab, sonst könnte ja jeder einfach Daten schicken und lesen.
Die Struktur
Bevor wir weiter machen, ein paar Worte zur zugrunde liegenden Struktur. Auf meiner Webseite habe verschiedene Bereiche, die ich nutze, um Inhalte ein wenig voneinander trennen zu können. Im Wesentlichen sind das Blog
, Hub
, Notes
und demnächst noch ein vierter Bereich. Hier spreche ich von Kanälen.
Damit ich nicht endlose lange Verzeichnislisten habe, sind die jeweiligen Kanäle noch einmal aufgeteilt, im Blog und in den Notizen nach dem Jahr und im Hub nach Themen.
Die Struktur ist also:
/blog/2025/article-slug/template.en.md
oder
/hub/built-with-kirby/article-slug/template.en.md
Mit einem Blick auf die Metadaten in Obsidian dürftest du davon schon etwas gesehen haben:
channel: blog
slug: article-slug
Eine Ebene fehlt, das Jahr bzw. das Thema. Hier habe ich mich dazu entschlossen, die Struktur in Obsidian nachzubilden. Schaut man sich also den Obsidian-Verzeichnisbaum an, ist dieser identisch mit der Kirby-Struktur:
Das Verzeichnis ATTACHMENTS kann ignoriert werden, hier legt Obsidian verknüpfte Dateien ab, Bilder z.B.
Wie man aber sieht, gibt es hier die Jahres-Verzeichnisse, wie es sie auch bei Kirby gibt.
Auf diese Weise baue ich mir dann im Obsidian-Plug-in die API-Endpunkt-URL zusammen:
const options = {
url: `${this.settings.apiBaseUrl}/${channel}/${folder}/${slug}`,
method: 'POST|PUT|GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.settings.apiToken}`
},
body: JSON.stringify(data)
};
Wir haben eine Base-URL, das ist die URL meiner Webseite plus den oben erwähnten ENDPOINT. Gefolgt vom Kanal, z.B. blog
, dem folder
, für diesen Beitrag also ´2025´. Schließlich der slug
. Der Slug ist letztendlich der Dateiname der Markdown-Datei in Obsidian, bei Kirby ist es der Verzeichnisname des Posts.
Fall 1: Eine neue Seite erstellen
Einer der Anwendungsfälle ist das Erstellen einer neuen Seite in Kirby. Ich habe also in Obsidian bereits einen Text geschrieben. Dieser soll jetzt – wir bleiben beim Beispiel – im Blog veröffentlicht werden.
Damit nicht einfach wild synchronisiert wird, was gar nicht synchronisiert werden soll, habe ich einen Sync-Haken eingebaut. Nur, wenn dieser aktiviert ist, kann eine Datei überhaupt synchronisiert werden. Das sieht man weiter oben im Screenshot.
Überhaupt: Die Frage, was wann wie synchronisiert wird und ob es bereits synchronisiert wurde, stellte sich ziemlich früh.
Hier wird eine Mischung aus verschiedenen Status und dem Sync-Haken tätig.
how-to-burn-money
ist nicht synchronisiert und als Idee abgelegt. Das bedeutet, dass es meist nur ein paar Stichpunkte gibt und ich noch gar nicht weiß, ob daraus mal ein Text wird.
obsidian-kirby-sync
ist bereits in Arbeit, aber wurde noch nicht synchronisiert. Er liegt als neue Datei vor, hat aber seinen Weg noch nicht in Richtung Kirby gefunden.
example 2
wird synchronisiert und liegt als draft
bei Kirby.
example
wird synchronisiert und wurde veröffentlicht, allerdings nicht gelistet.
sociabli
wird synchronisiert, wurde veröffentlicht und ist im Blog sichtbar.
In unserem Beispiel haben wir also einen Artikel in Arbeit, dieser wurde aber bisher nicht synchronisiert. Im Plug-in müssen wir daher zunächst lokal ein paar Daten sammeln, damit wir diese an Kirby schicken können:
const data = await this.readFileData(file);
const channel = data.frontmatter.channel;
const currentYear = new Date().getFullYear();
const folder = file.parent?.name || currentYear.toString();
try {
// REQUEST CODE FROM ABOVE WITH THIS CHANGES
// url: `${this.settings.apiBaseUrl}/${channel}/${folder}`,
// method: 'POST',
const response = await requestUrl(options);
if (response.status !== 200) {
console.error(`Failed to fetch API data from ${channel}`);
return \{\};
}
this.syncFile(file, response.json);
}
Zunächst einmal lesen wir die aktuelle Markdown-Datei aus, holen uns aus den Frontmatter-Daten den Kanal und holen uns das aktuelle Jahr. Schließlich legen wir fest, wie der Folder lauten wird. Hat die Datei ein übergeordnetes Verzeichnis? Dann nehmen wir dessen Namen, wenn nicht, das Jahr. Der letzte Fall dürfte eigentlich nie eintreten, ich will hier aber auf Nummer sicher gehen.
Wie der eigentliche API-Call dann funktioniert, habe ich oben schon gezeigt. Weil wir einen neuen Artikel erstellen, ist es ein POST
Request.
Schlägt der Aufruf aus irgendeinem Grund fehl, wird der Fehler geloggt und abgebrochen. Ist alles gut gelaufen, bekommen wir von der API die gespeicherten und durch Kirby angereicherten Daten zurück.
Läuft etwas komplett schief, greift der catch
Block und macht Alarm:
catch (error) {
console.error(`Error: Could not create page`);
new Notice(`Error: Could not create page`);
this.updateStatus('Error: Could not create page');
return \{\};
}
Erst wird der Fehler geloggt, ich zeige außerdem noch eine Notification an und unten in der Statuszeile von Obsidian auch noch eine Meldung. Es sollte also nicht zu übersehen sein.
Kirby-Plug-in
Wie oben in der Route schon gezeigt, empfängt Kirby die Daten und prüft den Token. Kanal, Folder und Slug werden über die URL mitgegeben und direkt in die Route hereingereicht, der Text steht im POST-Body und muss ausgelesen werden.
Als Nächstes prüfe ich dann, ob es den Folder überhaupt gibt. Sollte das nicht der Fall sein, breche ich sofort ab:
$parent = kirby()->page($channel . '/' . $folder);
if (is_null($parent)) {
return new Response('Not Found', 'text/plain', 404);
}
Nun geht es daran, die Daten ins Kirby-Format zu bringen und zu speichern:
$newData = [
'title' => $requestData['frontmatter']['title'],
'intro' => $requestData['frontmatter']['intro'],
'text' => $requestData['content'],
'tags' => $requestData['frontmatter']['tags'],
];
kirby()->impersonate('kirby');
$page = Page::create([
'parent' => $parent,
'slug' => Str::slug($requestData['frontmatter']['title']),
'template' => 'post',
'content' => $newData
]);
Ich baue mir hier also ein Array zusammen, das ich dem Page::create
Aufruf hineingeben kann. Zusätzlich erzeuge ich hier noch einen slug aus dem Titel. Als Template dient mein Standard post
Template.
Damit erzeugt Kirby eine neue Seite und liefert mir diese auch direkt zurück, sodass ich gleich über $page
darauf zugreifen kann.
Erst einmal schaue ich aber, welchen Status die Seite haben soll. Ich könnte sie nämlich in Obsidian auch gleich so einstellen, dass sie direkt veröffentlicht wird:
if (in_array($requestData['frontmatter']['status'], ['listed', 'unlisted'])) {
$page->changeStatus($requestData['frontmatter']['status']);
}
Der initiale Zustand ist immer draft
weshalb ich hier nur listed
und unlisted
abfragen muss. Ist eines davon gesetzt, wird die Seite entsprechend veröffentlicht.
Jetzt reichern wir die Daten noch etwas an und schicken sie zurück an Obsidian:
$data = [
'frontmatter' => [
'tags' => $page->tags()->split(','),
'date' => $page->date()->toDate('c'),
'status' => $page->status(),
'title' => $page->title()->value(),
'intro' => $page->intro()->value(),
'sync' => true,
'slug' => $page->slug(),
'channel' => $channel,
],
'modified' => $page->modified('c'),
'content' => $page->text()->value(),
'folder' => $page->parent()->uid(),
'headers' => $requestHeaders,
];
return new Response(json_encode($data), 'application/json');
Hier ist wesentlich, dass ich das Datum und den Slug zurückliefere, denn diese Information hatte Obsidian bisher nicht. Im Obsidian Plug-in gibt es eine Methode zum Aktualisieren der lokalen Datei, die diese Daten in Empfang nimmt.
Daten in Obsidian empfangen
Um unnötige Aktualisierungen und Endlosschleifen zu unterbinden, prüfe ich zunächst, ob es überhaupt Änderungen gibt:
const originalContent = await this.app.vault.read(file);
const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
let updatedContent = originalContent;
Ich lese mir also den aktuellen, lokalen Stand aus. Diesen schreibe ich in updatedcontent
, das wirkt etwas schräg, erklärt sich aber gleich.
Zuerst geht es an die Metadaten:
if (frontmatter) {
const frontmatterEndIndex = originalContent.indexOf("---", 3) + 3;
const existingFrontmatter = parseYaml(originalContent.slice(3, frontmatterEndIndex - 3));
const updatedFrontmatter = { ...existingFrontmatter, ...apiData.frontmatter };
const newFrontmatterBlock = `---\n${stringifyYaml(updatedFrontmatter)}---`;
updatedContent = newFrontmatterBlock + originalContent.slice(frontmatterEndIndex);
} else {
apiData.frontmatter.sync = true;
updatedContent = `---\n${stringifyYaml(apiData.frontmatter)}---\n${originalContent}`;
}
Ich mache aus dem Frontmatter-Text-Block erst einmal ein Objekt. Dieses überschreibe ich dann einfach mit den Daten aus der API. Ich kümmere mich hier gar nicht um einzelne Felder oder so, da bin ich ziemlich risikobereit. Am Ende schreibe ich die Daten in updatedContent
und hänge den Text unten dran.
Hat die Datei noch gar kein Frontmatter, übernehme ich 1:1 die Daten aus der API. Das kommt später zum Tragen, wenn wir in Obsidian eine neue Datei mit den Daten aus Kirby anlegen.
Jetzt aktualisiere ich noch den eigentlichen Inhalt:
if (apiData.content) {
const frontmatterEndIndex = updatedContent.indexOf("---", 3) + 3;
updatedContent = updatedContent.slice(0, frontmatterEndIndex) + "\n" + apiData.content;
}
Auch hier nehme ich stumpf das, was die API liefert.
Jetzt wird es tricky, es geht um den Slug. Der lokale Dateiname soll immer dem remote Slug entsprechen, also müssen wir da ran:
const newSlug = apiData.frontmatter.slug;
const currentTitle = file.name.replace(/\.md$/, "");
console.log(`Checking slug ${file.path} vs ${newSlug}.md`);
if(apiData.folder !== file.parent?.name) {
console.log(`Moving file ${file.path} to ${apiData.folder}`);
await this.moveFile(file, apiData.folder, newSlug);
} else if (newSlug && newSlug !== currentTitle) {
console.log(`Renaming file ${file.path} to ${newSlug}.md`);
await this.renameFile(file, newSlug);
}
Ich schnappe mir den Slug von der API. Ich schnappe mir den aktuellen Dateinamen und entferne die Dateierweiterung. Hat sich der Folder geändert, bewege ich die Datei und benenne sie nach dem neuen Slug um. Hat sich nur der Slug geändert, nenne ich die Datei um.
Auch das greift schon ein wenig dem Voraus, was wir für andere Syncs brauchen.
Final schreiben wir nun noch die Datei, sollte das nötig sein:
if (originalContent !== updatedContent) {
this.pluginStatus == "pulled"
await this.app.vault.modify(file, updatedContent);
}
this.updateStatus('Sync Complete');
Fall 2: Eine Seite herunterladen
Der zweite Fall betrifft die Gegenrichtung. Es gibt bereits eine Seite in Kirby und ich möchte sie nun auch in Obsidian haben. Das betrifft meist alte Artikel, die ich geschrieben habe, bevor ich die Plug-ins am Start hatte.
Auf Seiten von Kirby ist das der simpelste Fall. Die Route horcht auf den Kanal, den Folder und Slug. Es handelt sich um einen GET
Request. Wie immer wird erst der Token geprüft und dann hole ich mir die Seite:
$page = kirby()->page($channel . '/' . $folder . '/' . $slug);
if (is_null($page)) {
return new Response('Not Found', 'text/plain', 404);
}
Gibt es sie nicht, wird das entsprechend quittiert und der Prozess beendet. Gibt es die Seite, wird das gleiche Array wie schon beim Create erzeugt und an Obsidian zurückgeliefert.
Damit das funktioniert, brauche ich die Möglichkeit, Folder und Slug etc. anzugeben, dafür gibt es einen Dialog:
Sehr spartanisch, ich weiß, es soll erst einmal nur funktionieren. Hier muss ich den Slug von Kirby wissen. Ich muss also ins Panel schauen. Das ist nicht ganz optimal, da der Fall aber sehr selten vorkommt, ist das okay für mich.
Klicke ich submit
wird der GET-Request abgeschickt:
const options = {
url: `${this.settings.apiBaseUrl}/${channel}/${folder}/${slug}`,
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.settings.apiToken}`
},
};
Da ich hier wie beim Create dieselbe Datenstruktur zurückbekomme, kann ich im Erfolgsfall einfach dieselbe Methode aufrufen, die ich am Ende von Fall 1 aufgerufen habe. Die Logik bleibt dieselbe. Kirby schickt mir alle Daten zurück, insbesondere hier den Slug, der nun genutzt wird, um die Datei entsprechend zu benennen und ggf. ins richtige Verzeichnis zu schieben.
Fall 3: Kirby mit lokalen Daten aktualisieren
In diesem Fall synchronisieren sich beide Enden bereits. Ich möchte jetzt gezielt lokale Änderungen zu Kirby schicken. Ein PUT
Request. Auf Seite von Kirby funktioniert dieser fast genauso, wie der POST
Request.
Ich hole mir wieder die betroffene Seite, erstelle mir ein Array mit den neuen Daten und rufe dann ein update
statt eines creates
auf:
$page->update($newData);
Danach gebe ich die Daten der Seite wie schon beim Create zurück und Obsidian kann die lokale Datei wieder aktualisieren, sofern nötig.
Fall 4: Die lokale Datei aktualisieren
Der letzte Fall. Auch hier synchronisieren sich beide Enden bereits und wir wollen jetzt Änderungen, die ich im Panel vorgenommen habe, auch lokal aktualisieren.
Lokal haben wir bereits alle Daten, die wir brauchen, um den GET
Request abzusetzen. Für Kirby ein leichtes Spiel. Ich hole mir die Seite, lese die Daten aus und liefere sie wieder im bekannten Format zurück an Obsidian.
Tja, und Obsidian kann dann einfach wieder die Methode zum Aktualisieren der Datei nutzen.
Der Feinschliff
Wagemutig hatte ich mich zunächst dazu entschieden, beim lokalen Öffnen einer Datei immer einen Sync auszuführen und mir Daten aus Kirby zu holen (sofern der Haken gesetzt ist, natürlich).
Ich meinte, das Risiko eingehen zu können, weil ich in Obsidian auf Änderungen gelauscht habe und immer dann in Richtung Kirby synchronisiert habe, wenn Obsidian die Datei gespeichert hat.
Das war etwas zu mutig und führte im Produktivbetrieb dann zu Datenverlust. Ich hatte lokal schon viel geschrieben, remote stand noch ein "Test" im Text und irgendwie schaffte ich es, die Datei zu öffnen, ohne dass sie vorher in Richtung Kirby geschickt wurde. Resultat: Mein Text war weg, es lachte mir ein freudiges "Test" an. Dank Obsidian Sync konnte ich dann aber den letzten Stand wiederherstellen.
Ich habe mich dann gegen automatisches Synchronisieren entschieden …
Ich hatte von Beginn an schon die Möglichkeit, einen Sync per Befehl zu starten. Dazu rufe ich die Befehlszeile mit cmd+p
auf und tippe kirby
. Dann kann ich auswählen, was ich machen möchte:
Wie man sieht, sind hier alle vier Fälle aufgelistet. Bei "Create page from remote" geht der oben gezeigte Dialog zur Eingabe des Slugs auf. Ansonsten werden die beschriebenen Prozesse ausgelöst.
Ein solches Command kann man in Obsidian wie folgt anlegen:
this.addCommand({
id: 'kirby-pull-remote',
name: 'Update local file',
checkCallback: (checking: boolean) => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && activeFile.extension === "md") {
if (!checking) {
this.pluginStatus = "pulling";
this.handleFileModify(activeFile, true);
}
return true;
}
return false;
}
});
Ein paar Abfragen, um sicherzugehen, dass auch alle Voraussetzungen erfüllt sind, dann wird der Sync aufgerufen. Die anderen Befehle funktionieren ähnlich.
Schließlich habe ich noch ein Plug-in im Einsatz, mit dem ich den Dateien Icons geben kann. Dieses Plug-in zapfe ich an, um entsprechend dem Datei-Status ein passendes Icon zu setzen, das hat man oben im Screenshot schon gesehen.
Fazit
Mit diesem Set-up bin ich bisher sehr zufrieden. Ich habe immer alles auf dem gleichen Stand. Dank Obsidian Sync läuft mein Plug-in auf allen Geräten.
Natürlich passiert noch ein wenig mehr, oben sieht man schon den pluginStatus
, der sicherstellt, dass es keine Loops gibt und sich nichts überschneidet. In Kirby wandle ich noch die Obsidian-Syntax für Bilder in einen Kirbytag um. Aus ! [ [obsidian-8.png ] ]
wird dann ( image: obsidian-8.png )
wenn die Page in Kirby gerendert wird (die Leerzeichen habe ich ergänzt, damit die Tags nicht ersetzt werden).
Apropos Bilder! Die synchronisiere ich bislang nicht, die müssen also extra hochgeladen werden. Das wäre noch ein nettes Feature für die Zukunft.
Ich kann also munter synchronisieren, mein Obsidian sieht dank Theme und Font toll aus und ich kann dort sehr angenehm schreiben. Dank Obsidian Sync habe ich keine Angst vor Datenverlust und kann auf allen Geräten auf meinen Vault zugreifen. Ich habe gute Stil- und Grammatikprüfung über das Plug-in und ich kann den Status meiner Dateien auf den ersten Blick im Verzeichnisbaum sehen.
Es gibt noch massives Potenzial für Verbesserungen. Das alles ist wild zusammen gehackt, weil ich viel ausprobiert und neu gelernt habe. Deshalb sind beide Plug-ins noch etwas davon entfernt, veröffentlicht werden zu können. Und ich bin auch gar nicht sicher, wie weit ihr dafür überhaupt Bedarf besteht. Ihr könnt mir ja mal einen Kommentar schreiben.
Ich werde hier jetzt noch einmal die Rechtschreibprüfung drüber schicken und dann via Obsidian eine neue Seite bei Kirby anlegen. Wie jedes Mal freue ich mich aufs Neue darüber, dass das jetzt möglich ist.
-
CRUD steht für Create, Read, Update, Delete ↩
Kommentar schreiben