Implement public audit pages

This commit is contained in:
Matthias
2026-06-05 14:14:07 +02:00
parent 03cb65fde4
commit 47ee2c2d51
25 changed files with 1039 additions and 45 deletions

View File

@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { revalidatePublicAudit } from "@/lib/audits/public-audit-revalidation";
import { parsePublicAuditSlug } from "@/lib/audits/slugs";
export async function POST(request: Request) {
const secret = process.env.PUBLIC_AUDIT_REVALIDATION_SECRET;
const authorization = request.headers.get("authorization");
if (!secret || authorization !== `Bearer ${secret}`) {
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
}
const body = (await request.json().catch(() => null)) as { slug?: unknown } | null;
const normalizedSlug =
typeof body?.slug === "string" ? parsePublicAuditSlug(body.slug) : null;
if (!normalizedSlug) {
return NextResponse.json({ ok: false, error: "Invalid slug" }, { status: 400 });
}
revalidatePublicAudit(normalizedSlug);
return NextResponse.json({ ok: true });
}

View File

@@ -1,27 +1,66 @@
import { FileText } from "lucide-react";
import type { Metadata } from "next";
import { Suspense } from "react";
import { cacheLife, cacheTag } from "next/cache";
import { fetchQuery } from "convex/nextjs";
export default async function PublicAuditPage({
params,
}: {
import { PublicAuditPage } from "@/components/public-audit/public-audit-page";
import { PublicAuditStatus } from "@/components/public-audit/public-audit-status";
import { api } from "@/convex/_generated/api";
import { publicAuditCacheTag } from "@/lib/audits/public-audit-cache";
import { toPublicAuditRenderState } from "@/lib/audits/public-audit-presenter";
import type { PublicAuditLookupResult } from "@/lib/audits/public-audit-types";
import { parsePublicAuditSlug } from "@/lib/audits/slugs";
export const metadata: Metadata = {
title: "Website-Audit",
robots: {
index: false,
follow: false,
googleBot: {
index: false,
follow: false,
},
},
};
type PublicAuditRouteProps = {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
};
async function getCachedPublicAudit(slug: string): Promise<PublicAuditLookupResult> {
"use cache";
const normalizedSlug = parsePublicAuditSlug(slug);
if (!normalizedSlug) {
return null;
}
cacheTag(publicAuditCacheTag(normalizedSlug));
cacheLife("days");
return await fetchQuery(api.audits.getPublicBySlug, { slug: normalizedSlug });
}
async function PublicAuditContent({ params }: PublicAuditRouteProps) {
const { slug } = await params;
const result = await getCachedPublicAudit(slug);
const renderState = toPublicAuditRenderState(result);
if (renderState.kind === "pending") {
return <PublicAuditStatus status="pending" />;
}
if (renderState.kind === "unavailable") {
return <PublicAuditStatus status="unavailable" />;
}
return <PublicAuditPage audit={renderState.audit} />;
}
export default function PublicAuditRoute({ params }: PublicAuditRouteProps) {
return (
<main className="flex min-h-dvh items-center justify-center bg-background px-6 py-12">
<section className="w-full max-w-2xl rounded-lg border bg-card p-6 text-card-foreground">
<FileText className="mb-5 size-6 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">
Audit: {slug}
</p>
<h1 className="mt-3 text-3xl font-semibold tracking-normal">
Dieser Audit ist noch nicht freigegeben
</h1>
<p className="mt-4 text-sm leading-6 text-muted-foreground">
Sobald der Bericht manuell geprueft und veroeffentlicht wurde,
erscheinen hier die freigegebenen Beobachtungen und Empfehlungen.
</p>
</section>
</main>
<Suspense fallback={<PublicAuditStatus status="pending" />}>
<PublicAuditContent params={params} />
</Suspense>
);
}

7
app/audit/layout.tsx Normal file
View File

@@ -0,0 +1,7 @@
export default function AuditLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <div className="bg-slate-50 text-slate-950">{children}</div>;
}

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Suspense } from "react";
import { ConvexClientProvider } from "@/components/convex-client-provider";
import { getToken } from "@/lib/auth-server";
import "./globals.css";
@@ -19,20 +20,30 @@ export const metadata: Metadata = {
description: "Interner Akquise-Agent fuer lokale Webdesign-Leads",
};
export default async function RootLayout({
async function AuthenticatedConvexProvider({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const token = await getToken();
return <ConvexClientProvider initialToken={token}>{children}</ConvexClientProvider>;
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="de"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">
<ConvexClientProvider initialToken={token}>{children}</ConvexClientProvider>
<Suspense fallback={null}>
<AuthenticatedConvexProvider>{children}</AuthenticatedConvexProvider>
</Suspense>
</body>
</html>
);

14
app/sitemap.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { MetadataRoute } from "next";
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: siteUrl,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1,
},
];
}