Files
qrcode-generator/README.md
T
Florian Wilde 7f7cb6fa20 Phase A: acht weitere QR-Typen
E-Mail, SMS, WhatsApp, WLAN, Standort, vCard, Zahlung und App-Link sind
umgesetzt. Damit sind 9 der 15 Typen nutzbar.

Erweiterungen am Typ-Vertrag, die dafuer noetig waren:

- supportsStatic: nicht jeder Typ kann statisch. Beim App-Link steht das
  Ziel erst beim Scannen fest, also gibt es ihn nur dynamisch.
- RedirectContext: die Weiterleitung reicht den User-Agent durch, damit der
  App-Link iOS, Android und alles Uebrige unterscheiden kann.
- assertDynamicPossible(): supportsDynamic ist eine Eigenschaft des Typs,
  aber nicht jede Eingabe taugt dafuer. Ein GiroCode ist kein Web-Ziel,
  obwohl der Typ 'Zahlung' als Zahlungslink sehr wohl dynamisch sein kann.
  Ohne diese Probe liess sich ein Code speichern, der erst beim Scannen
  aufgefallen waere - also nach dem Drucken. Greift beim Anlegen und beim
  Bearbeiten.

Sorgfalt bei den Formaten, gesammelt in src/lib/qr/encoding.ts:

- WLAN maskiert ; , : \\ und setzt rein hexadezimale Werte in
  Anfuehrungszeichen, sonst deuten Geraete sie als Hexzahl.
- vCard 3.0 mit CRLF, Maskierung und Faltung nach 75 Oktetten, oktettweise
  gezaehlt, damit kein mehrbytiges Zeichen zerrissen wird.
- GiroCode nach EPC069-12 mit fester Feldreihenfolge, IBAN-Pruefsumme nach
  Modulo 97 und der 331-Byte-Grenze.
- Rufnummern werden nach E.164 normalisiert, '(0)' faellt weg.
- Standort nimmt Koordinaten auch aus einem Google-Maps- oder Apple-Link an.

Formular-Bausteine (Select, Textarea, Checkbox, FieldRow, FieldSet) fuer die
mehrfeldrigen Typen ergaenzt.

Geprueft: 225 Pruefungen, davon 80 neu - Dekodierung je Typ aus dem fertigen
PNG, Validierung, Betriebsarten, geraeteabhaengige Weiterleitung und die
Formulare im Browser.
2026-08-14 11:30:09 +02:00

18 KiB

QR-Code-Generator

Eine Webanwendung zum Erzeugen von QR-Codes — als Docker-Compose-Stack aus zwei Containern: der Next.js-Anwendung und einer PostgreSQL-Datenbank.

Umgesetzt sind 9 der 15 QR-Typen. Die übrigen sechs sind in der Oberfläche sichtbar, als Platzhalter markiert und lassen sich über die Verwaltung ausblenden.


Inhalt


Schnellstart

Voraussetzung: Docker mit Docker Compose.

cp .env.example .env      # anschließend POSTGRES_PASSWORD und BASE_URL anpassen
docker compose up -d --build

Danach ist die Anwendung unter http://localhost:3000 erreichbar. Die Datenbankmigrationen laufen beim Start des App-Containers automatisch — es sind keine weiteren Schritte nötig.

Nützliche Befehle:

docker compose logs -f app     # Protokoll der Anwendung
docker compose ps              # Status beider Container
docker compose down            # Stack stoppen (Daten bleiben erhalten)
docker compose down -v         # Stack stoppen und Datenbank löschen

Aktualisieren

Für neue Versionen liegt ein Update-Skript bei — einmal für Linux/macOS und einmal für Windows. Es holt den aktuellen Stand aus Git, baut den Stack neu und wartet, bis die Anwendung wieder antwortet.

./update.sh            # Linux / macOS
.\update.ps1           # Windows

Beim ersten Aufruf unter Linux muss das Skript einmal ausführbar gemacht werden: chmod +x update.sh.

