Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 72 additions & 13 deletions app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import { notFound } from "next/navigation";
import { headers } from "next/headers";
import { Metadata } from "next";

import { Locale } from "locales/types";
import { Locale, locales } from "locales/types";
import { getI18n } from "locales/server";
import { generateStructuredData, StructuredDataScript } from "@/shared/lib/structured-data";
import { getSessionTitle, getProgramTitle } from "@/features/programs/lib/translations-mapper";
import {
getProgramSlug,
getProgramTitle,
getSessionSlug,
getSessionTitle,
} from "@/features/programs/lib/translations-mapper";
import { generateSessionMetadata } from "@/features/programs/lib/session-metadata";
import { getSessionBySlug } from "@/features/programs/actions/get-session-by-slug.action";
import { auth } from "@/features/auth/lib/better-auth";
import type { ProgramI18nReference } from "@/entities/program/types/program.types";
import type { ProgramSessionWithExercises } from "@/entities/program-session/types/program-session.types";
import { Breadcrumbs } from "@/components/seo/breadcrumbs";

// Import the existing session client component
Expand All @@ -18,6 +25,53 @@ interface SessionDetailPageProps {
params: Promise<{ slug: string; sessionSlug: string; locale: Locale }>;
}

const getHrefLang = (locale: Locale): string => {
switch (locale) {
case "en":
return "en-US";
case "fr":
return "fr-FR";
case "es":
return "es-ES";
case "pt":
return "pt-PT";
case "ru":
return "ru-RU";
default:
return locale;
}
};

const buildSessionUrl = (locale: Locale, programSlug: string, sessionSlug: string, includeLocalePrefix = true): string => {
const localePrefix = includeLocalePrefix ? `/${locale}` : "";

return `https://www.workout.cool${localePrefix}/programs/${programSlug}/session/${sessionSlug}`;
};

const getAlternateSessionUrl = (
locale: Locale,
program: ProgramI18nReference,
session: ProgramSessionWithExercises,
includeLocalePrefix = true,
): string | null => {
const programSlug = getProgramSlug(program, locale) || program.slug;
const sessionSlug = getSessionSlug(session, locale);

return sessionSlug ? buildSessionUrl(locale, programSlug, sessionSlug, includeLocalePrefix) : null;
};

const getCanonicalSessionUrl = (
locale: Locale,
program: ProgramI18nReference,
session: ProgramSessionWithExercises,
includeLocalePrefix = true,
): string => {
const programSlug = getProgramSlug(program, locale) || program.slug;
const sessionSlug = getSessionSlug(session, locale) || session.slug;

return buildSessionUrl(locale, programSlug, sessionSlug, includeLocalePrefix);
};

