Das Problem

Heute habe ich noch ein kleines Projekt fertiggestellt. Meine Partnerin und ich haben jeder eine Sammlung von Kochrezepten. Teilweise in Markdown, teilweise als Orgdateien, teilweise in papierform liegen die Rezepte vor. Wie können wir die Rezepte möglichst zentral für beide bereitstellen, damit wir sie beim Kochen schnell parat haben?

Die Lösung

Antwort: Ich erstelle eine kleine Webseite und lade sie auf meinen Raspberry Pi. Im lokalen Netzwerk können wir dann beide auf die Rezepte zugreifen.

Abbildung 1: Startseite der Rezeptsammlung

Abbildung 1: Startseite der Rezeptsammlung

Die Verwaltung der Rezepte erfolgt über Emacs.

Die Umsetzung

Ich habe mir einen Ordner in meinem normalen Verzeichnis ~/org/rezepte angelegt. Darin lege ich die Rezepte als einzelne Org-Dateien ab. Zusätzlich gibt es eine zusätzliche Org-Datei index.org. Diese Datei wird später die zentrale Seite der Homepage werden.

Das ganze soll dann mittels org-publish als fertige HTML-Seite exportiert und auf den Raspberry Pi kopiert werden.

Vorbereitung der index.org

Die index.org ist eine normale Org-Datei. Ich habe darin Überschriften für die verschiedenen Kategorien wie “Suppen”, “Hauptgerichte”, “Desserts”, “Kuchen” angelegt. Darunter sind dann die einzelnen Rezepte verlinkt.

Im Kopf als Option ist zusätzlich eine HTML-Code für den Export enthalten. Dieser sorgt dafür, dass auf der fertigen HTML-Seite eine Suchfeld angezeigt wird. Damit die Suche auch funktioniert, ist am Ende der Org-Datei ein wenig Javascript enthalten.

#+TITLE: Unsere Rezeptsammlung
#+OPTIONS: toc:nil num:nil html-postamble:nil
#+STARTUP: show2levels
#+HTML: <input type="text" id="rezeptSuche" onkeyup="filterRezepte()" placeholder="Nach Rezepten oder Tags suchen...">

* Suppen
** [[file:bananen-curry-suppe.org][Bananen-Curry-Suppe]] :curry:bananen:
** [[file:erbsensuppe.org][Erbsensuppe]]
** [[file:französische-zwiebelsuppe.org][Französische Zwiebelsuppe]]
** [[file:kalte-avocado-gurkensuppe-mit-croutons.org][Kalte Joghurt-Avocado-Gurkensuppe mit Minze, Pinienkernen und Croutons]] :avocado:gurke:kalt:schnell:
** [[file:karotten-ingwer-suppe.org][Karotten-Ingwer-Suppe]]
** [[file:kürbissuppe.org][Kürbissuppe]]
* Hauptspeisen
** [[file:feta-pasta-mit-nudeln.org][Feta Gemüse Pasta]] :feta:gemüse:nudeln:pasta:paprika:karotten:
** [[file:hähnchengulasch-mit-frühlingszwiebeln.org][Hähnchengulasch mit Frühlingszwiebeln]] :hähnchen:leicht:
** [[file:kartoffel-zucchini-auflauf-mit-feta-und-metaxa.org][Kartoffel-Zucchini-Auflauf mit Feta und Metaxa]] :kartoffeln:gemüse:metaxa:mediteran:
** [[file:ofenpasta-mit-feta-und-tomatensauce.org][Ofenpasta mit Feta und Tomatensauce]] :nudeln:feta:
* Desserts
** [[file:haferkekse.org][Haferkekse]] :kekse:haferflocken:
** [[file:schokoladenpudding-mit-seidentofu.org][Schokoladenpudding mit Seidentofu]] :schokolade:tofu:thermomix:
* Kuchen
** [[file:bananenbrot.org][Bananenbrot]] :bananen:
** [[file:brownies.org][Brownies]] :schokolade:
** [[file:kalter-hund.org][Kalter Hund]] :schokolade:kekse:
** [[file:kanelbullar-schwedische-zimtschnecken.org][Kanelbullar - Schwedische Zimtschnecken]] :zimt:schweden:
** [[file:karottenkuchen.org][Karottenkuchen]] :karotten:
** [[file:Schoko-Bananenkuchen.org][Schoko-Bananenkuchen]] :schokolade:banane:
** [[file:zebra-himbeer-käsekuchen.org][Zebra-Himbeer-Käsekuchen]] :himbeeren:
** [[file:zitronenkuchen.org][Zitronenkuchen]] :zitronen:
*
#+BEGIN_EXPORT html
<script>
function filterRezepte() {
   var input = document.getElementById('rezeptSuche');
   var filter = input.value.toLowerCase();
   var items = document.querySelectorAll('.org-ul li, .outline-3');

   items.forEach(function(item) {
       var text = item.textContent || item.innerText;
       if (text.toLowerCase().indexOf(filter) > -1) {
           item.style.display = "";
       } else {
           item.style.display = "none";
       }
   });
}
</script>
#+END_EXPORT

