chore: app router - /team, /org, /[user] booking pages (excl. embeds) (#18186)

* update env vars

* update middleware

* remove pages router and move pages to /app

* move to /team

* update imports

* fix

* remove pages router and move org pages to /app

* wip

* fix orgSlug/user pages

* fix orgSlug/user/type pages

* remove generateMetadata from embed pages

* fix

* remove pages router for /user pages

* generateMetadata is not needed in embed pages

* remove future/org

* no layout in [user] page

* simplify

* fix

* fix OG image for [user]

* fix OG images for org/user and team/slug

* fix OG images for booking page

* fix all metadata

* use isBrandingHidden

* remove BookerSeo and its usages

* rename excludeAppNameFromTitle -> hideBranding

* remove logic for meeting type in HeadSeo

* remove BookerSeo instances from team-view and users-public-view

* create generateMeetingMetadata util and use it to reduce duplicate code

* remove BookerSeo imports

* fix spacing

* remove constructMeetingImage mock from head-seo.test

* fetch avatarUrl using user id for user page metadata

* fix test

* remove unused test cases

* index and follow must be true by default

* invert noindex/nofollow flags

* remove HeadSeo for already migrated pages and refactor prepareMetadata

* fix organization-settings.e2e.ts

* fix order

* enable parallel test execution for dynamic-booking e2e test

* fix

* + could be %2B in app router

* refactor handling logic for embeds in app router

* fix isEmbed

* fix embed-core

* remove dead code

* move embed pages back to /future

* add back embed pages in pages router

* revert some changes

* fix import type checks

* simplify

* fix

* feat: Implement generateBookingPageMetadata function for improved SEO and metadata handling across user and event pages (#18440)

- Added a new utility function `generateBookingPageMetadata` to streamline the generation of metadata for booking and user profile pages.
- Updated multiple page components to utilize the new function, enhancing SEO indexing and metadata consistency.
- Removed redundant code related to previous metadata generation methods, improving code clarity and maintainability.

* fix dirs

* Pr-review-fixes-app-router-team-pages (#18450)

* Remove unnecessary getPublicEvent call

* Remove unused variable

* Fix 404 for team page (#18451)

* Remove console log

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
Benny Joo
2025-01-03 22:26:30 +00:00
committed by GitHub
co-authored by Hariom Balhara
parent 39173900b1
commit 2f9c8cb441
60 changed files with 651 additions and 825 deletions
@@ -1,9 +1,8 @@
import { render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { describe, it, vi } from "vitest";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { HeadSeo } from "@calcom/ui";
import UserPage from "./users-public-view";
@@ -71,33 +70,5 @@ describe("UserPage Component", () => {
});
render(<UserPage {...mockData.props} />);
const expectedDescription = mockData.props.markdownStrippedBio;
const expectedTitle = expectedDescription;
expect(HeadSeo).toHaveBeenCalledWith(
{
origin: `${mockData.props.entity.orgSlug}.cal.local`,
title: `${mockData.props.profile.name}`,
description: expectedDescription,
meeting: {
profile: {
name: mockData.props.profile.name,
image: mockData.props.users[0].avatarUrl,
},
title: expectedTitle,
users: [
{
name: mockData.props.users[0].name,
username: mockData.props.users[0].username,
},
],
},
nextSeoProps: {
nofollow: !mockData.props.profile.allowSEOIndexing,
noindex: !mockData.props.profile.allowSEOIndexing,
},
},
{}
);
});
});
@@ -11,12 +11,11 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { HeadSeo, Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
import { Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
import type { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
@@ -60,112 +59,89 @@ export function UserPage(props: PageProps) {
const isEventListEmpty = eventTypes.length === 0;
const isOrg = !!user?.profile?.organization;
const allowSEOIndexing = isOrg
? isOrgSEOIndexable
? profile.allowSEOIndexing
: false
: profile.allowSEOIndexing;
return (
<>
<HeadSeo
origin={getOrgFullOrigin(entity.orgSlug ?? null)}
title={profile.name}
description={markdownStrippedBio}
meeting={{
title: markdownStrippedBio,
profile: { name: `${profile.name}`, image: user.avatarUrl || null },
users: [{ username: `${user.username}`, name: `${user.name}` }],
}}
nextSeoProps={{
noindex: !allowSEOIndexing,
nofollow: !allowSEOIndexing,
}}
/>
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<UserAvatar
size="xl"
user={{
avatarUrl: user.avatarUrl,
profile: user.profile,
name: profile.name,
username: profile.username,
}}
/>
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
{profile.name}
{!isOrg && user.verified && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
/>
)}
{isOrg && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
/>
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<UserAvatar
size="xl"
user={{
avatarUrl: user.avatarUrl,
profile: user.profile,
name: profile.name,
username: profile.username,
}}
/>
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
{profile.name}
{!isOrg && user.verified && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
/>
)}
</div>
{isOrg && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
/>
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
)}
</div>
<div
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
data-testid="event-types">
{eventTypes.map((type) => (
<Link
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
prefetch={false}
href={{
pathname: `/${user.profile.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
data-testid="event-type-link">
<Icon
name="arrow-right"
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<div className="flex flex-wrap items-center">
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
<div
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
data-testid="event-types">
{eventTypes.map((type) => (
<Link
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
prefetch={false}
href={{
pathname: `/${user.profile.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
data-testid="event-type-link">
<Icon
name="arrow-right"
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<div className="flex flex-wrap items-center">
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
</Link>
))}
</div>
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
</div>
</Link>
))}
</div>
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
</main>
<Toaster position="bottom-right" />
</div>
</>
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
</main>
<Toaster position="bottom-right" />
</div>
);
}
UserPage.isBookingPage = true;
@@ -1,13 +1,12 @@
"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 { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "app/WithEmbedSSR";
import type { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
@@ -23,40 +22,11 @@ export const getMultipleDurationValue = (
return defaultValue;
};
function Type({
slug,
user,
isEmbed,
booking,
isBrandingHidden,
isSEOIndexable,
rescheduleUid,
eventData,
orgBannerUrl,
}: PageProps) {
function Type({ slug, user, isEmbed, booking, isBrandingHidden, eventData, orgBannerUrl }: PageProps) {
const searchParams = useSearchParams();
const { profile, users, hidden, title } = eventData;
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
isSEOIndexable={isSEOIndexable ?? true}
eventData={
profile && users && title && hidden !== undefined
? {
profile,
users,
title,
hidden,
}
: undefined
}
entity={eventData.entity}
bookingData={booking}
/>
<Booker
username={user}
eventSlug={slug}