@@ -25,7 +25,9 @@ import { getTranslations } from 'next-intl/server';
2525import { getCurrentTenantId , getCurrentUserId } from '@/lib/supabase/tenant'
2626import { hasCourseAccess } from '@/lib/services/course-access'
2727import type { Metadata } from 'next' ;
28- import { buildPageMetadata } from '@/lib/seo' ;
28+ import { buildPageMetadata , getSeoContext } from '@/lib/seo' ;
29+ import { pickCourseProduct } from '@/lib/course-pricing' ;
30+ import { JsonLd , courseJsonLd } from '@/lib/structured-data' ;
2931import { AutoFreeEnrollButton , FreeEnrollButton } from '@/components/public/free-enroll-button' ;
3032import { PlanEnrollButton } from '@/components/public/plan-enroll-button' ;
3133import { createAdminClient } from '@/lib/supabase/admin' ;
@@ -83,7 +85,7 @@ interface Lesson {
8385}
8486
8587export default async function CourseDetailsPage ( props : {
86- params : Promise < { id : string } > ;
88+ params : Promise < { id : string ; locale : string } > ;
8789 searchParams : Promise < { enroll ?: string } > ;
8890} ) {
8991 const [ params , searchParams , t , supabase , userId , tenantId ] = await Promise . all ( [
@@ -167,13 +169,14 @@ export default async function CourseDetailsPage(props: {
167169 } ) ( )
168170 : Promise . resolve ( false ) ;
169171
170- const [ { data : author } , hasAccess , { data : productCourses } , planCoversCourse , socialProof , { data : lessonRows } ] = await Promise . all ( [
172+ const [ { data : author } , hasAccess , { data : productCourses } , planCoversCourse , socialProof , { data : lessonRows } , seo ] = await Promise . all ( [
171173 authorPromise ,
172174 accessPromise ,
173175 productCoursesPromise ,
174176 planCoveragePromise ,
175177 getCourseSocialProof ( courseId , tenantId ) ,
176178 lessonsPromise ,
179+ getSeoContext ( ) ,
177180 ] ) ;
178181
179182 const { averageRating, reviewCount, studentCount, recentReviews } = socialProof ;
@@ -184,14 +187,9 @@ export default async function CourseDetailsPage(props: {
184187 const estimatedMinutes = ( totalLessons * 10 ) % 60 ;
185188
186189 type CourseProduct = { price : number | string ; currency : string | null } ;
187- const linkedProducts = ( productCourses ?? [ ] )
188- . map ( ( { product } ) => product as unknown as CourseProduct | null )
189- . filter ( ( product ) : product is CourseProduct => product !== null ) ;
190- // Deterministic pick: cheapest paid product (catalog cards should agree).
191- const paidProducts = linkedProducts
192- . filter ( ( product ) => Number ( product . price ) > 0 )
193- . sort ( ( a , b ) => Number ( a . price ) - Number ( b . price ) ) ;
194- const courseProduct = paidProducts [ 0 ] ?? linkedProducts [ 0 ] ;
190+ const courseProduct = pickCourseProduct (
191+ ( productCourses ?? [ ] ) . map ( ( { product } ) => product as unknown as CourseProduct | null )
192+ ) ;
195193 const isFree = ! courseProduct || Number ( courseProduct . price ) === 0 ;
196194 const priceDisplay = isFree
197195 ? t ( 'pricing.free' )
@@ -213,8 +211,27 @@ export default async function CourseDetailsPage(props: {
213211 ? new Intl . DateTimeFormat ( undefined , { year : 'numeric' , month : 'long' , day : 'numeric' } ) . format ( new Date ( course . published_at ) )
214212 : null ;
215213
214+ // schema.org Course rich-result markup. Offer mirrors the deterministic
215+ // product pick above so the structured price always matches the page.
216+ const courseUrl = `${ seo . baseUrl } /${ params . locale } /courses/${ course . course_id } ` ;
217+ const structuredData = courseJsonLd ( {
218+ name : course . title ,
219+ description : course . description ,
220+ url : courseUrl ,
221+ image : course . thumbnail_url ,
222+ providerName : seo . siteName ,
223+ providerUrl : seo . baseUrl ,
224+ datePublished : course . published_at ,
225+ price : ! isFree && courseProduct ? Number ( courseProduct . price ) : null ,
226+ currency : ! isFree && courseProduct ? courseProduct . currency : null ,
227+ isFree,
228+ averageRating,
229+ reviewCount,
230+ } ) ;
231+
216232 return (
217233 < div className = "min-h-screen bg-[#09090b] text-zinc-100 font-sans" >
234+ < JsonLd data = { structuredData } />
218235 { /* Breadcrumbs */ }
219236 < nav aria-label = "Breadcrumb" className = "bg-[#18181b]/50 border-b border-zinc-800" >
220237 < div className = "container mx-auto px-4 py-3 flex items-center gap-2 text-sm text-zinc-400" >
0 commit comments