export async function generateMetadata({ params }: SessionDetailPageProps): Promise<Metadata> {
const { slug, sessionSlug, locale } = await params;
const t = await getI18n();
Expand All @@ -29,6 +83,8 @@ export async function generateMetadata({ params }: SessionDetailPageProps): Prom

const sessionMetadata = generateSessionMetadata(response.session, response.program, locale);
const imageUrl = response.session.exercises[0]?.exercise.fullVideoImageUrl || "/images/default-workout.jpg";
const canonicalUrl = getCanonicalSessionUrl(locale, response.program, response.session);
const xDefaultUrl = getAlternateSessionUrl("en", response.program, response.session, false);

return {
title: sessionMetadata.title,
Expand All @@ -37,7 +93,7 @@ export async function generateMetadata({ params }: SessionDetailPageProps): Prom
openGraph: {
title: sessionMetadata.title,
description: sessionMetadata.description,
url: `https://www.workout.cool/${locale}/programs/${slug}/session/${sessionSlug}`,
url: canonicalUrl,
siteName: "Workout Cool",
images: [
{
Expand All @@ -58,15 +114,16 @@ export async function generateMetadata({ params }: SessionDetailPageProps): Prom
creator: "@WorkoutCool",
},
alternates: {
canonical: `https://www.workout.cool/${locale}/programs/${slug}/session/${sessionSlug}`,
canonical: canonicalUrl,
languages: {
"fr-FR": `https://www.workout.cool/fr/programs/${slug}/session/${sessionSlug}`,
"en-US": `https://www.workout.cool/en/programs/${slug}/session/${sessionSlug}`,
"es-ES": `https://www.workout.cool/es/programs/${slug}/session/${sessionSlug}`,
"pt-PT": `https://www.workout.cool/pt/programs/${slug}/session/${sessionSlug}`,
"ru-RU": `https://www.workout.cool/ru/programs/${slug}/session/${sessionSlug}`,
"zh-CN": `https://www.workout.cool/zh-CN/programs/${slug}/session/${sessionSlug}`,
"x-default": `https://www.workout.cool/programs/${slug}/session/${sessionSlug}`,
...Object.fromEntries(
locales.flatMap((targetLocale) => {
const sessionUrl = getAlternateSessionUrl(targetLocale, response.program, response.session);

return sessionUrl ? [[getHrefLang(targetLocale), sessionUrl]] : [];
}),
),
...(xDefaultUrl ? { "x-default": xDefaultUrl } : {}),
},
},
robots: {
Expand Down Expand Up @@ -102,6 +159,8 @@ export default async function SessionDetailPage({ params }: SessionDetailPagePro
const t = await getI18n();
const sessionTitle = getSessionTitle(response.session, locale);
const programTitle = getProgramTitle(response.program, locale);
const programSlug = getProgramSlug(response.program, locale) || response.program.slug;
const canonicalUrl = getCanonicalSessionUrl(locale, response.program, response.session);

// Generate breadcrumb items
const breadcrumbItems = [
Expand All @@ -115,7 +174,7 @@ export default async function SessionDetailPage({ params }: SessionDetailPagePro
},
{
label: programTitle,
href: `/${locale}/programs/${slug}`,
href: `/${locale}/programs/${programSlug}`,
},
{
label: sessionTitle,
Expand All @@ -129,7 +188,7 @@ export default async function SessionDetailPage({ params }: SessionDetailPagePro
locale,
title: `${sessionTitle} - ${programTitle}`,
description: response.session.description || `${sessionTitle} workout session`,
url: `https://www.workout.cool/${locale}/programs/${slug}/session/${sessionSlug}`,
url: canonicalUrl,
image: response.session.exercises[0]?.exercise.fullVideoImageUrl || undefined,
sessionData: {
duration: Math.round(response.session.exercises.length * 3), // Estimate 3 min per exercise
Expand Down
2 changes: 1 addition & 1 deletion src/features/programs/lib/translations-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function getProgramDescription(program: ProgramDetail | PublicProgram, lo
return getI18nField(program, "description", locale);
}

export function getProgramSlug(program: ProgramDetail | PublicProgram, locale: Locale): string {
export function getProgramSlug(program: ProgramDetail | PublicProgram | ProgramI18nReference, locale: Locale): string {
return getI18nField(program, "slug", locale);
}

Expand Down
9 changes: 5 additions & 4 deletions src/features/programs/ui/program-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function ProgramDetailPage({ program, isAuthenticated }: ProgramDetailPag
const searchParams = useSearchParams();
const router = useRouter();
const currentLocale = useCurrentLocale();
const programSlug = getSlugForLocale(program, currentLocale) || program.slug;
const isPremium = useIsPremium();
const programTitle = getProgramTitle(program, currentLocale);
const programDescription = getProgramDescription(program, currentLocale);
Expand Down Expand Up @@ -136,7 +137,7 @@ export function ProgramDetailPage({ program, isAuthenticated }: ProgramDetailPag

const handleCTAClick = () => {
if (isAuthenticated && hasJoinedProgram) {
router.push(`/programs/${program.slug}/?tab=sessions`);
router.push(`/${currentLocale}/programs/${programSlug}/?tab=sessions`);
} else {
setShowWelcomeModal(true);
}
Expand All @@ -145,7 +146,7 @@ export function ProgramDetailPage({ program, isAuthenticated }: ProgramDetailPag
const handleJoinProgram = async () => {
setShowWelcomeModal(false);

router.push(`/programs/${program.slug}/?tab=sessions`);
router.push(`/${currentLocale}/programs/${programSlug}/?tab=sessions`);

// if (isAuthenticated && hasJoinedProgram) {
// // Navigate to current session if user has already joined
Expand Down Expand Up @@ -447,7 +448,7 @@ export function ProgramDetailPage({ program, isAuthenticated }: ProgramDetailPag
}

return currentWeekSessions.map((session) => {
const sessionSlug = getSlugForLocale(session, currentLocale);
const sessionSlug = getSlugForLocale(session, currentLocale) || session.slug;
const sessionName = getSessionTitle(session, currentLocale);
const sessionDescription = getSessionDescription(session, currentLocale);

Expand All @@ -463,7 +464,7 @@ export function ProgramDetailPage({ program, isAuthenticated }: ProgramDetailPag
}`}
key={session.id}
onClick={() => {
window.location.href = `/${currentLocale}/programs/${program.slug}/session/${sessionSlug}`;
window.location.href = `/${currentLocale}/programs/${programSlug}/session/${sessionSlug}`;
}}
>
{/* Session Number Badge */}
Expand Down