Skip to content

Commit bbfb4b7

Browse files
royalpinto007claude
andcommitted
7 improvements: spam protection, comment moderation, SEO, sitemap, 404
- Rate limit comments to 5/hour per IP (same pattern as votes) - Admin Comments tab: view/hide/remove comments by status - Fix APM-XXXX -> APM-0001 in about page - Add revalidate=60 to /agent/[slug] and /tag/[slug] pages - Sitemap now includes all individual agent and tag URLs - Custom 404 page matching site design - Admin API route for comment moderation (GET/PATCH) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0a0c94a commit bbfb4b7

8 files changed

Lines changed: 550 additions & 257 deletions

File tree

app/(public)/about/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default function AboutPage() {
6262
<p>
6363
Every submission is reviewed before publication. We reject cases that
6464
are vague, unverifiable, or appear to be targeted harassment. Approved
65-
cases are assigned a permanent case number (APM-XXXX) and indexed
65+
cases are assigned a permanent case number (APM-0001) and indexed
6666
immediately.
6767
</p>
6868

app/(public)/agent/[slug]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { PostCard } from "@/components/post/PostCard";
55
import { AGENTS } from "@/lib/constants/agents";
66
import { fetchPostsByAgent } from "@/lib/db/posts";
77

8+
export const revalidate = 60;
9+
810
interface PageProps {
911
params: { slug: string };
1012
}

app/(public)/tag/[slug]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { PostCard } from "@/components/post/PostCard";
55
import { TAGS } from "@/lib/constants/tags";
66
import { fetchPostsByTag } from "@/lib/db/posts";
77

8+
export const revalidate = 60;
9+
810
interface PageProps {
911
params: { slug: string };
1012
}

app/admin/page.tsx

Lines changed: 422 additions & 255 deletions
Large diffs are not rendered by default.

app/api/admin/comments/route.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { createSupabaseAdminClient } from "@/lib/supabase/admin";
3+
4+
function checkAuth(req: NextRequest): boolean {
5+
const pwd = req.headers.get("x-admin-password");
6+
return pwd === process.env.ADMIN_PASSWORD;
7+
}
8+
9+
export async function GET(req: NextRequest) {
10+
if (!checkAuth(req)) {
11+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
12+
}
13+
14+
const rawStatus = req.nextUrl.searchParams.get("status") ?? "visible";
15+
const status = ["visible", "hidden", "removed"].includes(rawStatus)
16+
? (rawStatus as "visible" | "hidden" | "removed")
17+
: ("visible" as const);
18+
const supabase = createSupabaseAdminClient();
19+
20+
const { data, error } = await supabase
21+
.from("comments")
22+
.select(
23+
"id, body, is_anonymous, author_handle, status, created_at, post_id, posts(case_number, title)",
24+
)
25+
.eq("status", status)
26+
.order("created_at", { ascending: false })
27+
.limit(100);
28+
29+
if (error) return NextResponse.json({ comments: [] });
30+
return NextResponse.json({ comments: data ?? [] });
31+
}
32+
33+
export async function PATCH(req: NextRequest) {
34+
if (!checkAuth(req)) {
35+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
36+
}
37+
38+
const { id, status } = await req.json();
39+
if (!id || !["visible", "hidden", "removed"].includes(status)) {
40+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
41+
}
42+
43+
const supabase = createSupabaseAdminClient();
44+
const { error } = await supabase
45+
.from("comments")
46+
.update({ status })
47+
.eq("id", id);
48+
49+
if (error) return NextResponse.json({ error: "Failed" }, { status: 500 });
50+
return NextResponse.json({ ok: true });
51+
}

app/api/comments/route.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
22
import { createHash } from "crypto";
33
import { createSupabaseServerClient } from "@/lib/supabase/server";
44
import { createSupabaseAdminClient } from "@/lib/supabase/admin";
5+
import { consumeSharedRateLimit } from "@/lib/rate-limit/shared";
56

67
function hashIp(ip: string): string {
78
const pepper = process.env.IP_HASH_PEPPER ?? "default-pepper";
@@ -47,7 +48,21 @@ export async function POST(req: NextRequest) {
4748
);
4849
}
4950

50-
const ip_hash = hashIp(getIp(req));
51+
const ip = getIp(req);
52+
const ip_hash = hashIp(ip);
53+
54+
const rateLimit = await consumeSharedRateLimit(
55+
`comment:${ip_hash}`,
56+
3600,
57+
5,
58+
);
59+
if (!rateLimit.allowed) {
60+
return NextResponse.json(
61+
{ error: "Too many comments. Try again later." },
62+
{ status: 429 },
63+
);
64+
}
65+
5166
const supabase = createSupabaseAdminClient();
5267

5368
const { data, error } = await supabase

app/not-found.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Link from "next/link";
2+
3+
export default function NotFound() {
4+
return (
5+
<div className="flex min-h-[60vh] flex-col items-center justify-center px-4 text-center">
6+
<div className="mb-6 flex items-center gap-2">
7+
<div className="h-1.5 w-1.5 rounded-sm bg-accent-red" />
8+
<span className="font-mono text-[10px] uppercase tracking-widest text-text-tertiary">
9+
Case Not Found
10+
</span>
11+
</div>
12+
13+
<div className="mb-2 font-mono text-6xl font-bold tabular-nums text-border-strong">
14+
404
15+
</div>
16+
17+
<h1 className="mt-4 font-serif text-2xl font-normal text-text-primary">
18+
No case on file.
19+
</h1>
20+
21+
<p className="mt-3 max-w-sm text-sm leading-relaxed text-text-secondary">
22+
The case you&apos;re looking for doesn&apos;t exist, was removed, or the
23+
URL is incorrect.
24+
</p>
25+
26+
<div className="mt-8 flex flex-wrap items-center justify-center gap-4">
27+
<Link
28+
href="/"
29+
className="rounded border border-accent-red bg-accent-red-soft px-5 py-2.5 font-mono text-[11px] uppercase tracking-wider text-accent-red transition-all hover:bg-accent-red hover:text-white"
30+
>
31+
← Back to Registry
32+
</Link>
33+
<Link
34+
href="/submit"
35+
className="font-mono text-[11px] uppercase tracking-wider text-text-tertiary transition-colors hover:text-text-primary"
36+
>
37+
File a Report →
38+
</Link>
39+
</div>
40+
</div>
41+
);
42+
}

app/sitemap.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { MetadataRoute } from "next";
22
import { createSupabaseAdminClient } from "@/lib/supabase/admin";
3+
import { AGENTS } from "@/lib/constants/agents";
4+
import { TAGS } from "@/lib/constants/tags";
35

46
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
57
const siteUrl =
@@ -63,5 +65,17 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
6365
priority: 0.4,
6466
},
6567
...postUrls,
68+
...AGENTS.map((a) => ({
69+
url: `${siteUrl}/agent/${a.slug}`,
70+
lastModified: new Date(),
71+
changeFrequency: "weekly" as const,
72+
priority: 0.6,
73+
})),
74+
...TAGS.map((t) => ({
75+
url: `${siteUrl}/tag/${t.slug}`,
76+
lastModified: new Date(),
77+
changeFrequency: "weekly" as const,
78+
priority: 0.5,
79+
})),
6680
];
6781
}

0 commit comments

Comments
 (0)