Skip to content

Commit 87ad666

Browse files
fix: resolve Cache Components prerender errors blocking production build
Dashboard pages are auth-gated and render live data per request, but were being prerendered into the static shell, tripping Cache Components rules (Date.now in lib/config, uncached Sanity fetches outside Suspense). - Refactor dashboard pages to the three-layer pattern: static shell plus a Suspense-wrapped child that calls connection() before fetching (config, content, pipeline, review, review/[id], sponsors, videos) - Wrap the live time-reading components on the dashboard index in Suspense - Wrap NavHeaderSlot and Footer in the (main) layout in Suspense so the one dynamic route (/podcast/preview/[token]) can produce a static shell - Make /api/youtube/rss.xml dynamic via connection() (external fetch + Date) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1637d9b commit 87ad666

11 files changed

Lines changed: 257 additions & 115 deletions

File tree

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,36 @@
1+
import { Suspense } from "react";
2+
import { connection } from "next/server";
13
import { getEngineConfig } from "@/lib/config";
24
import { ConfigForm } from "./config-form";
35

4-
export default async function ConfigPage() {
6+
export default function ConfigPage() {
7+
return (
8+
<div className="flex flex-col gap-6">
9+
<div>
10+
<h1 className="text-3xl font-bold tracking-tight">Engine Config</h1>
11+
<p className="text-muted-foreground">
12+
Configure the automated content engine. Changes propagate within 5 minutes.
13+
</p>
14+
</div>
15+
16+
<Suspense
17+
fallback={
18+
<div className="rounded-lg border p-6">
19+
<p className="text-sm text-muted-foreground">Loading configuration...</p>
20+
</div>
21+
}
22+
>
23+
<ConfigContent />
24+
</Suspense>
25+
</div>
26+
);
27+
}
28+
29+
async function ConfigContent() {
30+
// The dashboard is auth-gated and renders live config. getEngineConfig uses an
31+
// in-memory TTL (Date.now), so this must run dynamically, not at prerender.
32+
await connection();
33+
534
let config = null;
635
let error = null;
736

@@ -11,29 +40,24 @@ export default async function ConfigPage() {
1140
error = err instanceof Error ? err.message : "Failed to load config";
1241
}
1342

14-
return (
15-
<div className="flex flex-col gap-6">
16-
<div>
17-
<h1 className="text-3xl font-bold tracking-tight">Engine Config</h1>
18-
<p className="text-muted-foreground">
19-
Configure the automated content engine. Changes propagate within 5 minutes.
43+
if (error) {
44+
return (
45+
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-6">
46+
<p className="text-sm text-destructive">{error}</p>
47+
<p className="mt-2 text-xs text-muted-foreground">
48+
Make sure the engineConfig singleton exists in Sanity Studio.
2049
</p>
2150
</div>
51+
);
52+
}
2253

23-
{error ? (
24-
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-6">
25-
<p className="text-sm text-destructive">{error}</p>
26-
<p className="mt-2 text-xs text-muted-foreground">
27-
Make sure the engineConfig singleton exists in Sanity Studio.
28-
</p>
29-
</div>
30-
) : config ? (
31-
<ConfigForm initialConfig={config} />
32-
) : (
33-
<div className="rounded-lg border p-6">
34-
<p className="text-sm text-muted-foreground">Loading configuration...</p>
35-
</div>
36-
)}
54+
if (config) {
55+
return <ConfigForm initialConfig={config} />;
56+
}
57+
58+
return (
59+
<div className="rounded-lg border p-6">
60+
<p className="text-sm text-muted-foreground">Loading configuration...</p>
3761
</div>
3862
);
3963
}

apps/web/app/(dashboard)/dashboard/content/page.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Suspense } from "react";
2+
import { connection } from "next/server";
13
import { dashboardQuery } from "@/lib/sanity/dashboard";
24
import { ContentIdeasTable } from "./content-ideas-table";
35
import { PageRefreshButton } from "@/components/page-refresh-button";
@@ -24,15 +26,7 @@ const CONTENT_IDEAS_QUERY = `*[_type == "contentIdea"] | order(_createdAt desc)
2426
collectedAt
2527
}`;
2628

27-
export default async function ContentPage() {
28-
let ideas: ContentIdea[] = [];
29-
30-
try {
31-
ideas = await dashboardQuery<ContentIdea[]>(CONTENT_IDEAS_QUERY);
32-
} catch (error) {
33-
console.error("Failed to fetch content ideas:", error);
34-
}
35-
29+
export default function ContentPage() {
3630
return (
3731
<div className="flex flex-col gap-6">
3832
<div className="flex items-center justify-between">
@@ -48,7 +42,27 @@ export default async function ContentPage() {
4842
<PageRefreshButton />
4943
</div>
5044

51-
<ContentIdeasTable ideas={ideas} />
45+
<Suspense
46+
fallback={
47+
<p className="text-sm text-muted-foreground">Loading content ideas...</p>
48+
}
49+
>
50+
<ContentIdeasContent />
51+
</Suspense>
5252
</div>
5353
);
5454
}
55+
56+
async function ContentIdeasContent() {
57+
await connection();
58+
59+
let ideas: ContentIdea[] = [];
60+
61+
try {
62+
ideas = await dashboardQuery<ContentIdea[]>(CONTENT_IDEAS_QUERY);
63+
} catch (error) {
64+
console.error("Failed to fetch content ideas:", error);
65+
}
66+
67+
return <ContentIdeasTable ideas={ideas} />;
68+
}

apps/web/app/(dashboard)/dashboard/page.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Suspense } from "react";
12
import { SectionCardsLive } from "@/components/section-cards-live";
23
import { RecentActivity } from "@/components/recent-activity";
34
import { PipelineStatus } from "@/components/pipeline-status";
@@ -15,17 +16,26 @@ export default function DashboardPage() {
1516
</p>
1617
</div>
1718

18-
<SectionCardsLive />
19+
{/* These live components read the current time on the client, so they
20+
must sit under a Suspense boundary to be treated as dynamic holes
21+
under Cache Components (they can't be prerendered statically). */}
22+
<Suspense fallback={<div className="h-24 rounded-lg border" />}>
23+
<SectionCardsLive />
24+
</Suspense>
1925

2026
<div className="grid gap-4 md:grid-cols-2">
21-
<RecentActivity />
27+
<Suspense fallback={<div className="h-48 rounded-lg border" />}>
28+
<RecentActivity />
29+
</Suspense>
2230
<div className="rounded-lg border p-6">
2331
<h2 className="text-lg font-semibold">Pipeline Status</h2>
2432
<p className="mt-2 text-sm text-muted-foreground">
2533
Real-time view of content moving through the pipeline.
2634
</p>
2735
<div className="mt-4">
28-
<PipelineStatus />
36+
<Suspense fallback={<div className="h-24" />}>
37+
<PipelineStatus />
38+
</Suspense>
2939
</div>
3040
</div>
3141
</div>

apps/web/app/(dashboard)/dashboard/pipeline/page.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Suspense } from "react";
2+
import { connection } from "next/server";
13
import { dashboardQuery } from "@/lib/sanity/dashboard";
24
import {
35
Card,
@@ -41,7 +43,26 @@ interface PipelineVideo {
4143
_updatedAt: string;
4244
}
4345

44-
export default async function PipelinePage() {
46+
export default function PipelinePage() {
47+
return (
48+
<div className="flex flex-col gap-6">
49+
<div>
50+
<h1 className="text-3xl font-bold tracking-tight">Pipeline Status</h1>
51+
</div>
52+
<Suspense
53+
fallback={
54+
<p className="text-sm text-muted-foreground">Loading pipeline...</p>
55+
}
56+
>
57+
<PipelineContent />
58+
</Suspense>
59+
</div>
60+
);
61+
}
62+
63+
async function PipelineContent() {
64+
await connection();
65+
4566
// Fetch counts for all statuses in a single query
4667
const counts = await dashboardQuery<Record<string, number>>(
4768
`{
@@ -73,13 +94,10 @@ export default async function PipelinePage() {
7394
);
7495

7596
return (
76-
<div className="flex flex-col gap-6">
77-
<div>
78-
<h1 className="text-3xl font-bold tracking-tight">Pipeline Status</h1>
79-
<p className="text-muted-foreground">
80-
Overview of {totalVideos} videos across all pipeline stages.
81-
</p>
82-
</div>
97+
<>
98+
<p className="text-muted-foreground">
99+
Overview of {totalVideos} videos across all pipeline stages.
100+
</p>
83101

84102
{/* Status Count Cards */}
85103
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
@@ -195,6 +213,6 @@ export default async function PipelinePage() {
195213
</CardContent>
196214
</Card>
197215
</div>
198-
</div>
216+
</>
199217
);
200218
}

apps/web/app/(dashboard)/dashboard/review/[id]/page.tsx

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Suspense } from "react";
12
import { notFound } from "next/navigation";
23
import Link from "next/link";
34
import { dashboardQuery } from "@/lib/sanity/dashboard";
@@ -9,7 +10,29 @@ interface Props {
910
params: Promise<{ id: string }>;
1011
}
1112

12-
export default async function ReviewDetailPage({ params }: Props) {
13+
export default function ReviewDetailPage({ params }: Props) {
14+
return (
15+
<div className="flex flex-col gap-6">
16+
<div>
17+
<Link href="/dashboard/review">
18+
<Button variant="ghost" size="sm" className="min-h-[44px] gap-1">
19+
<ArrowLeft className="h-4 w-4" />
20+
Back to Review Queue
21+
</Button>
22+
</Link>
23+
</div>
24+
<Suspense
25+
fallback={
26+
<p className="text-sm text-muted-foreground">Loading video...</p>
27+
}
28+
>
29+
<ReviewDetailContent params={params} />
30+
</Suspense>
31+
</div>
32+
);
33+
}
34+
35+
async function ReviewDetailContent({ params }: Props) {
1336
const { id } = await params;
1437

1538
const video = await dashboardQuery(
@@ -33,17 +56,6 @@ export default async function ReviewDetailPage({ params }: Props) {
3356
notFound();
3457
}
3558

36-
return (
37-
<div className="flex flex-col gap-6">
38-
<div>
39-
<Link href="/dashboard/review">
40-
<Button variant="ghost" size="sm" className="min-h-[44px] gap-1">
41-
<ArrowLeft className="h-4 w-4" />
42-
Back to Review Queue
43-
</Button>
44-
</Link>
45-
</div>
46-
<ReviewDetailClient video={video as any} />
47-
</div>
48-
);
59+
// biome-ignore lint/suspicious/noExplicitAny: video shape comes from a loose GROQ projection
60+
return <ReviewDetailClient video={video as any} />;
4961
}

apps/web/app/(dashboard)/dashboard/review/page.tsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { Suspense } from "react";
12
import Link from "next/link";
3+
import { connection } from "next/server";
24
import { dashboardQuery } from "@/lib/sanity/dashboard";
35
import {
46
Card,
@@ -28,15 +30,7 @@ function QualityBadge({ score }: { score: number | null }) {
2830
return <Badge variant="destructive">{score}</Badge>;
2931
}
3032

31-
export default async function ReviewQueuePage() {
32-
const videos = await dashboardQuery<ReviewVideo[]>(
33-
`*[_type == "automatedVideo" && status == "pending_review"] | order(_updatedAt desc) [0..49] {
34-
_id, title, qualityScore, qualityIssues, status, _updatedAt,
35-
"thumbnailUrl": thumbnailHorizontal.asset->url,
36-
scriptQualityScore
37-
}`
38-
);
39-
33+
export default function ReviewQueuePage() {
4034
return (
4135
<div className="flex flex-col gap-6">
4236
<div>
@@ -46,6 +40,30 @@ export default async function ReviewQueuePage() {
4640
</p>
4741
</div>
4842

43+
<Suspense
44+
fallback={
45+
<p className="text-sm text-muted-foreground">Loading review queue...</p>
46+
}
47+
>
48+
<ReviewQueueContent />
49+
</Suspense>
50+
</div>
51+
);
52+
}
53+
54+
async function ReviewQueueContent() {
55+
await connection();
56+
57+
const videos = await dashboardQuery<ReviewVideo[]>(
58+
`*[_type == "automatedVideo" && status == "pending_review"] | order(_updatedAt desc) [0..49] {
59+
_id, title, qualityScore, qualityIssues, status, _updatedAt,
60+
"thumbnailUrl": thumbnailHorizontal.asset->url,
61+
scriptQualityScore
62+
}`
63+
);
64+
65+
return (
66+
<>
4967
{videos.length === 0 ? (
5068
<Card>
5169
<CardContent className="flex flex-col items-center justify-center py-12">
@@ -102,6 +120,6 @@ export default async function ReviewQueuePage() {
102120
))}
103121
</div>
104122
)}
105-
</div>
123+
</>
106124
);
107125
}

0 commit comments

Comments
 (0)