diff --git a/public/shop-seite.html b/public/shop-seite.html
new file mode 100644
index 0000000..203a116
--- /dev/null
+++ b/public/shop-seite.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+Shop
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/index.js b/server/index.js
index 3bbb0ae..3b82457 100644
--- a/server/index.js
+++ b/server/index.js
@@ -19,6 +19,7 @@ import funktionenRoutes from './routes/funktionen.js'
import termineRoutes from './routes/termine.js'
import shopRoutes from './routes/shop.js'
import shopWebhookRoutes from './routes/shopWebhook.js'
+import { shopSeiteRouter } from './routes/shopSeite.js'
import takeoverCreditRoutes from './routes/takeoverCredits.js'
import { purgeExpiredUploads } from './services/takeoverCredits.js'
import { purgePendingChatUploads } from './services/chatAttachments.js'
@@ -120,6 +121,8 @@ app.use('/embed/', (_req, res, next) => {
})
// Adressen ohne Fassung: aktuelle Fassung der Rechtstexte (Analyse und Terminplaner; vor express.static)
app.use('/rechtliches', legalRouter())
+// Webshop-Seiten fuer Kaeufer: /shop/:siteKey und /shop/:siteKey/widerruf (vor express.static)
+app.use(shopSeiteRouter())
// Abgeschaltetes Admin-Panel und Rechnungsanalyse: Links aus alten Team-Mails ins Ticketsystem
app.use('/admin.html', legacyAdminRedirect.admin)
app.use('/kalkulation.html', legacyAdminRedirect.kalkulation)
diff --git a/server/routes/shopSeite.js b/server/routes/shopSeite.js
new file mode 100644
index 0000000..4503411
--- /dev/null
+++ b/server/routes/shopSeite.js
@@ -0,0 +1,62 @@
+import { readFileSync } from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { Router } from 'express'
+
+/**
+ * Oeffentliche Standalone-Seiten des Webshops (Plan 2026-10-05-webshop.md, H2; Spec 5.4):
+ * GET /shop/:siteKey Shop mit dem Widget public/embed/shop.js
+ * GET /shop/:siteKey/widerruf dieselbe Seite, das Widget oeffnet gleich die Widerrufsfunktion
+ * Die Seite verraet nicht, ob der Schluessel existiert oder der Shop aktiv ist: jeder syntaktisch gueltige Schluessel
+ * bekommt dieselbe Seite, ob der Shop laeuft entscheidet das Widget ueber die oeffentliche Schnittstelle. Alles andere ist 404.
+ */
+
+const SITE_KEY_RE = /^[a-z2-7]{16}$/
+const DATEI = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'shop-seite.html')
+const CSP = [
+ "default-src 'self'",
+ "img-src 'self' data:",
+ "script-src 'self'",
+ "style-src 'self' 'unsafe-inline'", // das Widget legt ein