web: real 404s for unknown event/source/sensor/country/category slugs (existence-check layouts outside loading boundaries); pricing classification gated on pricing context
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6 changed files +69 −17
added
apps/web/src/app/category/[channel]/layout.tsx
+15 −0
@@ -0,0 +1,15 @@ | ||
| 1 | +import { notFound } from "next/navigation"; | |
| 2 | +import type { ReactNode } from "react"; | |
| 3 | +import { api } from "@/lib/api"; | |
| 4 | +import { CATEGORIES, FEED_CHANNELS } from "@websensor/core/client"; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * Existence check outside the `loading.tsx` boundary so unknown category channels return a real 404 instead of a | |
| 8 | + * 200 shell followed by the not-found UI (the page's own call is deduplicated by fetch memoization). | |
| 9 | + */ | |
| 10 | +export default async function CategoryLayout({ params, children }: { params: Promise<{ channel: string }>; children: ReactNode }) { | |
| 11 | + const { channel } = await params; | |
| 12 | + const ch = channel.toLowerCase(); | |
| 13 | + if (!FEED_CHANNELS[ch] && !(CATEGORIES as readonly string[]).includes(ch) && !(await api.categoryDesk(ch))?.recent.length) notFound(); | |
| 14 | + return children; | |
| 15 | +} | |
added
apps/web/src/app/country/[slug]/layout.tsx
+15 −0
@@ -0,0 +1,15 @@ | ||
| 1 | +import { notFound } from "next/navigation"; | |
| 2 | +import type { ReactNode } from "react"; | |
| 3 | +import { api } from "@/lib/api"; | |
| 4 | +import { countryBySlug, COUNTRIES } from "@websensor/core/client"; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * Existence check outside the `loading.tsx` boundary so unknown country slugs return a real 404 instead of a | |
| 8 | + * 200 shell followed by the not-found UI (the page's own call is deduplicated by fetch memoization). | |
| 9 | + */ | |
| 10 | +export default async function CountryLayout({ params, children }: { params: Promise<{ slug: string }>; children: ReactNode }) { | |
| 11 | + const { slug } = await params; | |
| 12 | + if (!countryBySlug(slug) && !COUNTRIES[slug.toUpperCase()]) notFound(); | |
| 13 | + if (!(await api.country(slug))) notFound(); | |
| 14 | + return children; | |
| 15 | +} | |
deleted
apps/web/src/app/country/loading.tsx
+0 −17
@@ -1,17 +0,0 @@ | ||
| 1 | −import { Skeleton, SkeletonRows } from "@/components/ui"; | |
| 2 | − | |
| 3 | −export default function Loading() { | |
| 4 | − return ( | |
| 5 | − <> | |
| 6 | − <div className="mb-3"> | |
| 7 | − <Skeleton className="mb-1 h-2.5 w-12" /> | |
| 8 | − <Skeleton className="h-5 w-32" /> | |
| 9 | − <Skeleton className="mt-2 h-3 w-full max-w-2xl" /> | |
| 10 | − </div> | |
| 11 | − <div className="panel"> | |
| 12 | − <div className="border-b border-line px-3 py-2"><Skeleton className="h-2.5 w-64" /></div> | |
| 13 | − <SkeletonRows rows={16} /> | |
| 14 | − </div> | |
| 15 | − </> | |
| 16 | − ); | |
| 17 | −} | |
added
apps/web/src/app/event/[slug]/layout.tsx
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +import { notFound } from "next/navigation"; | |
| 2 | +import type { ReactNode } from "react"; | |
| 3 | +import { api } from "@/lib/api"; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * Existence check outside the `loading.tsx` boundary so unknown event slugs return a real 404 instead of a | |
| 7 | + * 200 shell followed by the not-found UI (the page's own call is deduplicated by fetch memoization). | |
| 8 | + */ | |
| 9 | +export default async function EventLayout({ params, children }: { params: Promise<{ slug: string }>; children: ReactNode }) { | |
| 10 | + const { slug } = await params; | |
| 11 | + if (!(await api.event(slug))) notFound(); | |
| 12 | + return children; | |
| 13 | +} | |
added
apps/web/src/app/sensor/[id]/layout.tsx
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +import { notFound } from "next/navigation"; | |
| 2 | +import type { ReactNode } from "react"; | |
| 3 | +import { api } from "@/lib/api"; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * Existence check outside the `loading.tsx` boundary so unknown sensor ids return a real 404 instead of a | |
| 7 | + * 200 shell followed by the not-found UI (the page's own call is deduplicated by fetch memoization). | |
| 8 | + */ | |
| 9 | +export default async function SensorLayout({ params, children }: { params: Promise<{ id: string }>; children: ReactNode }) { | |
| 10 | + const { id } = await params; | |
| 11 | + if (!(await api.sensor(id))) notFound(); | |
| 12 | + return children; | |
| 13 | +} | |
added
apps/web/src/app/source/[id]/layout.tsx
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +import { notFound } from "next/navigation"; | |
| 2 | +import type { ReactNode } from "react"; | |
| 3 | +import { api } from "@/lib/api"; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * Existence check outside the `loading.tsx` boundary so unknown source ids return a real 404 instead of a | |
| 7 | + * 200 shell followed by the not-found UI (the page's own call is deduplicated by fetch memoization). | |
| 8 | + */ | |
| 9 | +export default async function SourceLayout({ params, children }: { params: Promise<{ id: string }>; children: ReactNode }) { | |
| 10 | + const { id } = await params; | |
| 11 | + if (!(await api.source(id))) notFound(); | |
| 12 | + return children; | |
| 13 | +} | |
| 14 | ||