Init.el anpassen

Für org-publish müssen wir ein Projekt definieren. Die Konfiguration von mir exportiert die Rezepte und die index.org in das Verzeichnis ~/Rezepte_html. Es werden keine Inhaltsverzechnisse, Nummerierungen oder die Default-Fußzeile erzeugt. Es wird aber in jeder Datei eine css-Datei eingebunden, damit die fertige Seite optisch etwas ansprechender ist.

Damit auch statische Dateien wie Bilder oder eben auch die besagte css-Datei exportiert werden, benötigen wir zusätzlich noch rezepte-static

(require 'ox-publish)

(setq org-publish-project-alist
      '(("rezepte-html"
       :base-directory "~/org/rezepte/"
       :base-extension "org"
       :publishing-directory "~/Rezepte_html/"
       :recursive t
       :publishing-function org-html-publish-to-html
       :headline-levels 4
       :auto-preamble t
       :with-toc nil                          ; Schaltet das Inhaltsverzeichnis ab (toc:nil)
       :section-numbers nil                   ; Schaltet die Nummerierung ab (num:nil)
       :html-postamble nil                    ; Entfernt die Fußzeile (html-postamble:nil)
       :html-head-include-default-style nil
       :html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"bambus.css\" />"
       :html-head-extra "")

        ("rezepte-static"
         :base-directory "~/org/rezepte/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf"
         :publishing-directory "~/Rezepte_html/"
         :recursive t
         :publishing-function org-publish-attachment)

        ("rezepte" :components ("rezepte-html" "rezepte-static"))))

Wenn ich über M-x org-publish aufrufe, habe ich in der Auswahl sowohl rezepte-html, rezepte-static und auch rezepte. Mit letzterem exportiere ich alle org-Dateien und auch die Anhänge.

Sync auf den Raspberry Pi

Ich habe dafür meine Methode kopiert, die ich bereits auch für diesen Blog verwende Blogging mit Emacs - Howto. Dort hatte ich die Funktion nicht aufgeführt. Im Prinzip ist es hier einfacher, da der Export über Hugo nicht notwendig ist. Es wird von Emacs lediglich rsync mit den entsprechenden Parametern aufgerufen.

(defun jan/rezepte-deploy ()
  (interactive)
  (let ((default-directory "~/Rezepte_html/"))
    (if (zerop  (shell-command "rsync -avz --delete ~/Rezepte_html/ jan@pi.fritz.box:/home/jan/smarthome/rezepte/"))
          (message "Rezeptsammlung erfolgreich veröffentlicht!")
      (message "Fehler beim Upload!"))))

Fazit

Das war recht spaßig, so etwas aufzusetzen. Ich habe meine bisherigen Rezepte in diese Sammlung überführt. Wenn ich jetzt in der Küche bin, brauche ich einfach nur mittels Handy oder Tablet die Seite auf dem Pi aufrufen. Und schon habe ich das passende Rezept übersichtlich und einfach im Zugriff. Wieder ein schönes Beispiel für mich, dass Emacs mehr als ein Texteditor ist. Der gesamte Prozess vom Schreiben bis zum Export und der Übertragung läuft aus Emacs heraus.

Jetzt brauche ich nur mehr Rezepte ☺️

Abbildung 2: Rezept für einen Zitronenkuchen

Abbildung 2: Rezept für einen Zitronenkuchen

Anhang CSS

Falls jemand die CSS-Datei haben möchte, hier ist sie.
body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: 18px;
    line-height: 1.6;
    color: #333;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
    background-color: #fefefe;
}

h1.title {
    font-size: 2.2em;
    color: #2c3e50;
    border-bottom: 2px solid #ecf0f1;
    padding-bottom: 10px;
    margin-top: 10px;
}

h2 {
    font-size: 1.5em;
    color: #16a085;
    margin-top: 30px;
    border-bottom: 1px solid #f2f2f2;
    padding-bottom: 5px;
}

ul, ol {
    padding-left: 25px;
    margin-bottom: 20px;
}

li {
    margin-bottom: 8px;
}

a {
    color: #2980b9;
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

@media (max-width: 600px) {
    body {
        padding: 15px;
        font-size: 16px;
    }
    h1.title {
        font-size: 1.8em;
    }
}

.tag {
    float: none;
    display: inline-block;
    vertical-align: middle;
    margin-left: 15px;
    font-size: 0.65em;
    font-weight: normal;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.tag span {
    background-color: #eef2f5;
    color: #57606a;
    padding: 3px 8px;
    margin-right: 5px;
    border-radius: 12px;
    border: 1px solid #d0d7de;
    text-transform: lowercase;
}

#rezeptSuche {
    width: 100%;
    font-size: 16px;
    padding: 12px 20px;
    margin: 20px 0 30px 0;
    border: 1px solid #d0d7de;
    border-radius: 6px;
    box-sizing: border-box;
    background-color: #fafafa;
}

#rezeptSuche:focus {
    outline: none;
    border-color: #16a085;
    background-color: #fff;
}

.rezept-versteckt {
    display: none !important;
}