From 113dc5fca6be628af4a46891a6ff121d86f7038a Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Mon, 13 Jan 2025 06:35:10 -0500 Subject: [PATCH] chore: try using custom 404 in pages/_error (#18622) --- apps/web/components/error/404-page.tsx | 246 +++++++++++++++++++++++++ apps/web/middleware.ts | 6 - apps/web/pages/_error.tsx | 119 +----------- 3 files changed, 254 insertions(+), 117 deletions(-) create mode 100644 apps/web/components/error/404-page.tsx diff --git a/apps/web/components/error/404-page.tsx b/apps/web/components/error/404-page.tsx new file mode 100644 index 0000000000..aa5af61d11 --- /dev/null +++ b/apps/web/components/error/404-page.tsx @@ -0,0 +1,246 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; + +import { + getOrgDomainConfigFromHostname, + subdomainSuffix, +} from "@calcom/features/ee/organizations/lib/orgDomains"; +import { DOCS_URL, IS_CALCOM, WEBSITE_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { HeadSeo } from "@calcom/ui"; +import { Icon } from "@calcom/ui"; + +import PageWrapper from "@components/PageWrapper"; + +enum pageType { + ORG = "org", + TEAM = "team", + USER = "user", + OTHER = "other", +} + +export default function Custom404() { + const pathname = usePathname(); + const { t } = useLocale(); + const [username, setUsername] = useState(""); + const [currentPageType, setCurrentPageType] = useState(pageType.USER); + + const links = [ + { + title: t("enterprise"), + description: "Learn more about organizations and subdomains in our enterprise plan.", + icon: "shield" as const, + href: `${WEBSITE_URL}/enterprise`, + }, + { + title: t("documentation"), + description: t("documentation_description"), + icon: "file-text" as const, + href: DOCS_URL, + }, + { + title: t("blog"), + description: t("blog_description"), + icon: "book-open" as const, + href: `${WEBSITE_URL}/blog`, + }, + ]; + + const [url, setUrl] = useState(`${WEBSITE_URL}/signup`); + useEffect(() => { + const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({ + hostname: window.location.host, + }); + + const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? []; + if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) { + const splitPath = routerUsername.split("/"); + if (splitPath[1] === "team" && splitPath.length === 3) { + // Accessing a non-existent team + setUsername(splitPath[2]); + setCurrentPageType(pageType.TEAM); + setUrl( + `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}` + ); + } else { + setUsername(routerUsername); + setUrl(`${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`); + } + } else { + setUsername(currentOrgDomain ?? ""); + setCurrentPageType(pageType.ORG); + setUrl( + `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${ + currentOrgDomain?.replace("/", "") ?? "" + }` + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const isSuccessPage = pathname?.startsWith("/booking"); + const isSubpage = pathname?.includes("/", 2) || isSuccessPage; + + /** + * If we're on 404 and the route is insights it means it is disabled + * TODO: Abstract this for all disabled features + **/ + const isInsights = pathname?.startsWith("/insights"); + if (isInsights) { + return ( + <> + +
+
+
+

{t("error_404")}

+

+ Feature is currently disabled +

+
+
+
+ + {t("or_go_back_home")} + + +
+
+
+
+ + ); + } + + if (!username) return null; + + return ( + <> + +
+
+
+

{t("error_404")}

+

+ {isSuccessPage ? "Booking not found" : t("page_doesnt_exist")} +

+ {isSubpage && currentPageType !== pageType.TEAM ? ( + {t("check_spelling_mistakes_or_go_back")} + ) : IS_CALCOM ? ( + + {t(`404_the_${currentPageType.toLowerCase()}`)}{" "} + {username} {t("is_still_available")}{" "} + {t("register_now")}. + + ) : ( + + {t(`404_the_${currentPageType.toLowerCase()}`)}{" "} + {username}{" "} + {t("is_still_available")} + + )} +
+
+ {((!isSubpage && IS_CALCOM) || + currentPageType === pageType.ORG || + currentPageType === pageType.TEAM) && ( + + )} +

+ {t("popular_pages")} +

+ +
+ + {t("or_go_back_home")} + + +
+
+
+
+ + ); +} + +Custom404.PageWrapper = PageWrapper; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index c6a1098260..d1d6c3b3fe 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -22,12 +22,6 @@ const safeGet = async (key: string): Promise => { const middleware = async (req: NextRequest): Promise> => { const url = req.nextUrl; const requestHeaders = new Headers(req.headers); - const response = NextResponse.next(); - - if (response.headers.get("x-pages-router-error") === "true") { - return NextResponse.rewrite(new URL("/_not-found", req.url)); - } - requestHeaders.set("x-url", req.url); if (!url.pathname.startsWith("/api")) { diff --git a/apps/web/pages/_error.tsx b/apps/web/pages/_error.tsx index 76089811d6..d5e753d1c7 100644 --- a/apps/web/pages/_error.tsx +++ b/apps/web/pages/_error.tsx @@ -1,115 +1,12 @@ -/** - * Typescript class based component for custom-error - * @link https://nextjs.org/docs/advanced-features/custom-error-page - */ -import type { NextPage, NextPageContext } from "next"; -import type { ErrorProps } from "next/error"; -import NextError from "next/error"; -import React from "react"; +import Custom404 from "@components/error/404-page"; -import { getErrorFromUnknown } from "@calcom/lib/errors"; -import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; -import { redactError } from "@calcom/lib/redactError"; - -import { ErrorPage } from "@components/error/error-page"; - -// Adds HttpException to the list of possible error types. -type AugmentedError = (NonNullable & HttpError) | null; -type CustomErrorProps = { - err?: AugmentedError; - message?: string; - hasGetInitialPropsRun?: boolean; -} & Omit; - -type AugmentedNextPageContext = Omit & { - err: AugmentedError; -}; - -const log = logger.getSubLogger({ prefix: ["[error]"] }); - -const CustomError: NextPage = (props) => { - const { statusCode, err, message, hasGetInitialPropsRun } = props; - - if (!hasGetInitialPropsRun && err) { - // getInitialProps is not called in case of - // https://github.com/vercel/next.js/issues/8592. As a workaround, we pass - // err via _app.tsx so it can be captured - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const e = getErrorFromUnknown(err); - // can be captured here - // e.g. Sentry.captureException(e); - } - return ; -}; - -/** - * Partially adapted from the example in - * https://github.com/vercel/next.js/tree/canary/examples/with-sentry - */ -CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => { - const { res, err, asPath } = ctx; - const errorInitialProps = (await NextError.getInitialProps({ - res, - err, - } as NextPageContext)) as CustomErrorProps; - - // Workaround for https://github.com/vercel/next.js/issues/8592, mark when - // getInitialProps has run - errorInitialProps.hasGetInitialPropsRun = true; - - // If a HttpError message, let's override defaults - if (err instanceof HttpError) { - const redactedError = redactError(err); - errorInitialProps.statusCode = err.statusCode; - errorInitialProps.title = redactedError.name; - errorInitialProps.message = redactedError.message; - errorInitialProps.err = { - ...redactedError, - url: err.url, - statusCode: err.statusCode, - cause: err.cause, - method: err.method, - }; - } - - if (res) { - // Running on the server, the response object is available. - // - // Next.js will pass an err on the server if a page's `getInitialProps` - // threw or returned a Promise that rejected - - // Overrides http status code if present in errorInitialProps - res.statusCode = errorInitialProps.statusCode; - - log.debug(`server side logged this: ${err?.toString() ?? JSON.stringify(err)}`); - log.info("return props, ", errorInitialProps); - - res.setHeader("x-pages-router-error", "true"); - - return errorInitialProps; - } else { - // Running on the client (browser). - // - // Next.js will provide an err if: - // - // - a page's `getInitialProps` threw or returned a Promise that rejected - // - an exception was thrown somewhere in the React lifecycle (render, - // componentDidMount, etc) that was caught by Next.js's React Error - // Boundary. Read more about what types of exceptions are caught by Error - // Boundaries: https://reactjs.org/docs/error-boundaries.html - if (err) { - log.info("client side logged this", err); - return errorInitialProps; - } - } - - // If this point is reached, getInitialProps was called without any - // information about what the error might be. This is unexpected and may - // indicate a bug introduced in Next.js - new Error(`_error.tsx getInitialProps missing data at path: ${asPath}`); - - return errorInitialProps; +const CustomError = () => { + return ; }; export default CustomError; + +// `pages/_error` needs to exist until all routes are +// fully migrated to App Router. Dynamic routes in Pages Router +// like `/[user]/[type]` or `/team/[slug]` cannot use App Router's +// error handling