Was das Skript tut:

  1. prüft, ob git, docker compose und die .env vorhanden sind,
  2. bricht ab, wenn es lokale Änderungen gibt — sonst würde git pull im Konflikt enden,
  3. holt den neuen Stand mit git merge --ff-only und zeigt die neuen Commits,
  4. baut den Stack nur neu, wenn es tatsächlich Änderungen gab,
  5. wartet, bis die Anwendung auf APP_PORT antwortet, und zeigt sonst das Protokoll,
  6. gibt die Migrationszeilen aus dem Start-Protokoll und den Containerstatus aus.

Optionen:

Option Wirkung
--force / -Force Auch ohne neue Commits neu bauen
--prune / -Prune Danach verwaiste Docker-Images entfernen (docker image prune)
--help Hilfe anzeigen

Die Datenbank bleibt beim Update erhalten — neue Migrationen wendet der App-Container beim Start automatisch an. Wer lieber von Hand aktualisiert:

git pull
docker compose up -d --build

Konfiguration

Alle Einstellungen stehen in der .env im Projektverzeichnis (Vorlage: .env.example).

Variable Bedeutung Standard
BASE_URL Öffentliche Adresse der App — landet in dynamischen QR-Codes http://localhost:3000
APP_PORT Host-Port, auf dem die App veröffentlicht wird 3000
DATABASE_URL Verbindungsstring der App zur Datenbank —
POSTGRES_USER Datenbankbenutzer qrcode
POSTGRES_PASSWORD Datenbankpasswort (muss gesetzt sein) —
POSTGRES_DB Name der Datenbank qrcode

Wichtig: BASE_URL richtig setzen

Ein dynamischer QR-Code enthält nicht das eigentliche Ziel, sondern die Kurz-URL {BASE_URL}/r/{slug}. Diese Adresse muss von den Geräten erreichbar sein, die den Code später scannen.

  • Nur am selben Rechner testen → http://localhost:3000 genügt.
  • Im LAN mit dem Handy scannen → die LAN-Adresse des Servers eintragen, z. B. http://192.168.1.50:3000.

Wird BASE_URL nachträglich geändert, zeigen bereits gedruckte dynamische Codes weiterhin auf die alte Adresse. Am besten also vor dem ersten produktiven Code festlegen.

Nach einer Änderung an der .env:

docker compose up -d

Ändern sich POSTGRES_*-Werte, muss DATABASE_URL entsprechend mitgezogen werden — der Hostname darin bleibt db (der Servicename aus docker-compose.yml).


Bedienung

1. Typ wählen

Die Startseite zeigt unter „Was möchtest du erstellen?“ ein Kachel-Grid der freigegebenen QR-Typen. Umgesetzte Typen sind anklickbar, die übrigen tragen das Badge „Bald verfügbar“.

Typ Kodiert als Statisch Dynamisch
Website die URL selbst ✓ ✓
E-Mail mailto: mit Betreff und Text ✓ ✓
SMS SMSTO: ✓ ✓
WhatsApp https://wa.me/… ✓ ✓
Standort geo: — dynamisch über OpenStreetMap ✓ ✓
vCard vCard 3.0 ✓ —
WLAN WIFI: ✓ —
App-Link Weiterleitung je nach Gerät — ✓
Zahlung GiroCode (EPC069-12) oder Zahlungslink ✓ nur Link

Warum manche Typen nur eine Betriebsart können:

  • vCard und WLAN werden vom Gerät direkt ausgewertet — die Kamera legt einen Kontakt an oder richtet das Netz ein. Über eine Kurz-URL würde stattdessen nur ein Browser aufgehen.
  • App-Link kann nicht statisch sein: welches Ziel richtig ist, steht erst beim Scannen fest. Die Weiterleitung wertet den User-Agent aus und schickt iOS in den App Store, Android zu Google Play und alles Übrige auf die Ausweichadresse.
  • GiroCode ist kein Web-Ziel, sondern ein Datensatz für Banking-Apps. Als dynamischer Code funktioniert daher nur die Variante „Zahlungslink“ — die Anwendung weist das beim Speichern ausdrücklich ab, statt einen Code entstehen zu lassen, der beim Scannen ins Leere führt.

