chore: Remove HeadSeo components where no longer needed + improve app router metadata logic (#18348)
* remove HeadSeo for already migrated pages and refactor prepareMetadata * create _generateMetadataWithoutImage and refactor _generateMetadata
This commit is contained in:
+33
-19
@@ -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, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => 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, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => 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],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<HeadSeo title={t("troubleshoot")} description={t("troubleshoot_availability")} />
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
|
||||
@@ -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<React.ComponentProps<typeof Success>>) {
|
||||
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<typeof Success>;
|
||||
}
|
||||
|
||||
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<any>();
|
||||
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(<Success {...mockObject.props} />);
|
||||
|
||||
const expectedTitle = `booking_confirmed`;
|
||||
const expectedDescription = expectedTitle;
|
||||
expect(HeadSeo).toHaveBeenCalledWith(
|
||||
{
|
||||
origin: `${mockObject.props.orgSlug}.cal.local`,
|
||||
title: expectedTitle,
|
||||
description: expectedDescription,
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
EmailInput,
|
||||
HeadSeo,
|
||||
useCalcomTheme,
|
||||
TextArea,
|
||||
showToast,
|
||||
@@ -434,7 +433,6 @@ export default function Success(props: PageProps) {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<HeadSeo origin={getOrgFullOrigin(orgSlug)} title={title} description={title} />
|
||||
<BookingPageTagManager eventType={eventType} />
|
||||
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
||||
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
||||
|
||||
@@ -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")}>
|
||||
<HeadSeo title={t("organization_members")} description={t("organization_description")} />
|
||||
|
||||
<MembersView />
|
||||
</Shell>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<HeadSeo title={t("meeting_unavailable")} description={t("meeting_unavailable")} />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<HeadSeo title={t("this_meeting_has_not_started_yet")} description={props.booking.title} />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<EmptyScreen
|
||||
Icon="clock"
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
||||
import { Button, EmptyScreen } from "@calcom/ui";
|
||||
|
||||
export default function NoMeetingFound() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo title={t("no_meeting_found")} description={t("no_meeting_found")} />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<EmptyScreen
|
||||
Icon="x"
|
||||
|
||||
Reference in New Issue
Block a user