diff --git a/.env.example b/.env.example index 4397eb9d29..6632a31ece 100644 --- a/.env.example +++ b/.env.example @@ -365,7 +365,6 @@ APP_ROUTER_APPS_CATEGORIES_ENABLED=0 # whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0 APP_ROUTER_APPS_ENABLED=0 -APP_ROUTER_TEAM_ENABLED=0 APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0 APP_ROUTER_AUTH_LOGIN_ENABLED=0 APP_ROUTER_AUTH_LOGOUT_ENABLED=0 diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts index a863b72e00..159f75c8d7 100644 --- a/apps/web/abTest/middlewareFactory.ts +++ b/apps/web/abTest/middlewareFactory.ts @@ -20,7 +20,6 @@ const ROUTES: [URLPattern, boolean][] = [ ["/auth/error", process.env.APP_ROUTER_AUTH_ERROR_ENABLED === "1"] as const, ["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const, ["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const, - ["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const, ].map(([pathname, enabled]) => [ new URLPattern({ pathname, diff --git a/apps/web/app/future/[user]/[type]/page.tsx b/apps/web/app/[user]/[type]/page.tsx similarity index 69% rename from apps/web/app/future/[user]/[type]/page.tsx rename to apps/web/app/[user]/[type]/page.tsx index 3a61c20e5b..5a55bc792f 100644 --- a/apps/web/app/future/[user]/[type]/page.tsx +++ b/apps/web/app/[user]/[type]/page.tsx @@ -1,6 +1,6 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; import type { PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; +import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata"; import { WithLayout } from "app/layoutHOC"; import { headers, cookies } from "next/headers"; @@ -18,7 +18,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); const props = await getData(legacyCtx); - const { booking, user: username, slug: eventSlug } = props; + const { booking, user: username, slug: eventSlug, isSEOIndexable, eventData, isBrandingHidden } = props; const rescheduleUid = booking?.uid; const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug); @@ -31,12 +31,21 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { }); const profileName = event?.profile?.name ?? ""; + const profileImage = event?.profile.image; const title = event?.title ?? ""; - - return await _generateMetadata( - (t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`, - (t) => `${rescheduleUid ? t("reschedule") : ""} ${title}` - ); + const metadata = await generateEventBookingPageMetadata({ + event: { + hidden: event?.hidden ?? false, + title, + users: event?.users ?? [], + }, + hideBranding: isBrandingHidden, + orgSlug: eventData?.entity.orgSlug ?? null, + isSEOIndexable: !!isSEOIndexable, + profile: { name: profileName, image: profileImage ?? "" }, + isReschedule: !!rescheduleUid, + }); + return metadata; }; const getData = withAppDirSsr(getServerSideProps); diff --git a/apps/web/app/[user]/page.tsx b/apps/web/app/[user]/page.tsx new file mode 100644 index 0000000000..6ae2fac2f6 --- /dev/null +++ b/apps/web/app/[user]/page.tsx @@ -0,0 +1,39 @@ +import { withAppDirSsr } from "app/WithAppDirSsr"; +import type { PageProps } from "app/_types"; +import { generateUserProfilePageMetadata } from "app/generateBookingPageMetadata"; +import { WithLayout } from "app/layoutHOC"; +import { headers, cookies } from "next/headers"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +import { getServerSideProps } from "@server/lib/[user]/getServerSideProps"; + +import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view"; +import LegacyPage from "~/users/views/users-public-view"; + +export const generateMetadata = async ({ params, searchParams }: PageProps) => { + const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams)); + + const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props; + + const isOrg = !!profile?.organization; + const allowSEOIndexing = !!( + (!isOrg && profile.allowSEOIndexing) || + (isOrg && isOrgSEOIndexable && profile.allowSEOIndexing) + ); + return await generateUserProfilePageMetadata({ + profile: { + name: profile.name, + image: profile.image ?? "", + username: profile.username ?? "", + markdownStrippedBio: markdownStrippedBio, + }, + event: null, + hideBranding: false, + orgSlug: entity.orgSlug ?? null, + isSEOIndexable: allowSEOIndexing, + }); +}; + +const getData = withAppDirSsr(getServerSideProps); +export default WithLayout({ getData, Page: LegacyPage })<"P">; diff --git a/apps/web/app/_utils.tsx b/apps/web/app/_utils.tsx index a487888800..d51c1a128c 100644 --- a/apps/web/app/_utils.tsx +++ b/apps/web/app/_utils.tsx @@ -3,7 +3,8 @@ import i18next from "i18next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { headers } from "next/headers"; -import { constructGenericImage } from "@calcom/lib/OgImages"; +import type { MeetingImageProps } from "@calcom/lib/OgImages"; +import { constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages"; import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants"; import { buildCanonical } from "@calcom/lib/next-seo.config"; import { truncateOnWord } from "@calcom/lib/text"; @@ -103,3 +104,21 @@ export const _generateMetadata = async ( }, }; }; + +export const generateMeetingMetadata = async ( + meeting: MeetingImageProps, + getTitle: (t: TFunction) => string, + getDescription: (t: TFunction) => string, + hideBranding?: boolean, + origin?: string +) => { + const metadata = await _generateMetadataWithoutImage(getTitle, getDescription, hideBranding, origin); + const image = SEO_IMG_OGIMG + constructMeetingImage(meeting); + return { + ...metadata, + openGraph: { + ...metadata.openGraph, + images: [image], + }, + }; +}; diff --git a/apps/web/app/d/[link]/[slug]/page.tsx b/apps/web/app/d/[link]/[slug]/page.tsx index 0d194c6853..73eb631fa8 100644 --- a/apps/web/app/d/[link]/[slug]/page.tsx +++ b/apps/web/app/d/[link]/[slug]/page.tsx @@ -17,7 +17,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) => const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); const pageProps = await getData(legacyCtx); - const { booking, user: username, slug: eventSlug, isTeamEvent } = pageProps; + const { booking, user: username, slug: eventSlug, isTeamEvent, isBrandingHidden } = pageProps; const rescheduleUid = booking?.uid; const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req); const org = isValidOrgDomain ? currentOrgDomain : null; @@ -34,7 +34,8 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) => const title = event?.title ?? ""; return await _generateMetadata( (t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`, - (t) => `${rescheduleUid ? t("reschedule") : ""} ${title}` + (t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`, + isBrandingHidden ); }; diff --git a/apps/web/app/future/[user]/page.tsx b/apps/web/app/future/[user]/page.tsx deleted file mode 100644 index e26286d1d5..0000000000 --- a/apps/web/app/future/[user]/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import { headers, cookies } from "next/headers"; - -import { getLayout } from "@calcom/features/MainLayoutAppDir"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; - -import { getServerSideProps } from "@server/lib/[user]/getServerSideProps"; - -import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view"; -import LegacyPage from "~/users/views/users-public-view"; - -export const generateMetadata = async ({ params, searchParams }: PageProps) => { - const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams)); - - const { profile, markdownStrippedBio } = props; - return await _generateMetadata( - () => profile.name, - () => markdownStrippedBio - ); -}; - -const getData = withAppDirSsr(getServerSideProps); -export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">; diff --git a/apps/web/app/future/org/[orgSlug]/[user]/[type]/embed/page.tsx b/apps/web/app/future/org/[orgSlug]/[user]/[type]/embed/page.tsx index 4cbd35891f..b008b6c5a6 100644 --- a/apps/web/app/future/org/[orgSlug]/[user]/[type]/embed/page.tsx +++ b/apps/web/app/future/org/[orgSlug]/[user]/[type]/embed/page.tsx @@ -1,10 +1,9 @@ -import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]"; -import Page from "@pages/org/[orgSlug]/[user]/[type]/embed"; import withEmbedSsrAppDir from "app/WithEmbedSSR"; import { WithLayout } from "app/layoutHOC"; +import { Page, type OrgTypePageProps } from "app/org/[orgSlug]/[user]/[type]/page"; import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps"; -const getEmbedData = withEmbedSsrAppDir(getServerSideProps); +const getEmbedData = withEmbedSsrAppDir(getServerSideProps); -export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page }); +export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page }); diff --git a/apps/web/app/future/org/[orgSlug]/[user]/[type]/page.tsx b/apps/web/app/future/org/[orgSlug]/[user]/[type]/page.tsx deleted file mode 100644 index b96a381cef..0000000000 --- a/apps/web/app/future/org/[orgSlug]/[user]/[type]/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]"; -import { withAppDirSsr } from "app/WithAppDirSsr"; -import { WithLayout } from "app/layoutHOC"; - -import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps"; - -const getData = withAppDirSsr(getServerSideProps); - -export default WithLayout({ getLayout: null, getData, Page }); diff --git a/apps/web/app/future/org/[orgSlug]/[user]/embed/page.tsx b/apps/web/app/future/org/[orgSlug]/[user]/embed/page.tsx index 02c31c9929..0ee2487d1e 100644 --- a/apps/web/app/future/org/[orgSlug]/[user]/embed/page.tsx +++ b/apps/web/app/future/org/[orgSlug]/[user]/embed/page.tsx @@ -1,8 +1,9 @@ -import { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]"; -import Page from "@pages/org/[orgSlug]/[user]/embed"; import withEmbedSsrAppDir from "app/WithEmbedSSR"; import { WithLayout } from "app/layoutHOC"; +import { Page, type OrgPageProps } from "app/org/[orgSlug]/[user]/page"; -const getEmbedData = withEmbedSsrAppDir(getServerSideProps); +import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps"; -export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page }); +const getEmbedData = withEmbedSsrAppDir(getServerSideProps); + +export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page }); diff --git a/apps/web/app/future/org/[orgSlug]/[user]/page.tsx b/apps/web/app/future/org/[orgSlug]/[user]/page.tsx deleted file mode 100644 index a783b6d61c..0000000000 --- a/apps/web/app/future/org/[orgSlug]/[user]/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import Page, { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]"; -import { withAppDirSsr } from "app/WithAppDirSsr"; -import { WithLayout } from "app/layoutHOC"; - -const getData = withAppDirSsr(getServerSideProps); - -export default WithLayout({ getLayout: null, getData, Page }); diff --git a/apps/web/app/future/org/[orgSlug]/page.tsx b/apps/web/app/future/org/[orgSlug]/page.tsx deleted file mode 100644 index 745d7e9547..0000000000 --- a/apps/web/app/future/org/[orgSlug]/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps as _PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import { cookies, headers } from "next/headers"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; - -import type { PageProps } from "~/team/team-view"; -import TeamPage from "~/team/team-view"; - -export const generateMetadata = async ({ params, searchParams }: _PageProps) => { - const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams)); - - return await _generateMetadata( - (t) => props.team.name || t("nameless_team"), - (t) => props.team.name || t("nameless_team") - ); -}; - -const getData = withAppDirSsr(getServerSideProps); - -export default WithLayout({ - Page: TeamPage, - getData, - getLayout: null, - isBookingPage: true, -})<"P">; diff --git a/apps/web/app/future/org/[orgSlug]/team/[slug]/[type]/page.tsx b/apps/web/app/future/org/[orgSlug]/team/[slug]/[type]/page.tsx deleted file mode 100644 index ea51f5cd9b..0000000000 --- a/apps/web/app/future/org/[orgSlug]/team/[slug]/[type]/page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps as _PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import { cookies, headers } from "next/headers"; - -import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { EventRepository } from "@calcom/lib/server/repository/event"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps"; - -import type { PageProps } from "~/team/type-view"; -import TypePage from "~/team/type-view"; - -export const generateMetadata = async ({ params, searchParams }: _PageProps) => { - const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); - const props = await getData(legacyCtx); - const { user: username, slug: eventSlug, booking } = props; - const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug); - - const event = await EventRepository.getPublicEvent({ - username, - eventSlug, - isTeamEvent: true, - org: isValidOrgDomain ? currentOrgDomain : null, - fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true", - }); - - const profileName = event?.profile?.name ?? ""; - const title = event?.title ?? ""; - - return await _generateMetadata( - (t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`, - (t) => `${booking?.uid ? t("reschedule") : ""} ${title}` - ); -}; -const getData = withAppDirSsr(getServerSideProps); - -export default WithLayout({ - Page: TypePage, - getData, - getLayout: null, - isBookingPage: true, -})<"P">; diff --git a/apps/web/app/future/org/[orgSlug]/team/[slug]/page.tsx b/apps/web/app/future/org/[orgSlug]/team/[slug]/page.tsx deleted file mode 100644 index 745d7e9547..0000000000 --- a/apps/web/app/future/org/[orgSlug]/team/[slug]/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps as _PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import { cookies, headers } from "next/headers"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; - -import type { PageProps } from "~/team/team-view"; -import TeamPage from "~/team/team-view"; - -export const generateMetadata = async ({ params, searchParams }: _PageProps) => { - const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams)); - - return await _generateMetadata( - (t) => props.team.name || t("nameless_team"), - (t) => props.team.name || t("nameless_team") - ); -}; - -const getData = withAppDirSsr(getServerSideProps); - -export default WithLayout({ - Page: TeamPage, - getData, - getLayout: null, - isBookingPage: true, -})<"P">; diff --git a/apps/web/app/generateBookingPageMetadata.ts b/apps/web/app/generateBookingPageMetadata.ts new file mode 100644 index 0000000000..566a5cae6f --- /dev/null +++ b/apps/web/app/generateBookingPageMetadata.ts @@ -0,0 +1,151 @@ +import type { TFunction } from "next-i18next"; + +import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; + +import { generateMeetingMetadata } from "./_utils"; + +type MetadataPayload = { + title: (t: TFunction) => string; + description: (t: TFunction) => string; + meeting: { + title: string; + profile: { name: string; image: string }; + users?: { name: string; username: string }[]; + }; + robots: { + index: boolean; + follow: boolean; + }; +}; + +type UserProfilePageProfile = { + name: string | null; + username: string | null; + image: string | null; + markdownStrippedBio: string | null | undefined; +}; + +type TeamProfilePageProfile = { + teamName: string | null; + image: string | null; + markdownStrippedBio: string | null | undefined; +}; + +type EventBookingPageProfile = { name: string; image: string }; +type BaseMetadataArgs = { + hideBranding: boolean; + orgSlug: string | null; + isSEOIndexable: boolean; + isReschedule?: boolean; +}; + +type TeamProfilePageArgs = BaseMetadataArgs & { + profile: TeamProfilePageProfile; + event: null; +}; + +type UserProfilePageArgs = BaseMetadataArgs & { + profile: UserProfilePageProfile; + event: null; +}; + +type EventBookingPageArgs = BaseMetadataArgs & { + profile: EventBookingPageProfile; + event: { + title: string; + hidden: boolean; + users: { name: string | null; username: string | null }[]; + }; +}; + +export const generateEventBookingPageMetadata = async (props: EventBookingPageArgs) => { + const { profile, isReschedule, hideBranding, orgSlug, isSEOIndexable, event } = props; + const fullOrigin = getOrgFullOrigin(orgSlug); + + const payload: MetadataPayload = { + title: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title} | ${profile.name}`, + description: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title}`, + meeting: { + title: event.title, + profile: { name: profile.name, image: profile.image }, + users: event.users.map((user) => ({ + name: `${user.name}`, + username: `${user.username}`, + })), + }, + robots: { + index: !(event.hidden || !isSEOIndexable), + follow: !(event.hidden || !isSEOIndexable), + }, + }; + + return { + ...(await generateMeetingMetadata( + payload.meeting, + payload.title, + payload.description, + hideBranding, + fullOrigin + )), + robots: payload.robots, + }; +}; + +export const generateUserProfilePageMetadata = async (props: UserProfilePageArgs) => { + const { profile, hideBranding, orgSlug, isSEOIndexable } = props; + const fullOrigin = getOrgFullOrigin(orgSlug); + + const payload: MetadataPayload = { + title: () => profile.name ?? "", + description: () => profile.markdownStrippedBio ?? "", + meeting: { + title: profile.markdownStrippedBio ?? "", + profile: { name: profile.name ?? "", image: profile.image ?? "" }, + users: [{ name: profile.name ?? "", username: profile.username ?? "" }], + }, + robots: { + index: isSEOIndexable, + follow: isSEOIndexable, + }, + }; + + return { + ...(await generateMeetingMetadata( + payload.meeting, + payload.title, + payload.description, + hideBranding, + fullOrigin + )), + robots: payload.robots, + }; +}; + +export const generateTeamProfilePageMetadata = async (props: TeamProfilePageArgs) => { + const { profile, hideBranding, orgSlug, isSEOIndexable } = props; + const fullOrigin = getOrgFullOrigin(orgSlug); + + const payload: MetadataPayload = { + title: (t) => profile.teamName || t("nameless_team"), + description: () => profile.markdownStrippedBio ?? "", + meeting: { + title: profile.markdownStrippedBio ?? "", + profile: { name: `${profile.teamName}`, image: profile.image ?? "" }, + }, + robots: { + index: isSEOIndexable, + follow: isSEOIndexable, + }, + }; + + return { + ...(await generateMeetingMetadata( + payload.meeting, + payload.title, + payload.description, + hideBranding, + fullOrigin + )), + robots: payload.robots, + }; +}; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 68b3325222..f2dd3620af 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -26,7 +26,10 @@ export const generateMetadata = () => prepareRootMetadata(); const getInitialProps = async (url: string) => { const { pathname, searchParams } = new URL(url); - const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null; + const isEmbedSnippetGeneratorPath = pathname.startsWith("/event-types"); + const isEmbed = + (pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null) && + !isEmbedSnippetGeneratorPath; const embedColorScheme = searchParams?.get("ui.color-scheme"); const req = { headers: headers(), cookies: cookies() }; diff --git a/apps/web/app/org/[orgSlug]/[user]/[type]/page.tsx b/apps/web/app/org/[orgSlug]/[user]/[type]/page.tsx new file mode 100644 index 0000000000..8bb4a1fe20 --- /dev/null +++ b/apps/web/app/org/[orgSlug]/[user]/[type]/page.tsx @@ -0,0 +1,54 @@ +import { withAppDirSsr } from "app/WithAppDirSsr"; +import type { PageProps } from "app/_types"; +import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata"; +import { WithLayout } from "app/layoutHOC"; +import { cookies, headers } from "next/headers"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; +import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps"; + +import type { PageProps as TeamTypePageProps } from "~/team/type-view"; +import TeamTypePage from "~/team/type-view"; +import UserTypePage from "~/users/views/users-type-public-view"; +import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view"; + +export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps; +const getData = withAppDirSsr(getServerSideProps); + +export const generateMetadata = async ({ params, searchParams }: PageProps) => { + const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); + const props = await getData(legacyCtx); + + const { booking, isSEOIndexable, eventData, isBrandingHidden } = props; + const rescheduleUid = booking?.uid; + + const profileName = eventData?.profile?.name ?? ""; + const profileImage = eventData?.profile.image ?? ""; + + return await generateEventBookingPageMetadata({ + profile: { name: profileName, image: profileImage }, + event: { + title: eventData?.title ?? "", + hidden: eventData?.hidden ?? false, + users: [ + ...(eventData?.users || []).map((user) => ({ + name: `${user.name}`, + username: `${user.username}`, + })), + ], + }, + hideBranding: isBrandingHidden, + orgSlug: eventData?.entity.orgSlug ?? null, + isSEOIndexable: !!isSEOIndexable, + isReschedule: !!rescheduleUid, + }); +}; + +export const Page = async (props: OrgTypePageProps) => { + if ((props as TeamTypePageProps)?.teamId) { + return ; + } + return ; +}; + +export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true }); diff --git a/apps/web/app/org/[orgSlug]/[user]/page.tsx b/apps/web/app/org/[orgSlug]/[user]/page.tsx new file mode 100644 index 0000000000..a72bc7134f --- /dev/null +++ b/apps/web/app/org/[orgSlug]/[user]/page.tsx @@ -0,0 +1,64 @@ +import { withAppDirSsr } from "app/WithAppDirSsr"; +import type { PageProps } from "app/_types"; +import { + generateTeamProfilePageMetadata, + generateUserProfilePageMetadata, +} from "app/generateBookingPageMetadata"; +import { WithLayout } from "app/layoutHOC"; +import { cookies, headers } from "next/headers"; + +import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; +import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps"; + +import type { PageProps as TeamPageProps } from "~/team/team-view"; +import TeamPage from "~/team/team-view"; +import UserPage from "~/users/views/users-public-view"; +import type { PageProps as UserPageProps } from "~/users/views/users-public-view"; + +export type OrgPageProps = UserPageProps | TeamPageProps; +const getData = withAppDirSsr(getServerSideProps); + +export const generateMetadata = async ({ params, searchParams }: PageProps) => { + const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); + const props = await getData(legacyCtx); + + if ((props as TeamPageProps)?.team) { + const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = props as TeamPageProps; + return await generateTeamProfilePageMetadata({ + profile: { + teamName: team.name, + image: getOrgOrTeamAvatar(team), + markdownStrippedBio: markdownStrippedBio, + }, + event: null, + hideBranding: false, + orgSlug: currentOrgDomain ?? null, + isSEOIndexable: !!isSEOIndexable, + }); + } else { + const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props as UserPageProps; + + const isOrg = !!profile?.organization; + const allowSEOIndexing = + (!isOrg && profile.allowSEOIndexing) || (isOrg && isOrgSEOIndexable && profile.allowSEOIndexing); + + return await generateUserProfilePageMetadata({ + profile: { name: profile.name, username: profile.username, image: profile.image, markdownStrippedBio }, + hideBranding: false, + orgSlug: entity.orgSlug ?? null, + isSEOIndexable: !!allowSEOIndexing, + event: null, + }); + } +}; + +export const Page = async (props: OrgPageProps) => { + if ((props as TeamPageProps)?.team) { + return ; + } + return ; +}; + +export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true }); diff --git a/apps/web/app/future/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx b/apps/web/app/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx similarity index 87% rename from apps/web/app/future/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx rename to apps/web/app/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx index 2786759be3..5626ac8c4e 100644 --- a/apps/web/app/future/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx +++ b/apps/web/app/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx @@ -4,7 +4,6 @@ import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { headers, cookies } from "next/headers"; -import { getLayout } from "@calcom/features/MainLayoutAppDir"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { EventRepository } from "@calcom/lib/server/repository/event"; @@ -16,7 +15,7 @@ import Page from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-mee export const generateMetadata = async ({ params, searchParams }: _PageProps) => { const context = buildLegacyCtx(headers(), cookies(), params, searchParams); - const { slug: eventSlug, user: username } = await getData(context); + const { slug: eventSlug, user: username, isBrandingHidden } = await getData(context); const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); const org = isValidOrgDomain ? currentOrgDomain : null; @@ -34,9 +33,10 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) => return await _generateMetadata( () => `${title} | ${profileName}`, - () => `${title}` + () => `${title}`, + isBrandingHidden ); }; const getData = withAppDirSsr(getServerSideProps); -export default WithLayout({ getLayout, getData, Page })<"P">; +export default WithLayout({ getData, Page, isBookingPage: true })<"P">; diff --git a/apps/web/app/org/[orgSlug]/page.tsx b/apps/web/app/org/[orgSlug]/page.tsx new file mode 100644 index 0000000000..99b51c0aa2 --- /dev/null +++ b/apps/web/app/org/[orgSlug]/page.tsx @@ -0,0 +1 @@ +export { default, generateMetadata } from "app/team/[slug]/page"; diff --git a/apps/web/app/org/[orgSlug]/team/[slug]/[type]/page.tsx b/apps/web/app/org/[orgSlug]/team/[slug]/[type]/page.tsx new file mode 100644 index 0000000000..ebcf985632 --- /dev/null +++ b/apps/web/app/org/[orgSlug]/team/[slug]/[type]/page.tsx @@ -0,0 +1 @@ +export { default, generateMetadata } from "app/team/[slug]/[type]/page"; diff --git a/apps/web/app/org/[orgSlug]/team/[slug]/page.tsx b/apps/web/app/org/[orgSlug]/team/[slug]/page.tsx new file mode 100644 index 0000000000..99b51c0aa2 --- /dev/null +++ b/apps/web/app/org/[orgSlug]/team/[slug]/page.tsx @@ -0,0 +1 @@ +export { default, generateMetadata } from "app/team/[slug]/page"; diff --git a/apps/web/app/future/team/[slug]/[type]/page.tsx b/apps/web/app/team/[slug]/[type]/page.tsx similarity index 63% rename from apps/web/app/future/team/[slug]/[type]/page.tsx rename to apps/web/app/team/[slug]/[type]/page.tsx index debf7575b8..6e8af71749 100644 --- a/apps/web/app/future/team/[slug]/[type]/page.tsx +++ b/apps/web/app/team/[slug]/[type]/page.tsx @@ -1,6 +1,6 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; import type { PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; +import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; @@ -15,7 +15,7 @@ import LegacyPage, { type PageProps as LegacyPageProps } from "~/team/type-view" export const generateMetadata = async ({ params, searchParams }: PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); const props = await getData(legacyCtx); - const { user: username, slug: eventSlug, booking } = props; + const { user: username, slug: eventSlug, booking, isSEOIndexable, eventData, isBrandingHidden } = props; const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug); const event = await EventRepository.getPublicEvent({ @@ -26,13 +26,26 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true", }); - const profileName = event?.profile?.name ?? ""; - const title = event?.title ?? ""; - - return await _generateMetadata( - (t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`, - (t) => `${booking?.uid ? t("reschedule") : ""} ${title}` - ); + return await generateEventBookingPageMetadata({ + profile: { + name: event?.profile?.name ?? "", + image: event?.profile.image ?? "", + }, + event: { + title: event?.title ?? "", + hidden: event?.hidden ?? false, + users: [ + ...(event?.users || []).map((user) => ({ + name: `${user.name}`, + username: `${user.username}`, + })), + ], + }, + hideBranding: isBrandingHidden, + orgSlug: eventData?.entity.orgSlug ?? null, + isSEOIndexable, + isReschedule: !!booking, + }); }; const getData = withAppDirSsr(getServerSideProps); diff --git a/apps/web/app/future/team/[slug]/page.tsx b/apps/web/app/team/[slug]/page.tsx similarity index 51% rename from apps/web/app/future/team/[slug]/page.tsx rename to apps/web/app/team/[slug]/page.tsx index 8cd056fe22..976e6e2d70 100644 --- a/apps/web/app/future/team/[slug]/page.tsx +++ b/apps/web/app/team/[slug]/page.tsx @@ -1,9 +1,11 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; import type { PageProps as _PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; +import { generateTeamProfilePageMetadata } from "app/generateBookingPageMetadata"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; +import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage"; + import { buildLegacyCtx } from "@lib/buildLegacyCtx"; import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; @@ -11,12 +13,21 @@ import type { PageProps } from "~/team/team-view"; import LegacyPage from "~/team/team-view"; export const generateMetadata = async ({ params, searchParams }: _PageProps) => { - const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams)); - - return await _generateMetadata( - (t) => props.team.name || t("nameless_team"), - (t) => props.team.name || t("nameless_team") + const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData( + buildLegacyCtx(headers(), cookies(), params, searchParams) ); + + return await generateTeamProfilePageMetadata({ + profile: { + teamName: team.name, + image: getOrgOrTeamAvatar(team), + markdownStrippedBio: markdownStrippedBio, + }, + event: null, + hideBranding: false, + orgSlug: currentOrgDomain ?? null, + isSEOIndexable: !!isSEOIndexable, + }); }; const getData = withAppDirSsr(getServerSideProps); @@ -25,4 +36,5 @@ export default WithLayout({ Page: LegacyPage, getData, getLayout: null, + isBookingPage: true, })<"P">; diff --git a/apps/web/lib/org/[orgSlug]/[user]/getServerSideProps.tsx b/apps/web/lib/org/[orgSlug]/[user]/getServerSideProps.tsx index 4fce1e1edd..6b5c35d4a4 100644 --- a/apps/web/lib/org/[orgSlug]/[user]/getServerSideProps.tsx +++ b/apps/web/lib/org/[orgSlug]/[user]/getServerSideProps.tsx @@ -1,4 +1,3 @@ -import { getServerSideProps as GSSUserPage } from "@pages/[user]"; import type { GetServerSidePropsContext } from "next"; import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; @@ -6,6 +5,8 @@ import prisma from "@calcom/prisma"; import { getServerSideProps as GSSTeamPage } from "@lib/team/[slug]/getServerSideProps"; +import { getServerSideProps as GSSUserPage } from "@server/lib/[user]/getServerSideProps"; + export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { const team = await prisma.team.findFirst({ where: { diff --git a/apps/web/lib/team/[slug]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/getServerSideProps.tsx index 4369bba85c..e37410b34a 100644 --- a/apps/web/lib/team/[slug]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/getServerSideProps.tsx @@ -55,7 +55,6 @@ const getTheLastArrayElement = (value: ReadonlyArray | string | undefine export const getServerSideProps = async (context: GetServerSidePropsContext) => { const slug = getTheLastArrayElement(context.query.slug) ?? getTheLastArrayElement(context.query.orgSlug); - const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig( context.req, context.params?.orgSlug ?? context.query?.orgSlug diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 65886551ca..43fce568e0 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -195,7 +195,10 @@ export const config = { "/settings/:path*", "/reschedule/:path*", "/availability/:path*", - "/booking/:path*", + "/org/:path*", + "/team/:path*", + "/:user/:type/", + "/:user/", ], }; diff --git a/apps/web/modules/d/[link]/d-type-view.tsx b/apps/web/modules/d/[link]/d-type-view.tsx index 46f7315074..d4b781b162 100644 --- a/apps/web/modules/d/[link]/d-type-view.tsx +++ b/apps/web/modules/d/[link]/d-type-view.tsx @@ -2,7 +2,6 @@ import { Booker } from "@calcom/atoms/monorepo"; import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses"; -import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo"; import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps"; @@ -20,13 +19,6 @@ export default function Type({ }: PageProps) { return (
- & EmbedProps; function Type({ slug, user, booking, isEmbed, isBrandingHidden, entity, eventTypeId, duration }: PageProps) { return (
- - -
-
-
- -
-

- {team.parent && `${team.parent.name} `} - {teamName} -

- {!isBioEmpty && ( - <> -
- - )} +
+
+
+
- {team.isOrganization ? ( - !teamOrOrgIsPrivate ? ( - - ) : ( -
-

{t("you_cannot_see_teams_of_org")}

-
- ) - ) : ( +

+ {team.parent && `${team.parent.name} `} + {teamName} +

+ {!isBioEmpty && ( <> - {(showMembers.isOn || !team.eventTypes?.length) && - (teamOrOrgIsPrivate ? ( -
-

- {t("you_cannot_see_team_members")} -

-
- ) : ( - - ))} - {!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && ( -
- - - {/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */} - {!team.hideBookATeamMember && !teamOrOrgIsPrivate && ( -
-
- - - -
- )} -
- )} +
)} -
- +
+ {team.isOrganization ? ( + !teamOrOrgIsPrivate ? ( + + ) : ( +
+

{t("you_cannot_see_teams_of_org")}

+
+ ) + ) : ( + <> + {(showMembers.isOn || !team.eventTypes?.length) && + (teamOrOrgIsPrivate ? ( +
+

+ {t("you_cannot_see_team_members")} +

+
+ ) : ( + + ))} + {!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && ( +
+ + + {/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */} + {!team.hideBookATeamMember && !teamOrOrgIsPrivate && ( +
+
+ + + +
+ )} +
+ )} + + )} +
); } diff --git a/apps/web/modules/team/type-view.tsx b/apps/web/modules/team/type-view.tsx index 4beca85d3a..895eee92c5 100644 --- a/apps/web/modules/team/type-view.tsx +++ b/apps/web/modules/team/type-view.tsx @@ -1,14 +1,13 @@ "use client"; +import type { EmbedProps } from "app/WithEmbedSSR"; import { useSearchParams } from "next/navigation"; import { Booker } from "@calcom/atoms/monorepo"; import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses"; -import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo"; import type { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; -import type { EmbedProps } from "app/WithEmbedSSR"; export type PageProps = inferSSRProps & EmbedProps; @@ -34,33 +33,11 @@ function Type({ teamMemberEmail, crmOwnerRecordType, crmAppSlug, - isSEOIndexable, }: PageProps) { const searchParams = useSearchParams(); - const { profile, users, hidden, title } = eventData; return (
-