Zum WLAN-Code: Das Passwort steht im Klartext im Code — wer ihn scannt oder abfotografiert, kennt es. Für Gästenetze ist das gewollt. Besteht ein Netzwerkname oder Passwort ausschließlich aus Hex-Ziffern (etwa abc123), setzt die Anwendung ihn wie in der Spezifikation vorgesehen in Anführungszeichen — sonst würden manche Geräte den Wert als Hexzahl deuten.

2. Statisch oder dynamisch

Statisch Dynamisch
Im Code kodiert die Ziel-URL selbst die Kurz-URL {BASE_URL}/r/{slug}
Ziel später änderbar nein ja, jederzeit
Braucht diesen Server nein ja — er leitet weiter
Scans werden gezählt nein ja
Wird gespeichert nein ja, in PostgreSQL

Ein dynamischer Code muss einmal gespeichert werden, denn erst dabei entsteht seine Kurz-URL. Bis dahin zeigt die Vorschau einen Platzhalter und der Download ist gesperrt — so kann kein Code entstehen, der ins Leere führt.

3. Gestalten

  • Farben: Vorder- und Hintergrund per Farbwähler oder als Hex-Wert. Fünf Voreinstellungen und ein Knopf zum Tauschen der Farben stehen bereit.
  • Kontrastwarnung: Die Anwendung berechnet das Kontrastverhältnis und warnt bei schlecht scanbaren Kombinationen — ebenso bei hellem Vordergrund auf dunklem Grund, den viele Scanner-Apps nicht erkennen.
  • Größe: 256 bis 2048 px (gilt für das PNG; das SVG skaliert verlustfrei).
  • Fehlerkorrektur: L, M (Standard), Q oder H. Höhere Level machen den Code robuster gegen Beschädigung, aber auch dichter.
  • Rand: Breite der Ruhezone in Modulen. Weniger als 4 kann das Scannen erschweren.

Die Vorschau wird bei jeder Änderung serverseitig neu erzeugt — sie zeigt also exakt das, was auch heruntergeladen wird.

4. Herunterladen

PNG für Druck und Office-Dokumente, SVG für verlustfreie Skalierung (Plakate, Schilder, Druckvorstufe).

5. Verwaltung: welche Typen erscheinen

Unter Verwaltung → QR-Typen lässt sich jeder der 15 Typen einzeln ein- und ausblenden. Ausgeblendete Typen verschwinden vollständig von der Startseite und lassen sich nicht mehr neu anlegen — praktisch, um die noch nicht umgesetzten Typen gar nicht erst zu zeigen.

Zwei Eigenschaften sind dabei zu unterscheiden:

Bedeutung Woher
umgesetzt Der Typ ist ausprogrammiert. In Version 1 gilt das nur für „Website“. Registry im Code
freigegeben Die Verwaltung zeigt den Typ an. Datenbank, jederzeit änderbar

Nur umgesetzt und freigegeben heißt benutzbar. Freigegeben, aber nicht umgesetzt, erscheint als Kachel „Bald verfügbar“.

Bereits gespeicherte Codes bleiben unberührt. Schaltet man einen Typ ab, leiten vorhandene dynamische Codes weiter wie bisher und lassen sich weiter bearbeiten. Ein gedruckter Code wird durch das Abschalten seines Typs also nicht ungültig — gesperrt ist nur das Anlegen neuer Codes.

Ohne Eintrag in der Datenbank gilt ein Typ als freigegeben. Eine frische Installation zeigt also alles, und Typen aus späteren Versionen erscheinen automatisch.

6. Meine Codes

