+
+ Vorschau-Link
+ Ohne Login, nur für wen du ihn teilst
+
+ {url ? (
+ <>
+
e.target.select()}
+ />
+
+
+
Öffnen
+
+
+
+ >
+ ) : (
+
+
+
+ )}
+ {message &&
{message}
}
+
+ )
+}
diff --git a/src/lib/projectAdminApi.js b/src/lib/projectAdminApi.js
index fc8f06f..99f73ba 100644
--- a/src/lib/projectAdminApi.js
+++ b/src/lib/projectAdminApi.js
@@ -57,6 +57,15 @@ export async function updateWebsiteProject(id, payload) {
})
}
+// Geheimer Vorschau-Link: neu erzeugen (der alte wird ungültig) bzw. abschalten
+export async function erzeugeVorschauLink(id) {
+ return adminFetch(`/api/admin/website-projects/${id}/vorschau-link`, { method: 'POST' })
+}
+
+export async function schalteVorschauLinkAb(id) {
+ return adminFetch(`/api/admin/website-projects/${id}/vorschau-link`, { method: 'DELETE' })
+}
+
export async function archiveWebsiteProject(id) {
return adminFetch(`/api/admin/website-projects/${id}/archive`, { method: 'POST' })
}
diff --git a/src/lib/vorschauLink.js b/src/lib/vorschauLink.js
new file mode 100644
index 0000000..6653caa
--- /dev/null
+++ b/src/lib/vorschauLink.js
@@ -0,0 +1,10 @@
+// Geheimer Vorschau-Link (2026-10-26): wer ihn kennt, sieht die Preview ohne Login.
+// Das Portal löst ihn ein (Cookie + Weiterleitung), siehe Kundenportal server/services/vorschauLink.js.
+const PORTAL = 'https://project.webklar.com'
+
+export function vorschauLinkUrl(project, base = PORTAL) {
+ const subdomain = String(project?.subdomain || '').trim().toLowerCase()
+ const token = String(project?.shareToken || '').trim()
+ if (!subdomain || !token) return ''
+ return `${String(base).replace(/\/$/, '')}/vorschau/${encodeURIComponent(subdomain)}/${encodeURIComponent(token)}`
+}
diff --git a/src/styles/global.css b/src/styles/global.css
index 535f31b..a3cf2ad 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1112,6 +1112,12 @@ button.link { text-align: left; } /* Reset und Gewicht von button.link stehen im
.proj-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px 16px; }
.proj-main { min-width: 0; overflow-wrap: anywhere; }
.proj-meta { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 12px; color: var(--text-2); }
+/* Geheimer Vorschau-Link: Feld volle Breite, Aktionen darunter */
+.vorschau-link { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; }
+.vorschau-link-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
+.vorschau-link-label { font-size: 13px; font-weight: 600; }
+.vorschau-link-url { width: 100%; min-width: 0; font-size: 13px; }
+.vorschau-link .msg { margin: 0; }
.ticket-projects-assign { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
.ticket-projects-assign .select { flex: 1 1 220px; min-width: 0; }
.ticket-projects-assign .input { flex: 1 1 160px; min-width: 0; }
diff --git a/tests/unit/vorschauLink.test.mjs b/tests/unit/vorschauLink.test.mjs
new file mode 100644
index 0000000..0b2b8ae
--- /dev/null
+++ b/tests/unit/vorschauLink.test.mjs
@@ -0,0 +1,19 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { loadModule } from './load.mjs'
+
+const { vorschauLinkUrl } = await loadModule('src/lib/vorschauLink.js')
+
+test('vorschauLinkUrl: Link aus Subdomain und Token', () => {
+ assert.equal(
+ vorschauLinkUrl({ subdomain: 'Doener-Nation', shareToken: 'abcDEF_123-xyzxyzxyzxyz' }),
+ 'https://project.webklar.com/vorschau/doener-nation/abcDEF_123-xyzxyzxyzxyz'
+ )
+ assert.equal(vorschauLinkUrl({ subdomain: 'a', shareToken: 't' }, 'http://x/'), 'http://x/vorschau/a/t')
+})
+
+test('vorschauLinkUrl: ohne Token oder Subdomain kein Link', () => {
+ assert.equal(vorschauLinkUrl({ subdomain: 'a', shareToken: '' }), '')
+ assert.equal(vorschauLinkUrl({ subdomain: '', shareToken: 'x' }), '')
+ assert.equal(vorschauLinkUrl(null), '')
+})