Wie bereits angekündigt, kommt hier die Anleitung, wie man Notion als flexibles CMS verwendet. Wir schauen uns das Ganze an, indem wir einen einfachen Blog erstellen. Bevor es ans Eingemachte geht, eine Auflistung der Schritte, die wir vor uns haben:

  1. Falls du noch keinen hast: Notion Account anlegen
  2. Neue Einbindung bei Notion erstellen
  3. Datenbank in einem Notion-Dokument anlegen
  4. Datenbank verbinden
  5. Nötige Libraries installieren
  6. Daten von Notion holen
  7. Daten Strukturieren und Formatieren
  8. Die erstellten Funktionen verwenden

1. Notion Account erstellen

Dieser Schritt sollte selbsterklärend sein. Du brauchst auf jeden Fall einen Account.

https://www.notion.so/signup

2. Neue Einbindung erstellen

Wenn du bei Notion eingeloggt bist, gehe oben links auf Einstellungen (das Zahnrad), sodass sich ein kleines Fenster mit vielen Optionen öffnet. In diesem Fenster gehe auf “Meine Verbindungen” und dann auf "Meine Einbindungen entwickeln oder verwalten” (wenn du eingeloggt bist, kannst du auch direkt diesen Link nutzen: https://www.notion.so/my-integrations).

Klicke auf den Button “Neue Einbindung” und vergib einen Namen, z.B. “CMS”. Anschließend gehe auf “Senden”. Zack, schon hast du den Grundstein für die Verbindung zwischen Notion und deiner Website geschaffen! Du wirst hier direkt zu der neu erstellten Einbindung weitergeleitet und kannst das “Internal Integration Secret” bzw. den API-Key einsehen. Jetzt nicht nervös werden, du kannst jederzeit zu dieser Seite zurückkehren, wenn du den Schlüssel brauchst.

3. Datenbank in einem Notion Dokument anlegen

Notion ist wirklich intuitiv und du solltest keine Schwierigkeiten haben, eine neue Seite zu erstellen.

Auf der erstellten Seite kannst du deine Datenbank platzieren. Tatsächlich kannst du sie überall platzieren – überlege dir also, wo es für dich am meisten Sinn ergibt!

Wenn dein Blog der Hauptgrund für dich ist, Notion zu nutzen, dann einfach ganz vorne. Vielleicht hast du aber bereits eine komplexe Struktur mit Unterseiten, dann ist das auch in Ordnung. Das ist das Coole: Du bist extrem flexibel.

Jetzt klicke irgendwo auf die Seite, um einen neuen Block (nicht Blog) zu erstellen. Anschließend gehe auf das kleine “+”, welches erscheint, wenn dein Cursor auf dem Block ruht.

Screenshot_2024-06-04_at_19.07.45.png

Wähle hier “Table” aus, um den Block in eine Tabelle umzuwandeln.

Jetzt trage in die obere Reihe folgende Begriffe ein:

Screenshot_2024-06-04_at_19.14.39.png

Jetzt markiere die gesamte obere Reihe, gehe auf “Options” und dann aktiviere “Header row”.

Screenshot_2024-06-04_at_19.15.44.png

Tadaa, eine Kopfzeile!

Jetzt kommt der entscheidende Schritt: Wenn du jetzt irgendwo in die Tabelle klickst und dann auf die drei Punkte, kannst du mit “Turn into Database” aus deiner Tabelle eine echte Datenbank machen!

Die Tabelle sollte jetzt so aussehen:

Screenshot_2024-06-04_at_19.23.26.png

Links neben den Spaltenköpfen siehst du jeweils ein Symbol, welches den Typ der jeweiligen Spalte anzeigt. Zunächst sind alle Spalten vom Typ “Text”. Das kannst du ändern, indem du auf den Kopf der jeweiligen Spalte klickst, auf “Edit property” und dann auf “Type” gehst.

Hier eine kleine Challenge für dich: Ändere die Typen, sodass die Datenbank am Ende so aussieht:

Screenshot_2024-06-04_at_19.28.41.png

Wo du Autor, Autoren-Link, Datum und Cover-Bild einträgst, sollte offensichtlich sein. Doch wo schreibst du den eigentlichen Blog-Artikel?

Gehe dazu mit dem Cursor auf das Feld “title”, sodass die Option “OPEN” erscheint. Da klickst du dann drauf!

Screenshot_2024-06-04_at_19.36.08.png

Simpel, oder?

Bleibt nur noch, ein paar Blog-Beiträge zu verfassen. Tatsächlich empfehle ich dir, zwei bis drei “Dummy-Posts” einzutragen, sodass du gleich echte Daten auslesen kannst. Also, lass dir was einfallen!

4. Datenbank verbinden

Wichtig: Öffne an dieser Stelle die Datenbank selbst, indem du auf die drei Punkte neben dem Datenbanktitel gehst und auf “View Database” klickst! Alles zwischen dem “/” und “?” in der Adresszeile deines Browsers ist die ID deiner Datenbank.

Screenshot_2024-06-04_at_19.46.38.png

Wähle hier die soeben erstellte Einbindung aus, in diesem Fall “CMS”.

Bestätige anschließend, dass deine Einbindung Zugriff auf die Datenbank bekommt.

Kaffeepause!

Das war’s erstmal bei Notion. Solltest du es bis hierher geschafft haben, klopf dir auf die Schulter und mach dir‘n Kaffee!

5. Nötige Libraries installieren

Es würde den Rahmen sprengen, alle kommenden Schritte im Einzelnen zu erklären, weshalb ich voraussetze, dass du weißt, wie man ein Projekt mit dem Node Package Manager (NPM) aufsetzt.

Steht dein Svelte-, React- oder Vue-Projekt, installiere zusätzlich den Notion Client, einen Notion-zu-Markdown-Konverter und einen Markdown-zu-HTML-Konverter:

npm install @notionhq/client notion-to-md marked

6. Daten von Notion holen

Jetzt beginnt der eigentliche Spaß: Wir schreiben ein Skript, welches aus zwei einfachen Funktionen besteht, die die nötigen Daten von Notion auslesen und formatieren, sodass du sie ganz einfach in dein Projekt einbinden kannst.

Zunächst erstellen wir eine neue Datei mit dem Namen “getBlogData.js” und importieren uns den Notion Client:

import notionHq from "@notionhq/client";
const { Client } = notionHq;

const notion = new Client({ auth: "secret_****************************" });
const databaseId = "****************************";

Bei “auth” trägst du den API Key aus Schritt 2 ein, bei “databaseId” die ID deiner Datenbank aus Schritt 4. Am Besten wäre natürlich eine .env-Datei, aber das sparen wir uns hier der Einfachheit halber.

Jetzt schreiben wir die erste Funktion: getPosts(), die alle Posts (ohne Inhalt) ausliest und als Array zurückgibt.

export async function getPosts() {
  const response = await notion.databases.query({ database_id: databaseId });
  
  return response;
}

Bisher lesen wir alle Posts aus, doch ist es natürlich unsinnig auch die unveröffentlichten zu holen. Also filtern wir sie heraus und sortieren die Posts auch direkt nach Datum.

export async function getPosts() {
  const response = await notion.databases.query({
    database_id: databaseId,
    filter: {
      property: "published",
      checkbox: {
        equals: true,
      },
    },
    sorts: [
      {
        property: "date",
        direction: "descending",
      },
    ],
  });
    
  return response;
}

Besser!

6. Strukturieren und Formatieren der Daten

Das Ergebnis (response) ist noch sehr unübersichtlich und enthält alle möglichen Informationen, die wir aktuell nicht benötigen. Deshalb nehmen wir die Daten und bauen uns ein einfaches Post-Objekt mit nur dem Nötigsten.

Sieh dir am Besten die rohen Daten einmal an, um ein Verständnis dafür zu bekommen, was folgender Code macht.

export async function getPosts() {
  const response = await notion.databases.query({
    database_id: databaseId,
    filter: {
      property: "published",
      checkbox: {
        equals: true,
      },
    },
    sorts: [
      {
        property: "date",
        direction: "descending",
      },
    ],
  });

  const posts = response.results.map(post => {
    const title = post.properties.title?.title[0]?.plain_text;
    const date = post.properties.date?.date?.start;
    const author = post.properties.author?.rich_text[0]?.plain_text;
    const authorLink = post.properties.author_link?.url;
    const img = post.properties.image.files[0].file.url;
    const id = post.id;

    return {
      title,
      date,
      author,
      authorLink,
      img,
      id,
    };
  });
  
  return posts;
}

Jetzt kommt etwas, das nicht zwingend notwendig ist, jedoch zu empfehlen ist: Die Verwendung einer Slugify-Funktion, welche die Titel unserer Posts in URL-taugliche Strings umwandelt. Eine sehr einfache Variante dieser Helfer-Funktion sieht so aus:

function slugify(text) {
  return text.toLowerCase().replace(/ /g, "-").replace(/[^a-z0-9-]/g, "");
}

Sie ermöglicht es, dass wir später lesbare Links haben. Anstelle von “exord.de/blog/j4zwh3” heißt die URL dann “exord.de/blog/mein-blog-titel”.