Unter „Meine Codes“ stehen alle dynamischen Codes mit Vorschaubild, Kurz-URL, aktuellem Ziel, Scan-Anzahl, Erstellungsdatum und letztem Scan. Dort lässt sich

  • das Ziel ändern — die Kurz-URL und damit der gedruckte Code bleiben gültig,
  • der Code löschen (mit Bestätigungsdialog),
  • das Bild erneut als PNG oder SVG herunterladen.

Statische Codes erscheinen hier nicht, weil sie nicht gespeichert werden.

Version 1 ist für den Einzelplatz-/LAN-Betrieb gedacht und hat keine Benutzerverwaltung. Wer die Anwendung erreicht, sieht und ändert alle Codes. Die App gehört daher nicht ungeschützt ins offene Internet.


Technischer Aufbau

Stack

Baustein Verwendet
Framework Next.js 16 (App Router, TypeScript), Frontend + API
Oberfläche React 19, Tailwind CSS 4
Datenbank PostgreSQL 18
ORM Prisma 7 mit @prisma/adapter-pg
QR-Erzeugung qrcode, serverseitig, Ausgabe als PNG und SVG
Validierung Zod 4, im Browser und noch einmal auf dem Server

Der Stack läuft ohne Reverse Proxy: Der App-Container wird direkt per Port-Mapping veröffentlicht.

Verzeichnisse

prisma/
  schema.prisma            Datenmodell (Modell QrCode)
  migrations/              Versionierte Migrationen
docker/
  entrypoint.sh            Migration + Serverstart im Container
update.sh / update.ps1     Update-Skript (git pull + Neubau + Prüfung)
src/
  app/
    page.tsx               Startseite mit dem Kachel-Grid
    erstellen/[type]/      Generator je QR-Typ
    meine-codes/           Übersicht der dynamischen Codes
    r/[slug]/route.ts      Weiterleitung dynamischer Codes (302)
    api/
      qr/preview/          Live-Vorschau (SVG als Data-URL)
      qr/download/         Download als PNG oder SVG
      codes/               Anlegen und Auflisten dynamischer Codes
      codes/[id]/          Ändern und Löschen
      codes/[id]/qr/       Bild eines gespeicherten Codes
  components/
    qr-forms/              Formulare je QR-Typ + deren Registry
    qr-generator.tsx       Generator-Oberfläche
    codes-list.tsx         „Meine Codes“
    design-panel.tsx       Farben, Größe, Fehlerkorrektur, Rand
  lib/
    qr/
      type-module.ts       Vertrag eines QR-Typs (defineQrType)
      registry.ts          Registry aller 15 Typen
      types/website.ts     Der einzige aktive Typ in V1
      design.ts            Gestaltungsoptionen + Zod-Schema
      contrast.ts          Kontrastbewertung
      render.ts            PNG-/SVG-Erzeugung
      request.ts           Anfrage-Schema und Auflösung des Code-Inhalts
    db.ts                  Prisma-Client
    env.ts                 Laufzeit-Konfiguration

Datenmodell

Nur dynamische Codes werden gespeichert. Das Modell QrCode hält neben Slug, Zähler und Gestaltung ein generisches data-Feld (JSONB) sowie den Typ — so brauchen die Typen aus Version 2 keine Schemaänderung.

Wie ein QR-Typ aufgebaut ist

Jeder Typ ist ein Modul, das über defineQrType erzeugt wird und vier Dinge beisteuert: Metadaten (Titel, Beschreibung, Icon), ein Zod-Schema, die Kodierung des Inhalts und eine Kurzfassung für Listen.

export const websiteType = defineQrType({
  id: "website",
  title: "Website",
  description: "Verlinke eine beliebige Webadresse.",
  icon: "globe",
  active: true,
  supportsDynamic: true,
  schema: websiteDataSchema,
  defaultData: { url: "" },
  encode: (data) => data.url,
  summary: (data) => describe(data.url),
});

Innerhalb des Moduls arbeitet man mit dem echten, aus dem Zod-Schema abgeleiteten Datentyp; nach außen zeigt die Registry eine einheitliche Schnittstelle. Deshalb kennen Vorschau, Download, Speicherung und Weiterleitung keinen einzigen typspezifischen Sonderfall.

