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 JooandHariom Balhara authored and GitHub committed 2025-01-03 22:26:30 +00:00
1 parent 39173900b1
commit 2f9c8cb441
60 files changed
+651 -825

No files matched your search

@@ -1,94 +0,0 @@
import { render } from "@testing-library/react";
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { trpc } from "@calcom/trpc/react";
import { HeadSeo } from "@calcom/ui";
import { BookerSeo } from "./BookerSeo";
// Mocking necessary modules and hooks
vi.mock("@calcom/trpc/react", () => ({
trpc: {
viewer: {
public: {
event: {
useQuery: vi.fn(),
},
},
},
},
}));
vi.mock("@calcom/lib/hooks/useLocale", () => ({
useLocale: () => ({ t: (key: string) => key }),
}));
vi.mock("@calcom/ee/organizations/lib/orgDomains", () => ({
getOrgFullOrigin: vi.fn(),
}));
vi.mock("@calcom/ui", () => ({
HeadSeo: vi.fn(),
}));
describe("BookerSeo Component", () => {
it("renders HeadSeo with correct props", () => {
const mockData = {
event: {
slug: "event-slug",
profile: { name: "John Doe", image: "image-url", username: "john" },
title: "30min",
hidden: false,
users: [{ name: "Jane Doe", username: "jane" }],
},
entity: { fromRedirectOfNonOrgLink: false, orgSlug: "org1" },
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
trpc.viewer.public.event.useQuery.mockReturnValueOnce({
data: mockData.event,
});
vi.mocked(getOrgFullOrigin).mockImplementation((text: string | null) => `${text}.cal.local`);
render(
<BookerSeo
username={mockData.event.profile.username}
eventSlug={mockData.event.slug}
rescheduleUid={undefined}
entity={mockData.entity}
/>
);
expect(HeadSeo).toHaveBeenCalledWith(
{
origin: `${mockData.entity.orgSlug}.cal.local`,
isBrandingHidden: undefined,
// Don't know why we are adding space in the beginning
title: ` ${mockData.event.title} | ${mockData.event.profile.name}`,
description: ` ${mockData.event.title}`,
meeting: {
profile: {
name: mockData.event.profile.name,
image: mockData.event.profile.image,
},
title: mockData.event.title,
users: [
{
name: mockData.event.users[0].name,
username: mockData.event.users[0].username,
},
],
},
nextSeoProps: {
nofollow: true,
noindex: true,
},
},
{}
);
});
});
@@ -1,88 +0,0 @@
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { HeadSeo } from "@calcom/ui";
interface BookerSeoProps {
username: string;
eventSlug: string;
rescheduleUid: string | undefined;
hideBranding?: boolean;
isSEOIndexable?: boolean;
isTeamEvent?: boolean;
eventData?: Omit<
Pick<NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>, "profile" | "title" | "users" | "hidden">,
"profile" | "users"
> & {
profile: {
image: string | undefined;
name: string | null;
username: string | null;
};
users: {
username: string;
name: string;
}[];
};
entity: {
fromRedirectOfNonOrgLink: boolean;
orgSlug?: string | null;
teamSlug?: string | null;
name?: string | null;
};
bookingData?: GetBookingType | null;
}
export const BookerSeo = (props: BookerSeoProps) => {
const {
eventSlug,
username,
rescheduleUid,
hideBranding,
isTeamEvent,
entity,
isSEOIndexable,
bookingData,
eventData,
} = props;
const { t } = useLocale();
const { data: _event } = trpc.viewer.public.event.useQuery(
{
username,
eventSlug,
isTeamEvent,
org: entity.orgSlug ?? null,
fromRedirectOfNonOrgLink: entity.fromRedirectOfNonOrgLink,
},
{ refetchOnWindowFocus: false, enabled: !eventData }
);
const event = eventData ?? _event;
const profileName = event?.profile.name ?? "";
const profileImage = event?.profile.image;
const title = event?.title ?? "";
return (
<HeadSeo
origin={getOrgFullOrigin(entity.orgSlug ?? null)}
title={`${rescheduleUid && !!bookingData ? t("reschedule") : ""} ${title} | ${profileName}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${title}`}
meeting={{
title: title,
profile: { name: profileName, image: profileImage },
users: [
...(event?.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
}}
nextSeoProps={{
nofollow: event?.hidden || !isSEOIndexable,
noindex: event?.hidden || !isSEOIndexable,
}}
isBrandingHidden={hideBranding}
/>
);
};
+1 -1
View File
@@ -178,7 +178,7 @@ export const getUsernameList = (users: string | string[] | undefined): string[]
// So, even though this code handles even if individual user is dynamic link, that isn't a possibility right now.
users = arrayCast(users);
const allUsers = users.map((user) => user.replace(/( |%20|%2b)/g, "+").split("+")).flat();
const allUsers = users.map((user) => user.replace(/( |%20|%2b)/gi, "+").split("+")).flat();
return Array.prototype.concat(...allUsers.map((userSlug) => slugify(userSlug)));
};
+13
View File
@@ -800,6 +800,19 @@ export class UserRepository {
return withSelectedCalendars(user);
}
static async getAvatarUrl(id: number) {
const user = await prisma.user.findUnique({
where: { id },
select: { avatarUrl: true },
});
if (!user?.avatarUrl) {
return null;
}
return user.avatarUrl;
}
static async findForAvailabilityCheck({ where }: { where: Prisma.UserWhereInput }) {
const user = await prisma.user.findFirst({
where,
+2 -15
View File
@@ -2,8 +2,8 @@ import type { NextSeoProps } from "next-seo";
import { NextSeo } from "next-seo";
import { usePathname } from "next/navigation";
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
import type { AppImageProps } from "@calcom/lib/OgImages";
import { constructAppImage, constructGenericImage } from "@calcom/lib/OgImages";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { buildCanonical, getSeoImage, seoConfig } from "@calcom/lib/next-seo.config";
import { truncateOnWord } from "@calcom/lib/text";
@@ -16,7 +16,6 @@ export type HeadSeoProps = {
canonical?: string;
nextSeoProps?: NextSeoProps;
app?: AppImageProps;
meeting?: MeetingImageProps;
isBrandingHidden?: boolean;
origin?: string;
};
@@ -83,7 +82,6 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
canonical = defaultUrl,
nextSeoProps = {},
app,
meeting,
isBrandingHidden,
} = props;
@@ -98,17 +96,6 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
siteName,
});
if (meeting) {
const pageImage = getSeoImage("ogImage") + constructMeetingImage(meeting);
seoObject = buildSeoMeta({
title: pageTitle,
description: truncatedDescription,
image: pageImage,
canonical,
siteName,
});
}
if (app) {
const pageImage =
getSeoImage("ogImage") + constructAppImage({ ...app, description: truncatedDescription });
@@ -45,9 +45,6 @@ vi.mock("@calcom/lib/OgImages", async () => {
constructGenericImage() {
return "constructGenericImage";
},
constructMeetingImage() {
return "constructMeetingImage";
},
};
});
@@ -149,12 +146,4 @@ describe("Tests for HeadSeo component", () => {
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructAppImage");
});
});
test("Should render with meeting props", async () => {
const { container } = render(<HeadSeo {...basicProps} meeting={{} as HeadSeoProps["meeting"]} />);
await waitFor(async () => {
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructMeetingImage");
});
});
});