diff --git a/apps/web/app/_utils.tsx b/apps/web/app/_utils.tsx index 22678de8ca..1368db9f44 100644 --- a/apps/web/app/_utils.tsx +++ b/apps/web/app/_utils.tsx @@ -4,7 +4,8 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { headers } from "next/headers"; import { constructGenericImage } from "@calcom/lib/OgImages"; -import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants"; +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"; //@ts-expect-error no type definitions import config from "@calcom/web/next-i18next.config"; @@ -36,32 +37,23 @@ export const getTranslate = async () => { return t; }; -export const _generateMetadata = async ( +const _generateMetadataWithoutImage = async ( getTitle: (t: TFunction) => string, getDescription: (t: TFunction) => string, - excludeAppNameFromTitle?: boolean + hideBranding?: boolean, + origin?: string ) => { const h = headers(); - const canonical = h.get("x-pathname") ?? ""; + const pathname = h.get("x-pathname") ?? ""; + const canonical = buildCanonical({ path: pathname, origin: origin ?? CAL_URL }); const locale = h.get("x-locale") ?? "en"; - const t = await getFixedT(locale, "common"); const title = getTitle(t); const description = getDescription(t); - - const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL); - - const image = - SEO_IMG_OGIMG + - constructGenericImage({ - title, - description, - }); - const titleSuffix = `| ${APP_NAME}`; - const displayedTitle = - title.includes(titleSuffix) || excludeAppNameFromTitle ? title : `${title} ${titleSuffix}`; + const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`; + const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL); return { title: title.length === 0 ? APP_NAME : displayedTitle, @@ -74,9 +66,31 @@ export const _generateMetadata = async ( url: canonical, type: "website", siteName: APP_NAME, - title, - images: [image], + title: displayedTitle, }, metadataBase, }; }; + +export const _generateMetadata = async ( + 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 + + constructGenericImage({ + title: metadata.title, + description: metadata.description, + }); + + return { + ...metadata, + openGraph: { + ...metadata.openGraph, + images: [image], + }, + }; +}; diff --git a/apps/web/app/booking/[uid]/page.tsx b/apps/web/app/booking/[uid]/page.tsx index e192d88b61..0f44e09f32 100644 --- a/apps/web/app/booking/[uid]/page.tsx +++ b/apps/web/app/booking/[uid]/page.tsx @@ -4,6 +4,7 @@ import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; +import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import { BookingStatus } from "@calcom/prisma/enums"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; @@ -12,7 +13,7 @@ import OldPage from "~/bookings/views/bookings-single-view"; import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps"; export const generateMetadata = async ({ params, searchParams }: _PageProps) => { - const { bookingInfo, eventType, recurringBookings } = await getData( + const { bookingInfo, eventType, recurringBookings, orgSlug } = await getData( buildLegacyCtx(headers(), cookies(), params, searchParams) ); const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation; @@ -21,7 +22,9 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) => (t) => t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`), (t) => - t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`) + t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`), + false, + getOrgFullOrigin(orgSlug) ); }; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 6d59291a11..ab000ce5d0 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -21,15 +21,7 @@ const calFont = localFont({ weight: "600", }); -export const generateMetadata = () => - prepareRootMetadata({ - twitterCreator: "@calcom", - twitterSite: "@calcom", - robots: { - index: false, - follow: false, - }, - }); +export const generateMetadata = () => prepareRootMetadata(); const getInitialProps = async (url: string) => { const { pathname, searchParams } = new URL(url); diff --git a/apps/web/lib/metadata.ts b/apps/web/lib/metadata.ts index 09dcb88e29..749d54e06c 100644 --- a/apps/web/lib/metadata.ts +++ b/apps/web/lib/metadata.ts @@ -1,14 +1,5 @@ import type { Metadata } from "next"; -type RootMetadataRecipe = Readonly<{ - twitterCreator: string; - twitterSite: string; - robots: { - index: boolean; - follow: boolean; - }; -}>; - export type PageMetadataRecipe = Readonly<{ title: string; canonical: string; @@ -18,7 +9,7 @@ export type PageMetadataRecipe = Readonly<{ metadataBase: URL; }>; -export const prepareRootMetadata = (recipe: RootMetadataRecipe): Metadata => ({ +export const prepareRootMetadata = (): Metadata => ({ icons: { icon: "/favicon.ico", apple: "/api/logo?type=apple-touch-icon", @@ -48,7 +39,6 @@ export const prepareRootMetadata = (recipe: RootMetadataRecipe): Metadata => ({ userScalable: false, viewportFit: "cover", }, - robots: recipe.robots, other: { "application-TileColor": "#ff0000", }, @@ -63,8 +53,12 @@ export const prepareRootMetadata = (recipe: RootMetadataRecipe): Metadata => ({ }, ], twitter: { - site: recipe.twitterSite, - creator: recipe.twitterCreator, + site: "@calcom", + creator: "@calcom", card: "summary_large_image", }, + robots: { + index: true, + follow: true, + }, }); diff --git a/apps/web/modules/availability/troubleshoot/troubleshoot-view.tsx b/apps/web/modules/availability/troubleshoot/troubleshoot-view.tsx index d5dab8c083..9827abd705 100644 --- a/apps/web/modules/availability/troubleshoot/troubleshoot-view.tsx +++ b/apps/web/modules/availability/troubleshoot/troubleshoot-view.tsx @@ -3,8 +3,7 @@ import dynamic from "next/dynamic"; import { Suspense } from "react"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { HeadSeo, Loader } from "@calcom/ui"; +import { Loader } from "@calcom/ui"; const TroubleshooterClientOnly = dynamic( () => import("@calcom/features/troubleshooter/Troubleshooter").then((mod) => mod.Troubleshooter), @@ -14,10 +13,8 @@ const TroubleshooterClientOnly = dynamic( ); function TroubleshooterPage() { - const { t } = useLocale(); return ( <> - diff --git a/apps/web/modules/bookings/views/bookings-single-view.test.tsx b/apps/web/modules/bookings/views/bookings-single-view.test.tsx deleted file mode 100644 index 24a3d3ff51..0000000000 --- a/apps/web/modules/bookings/views/bookings-single-view.test.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { render } from "@testing-library/react"; -import { useSession } from "next-auth/react"; -import React from "react"; -import { describe, it, expect, vi } from "vitest"; -import type { z } from "zod"; - -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; -import { BookingStatus } from "@calcom/prisma/enums"; -import { HeadSeo } from "@calcom/ui"; - -import Success from "./bookings-single-view"; - -function mockedSuccessComponentProps(props: Partial>) { - return { - eventType: { - id: 1, - title: "Event Title", - description: "", - locations: null, - length: 15, - userId: null, - eventName: "d", - timeZone: null, - recurringEvent: null, - requiresConfirmation: false, - disableGuests: false, - seatsPerTimeSlot: null, - seatsShowAttendees: null, - seatsShowAvailabilityCount: null, - schedulingType: null, - price: 0, - currency: "usd", - successRedirectUrl: null, - customInputs: [], - teamId: null, - team: null, - workflows: [], - hosts: [], - users: [], - owner: null, - isDynamic: false, - periodStartDate: "1", - periodEndDate: "1", - metadata: null, - bookingFields: [] as unknown as [] & z.BRAND<"HAS_SYSTEM_FIELDS">, - }, - profile: { - name: "John", - email: null, - theme: null, - brandColor: null, - darkBrandColor: null, - slug: null, - }, - bookingInfo: { - uid: "uid", - metadata: null, - customInputs: [], - startTime: new Date(), - endTime: new Date(), - id: 1, - user: null, - eventType: null, - seatsReferences: [], - userPrimaryEmail: null, - eventTypeId: null, - title: "Booking Title", - description: null, - location: null, - recurringEventId: null, - smsReminderNumber: "0", - cancellationReason: null, - rejectionReason: null, - status: BookingStatus.ACCEPTED, - attendees: [], - responses: { - name: "John", - }, - rescheduled: false, - fromReschedule: null, - }, - orgSlug: null, - userTimeFormat: 12, - requiresLoginToUpdate: false, - themeBasis: "dark", - hideBranding: false, - recurringBookings: null, - trpcState: { - queries: [], - mutations: [], - }, - dynamicEventName: "Event Title", - paymentStatus: null, - rescheduledToUid: null, - ...props, - } satisfies React.ComponentProps; -} - -describe("Success Component", () => { - it("renders HeadSeo correctly", () => { - vi.mocked(getOrgFullOrigin).mockImplementation((text: string | null) => `${text}.cal.local`); - vi.mocked(useRouterQuery).mockReturnValue({ - uid: "uid", - }); - vi.mock("@calcom/lib/constants", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - CURRENT_TIMEZONE: "Europe/London", - }; - }); - vi.mocked(useSession).mockReturnValue({ - update: vi.fn(), - status: "authenticated", - data: { - hasValidLicense: true, - upId: "1", - expires: "1", - user: { - name: "John", - id: 1, - profile: { - id: null, - upId: "1", - username: null, - organizationId: null, - organization: null, - }, - }, - }, - }); - - const mockObject = { - props: mockedSuccessComponentProps({ - orgSlug: "org1", - }), - }; - - render(); - - const expectedTitle = `booking_confirmed`; - const expectedDescription = expectedTitle; - expect(HeadSeo).toHaveBeenCalledWith( - { - origin: `${mockObject.props.orgSlug}.cal.local`, - title: expectedTitle, - description: expectedDescription, - }, - {} - ); - }); -}); diff --git a/apps/web/modules/bookings/views/bookings-single-view.tsx b/apps/web/modules/bookings/views/bookings-single-view.tsx index e541afd761..c6e45ca79d 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.tsx @@ -56,7 +56,6 @@ import { Badge, Button, EmailInput, - HeadSeo, useCalcomTheme, TextArea, showToast, @@ -434,7 +433,6 @@ export default function Success(props: PageProps) { )} -
diff --git a/apps/web/modules/members/members-view.tsx b/apps/web/modules/members/members-view.tsx index b03c97375f..21310995f4 100644 --- a/apps/web/modules/members/members-view.tsx +++ b/apps/web/modules/members/members-view.tsx @@ -3,7 +3,6 @@ import MembersView from "@calcom/features/ee/organizations/pages/members"; import Shell from "@calcom/features/shell/Shell"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { HeadSeo } from "@calcom/ui"; const MembersPage: React.FC = () => { const { t } = useLocale(); @@ -14,8 +13,6 @@ const MembersPage: React.FC = () => { description={t("organization_description")} withoutSeo subtitle={t("organization_description")}> - - ); diff --git a/apps/web/modules/videos/views/videos-meeting-ended-single-view.tsx b/apps/web/modules/videos/views/videos-meeting-ended-single-view.tsx index 85d8e22750..f920c4c226 100644 --- a/apps/web/modules/videos/views/videos-meeting-ended-single-view.tsx +++ b/apps/web/modules/videos/views/videos-meeting-ended-single-view.tsx @@ -4,7 +4,7 @@ import dayjs from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; -import { Button, HeadSeo, Icon } from "@calcom/ui"; +import { Button, Icon } from "@calcom/ui"; import type { getServerSideProps } from "@lib/video/meeting-ended/[uid]/getServerSideProps"; @@ -14,7 +14,6 @@ export default function MeetingUnavailable(props: PageProps) { return (
-
diff --git a/apps/web/modules/videos/views/videos-meeting-not-started-single-view.tsx b/apps/web/modules/videos/views/videos-meeting-not-started-single-view.tsx index c7d5e8b7d7..28603b71b7 100644 --- a/apps/web/modules/videos/views/videos-meeting-not-started-single-view.tsx +++ b/apps/web/modules/videos/views/videos-meeting-not-started-single-view.tsx @@ -4,7 +4,7 @@ import dayjs from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; -import { Button, HeadSeo, Icon, EmptyScreen } from "@calcom/ui"; +import { Button, Icon, EmptyScreen } from "@calcom/ui"; import type { getServerSideProps } from "@lib/video/meeting-not-started/[uid]/getServerSideProps"; @@ -14,7 +14,6 @@ export default function MeetingNotStarted(props: PageProps) { const { t } = useLocale(); return ( <> -
-