Warum 302 und kein 301?

Die Weiterleitung unter /r/{slug} antwortet mit 302 (temporär) und Cache-Control: no-store. Ein permanenter Redirect würde in Browsern hängen bleiben: Zieländerungen kämen nicht an und der Scan-Zähler bliebe stehen.


Entwicklung ohne Docker

Voraussetzung: Node.js 20.9+ und eine erreichbare PostgreSQL-Instanz.

npm install

# Datenbank z. B. als Einzelcontainer starten
docker run -d --name qr-db -p 5432:5432 \
  -e POSTGRES_USER=qrcode -e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=qrcode \
  postgres:18-alpine

# .env anlegen und DATABASE_URL auf localhost zeigen lassen:
#   DATABASE_URL=postgresql://qrcode:devpass@localhost:5432/qrcode?schema=public

npm run db:migrate        # Migrationen anwenden und Prisma Client erzeugen
npm run dev               # http://localhost:3000

Weitere Skripte:

npm run build             # Produktionsbuild
npm run typecheck         # TypeScript prüfen
npm run lint              # ESLint
npm run db:studio         # Prisma Studio (Datenbank ansehen)

Hinweis zu npm 11: Installationsskripte sind standardmäßig blockiert. Die Pakete, die welche ausführen dürfen (Prisma lädt darüber seine Engine-Binaries), stehen im Feld allowScripts der package.json.


API

Alle Endpunkte antworten im Fehlerfall mit { "error": "…" } und einer deutschen Meldung.

Methode Pfad Zweck
POST /api/qr/preview Vorschau als SVG-Data-URL
POST /api/qr/download PNG- oder SVG-Datei
GET /api/codes Alle dynamischen Codes
POST /api/codes Dynamischen Code anlegen
GET /api/codes/{id} Einzelnen Code lesen
PATCH /api/codes/{id} Ziel, Name oder Gestaltung ändern
DELETE /api/codes/{id} Code löschen
GET /api/codes/{id}/qr Bild des Codes (?format=png|svg&download=1)
GET /r/{slug} Weiterleitung, erhöht den Scan-Zähler
GET /api/admin/types Zustand aller QR-Typen
PATCH /api/admin/types Typ freigeben oder abschalten

Beispiel:

curl -X POST http://localhost:3000/api/codes \
  -H 'Content-Type: application/json' \
  -d '{
        "type": "website",
        "data": { "url": "https://example.com" },
        "design": { "fgColor": "#0F172A", "bgColor": "#FFFFFF",
                    "size": 512, "ecLevel": "M", "margin": 4 },
        "label": "Plakat Eingang"
      }'

Ausblick Version 2

Geplant sind die übrigen 14 Typen: vCard, App-Link, Mehrere Links, Social Media, WhatsApp, PDF, Bild, Video, E-Mail, SMS, Standort, Zahlung, WLAN und Feedback.

Die Architektur ist darauf ausgelegt — ein neuer Typ besteht aus:

  1. einem Modul in src/lib/qr/types/ (Zod-Schema, Kodierung, Kurzfassung),
  2. einer Formular-Komponente in src/components/qr-forms/,
  3. je einem Eintrag in src/lib/qr/registry.ts (ersetzt dort den defineComingSoonType-Platzhalter) und in src/components/qr-forms/index.ts.

Am bestehenden Code aus Version 1 ist dafür nichts zu ändern: Vorschau, Download, Gestaltung, Speicherung dynamischer Codes und die Weiterleitung arbeiten für alle Typen gleich.

Typen, die statt einer Weiterleitung eine eigene Landingpage brauchen (etwa „Mehrere Links“ oder „Feedback“), setzen dafür redirectTarget auf eine interne Adresse — auch dafür ist keine Änderung am Datenmodell nötig, weil data als JSONB gespeichert wird.