Sprechende Kurz-Links: selbst vergeben, aendern, frueher gueltig halten
Schliesst Issue #1. Der Wunsch betraf den Kurz-Link, nicht das Ziel: Er soll sich von Anfang an bestimmen und spaeter aendern lassen. Beim Anlegen gibt es dafuer ein Feld, im Bearbeiten-Dialog laesst er sich aendern. Erlaubt sind Kleinbuchstaben, Ziffern, Bindestriche und „/" als Trenner — damit sind Adressen wie kunde-a/sommer moeglich. Umlaute bleiben draussen, weil sie in Adressen unleserlich kodiert werden; Gross- und Kleinschreibung ist beim Aufrufen egal. Beim Aendern bleibt die vorherige Adresse gueltig. Das ist der Kern: Auf gedruckten Codes steht die alte, und genau deren Bestand ist der Sinn eines dynamischen Codes. Frueher vergebene Adressen stehen im Dialog und lassen sich einzeln freigeben — fuer den Tippfehler, mit dem noch nichts gedruckt ist. Solange eine Adresse gehalten wird, kann sie kein anderer Code uebernehmen; sonst liessen sich fremde gedruckte Codes umlenken. Vergeben und Aendern ist standardmaessig Administratoren vorbehalten. Ein Kurz-Link ist eine oeffentliche Adresse, und wer ihn aendert, kann Aushaenge entwerten. Unter Verwaltung -> Einstellungen laesst sich das fuer Bearbeiter freigeben; ohne das Recht erscheint das Feld gar nicht, Codes anlegen und deren Ziel aendern geht weiterhin. Eigene Aktion in der Policy statt Teil von codes:update, weil sie mehr wiegt. Technisch: /r und /s sind Sammelrouten, weil ein Kurz-Link „/" enthalten darf. Die Eindeutigkeit gilt zusaetzlich auf der kleingeschriebenen Form; die Migration bricht ab, falls der Bestand das nicht zulaesst, statt still Daten zu verlieren. Der bestehende exakte Index bleibt — er traegt den haeufigen Fall, den gescannten Code.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { qrLandings } from "@/components/landing";
|
||||
import { LandingNotice } from "@/components/landing/shell";
|
||||
import { ensureDefaultDomain, resolveRequestDomain } from "@/lib/domains";
|
||||
import { getQrType } from "@/lib/qr/registry";
|
||||
import { isValidSlug } from "@/lib/slug";
|
||||
import { findCodeBySlug } from "@/lib/slug-resolve";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Sammelroute, weil ein Kurz-Link „/" enthalten darf.
|
||||
type PageProps = { params: Promise<{ slug: string[] }> };
|
||||
|
||||
/**
|
||||
* Öffentliche Landingpage eines dynamischen Codes.
|
||||
*
|
||||
* Hierher schickt `/r/[slug]` alle Typen, die keine fremde Adresse haben,
|
||||
* sondern eine Seite: Linksammlungen, hinterlegte Dateien, Feedback.
|
||||
*
|
||||
* Der Scan-Zähler wird bewusst **nicht** hier erhöht, sondern in `/r/[slug]`.
|
||||
* Sonst zählte jedes Neuladen dieser Seite als weiterer Scan.
|
||||
*/
|
||||
async function loadCode(abschnitte: string[] | undefined) {
|
||||
const slug = (abschnitte ?? []).join("/");
|
||||
if (!isValidSlug(slug)) return null;
|
||||
|
||||
const matched = await resolveRequestDomain(await headers());
|
||||
const domain = matched ?? { ...(await ensureDefaultDomain()), isActive: true };
|
||||
if (!domain.isActive) return null;
|
||||
|
||||
return (await findCodeBySlug(domain.id, slug))?.code ?? null;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const code = await loadCode(slug);
|
||||
const qrType = code ? getQrType(code.type) : undefined;
|
||||
|
||||
if (!code || !qrType?.hasLandingPage) {
|
||||
return { title: "Nicht gefunden" };
|
||||
}
|
||||
|
||||
let title = qrType.title;
|
||||
try {
|
||||
title = qrType.summary(code.data);
|
||||
} catch {
|
||||
// Daten passen nicht mehr zum Schema — der Typname tut es auch.
|
||||
}
|
||||
// Absolut, damit der Titel nicht mit dem Anwendungsnamen ergänzt wird:
|
||||
// Wer hier landet, hat mit dem Generator nichts zu tun.
|
||||
return { title: { absolute: title } };
|
||||
}
|
||||
|
||||
export default async function LandingPage({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
const code = await loadCode(slug);
|
||||
|
||||
if (!code) {
|
||||
return (
|
||||
<LandingNotice title="Diese Seite gibt es nicht">
|
||||
Der Code wurde gelöscht oder hat nie existiert. Bitte wende dich an
|
||||
denjenigen, der ihn erstellt hat.
|
||||
</LandingNotice>
|
||||
);
|
||||
}
|
||||
|
||||
const qrType = getQrType(code.type);
|
||||
const Landing = qrType?.hasLandingPage ? qrLandings[qrType.id] : undefined;
|
||||
|
||||
if (!qrType || !Landing) {
|
||||
return (
|
||||
<LandingNotice title="Diese Seite gibt es nicht">
|
||||
Dieser Code führt nicht auf eine Seite. Bitte wende dich an denjenigen,
|
||||
der ihn erstellt hat.
|
||||
</LandingNotice>
|
||||
);
|
||||
}
|
||||
|
||||
// Alte Datensätze können nach einer Schema-Änderung unbrauchbar sein. Das
|
||||
// ist kein Grund, den Besuchern einen Absturz zu zeigen.
|
||||
if (!qrType.validate(code.data).ok) {
|
||||
return (
|
||||
<LandingNotice title="Diese Seite ist gerade nicht verfügbar">
|
||||
Die hinterlegten Inhalte sind unvollständig. Bitte wende dich an
|
||||
denjenigen, der den Code erstellt hat.
|
||||
</LandingNotice>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Landing
|
||||
data={code.data}
|
||||
label={code.label}
|
||||
slug={code.slug}
|
||||
codeId={code.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user