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:
Benny Joo
2024-12-23 10:02:59 +01:00
committed by GitHub
parent b59bf7d1ee
commit 3eda7858c0
11 changed files with 50 additions and 211 deletions
+33 -19
View File
@@ -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],
},
};
};
+5 -2
View File
@@ -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)
);
};
+1 -9
View File
@@ -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);
+7 -13
View File
@@ -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"