Zum Inhalt

🥑 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:

  1. Dateien einlesendata.json und index.html mit fs.readFileSync()
  2. Platzhalter ersetzen – mit .replace() werden {%NAME%}, {%IMAGE%} und {%PRICE%} durch echte Werte ersetzt
  3. 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:

./start.sh

Stoppen:

./stop.sh

Zugriff im lokalen Netzwerk:

http://10.27.160.12:4008

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:

alt text

4008/api:

alt text

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:

bash start.sh

Öffnen Sie anschliessend die folgenden URLs im Browser oder mit curl:

Alle Produkte abrufen

GET http://localhost:4008/api

Erwartete Antwort:

[
  { "id": 0, "productName": "Fresh Avocados", ... },
  { "id": 1, "productName": "Goat and Sheep Cheese", ... }
]


Produkt mit id=0 abrufen

GET http://localhost:4008/api?id=0

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

GET http://localhost:4008/api?id=1

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)

GET http://localhost:4008/api?id=99

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=N kann 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 404 zurückgeben wenn query.id gesetzt, aber ungültig
  • Suche nach productName statt id ermöglichen (?name=avocado)
  • Weitere Filter wie ?organic=true unterstützen

Ergebnisse als Bild:

4008/api?id=0:

alt text