🥑 Server-Side HTML Templating¶
Dieses Projekt demonstriert, wie man mit Node.js Daten aus einer JSON-Datei ausliest und dynamisch in ein HTML-Template injiziert, bevor die Seite an den Client ausgeliefert wird.
Info
Erstellung eines HTML-Templates, das Produktdaten (Name, Bild, Preis) aus einer externen data.json Datei lädt und anzeigt.
Der Server läuft auf einem Raspberry Pi und ist über das lokale Netzwerk erreichbar.
📂 Dateistruktur¶
projekt/
├── data.json ← Produktdaten (Rohdaten)
├── index.html ← HTML-Template mit Platzhaltern
└── server.js ← Node.js Server (Logik)
| Datei | Aufgabe |
|---|---|
data.json |
Enthält die Produktdaten als JSON-Array |
index.html |
HTML-Gerüst mit {%PLATZHALTER%} |
server.js |
Liest Dateien, ersetzt Platzhalter, sendet HTML |
1. Die Daten – data.json¶
Hier liegen die Rohdaten der Produkte als Array von Objekten.
Jedes Objekt beschreibt ein Produkt mit allen relevanten Feldern.
[
{
"id": 0,
"productName": "Fresh Avocados",
"image": "🥑",
"from": "Spain",
"nutrients": "Vitamin B, Vitamin K",
"quantity": "4 🥑",
"price": "6.50",
"organic": true,
"description": "A ripe avocado yields to gentle pressure when held in the palm of the hand and squeezed. The fruit is not sweet, but distinctly and subtly flavored, with smooth texture."
},
{
"id": 1,
"productName": "Goat and Sheep Cheese",
"image": "🧀",
"from": "Portugal",
"nutrients": "Vitamin A, Calcium",
"quantity": "250g",
"price": "5.00",
"organic": false,
"description": "Creamy and distinct in flavor, goat cheese is a dairy product enjoyed around the world."
}
]
Warum JSON?
JSON (JavaScript Object Notation) ist ein leichtgewichtiges, menschenlesbares Datenformat.
Node.js kann es mit JSON.parse() direkt in ein JavaScript-Objekt umwandeln.
2. Das Template – index.html¶
Das HTML-Gerüst enthält keine festen Daten, sondern Platzhalter in der Syntax {%PLATZHALTER%}.
Diese werden vom Server gesucht und durch echte Werte ersetzt, bevor das HTML an den Browser gesendet wird.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Produktansicht</title>
</head>
<body>
<h1>Unser Produkt</h1>
<div class="product-card">
<!-- Wird ersetzt durch: dataObj[0].image -->
<span class="product-image">{%IMAGE%}</span>
<!-- Wird ersetzt durch: dataObj[0].productName -->
<h2 class="product-name">{%NAME%}</h2>
<!-- Wird ersetzt durch: dataObj[0].price -->
<p class="product-price">
Preis: <strong>{%PRICE%} CHF</strong>
</p>
</div>
<!--
KEIN <script> nötig!
Der Server übernimmt die Logik – der Browser zeigt nur fertiges HTML.
-->
</body>
</html>
Wichtig: Kein JavaScript im Browser
Bei Server-Side Templating läuft die gesamte Logik auf dem Server.
Der Browser empfängt nur das fertig befüllte HTML – die Platzhalter {%...%} sind nie sichtbar.
3. Die Server-Logik – server.js¶
Der Node.js-Server übernimmt drei Aufgaben:
- Dateien einlesen –
data.jsonundindex.htmlmitfs.readFileSync() - Platzhalter ersetzen – mit
.replace()werden{%NAME%},{%IMAGE%}und{%PRICE%}durch echte Werte ersetzt - Fertiges HTML senden – der Browser erhält die vollständige Seite
const http = require('http');
const fs = require('fs');
const port = 4008;
// 1. Dateien synchron einlesen (einmalig beim Serverstart)
const data = fs.readFileSync('data.json', 'utf-8');
const template = fs.readFileSync('index.html', 'utf-8');
const dataObj = JSON.parse(data);
const server = http.createServer((req, res) => {
const pathName = req.url;
// Home-Route: Template befüllen und senden
if (pathName === '/' || pathName === '/fish') {
res.writeHead(200, { 'Content-type': 'text/html; charset=utf-8' });
// 2. Platzhalter ersetzen – Regex /g ersetzt ALLE Vorkommen
let output = template.replace(/{%NAME%}/g, dataObj[0].productName);
output = output.replace(/{%IMAGE%}/g, dataObj[0].image);
output = output.replace(/{%PRICE%}/g, dataObj[0].price);
// 3. Fertiges HTML an den Browser senden
res.end(output);
}
// API-Route: Rohdaten als JSON zurückgeben
else if (pathName === '/api') {
res.writeHead(200, { 'Content-type': 'application/json; charset=utf-8' });
res.end(data);
}
// Weitere Routen
else if (pathName === '/ueberblick') {
res.writeHead(200, { 'Content-type': 'text/html; charset=utf-8' });
res.end('<h1>Ueberblick</h1>');
}
else if (pathName === '/produkte') {
res.writeHead(200, { 'Content-type': 'text/html; charset=utf-8' });
res.end('<h2>Hier sehen Sie unsere Produkte</h2>');
}
// 404-Fehlerseite
else {
res.writeHead(404, { 'Content-type': 'text/html; charset=utf-8' });
res.end('<h1>Seite nicht gefunden!</h1>');
}
});
// 0.0.0.0 = Server hört auf ALLEN Netzwerkschnittstellen
// → Erreichbar von anderen Geräten im lokalen Netzwerk
server.listen(port, '0.0.0.0', () => {
console.log('Server läuft auf Port ' + port);
});
Warum 0.0.0.0 statt localhost?
localhost (= 127.0.0.1) erlaubt nur Zugriffe vom Raspberry Pi selbst.
0.0.0.0 öffnet den Server für alle Netzwerkschnittstellen – dadurch ist er von anderen Geräten im Netzwerk erreichbar.
Vergleich: Client-Side vs. Server-Side Templating¶
Client-Side (fetch) |
Server-Side (dieses Projekt) | |
|---|---|---|
| Logik läuft in | Browser (index.js) |
Node.js Server (server.js) |
| HTML enthält | <template> + data-field |
{%NAME%} Platzhalter |
index.js nötig? |
✅ Ja | ❌ Nein |
| Browser sieht | Platzhalter → JS füllt aus | Fertig befülltes HTML |
| Daten sichtbar im Browser? | Ja (via DevTools / Network) | Nein, nur fertiges HTML |
Deployment auf dem Raspberry Pi¶
Die Anwendung wird über ein Shell-Skript gestartet.
Starten:
Stoppen:
Zugriff im lokalen Netzwerk:
Verfügbare Routen¶
| Route | Beschreibung |
|---|---|
/ |
Startseite mit Produkt-Template |
/api |
Alle Produkte als rohe JSON-Daten |
/ueberblick |
Einfache Übersichtsseite |
/produkte |
Produktliste (plain HTML) |
/* |
404 – Seite nicht gefunden |
Success
Beim Aufruf von http://10.27.160.12:4008/ sieht der Nutzer die fertige HTML-Karte
mit den Daten des ersten Produkts – ohne jemals die rohe JSON-Datei
oder die {%...%} Platzhalter zu sehen.
Bild ergebniss auf webseite:¶
4008:¶
4008/api:¶
3.4 API-Abfrage per URL-Parameter¶
Lernziel
Sie können Variablen aus der URL parsen und gezielt einzelne Objekte aus einem JSON-Array zurückgeben.
Übersicht¶
In dieser Aufgabe wird die bestehende Fish API so erweitert, dass über einen Query-Parameter (?id=0, ?id=1, …) gezielt ein einzelnes Produkt aus dem data.json-Array abgefragt werden kann.
| Route | Beschreibung |
|---|---|
/api |
Gibt alle Produkte zurück |
/api?id=0 |
Gibt nur das Produkt mit id = 0 zurück |
/api?id=1 |
Gibt nur das Produkt mit id = 1 zurück |
Ausgangslage – data.json¶
Das Array in data.json enthält mehrere Produkte, die jeweils über eine id angesprochen werden können:
[
{
"id": 0,
"productName": "Fresh Avocados",
"image": "🥑",
"from": "Spain",
"nutrients": "Vitamin B, Vitamin K",
"quantity": "4 🥑",
"price": "6.50",
"organic": true,
"description": "A ripe avocado yields to gentle pressure..."
},
{
"id": 1,
"productName": "Goat and Sheep Cheese",
"image": "🧀",
"from": "Portugal",
"nutrients": "Vitamin A, Calcium",
"quantity": "250g",
"price": "5.00",
"organic": false,
"description": "Creamy and distinct in flavor..."
}
]
Implementierung – server.js¶
Erklärung: url.parse() mit Query-Parametern¶
Node.js stellt das eingebaute Modul url bereit. Mit url.parse(req.url, true) wird die URL in ihre Bestandteile zerlegt. Das zweite Argument true sorgt dafür, dass Query-Parameter automatisch als Objekt (query) geliefert werden.
// Beispiel:
// Anfrage: GET /api?id=1
// url.parse liefert:
// pathname: '/api'
// query: { id: '1' }
Wichtig: Typ-Beachtung
Query-Parameter sind immer Strings ('0', '1', …).
Der Zugriff auf das Array mit dataObj[query.id] funktioniert trotzdem,
da JavaScript Arrays mit String-Indizes auflösen kann.
Vollständiger Code der /api-Route¶
const http = require('http');
const fs = require('fs');
const url = require('url'); // (1) URL-Modul einbinden
const port = 4008;
// (2) Daten einmalig beim Start einlesen
const data = fs.readFileSync('data.json', 'utf-8');
const dataObj = JSON.parse(data);
const server = http.createServer((req, res) => {
// (3) URL und Query-Parameter parsen
const { query, pathname } = url.parse(req.url, true);
// --- API Route ---
if (pathname === '/api') {
res.writeHead(200, { 'Content-type': 'application/json; charset=utf-8' });
// (4) Prüfen: Gibt es eine gültige id in der URL?
if (query.id !== undefined && dataObj[query.id]) {
// Einzelnes Produkt zurückgeben
const singleProduct = dataObj[query.id];
res.end(JSON.stringify(singleProduct));
} else {
// Kein Parameter → alle Produkte zurückgeben
res.end(data);
}
}
// --- 404 Fehler ---
else {
res.writeHead(404, { 'Content-type': 'text/html; charset=utf-8' });
res.end('<h1>Seite nicht gefunden!</h1>');
}
});
server.listen(port, '0.0.0.0', () => {
console.log(`Server läuft auf Port ${port}`);
});
Schritt-für-Schritt-Erklärung¶
(1) url-Modul → ermöglicht das Parsen der Anfrage-URL
(2) readFileSync → liest data.json einmal beim Start in den Arbeitsspeicher
(3) url.parse(req.url, true) → zerlegt z. B. "/api?id=1"
in pathname="/api" und query={id:'1'}
(4) query.id !== undefined → prüft ob überhaupt ein id-Parameter vorhanden ist
dataObj[query.id] → greift auf das Array-Element zu (falsy wenn nicht vorhanden)
Testen der Endpunkte¶
Starten Sie den Container mit:
Öffnen Sie anschliessend die folgenden URLs im Browser oder mit curl:
Alle Produkte abrufen¶
Erwartete Antwort:
[
{ "id": 0, "productName": "Fresh Avocados", ... },
{ "id": 1, "productName": "Goat and Sheep Cheese", ... }
]
Produkt mit id=0 abrufen¶
Erwartete Antwort:
{
"id": 0,
"productName": "Fresh Avocados",
"image": "🥑",
"from": "Spain",
"nutrients": "Vitamin B, Vitamin K",
"quantity": "4 🥑",
"price": "6.50",
"organic": true,
"description": "A ripe avocado yields to gentle pressure..."
}
Produkt mit id=1 abrufen¶
Erwartete Antwort:
{
"id": 1,
"productName": "Goat and Sheep Cheese",
"image": "🧀",
"from": "Portugal",
"nutrients": "Vitamin A, Calcium",
"quantity": "250g",
"price": "5.00",
"organic": false,
"description": "Creamy and distinct in flavor..."
}
Ungültige ID (Fehlerverhalten)¶
Erwartete Antwort (da dataObj[99] nicht existiert → Fallback auf alle):
[
{ "id": 0, "productName": "Fresh Avocados", ... },
{ "id": 1, "productName": "Goat and Sheep Cheese", ... }
]
Ablaufdiagramm¶
Client sendet GET /api?id=1
│
▼
url.parse(req.url, true)
│
├── pathname = '/api'
└── query = { id: '1' }
│
▼
Ist query.id vorhanden?
│
┌────────┴────────┐
JA NEIN
│ │
▼ ▼
dataObj[query.id] res.end(data)
existiert? → alle Produkte
│
┌────┴────┐
JA NEIN
│ │
▼ ▼
einzelnes res.end(data)
Produkt → alle Produkte
zurückgeben
Fazit & Lernerkenntnisse¶
Erreichte Ziele
- ✅ Das
url-Modul wird verwendet, um Query-Parameter aus der URL zu lesen - ✅ Mit
?id=Nkann ein spezifisches Produkt aus dem JSON-Array abgerufen werden - ✅ Ohne Parameter werden alle Produkte zurückgegeben (Fallback-Logik)
- ✅ Die Lösung funktioniert für beliebig viele Einträge in
data.json
Erweiterungsmöglichkeiten
- Fehler-Antwort mit
404zurückgeben wennquery.idgesetzt, aber ungültig - Suche nach
productNamestattidermöglichen (?name=avocado) - Weitere Filter wie
?organic=trueunterstützen


