Verfasst in: Englisch / Veröffentlicht: 15.04.2022 / Lesezeit: 27 Minute(n)
Gibt es einen Grund, Ihr eigenes Web Content Management System (WCMS) zu erstellen? Es gibt bereits Hunderds von ihnen.
Das ist wahr und deshalb gibt es keinen offensichtlichen Grund, ein solches Projekt zu starten. Nur wenn wir nicht völlig zufrieden sind über das, was derzeit verfügbar ist, oder wir wollen nur lernen und versuchen, was es bedeutet, dies zu tun.
Ursprünglich wollte ich meine Website neu gestalten beihttps://niklas-stephan.de, aber dann hatte ich Spaß, von Grund auf zu beginnen und baute die meisten Backend auf meinem eigenen.
Und das ist die Absicht hinter nCMS, “niklas stephan’s Content Managment System” oder “node-red Content Management System” oder “nicht ein anderes Content Management System”.
nCMS ist ein kopfloses WCMS basierend auf einer flachen Hierachie, also im Vergleich zu z. Wort Drücken Sie, dass wir keine Datenbank oder eine traditionelle serverseitige Programmiersprache verwenden. Stattdessen nutzt das Backend meine Lieblings Low-Code-Plattform Node-Red, die von einer node.js Instanz bereitgestellt wird. So ist unsere Programmiersprache sowohl für Frontend als auch Backend reines JavaScript.
Änderungen in unserem Backend-Code werden implementiert, wie es bei der Verwendung von Node-Red üblich ist. Additonal irgendwelche Änderungen, die nicht direkt im Node-Red-Flow vorgenommen werden, basieren auf Dateien, die synchronisiert und in Github versionisiert werden. Auch das Frontend basiert auf Implementierungen, die extern über einen Webhook oder manuell innerhalb des Node-Red-Flusses gestartet werden. Sobald die Frontend-Bereitstellung gestartet ist, generiert alle benötigten Dateien und stellt sie als statische Dateien einem einfachen Webserver Ihrer Wahl zur Verfügung.
Fazit, dass eine Website erstellt mit nCMS extrem schnell. Um die Geschichte abzuschließen, muss man so sagen, dass, wenn wir einen (neuen) Beitrag erstellen/erarbeiten möchten, wir die Markup-Sprache verwenden und die Datei über jede Texteditor-Anwendung bearbeiten können.
Integrierte Merkmale sind (bisher):
Und vieles mehr, dokumentiert und availabe bei den Projekten Github Repository:https://github.com/handtrixx/ncm
Ja, ja, ja. Ich werde hier erklären, warum und wie das erreicht wird.
Eine Docker-Compose-Umgebung wird verwendet, um ncms zu hosten. Sie können den Quellinhalt des Volumens findensrc
beihttps://github.com/handtrixx/ncm.
Inhaltdocker-compose.yml
wird hier gezeigt und basierend auf dem offiziellen Nod-Red-Bild von Docker Hub (https://hub.docker.com/r/nodered/node-red:
version: "3.0" services: ncms: container_name: ncms hostname: ncms image: nodered/node-red:latest restart: always environment: - TZ=Europe/Berlin networks: - dmz logging: options: max-size: "10m" max-file: "3" volumes: - ./data:/data - ./dist:/dist:rw - ./src:/src:rw - /etc/localtime:/etc/localtime:ro networks: dmz: external: true
Nicht viel zu erklären, denn es ist relativ nach vorne. Vielleicht sind die unterschiedlichen Volumina und die Netzwerkkonfiguration zu erwähnen. Genau wie die meisten in meinen anderen webbezogenen Beiträgen, nutze ich das nginx Managment-Tool “Nginx Proxy Manager” als umgekehrte Proxy (https://hub.docker.com/r/jc21/nginx-proxy-manager) Das Volumenversion: "3.0"
services:
ncms:
container_name: ncms
hostname: ncms
image: nodered/node-red:latest
restart: always
environment:
- TZ=Europe/Berlin
networks:
- dmz
logging:
options:
max-size: "10m"
max-file: "3"
volumes:
- ./data:/data
- ./dist:/dist:rw
- ./src:/src:rw
- /etc/localtime:/etc/localtime:ro
networks:
dmz:
external: true
Unsere Umwelt ist Schmutz mit dem umgekehrten Proxy über symbolische Verbindung verbunden./dist
. So müssen wir die Dateien während der Bereitstellung nicht zweimal kopieren oder generieren und die direkt verfügbare Datei ohne zusätzliche Webserverinstanz haben.
Die Node-Red GUI wird durch Konfiguration an einer separaten Subdomain erreichbar.
Direktln -s
enthält folgende Unterordner, die manuell erstellt werden sollen:
Ordner./src
und seine Unterordner halten alle Vermögenswerte wie die Quellen von Bootstrap 5 (https://getbootstrap.com/) und sicher unsere eigenen css-Stile und Javascript-Funktionen. Unterverzeichnisassets
enthält nur unsere statischen Übersetzungen.
Verzeichnisjson
und sein Unterordnermd
enthält unsere Postdateien, die durch eineposts
Suffix.
Unsere Bilder und andere Mediendateien werden im Ordner platziert.md
. Ich schrieb das System, um auch Unterordner für Dateien zu überprüfen, aber das derzeit auf funktioniert auf einer Ebene (so, Inhalt in einem Unter-Sub-Ordner wird ignoriert).
Später sehen wir, dass diese Dateien automatisch in das Speichern und Web-optimiertes Format konvertiert werdenmedia
und additon wird auch eine Miniaturansicht erzeugt.
Alle HTML-Elemente, die wir mehr als ein Mal verwenden möchten, werden im Ordner gespeichert.webp
.
Direktsnippets
enthält die Quellen für alle HTML-Seiten, auf denen wir unsere Snippets und andere Daten bei der späteren Bereitstellung enthalten.
Wenn Sie die Details sehen möchten, welche Dateien erstellt werden müssen und welche Inhalte sie enthalten könnten, überprüfen Sie bitte mein Github-Repository unter:https://github.com/handtrixx/ncm.
Die Vorlagendateien sind:templates
– Unsere Fehlerseite wird immer angezeigt, wenn eine angeforderte Seite einfach nicht existiert oder wenn ein Beitrag vielleicht noch nicht übersetzt wurde. Unsere404.html
wird verwendet, um einen Überblick über alle vorhandenen veröffentlichten Beiträge, die gefiltert und auf verschiedene Weise sortiert werden können. Dieblog.html
enthält einfach unsere Landing-Seite mit ihrem Inhalt. Eine besondere Rolle wird demindex.html
Datei. Es wird von jedem Post als Vorlage verwendet, um sicherzustellen, dass alle Beiträge die gleiche UX liefern.post.html
Stattdessen ist eine einfache Vorlage, um den Inhalt unserer Datenschutzerklärung und den Aufdruck bereitzustellen, der von jeder Seite durch EU-Recht direkt erreichbar ist. Dieprivacy-policy.html
Template wird verwendet, um Suchmaschinen-Crawler einige grundlegende Informationen über unsere Website zur Verfügung zu stellen. Endlich gibt esrobots.txt
Vorlage, die die lokal indizierte Suchfunktionalität des Frontends umfasst.
Die sogenannten Snippets sind grundlegende Komponenten, die auf jede Template-basierte Seite eingefügt werden. Sie werden getrennt, um Zeit zu sparen und unseren Code sauber zu halten. So zum Beispiel eine Änderung des Navigationsschnipselssearch.html
wird automatisch auf allen Seiten reflektiert.
Die verwendeten Nippel sind:navbar.html
,footer.html
,head.html
,navbar.html
.
Nod-Red selbst basiert auf node.js und einer Plattform zur Low-Code-Programmierung von ereignisgetriebenen Anwendungen. Wir können so genannte Knoten verwenden, die beispielsweise eine Javascript-Funktion darstellen können und dann viele dieser Knoten als Fluss verknüpfen.
Für nCMS verwenden wir fast nur diese Fähigkeiten, aber ignorieren Sie den "Low-Code" Teil ein bisschen, da wir nur mit vollen geblasenen Javascript-Funktionen arbeiten.
Was wir stattdessen tun, um andere npm-Module bei Bedarf in die spezifischen Knoten zu integrieren, was einfach über jeden Nodes-Setup-Tab erfolgen kann.
Weitere Informationen zu den erstellten Knoten und deren Details finden Sie hier.
Diescript.html
Knoten ist einfach/deploy
die es uns ermöglicht, unsere Bereitstellungen durch einen Webhook zu starten. Das bedeutet, indem man z.http in
von überall starten wir eine Anfrage für ein Deplyoment.
Sicher, dass wir nicht zulassen wollen, dass nur jemand den Einsatz startet, also diecurl -X POST -d 'key=----' https://ncms.niklas-stephan.de/deploy
node vergleicht die vom Webhook-Call gesendete "key"-Variable mit einem in unserem Backend gespeicherten geheimen Schlüssel.catch key
Ordner unserer Docker Umgebung, um sicher zu sein, dass wir nicht versehentlich synchronisieren, dass Schlüssel zu Github sowie.
Wie bereits erwähnt, fügen wir ein additioanl npm Modul hinzudata
und machen es availabel alsfs-extra
auf der Setup-Tab des Knotens.
Damit können wir auf das Container-Dateisystem zugreifen, um die Schlüssel zu vergleichen.
Die komplette Funktion im Knoten ist:
fse
Es ist zu erwähnen, dass dieser Knoten zwei Austrittspfade aufweist. Exit one wird verwendet, wenn die Tasten übereinstimmen und das Deplyoment fortsetzen, während Ausgang zwei aufgerufen wird, wenn die Authentifizierung ausfällt und das Deplyoment abbrechen wird.
Dieconst transferedKey = msg.payload.key;
const systemKey = fse.readFileSync('/data/deploy.key', 'utf8')
if (transferedKey == systemKey) {
msg.payload = "Deployment Started";
msg.statusCode = 200;
msg.type = "webhook";
msg.starttime = Date.now();
return [null,msg];
} else {
msg.payload = "Wrong authentication!"
msg.statusCode = 400;
return [msg,null];
}
node ist zum Starten einer Bereitstellung manuell durch Klicken auf die Node-Red GUI.
Die 28 Linien derconst transferedKey = msg.payload.key;
const systemKey = fse.readFileSync('/data/deploy.key', 'utf8')
if (transferedKey == systemKey) {
msg.payload = "Deployment Started";
msg.statusCode = 200;
msg.type = "webhook";
msg.starttime = Date.now();
return [null,msg];
} else {
msg.payload = "Wrong authentication!"
msg.statusCode = 400;
return [msg,null];
}
Knoten sind genug, um viel zu erreichen. Die Funktion wird alle Beiträge lesen, die wir erstellt haben und das Markup in gültiges HTML konvertieren. Außerdem manipuliert es die Quellen durch Änderung von Links, Bildern und Extraktion der in dendeploy
Dateien. Jeder Ausgang wird in einem Array für eine spätere Nutzung gespeichert.
Neben der bereits bekanntenget posts
npm Pakete, die wir einrichten.md
und Plugins dafür.
Markdown-it (https://github.com/markdown-it/markdown-it) macht die Magie der Umwandlung von Markup zu HTML und der Hauptgrund, warum unser eigener Code so einfach ist, wie 28 Zeilen sein können.
fs-extra
Wieder verwenden wirmarkdown-it
Modul in unserer Funktion, diesmal, um den Inhalt frorm jeden Schnipsel zu lesen, um es in unserem Fluss als Array zu speichernmsg.baseurl = "https://niklas-stephan.de"
msg.dist = {};
msg.posts = [];
const path = '/src/md/posts/';
const postfiles = fse.readdirSync(path)
const alength = postfiles.length;
for (var i=0; i<alength; i++) {
var srcFile = path+postfiles[i];
var distFilename = postfiles[i].split('.')[0]+".html";
var srcContent = fse.readFileSync(srcFile, 'utf8')
var md = new markdownIt({
html: true,linkify: true,typographer: true,breaks: true})
.use(markdownItFrontMatter, function(metainfo) {meta = JSON.parse(metainfo);})
.use(markdownItLinkifyImages, {target: '_blank',linkClass: 'custom-link-class',imgClass: 'custom-img-class'})
.use(markdownItLinkAttributes, { attrs: {target: "_blank",rel: "noopener",}
});
distContent = md.render(srcContent);
let data = {"srcFile":""+srcFile+"","srcContent":""+srcContent+"","distContent":""+distContent+"","distFilename":""+distFilename+"",...meta};
msg.posts.push(data)
}
return msg;
.
msg.baseurl = "https://niklas-stephan.de" msg.dist = {}; msg.posts = []; const path = '/src/md/posts/'; const postfiles = fse.readdirSync(path) const alength = postfiles.length; for (var i=0; i<alength; i++) { var srcFile = path+postfiles[i]; var distFilename = postfiles[i].split('.')[0]+".html"; var srcContent = fse.readFileSync(srcFile, 'utf8') var md = new markdownIt({ html: true,linkify: true,typographer: true,breaks: true}) .use(markdownItFrontMatter, function(metainfo) {meta = JSON.parse(metainfo);}) .use(markdownItLinkifyImages, {target: '_blank',linkClass: 'custom-link-class',imgClass: 'custom-img-class'}) .use(markdownItLinkAttributes, { attrs: {target: "_blank",rel: "noopener",} }); distContent = md.render(srcContent); let data = {"srcFile":""+srcFile+"","srcContent":""+srcContent+"","distContent":""+distContent+"","distFilename":""+distFilename+"",...meta}; msg.posts.push(data) } return msg;
Und eine weitere Zeit, das gleiche für die Vorlagen zu tun und sie als Objekte zu speichernfs-extra
.
msg.snippets
Jetzt können wir beginnen, unsere HTML-Dateien für die Ausgabe vorzubereiten. Erst gehen wir mitmsg.snippets = {};
const path = '/src/snippets/';
const files = fse.readdirSync(path)
const alength = files.length;
for (var i=0; i<alength; i++) {
var srcFile = path+files[i];
var srcContent = fse.readFileSync(srcFile, 'utf8')
msg.snippets[files[i]] = srcContent;
}
return msg;
, wo wir zunächst die Seiten bestimmte Metadaten generieren und einfügen und dann die Platzhalter der Vorlage durch den Inhalt unserer Schnipsel ersetzen.
Auch setzen wir einen Seitentitel, um endlich den generierten Inhalt zu speichern alsmsg.snippets = {};
const path = '/src/snippets/';
const files = fse.readdirSync(path)
const alength = files.length;
for (var i=0; i<alength; i++) {
var srcFile = path+files[i];
var srcContent = fse.readFileSync(srcFile, 'utf8')
msg.snippets[files[i]] = srcContent;
}
return msg;
die später verwendet werden, um unseremsg.templates
Datei.
msg.templates = {}; const path = '/src/templates/'; const files = fse.readdirSync(path) const alength = files.length; for (var i=0; i<alength; i++) { var srcFile = path+files[i]; var srcContent = fse.readFileSync(srcFile, 'utf8') msg.templates[files[i]] = srcContent; } return msg;
Die Erstellung unserer Fehlerseiteninhalte erfolgt schnell. Auch hier legen wir den Inhalt der Snippets ein und setzen einen Seitentitel.
All das kann dann verwendet werden, um zu schreibenmsg.templates = {};
const path = '/src/templates/';
const files = fse.readdirSync(path)
const alength = files.length;
for (var i=0; i<alength; i++) {
var srcFile = path+files[i];
var srcContent = fse.readFileSync(srcFile, 'utf8')
msg.templates[files[i]] = srcContent;
}
return msg;
vonindex.html
später.
msg.dist.index
Gleiches bezüglich der Datenschutzseite, die wir alsindex.html
Objekt durch folgendes Skript.
msg.dist.index = ""; var ogmetalang = "de_DE"; var ogmeta = ` <meta property="og:type" content="website"> <meta property="og:locale" content="`+ogmetalang+`"> <meta property="og:site_name" content="niklas-stephan.de"> <link rel="canonical" href="`+msg.baseurl+`/index.html"> <meta name="description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr"> <meta property="og:title" content="Projects & Blog - niklas-stephan.de"> <meta property="og:description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr"> <meta property="og:url" content="`+msg.baseurl+`/index.html"> <meta property="og:image" content="`+msg.baseurl+`/assets/img/me_logo.webp"> <meta property="og:image:secure_url" content="`+msg.baseurl+`/assets/img/me_logo.webp"> <meta name="twitter:card" content="summary"> <meta name="twitter:description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr"> <meta name="twitter:title" content="Projects & Blog - niklas-stephan.de"> <meta name="twitter:image" content="`+msg.baseurl+`/assets/img/me_logo.webp">` msg.dist.index = msg.templates["index.html"].replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]); msg.dist.index = msg.dist.index.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]); msg.dist.index = msg.dist.index.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]); msg.dist.index = msg.dist.index.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]); msg.dist.index = msg.dist.index.replace("<!-- PAGE TITLE -->","Home"); msg.dist.index = msg.dist.index.replace("<!-- meta tags -->",ogmeta); return msg;
Bevor Sie ein bisschen komplexer die einfache Generation dermsg.dist.index = "";
var ogmetalang = "de_DE";
var ogmeta = `
<meta property="og:type" content="website">
<meta property="og:locale" content="`+ogmetalang+`">
<meta property="og:site_name" content="niklas-stephan.de">
<link rel="canonical" href="`+msg.baseurl+`/index.html">
<meta name="description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta property="og:title" content="Projects & Blog - niklas-stephan.de">
<meta property="og:description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta property="og:url" content="`+msg.baseurl+`/index.html">
<meta property="og:image" content="`+msg.baseurl+`/assets/img/me_logo.webp">
<meta property="og:image:secure_url" content="`+msg.baseurl+`/assets/img/me_logo.webp">
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta name="twitter:title" content="Projects & Blog - niklas-stephan.de">
<meta name="twitter:image" content="`+msg.baseurl+`/assets/img/me_logo.webp">`
msg.dist.index = msg.templates["index.html"].replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]);
msg.dist.index = msg.dist.index.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]);
msg.dist.index = msg.dist.index.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]);
msg.dist.index = msg.dist.index.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]);
msg.dist.index = msg.dist.index.replace("<!-- PAGE TITLE -->","Home");
msg.dist.index = msg.dist.index.replace("<!-- meta tags -->",ogmeta);
return msg;
Objekt, das den Inhalt unserer Suchseite hält.
404.html
Nun wollen wir unseren Suchindex aufbauen, der, wie bereits erwähnt, erstellt wird, damit unser Besucher unsere Blog-Inhalte durchsuchen kann, ohne einzelne Server-Anfragen zu starten. Um diesen Index alsmsg.dist.errorpage
für späteres Schreibenmsg.dist.errorpage = "";
msg.dist.errorpage = msg.templates["404.html"].replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]);
msg.dist.errorpage = msg.dist.errorpage.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]);
msg.dist.errorpage = msg.dist.errorpage.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]);
msg.dist.errorpage = msg.dist.errorpage.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]);
msg.dist.errorpage = msg.dist.errorpage.replace("<!-- PAGE TITLE -->","Page not found");
return msg;
wir verwenden eine Schleife, die durch alle Elemente gehtmsg.dist.errorpage = "";
msg.dist.errorpage = msg.templates["404.html"].replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]);
msg.dist.errorpage = msg.dist.errorpage.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]);
msg.dist.errorpage = msg.dist.errorpage.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]);
msg.dist.errorpage = msg.dist.errorpage.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]);
msg.dist.errorpage = msg.dist.errorpage.replace("<!-- PAGE TITLE -->","Page not found");
return msg;
(unsere Beiträge) und speichern sie als Json-Objekt.
msg.dist.privacy
Ähnlich wie der Suchindex ist die Erstellung unserer Sitemap. Anstelle einer Schleife verwenden wir hier diemsg.dist.privacy = "";
msg.dist.privacy = msg.templates["privacy-policy.html"].replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]);
msg.dist.privacy = msg.dist.privacy.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]);
msg.dist.privacy = msg.dist.privacy.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]);
msg.dist.privacy = msg.dist.privacy.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]);
msg.dist.privacy = msg.dist.privacy.replace("<!-- PAGE TITLE -->","Datenschutz & Impressum");
return msg;
Funktion, die im Grunde das gleiche, aber etwas schöner und modern in vielen Aspekten tut.
msg.dist.privacy = ""; msg.dist.privacy = msg.templates["privacy-policy.html"].replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]); msg.dist.privacy = msg.dist.privacy.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]); msg.dist.privacy = msg.dist.privacy.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]); msg.dist.privacy = msg.dist.privacy.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]); msg.dist.privacy = msg.dist.privacy.replace("<!-- PAGE TITLE -->","Datenschutz & Impressum"); return msg;
Während wir den Inhalt unserer Posts bereits erstellt haben und auf Array gespeichert habenmsg.dist.searchindex
, wir müssen sie immer noch erweitern, um die Schnipseldaten sowie die Social Media Tags zu jedem von ihnen hinzuzufügen. Sobald jeder Post erledigt ist als Objekt verfügbarmsg.dist.search = "";
msg.dist.search = msg.templates["search.html"].replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]);
msg.dist.search = msg.dist.search.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]);
msg.dist.search = msg.dist.search.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]);
msg.dist.search = msg.dist.search.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]);
msg.dist.search = msg.dist.search.replace("<!-- PAGE TITLE -->","Suche");
return msg;
.
msg.dist.search = ""; msg.dist.search = msg.templates["search.html"].replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]); msg.dist.search = msg.dist.search.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]); msg.dist.search = msg.dist.search.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]); msg.dist.search = msg.dist.search.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]); msg.dist.search = msg.dist.search.replace("<!-- PAGE TITLE -->","Suche"); return msg;
Bevor wir endlich alle generierten Dateien auf die Festplatte schreiben können, müssen wir uns noch um unsere Blog-Seite kümmern.
Wie Sie sehen können, enthält die Funktion ein wenig mehr Aktion dann die meisten anderen Funktionen.
Grundsätzlich schleifen wir alle Beiträge durch, um die in ihren Metadaten definierten Kategorien zu erfassen und als Array zu speichern.
Dann entfernen wir alle douplicate Einträge aus diesem Array. Im nächsten Schritt geben wir jeder Kategorie eine Farbe zu, die auch in unserer Frontend-Datei “style.css” definiert sind.
Wir müssen auch die Meta-Daten von jedem Beitrag zu extrahieren, um ihre Vorschauen zu erstellen: den Titel, die Beschreibung, das Datum erstellen und das Vorschaubild. Hier haben wir auch eine, wenn Aussagen zur Vermeidung von nicht veröffentlichten Beiträgen berücksichtigt werden.
Der Rest ist wie zuvor, wir setzen die Metadaten der Seite und injizieren die Daten der Schnipsel, um schließlich alles als Objekt zu speichernmsg.dist.searchindex
.
index.json
Jetzt wollen wir alle generierten Objekte an schreibenmsg.posts
zu Dateien.
Dazu wird die Funktion "Dateien schreiben" auch mit npm Paket verwendetvar alength = msg.posts.length;
var index = "[";
for (var i=0; i<alength; i++) {
index = index+`{"lang":"`+msg.posts[i].language+`","link":"/posts/`+msg.posts[i].distFilename+`","headline":"`+msg.posts[i].title+`","content":"`+msg.posts[i].distContent.replace(/[^a-zA-Z0-9]/g, ' ')+`"},`;
}
index = index.slice(0, -1);
index = index+"]";
msg.dist.searchindex = index;
return msg;
wievar alength = msg.posts.length;
var index = "[";
for (var i=0; i<alength; i++) {
index = index+`{"lang":"`+msg.posts[i].language+`","link":"/posts/`+msg.posts[i].distFilename+`","headline":"`+msg.posts[i].title+`","content":"`+msg.posts[i].distContent.replace(/[^a-zA-Z0-9]/g, ' ')+`"},`;
}
index = index.slice(0, -1);
index = index+"]";
msg.dist.searchindex = index;
return msg;
.
Nach einer allgemeinen Aufräumung des ZielsforEach()
, um Unstimmigkeiten zu vermeiden, erstellen wir die grundlegende Dateistruktur.
Durch die Nutzung des Befehls "erwarten" stellen wir sicher, dass die Erstellung dieser Verzeichnisse beendet ist, bevor wir auf die nächsten Schritte vorangehen.
Dann kopieren wir alle unsere Vermögenswerte vonvar xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>`+msg.baseurl+`/</loc>
<priority>1.00</priority>
</url>
<url>
<loc>`+msg.baseurl+`/index.html</loc>
<priority>0.80</priority>
</url>
<url>
<loc>`+msg.baseurl+`/blog.html</loc>
<priority>0.80</priority>
</url>`
msg.posts.forEach(postxml);
xml = xml + `
</urlset>`
msg.dist.sitemap = xml;
return msg;
function postxml(item) {
if (item.published == true ) {
xml = xml + `
<url>
<loc>`+msg.baseurl+`/posts/`+item.distFilename+`.html</loc>
<priority>0.64</priority>
</url>`
}
}
bisvar xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>`+msg.baseurl+`/</loc>
<priority>1.00</priority>
</url>
<url>
<loc>`+msg.baseurl+`/index.html</loc>
<priority>0.80</priority>
</url>
<url>
<loc>`+msg.baseurl+`/blog.html</loc>
<priority>0.80</priority>
</url>`
msg.posts.forEach(postxml);
xml = xml + `
</urlset>`
msg.dist.sitemap = xml;
return msg;
function postxml(item) {
if (item.published == true ) {
xml = xml + `
<url>
<loc>`+msg.baseurl+`/posts/`+item.distFilename+`.html</loc>
<priority>0.64</priority>
</url>`
}
}
, wie wir in Bezug auf diemsg.posts
Datei.
Letzter Schritt ist, alle Dateien und jede Post-Datei zu erstellen, basierend auf dem Objektnamen (Schlüssel) durch eine Schleife, ausmsg.dist.posts
.
msg.dist.posts = {}; var alength = msg.posts.length; var data = ""; var ogmetalang = ""; var ogmeta = ""; var postdate = ""; for (var i=0; i<alength; i++) { if (msg.posts[i].language == "de") { ogmetalang = "de_DE" } else { ogmetalang = "en_US" } img = msg.posts[i].imgurl.split('.')[0]+".webp"; ogmeta = ` <meta property="og:type" content="website"> <meta property="og:locale" content="`+ogmetalang+`"> <meta property="og:site_name" content="niklas-stephan.de"> <link rel="canonical" href="`+msg.baseurl+`/posts/`+msg.posts[i].language+`/`+msg.posts[i].distFilename+`"> <meta name="description" content="`+msg.posts[i].excerpt+`"> <meta property="og:title" content="`+msg.posts[i].title+`"> <meta property="og:description" content="`+msg.posts[i].excerpt+`"> <meta property="og:url" content="`+msg.baseurl+`/posts/`+msg.posts[i].language+`/`+msg.posts[i].distFilename+`"> <meta property="og:image" content="`+msg.baseurl+`/media/full/`+img+`"> <meta property="og:image:secure_url" content="`+msg.baseurl+`/media/full/`+img+`"> <meta name="twitter:card" content="summary"> <meta name="twitter:description" content="`+msg.posts[i].excerpt+`"> <meta name="twitter:title" content="`+msg.posts[i].title+`"> <meta name="twitter:image" content="`+msg.baseurl+`/media/full/`+img+`">` postdate = '<small class="c-gray pb-3" id="postdate">'+msg.posts[i].date+'</small>'; data = ""; data = msg.templates["post.html"]; data = data.replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]); data = data.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]); data = data.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]); data = data.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]); data = data.replace("<!-- mardown content from posts -->",msg.posts[i].distContent); data = data.replace("<!-- Post Headline -->", msg.posts[i].title); data = data.replace("<!-- postdate -->", postdate); data = data.replace("<!-- Post Image -->", '<img src="/media/thumb/'+img+'" class="img-fluid mb-2" alt="postImage">'); data = data.replace("<!-- PAGE TITLE -->",msg.posts[i].title); data = data.replace("<!-- meta tags -->",ogmeta); msg.dist.posts[msg.posts[i].distFilename] = data; } return msg;
Ein bisschen Komplexität ist auch in Bezug auf unsere Mediendateien, die meist für Beiträge verwendet werden. Ich schrieb einen kleinen Medienmanager, der die Quelldatei in mehreren Ausgabeformaten konvertiert. Dies geschieht durch Erstellen einer Miniaturansicht, ein Platzsparenmsg.dist.posts = {};
var alength = msg.posts.length;
var data = "";
var ogmetalang = "";
var ogmeta = "";
var postdate = "";
for (var i=0; i<alength; i++) {
if (msg.posts[i].language == "de") {
ogmetalang = "de_DE"
} else {
ogmetalang = "en_US"
}
img = msg.posts[i].imgurl.split('.')[0]+".webp";
ogmeta = `
<meta property="og:type" content="website">
<meta property="og:locale" content="`+ogmetalang+`">
<meta property="og:site_name" content="niklas-stephan.de">
<link rel="canonical" href="`+msg.baseurl+`/posts/`+msg.posts[i].language+`/`+msg.posts[i].distFilename+`">
<meta name="description" content="`+msg.posts[i].excerpt+`">
<meta property="og:title" content="`+msg.posts[i].title+`">
<meta property="og:description" content="`+msg.posts[i].excerpt+`">
<meta property="og:url" content="`+msg.baseurl+`/posts/`+msg.posts[i].language+`/`+msg.posts[i].distFilename+`">
<meta property="og:image" content="`+msg.baseurl+`/media/full/`+img+`">
<meta property="og:image:secure_url" content="`+msg.baseurl+`/media/full/`+img+`">
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="`+msg.posts[i].excerpt+`">
<meta name="twitter:title" content="`+msg.posts[i].title+`">
<meta name="twitter:image" content="`+msg.baseurl+`/media/full/`+img+`">`
postdate = '<small class="c-gray pb-3" id="postdate">'+msg.posts[i].date+'</small>';
data = "";
data = msg.templates["post.html"];
data = data.replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]);
data = data.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]);
data = data.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]);
data = data.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]);
data = data.replace("<!-- mardown content from posts -->",msg.posts[i].distContent);
data = data.replace("<!-- Post Headline -->", msg.posts[i].title);
data = data.replace("<!-- postdate -->", postdate);
data = data.replace("<!-- Post Image -->", '<img src="/media/thumb/'+img+'" class="img-fluid mb-2" alt="postImage">');
data = data.replace("<!-- PAGE TITLE -->",msg.posts[i].title);
data = data.replace("<!-- meta tags -->",ogmeta);
msg.dist.posts[msg.posts[i].distFilename] = data;
}
return msg;
Datei und eine Kopie der Originaldatei vonmsg.dist.blog
bis//get categories from all posts and extract unique ones
var categories = [];
for (var i=0 ; i<msg.posts.length;i++) {
if (msg.posts[i].published == true) {
for (var j = 0; j < msg.posts[i].keywords.length; j++) {
categories.push(msg.posts[i].keywords[j]);
}
}
}
var uniqueCategories = [...new Set(categories)];
// define a color to stick for each category
const colorcat = {};
var catcolors = ["green", "red", "blue", "orange", "yellow", "pink", "purple","indigo"];
var c=0;
for (const key of uniqueCategories) {
colorcat[key] = catcolors[c];
c = c+1;
}
//generate and set html for categorie selection
var cathtml = "";
for (var k = 0; k < uniqueCategories.length; k++) {
cathtml = cathtml + `<button data-filter=".cat-`+uniqueCategories[k]+`" type="button"
onclick="sort()" class="btn bg-` + catcolors[k] + ` c-white me-2">` + uniqueCategories[k]+`</button>`;
}
msg.dist.blog = msg.templates["blog.html"].replace("<!-- CATEGORIES -->", cathtml);
// get card content from all posts and generate html
var posthtml = "";
for (var l = 0; l < msg.posts.length; l++) {
if (msg.posts[l].published == true) {
var link = msg.posts[l].filename.slice(0, -3)+".html";
//get color for current post
var postcolor = "";
for (const key in colorcat) {
if (key == msg.posts[l].keywords[0]) {
postcolor = colorcat[key];
}
}
imgurl = msg.posts[l].imgurl.split('.')[0]+".webp";
posthtml = posthtml + `
<div class="col-sm-6 col-lg-4 my-4 filterDiv cat-`+msg.posts[l].keywords[0]+` lang-`+ msg.posts[l].language + `">
<span class="date hidden d-none">`+ msg.posts[l].date + `</span>
<span class="name hidden d-none">`+ msg.posts[l].title + `</span>
<div onclick="goto('`+ link+ `','blog')" class="card h-100 d-flex align-items-center bo-`+postcolor+`">
<div class="card-header bg-`+ postcolor + `">` + msg.posts[l].keywords[0] + `</div>
<div class="card-img-wrapper d-flex align-items-center">
<img src="media/thumb/`+ imgurl + `"
class="card-img-top" alt="iot">
</div>
<div class="card-body">
<h5 class="card-title">`+ msg.posts[l].title + `</h5>
<p class="card-text">
`+ msg.posts[l].excerpt + `
</p>
</div>
<div class="card-footer small text-center c-gray pdate">
`+msg.posts[l].date+`
</div>
</div>
</div>
`;
}
}
msg.dist.blog = msg.dist.blog.replace("<!-- POSTS -->", posthtml);
var ogmetalang = "de_DE";
var ogmeta = `
<meta property="og:type" content="website">
<meta property="og:locale" content="`+ogmetalang+`">
<meta property="og:site_name" content="niklas-stephan.de">
<link rel="canonical" href="`+msg.baseurl+`/blog.html">
<meta name="description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta property="og:title" content="Projects & Blog - niklas-stephan.de">
<meta property="og:description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta property="og:url" content="`+msg.baseurl+`/blog.html">
<meta property="og:image" content="`+msg.baseurl+`/assets/img/me_logo.webp">
<meta property="og:image:secure_url" content="`+msg.baseurl+`/assets/img/me_logo.webp">
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta name="twitter:title" content="Projects & Blog - niklas-stephan.de">
<meta name="twitter:image" content="`+msg.baseurl+`/assets/img/me_logo.webp">`
msg.dist.blog = msg.dist.blog.replace("<!-- meta tags -->",ogmeta);
msg.dist.blog = msg.dist.blog.replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]);
msg.dist.blog = msg.dist.blog.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]);
msg.dist.blog = msg.dist.blog.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]);
msg.dist.blog = msg.dist.blog.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]);
msg.dist.blog = msg.dist.blog.replace("<!-- PAGE TITLE -->","Projekte & Blog");
return msg;
,//get categories from all posts and extract unique ones
var categories = [];
for (var i=0 ; i<msg.posts.length;i++) {
if (msg.posts[i].published == true) {
for (var j = 0; j < msg.posts[i].keywords.length; j++) {
categories.push(msg.posts[i].keywords[j]);
}
}
}
var uniqueCategories = [...new Set(categories)];
// define a color to stick for each category
const colorcat = {};
var catcolors = ["green", "red", "blue", "orange", "yellow", "pink", "purple","indigo"];
var c=0;
for (const key of uniqueCategories) {
colorcat[key] = catcolors[c];
c = c+1;
}
//generate and set html for categorie selection
var cathtml = "";
for (var k = 0; k < uniqueCategories.length; k++) {
cathtml = cathtml + `<button data-filter=".cat-`+uniqueCategories[k]+`" type="button"
onclick="sort()" class="btn bg-` + catcolors[k] + ` c-white me-2">` + uniqueCategories[k]+`</button>`;
}
msg.dist.blog = msg.templates["blog.html"].replace("<!-- CATEGORIES -->", cathtml);
// get card content from all posts and generate html
var posthtml = "";
for (var l = 0; l < msg.posts.length; l++) {
if (msg.posts[l].published == true) {
var link = msg.posts[l].filename.slice(0, -3)+".html";
//get color for current post
var postcolor = "";
for (const key in colorcat) {
if (key == msg.posts[l].keywords[0]) {
postcolor = colorcat[key];
}
}
imgurl = msg.posts[l].imgurl.split('.')[0]+".webp";
posthtml = posthtml + `
<div class="col-sm-6 col-lg-4 my-4 filterDiv cat-`+msg.posts[l].keywords[0]+` lang-`+ msg.posts[l].language + `">
<span class="date hidden d-none">`+ msg.posts[l].date + `</span>
<span class="name hidden d-none">`+ msg.posts[l].title + `</span>
<div onclick="goto('`+ link+ `','blog')" class="card h-100 d-flex align-items-center bo-`+postcolor+`">
<div class="card-header bg-`+ postcolor + `">` + msg.posts[l].keywords[0] + `</div>
<div class="card-img-wrapper d-flex align-items-center">
<img src="media/thumb/`+ imgurl + `"
class="card-img-top" alt="iot">
</div>
<div class="card-body">
<h5 class="card-title">`+ msg.posts[l].title + `</h5>
<p class="card-text">
`+ msg.posts[l].excerpt + `
</p>
</div>
<div class="card-footer small text-center c-gray pdate">
`+msg.posts[l].date+`
</div>
</div>
</div>
`;
}
}
msg.dist.blog = msg.dist.blog.replace("<!-- POSTS -->", posthtml);
var ogmetalang = "de_DE";
var ogmeta = `
<meta property="og:type" content="website">
<meta property="og:locale" content="`+ogmetalang+`">
<meta property="og:site_name" content="niklas-stephan.de">
<link rel="canonical" href="`+msg.baseurl+`/blog.html">
<meta name="description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta property="og:title" content="Projects & Blog - niklas-stephan.de">
<meta property="og:description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta property="og:url" content="`+msg.baseurl+`/blog.html">
<meta property="og:image" content="`+msg.baseurl+`/assets/img/me_logo.webp">
<meta property="og:image:secure_url" content="`+msg.baseurl+`/assets/img/me_logo.webp">
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="Projekte und Posts aus der Welt von IoT, Musik und mehr">
<meta name="twitter:title" content="Projects & Blog - niklas-stephan.de">
<meta name="twitter:image" content="`+msg.baseurl+`/assets/img/me_logo.webp">`
msg.dist.blog = msg.dist.blog.replace("<!-- meta tags -->",ogmeta);
msg.dist.blog = msg.dist.blog.replace("<!-- html head from head.html snipppet -->",msg.snippets["head.html"]);
msg.dist.blog = msg.dist.blog.replace("<!-- Top Navigation from navbar.html snipppet -->",msg.snippets["navbar.html"]);
msg.dist.blog = msg.dist.blog.replace("<!-- footer navigation from footer.html snipppet -->",msg.snippets["footer.html"]);
msg.dist.blog = msg.dist.blog.replace("<!-- Javascript from script.html snipppet -->",msg.snippets["script.html"]);
msg.dist.blog = msg.dist.blog.replace("<!-- PAGE TITLE -->","Projekte & Blog");
return msg;
,msg.dist
. Das geschieht für alle Dateien des Typs “.gif”, “.jpg”, “.jpeg” und “.webp”. Alle anderen Dateien werden nur kopiert ohne Thumbnail-Generation oder Konvertierung.
Dazu verwenden wir npm Modulefs-extra
undfse
.
Zusätzlich zu erwähnen ist, dass wir nicht verwenden/dist
da die Datei-Erstellung einige Sekunden dauern kann, was zu einer längeren Depyloymentzeit führen würde. Stattdessen laufen diese Operationen im Hintergrund und weiter nach Deplyoment ist "offiziell" beendet. Das ist ein bisschen knifflig und vielleicht wird in der regelmäßigen Nutzung von nCMS geändert werden.
/src/assets/
Wir sind fast fertig. Am "finsih"-Knoten berechnen wir einfach die Dauer jeder Bereitstellung und erstellen einen Zeitstempel.
Beide Informationen werden in die Datei geschrieben/dist/assets/
, die vom Frontend aufgerufen wird, um seine Daten im Konsolenlogbuch des Besucherbrowsers anzuzeigen.
Je nachdem, ob die Bereitstellung manuell oder per Webhook angefordert wird, geben wir die Ergebnisse an beide oder nur einen der folgenden Knoten aus.
robots.txt
Endlich sind wir fertig. Beide Knoten “msg” und “http” existieren nur, um ein sauberes Ende unserer Bereitstellung zu haben.
Mit dem „http out“ werden die Bereitstellungsinformationen als Feedback bereitgestellt, wenn die Bereitstellung über Webhook aufgerufen wurde.
Während der "msg" Debugknoten zeigt uns die Ausgabe des msg-Objekts unseres Flusses.
Hier können Sie den kompletten und bereits etwas korrigierten/modifizierten Node-Red-Flow herunterladen:https://niklas-stephan.de/media/orig/ncms/flow.json(Version 0.60).
Ich muss zugeben, dass das ganze Projekt etwas zeitraubend war. Aber von Anfang bis Ende wurde ich von dem Gedanken "nur diese eine noch kleine Sache" angetrieben und aufgeregt. Additonally hatte ich viel Spaß mit Node-Red zu erstellen und zu optimieren in mehr und mehr erweiterte Funktionen. Der integrierte Debugger und die schöne GUI sind äußerst hilfreich, um Fehler zu finden und jederzeit den Überblick zu behalten.
Die, zumindest für mich, neue Funktion, um zusätzliche node.js Module in meinen eigenen Funktionen einfach zu nutzen, machte den Trick für mich. Ansonsten wäre ich nicht in der Lage gewesen, das gleiche Ergebnis zu liefern oder zumindest nicht, dass fortgeschritten und gerade nach vorne, wie es ist, jetzt.
Wenn Sie jeden Plan ähnlich zu tun, sind Sie willkommen, mein Projekt zu verlassen oder einige Ideen zu fangen.
Sicher ist nCMS noch nicht perfekt.
Zum Beispiel machen alle Verweise auf das Frontend innerhalb meiner Funktionen es schwer, für andere zu lesen und zu verstehen.
Eigentlich der Fronted-Code und seine Funktionen, css und html wurde in meinem Post überhaupt nicht erwähnt. Vielleicht kann ich das noch mal machen.
Hier eine Zusammenfassung der meisten Quellen, Links und Dateien, die ich in meinem Beitrag erwähnte: