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:
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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)));
|
||||
};
|
||||
|
||||
|
||||
@@ -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,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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user