Die zweite Funktion heißt getPostBySlug() und und verwendet sowohl getPosts() als auch slugify(), um anhand der URL den passenden Post zu finden.

export async function getPostBySlug(slug) {
  const posts = await getPosts();

  let post = posts.find(post => slugify(post.title) === slug);

  if (!post) {

    return null;
  }

  return post;
}

Wenn der User also später “exord.de/blog/mein-blog-titel” eingibt bzw. auf einen entsprechenden Link klickt, wird der Teil nach “blog/” (slug) genommen, durch getPostBySlug() gejagt und letztere gibt dann den passenden Post zurück.

Die Funktion prüft ganz einfach, ob einer der Titel, nachdem er “slugified” wurde dem eingegebenen slug entspricht – und der dazugehörige Post wird dann returned.

So weit so gut, doch da wäre noch ein Problem: Der Post enthält title, author, date usw. – doch nicht den eigentlichen Blog-Beitrag!

Das werden wir jetzt ändern:

import notionHq from "@notionhq/client";
const { Client } = notionHq;
import notionToMarkdown from "notion-to-md";
const { NotionToMarkdown } = notionToMarkdown;
import notionToMarkdown from "notion-to-md";
const { NotionToMarkdown } = notionToMarkdown;

const notion = new Client({ auth: "secret_****************************" });
const databaseId = "****************************";
const n2m = new NotionToMarkdown({ notionClient: notion });

export async function getPosts() {
    ...
}

function slugify(text) {
    ...
}

export async function getPostBySlug(slug) {
  const posts = await getPosts();

  let post = posts.find(post => slugify(post.title) === slug);

  if (!post) {

    return null;
  }

  const blocks = await n2m.pageToMarkdown(post.id);
  const mdString = n2m.toMarkdownString(blocks).parent;
  const html = marked.parse(mdString);

  post.html = html;

  return post;
}

Kurze Erklärung: “notion-to-md” nutzt den Notion Client, um die Seiteninhalte (Blocks) auszulesen und Markdown daraus zu machen. Dann kommt “marked” ins Spiel und macht aus dem Markdown dann HTML.

Warum so kompliziert? Warum nicht gleich die Blöcke in HTML umwandeln?

Es gibt ganz einfach keine Library dazu und selbst ein Skript zu schreiben wäre ziemlich aufwendig. Das mehrfache Umwandeln der Daten passiert jedoch in wenigen Millisekunden und niemand wird wissen, dass wir diesen Umweg gegangen sind.

giphy.gif

8. Die erstellten Funktionen verwenden

Wir haben jetzt die Funktion getPosts(), die alle Posts (ohne die eigentlichen Artikel) zurückgibt und die Funktion getPostBySlug(), die uns einen bestimmten Post zurückgibt, wenn wir die jeweilige Slug als Input verwenden.

In der Praxis haben wir dann in der Regel einen oder mehrere Bereiche, wo alle oder ausgewählte Posts aufgelistet werden. So wie z.B. unter dem Artikel, den du gerade liest. Solch eine Auflistung wird dynamisch erstellt, indem über den Array mit den Posts iteriert wird.

Außerdem gibt es die Artikel-Seiten selbst, auf denen ein bestimmter Post angezeigt wird. Hier extrahierst du die enthaltene Slug (der Teil nach “blog/”) und rufst die Funktion getPostBySlug(slug) mit der jeweiligen Slug auf. Zurück kommt dann das entsprechende Objekt, welches alle nötigen Daten enthält, um die Post-Seite daraus zu bauen.

Aktuell werden die Seiten serverseitig gerendert, was für Suchmaschinen optimal ist. Einziger Nachteil: Das Laden der Seite dauert ein wenig länger. Man kann die Post-Inhalte auch clientseitig laden, sollte dann jedoch eine kleine API dafür bauen, um nicht Gefahr zu laufen, vertrauliche Daten zu leaken.

Abschließende Gedanken

Wer bereits mit JavaScript und einem Framework arbeitet, sollte keinerlei Schwierigkeiten haben, meine Ausführungen zu verstehen und umzusetzen. Für alle anderen gilt: JavaScript und ein Framework lernen – oder Exord dazuholen!

Ich habe in diesem Artikel natürlich nur an der Oberfläche gekratzt, um deutlich zu machen, wie einfach die Verknüpfung mit Notion ist. Bisher haben wir auch nur Daten ausgelesen, doch es geht auch umgekehrt. Tatsächlich kann Notion, soweit ich das überblicken kann, alles, was eine konventionelle Datenbank kann und ist dabei noch einfach manuell zu pflegen. Gerade für kleine Projekte oder MVPs einfach genial.