chore: migrate /apps/routing-forms/[...pages] to App Router (#18956)

* wip

* fix

* fix

* refactor

* rename to routingServerSidePropsConfig

* refactor

* refactor

* add routing-link to useIsBookingPage hook

* remove Head component

* fix

* redirect user to apps/routing-forms/forms if user goes to apps/routing-forms

* refactor

* remove log

* remove client component not needed

* clean up code

* remove unneeded metadata

* clean up further

* clean up further 2

* fix type check

* routing-link does not need shell

* Fix title for Routing Form public link and also remove any

* Remove ; in HTML

* Remove unnecessary page reload

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
This commit is contained in:
Benny JooandHariom Balhara authored and GitHub committed 2025-02-17 11:15:54 +05:30
1 parent fd651133c2
commit dc7a1c4680
24 files changed
+147 -434

No files matched your search

@@ -0,0 +1,55 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as ServerPageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import type { routingServerSidePropsConfig } from "@calcom/app-store/routing-forms/pages/app-routing.config";
import LayoutHandler from "@calcom/app-store/routing-forms/pages/layout-handler/[...appPages]";
import Shell from "@calcom/features/shell/Shell";
import { getServerSideProps } from "@lib/apps/routing-forms/[...pages]/getServerSideProps";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
const normalizePages = (pages: string[] | string | undefined) => {
const normalizedPages = Array.isArray(pages) ? pages : pages?.split("/") ?? [];
return {
mainPage: normalizedPages[0],
subPages: normalizedPages.slice(1),
};
};
export const generateMetadata = async ({ params }: { params: { pages: string[] } }) => {
const { mainPage } = normalizePages(params.pages);
return await _generateMetadata(
// TODO: Need to show the actual form name instead of "Form"
(t) => (mainPage === "routing-link" ? `Form | Cal.com Forms` : `${t("routing_forms")} | Cal.com Forms`),
() => ""
);
};
type GetServerSidePropsResult =
(typeof routingServerSidePropsConfig)[keyof typeof routingServerSidePropsConfig];
const getData = withAppDirSsr<GetServerSidePropsResult>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(context);
const { mainPage, subPages } = normalizePages(params.pages);
const componentProps = {
...props,
pages: subPages,
};
if (mainPage === "routing-link") {
return <LayoutHandler {...componentProps} />;
}
return (
<Shell withoutMain withoutSeo>
<LayoutHandler {...componentProps} />
</Shell>
);
};
export default ServerPage;
@@ -0,0 +1,7 @@
import { redirect } from "next/navigation";
const ServerPage = async () => {
redirect("/apps/routing-forms/forms");
};
export default ServerPage;
@@ -1,45 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsResult } from "next";
import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import z from "zod";
import { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import LegacyPage, { getLayout } from "~/apps/[slug]/[...pages]/pages-view";
const paramsSchema = z.object({
slug: z.string(),
pages: z.array(z.string()),
});
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const p = paramsSchema.safeParse(params);
if (!p.success) {
return notFound();
}
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
const data = await getData(legacyContext);
const form = "form" in data ? (data.form as { name?: string; description?: string }) : null;
const formName = form?.name;
const formDescription = form?.description;
return await _generateMetadata(
(t) => formName ?? t("routing_forms"),
(t) => formDescription ?? t("routing_forms_description")
);
};
const getData = withAppDirSsr<GetServerSidePropsResult<any>>(getServerSideProps);
export default WithLayout({
getLayout,
getData,
Page: LegacyPage,
});
@@ -1,135 +0,0 @@
import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
import { z } from "zod";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import type { AppProps } from "@lib/app-providers";
import { ssrInit } from "@server/lib/ssr";
type AppPageType = {
getServerSideProps?: AppGetServerSideProps;
// A component than can accept any properties
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: ((props: any) => JSX.Element) &
Pick<AppProps["Component"], "isBookingPage" | "getLayout" | "PageWrapper">;
};
type Found = {
notFound: false;
Component: AppPageType["default"];
getServerSideProps: AppPageType["getServerSideProps"];
};
type NotFound = {
notFound: true;
};
// TODO: It is a candidate for apps.*.generated.*
const AppsRouting = {
"routing-forms": RoutingFormsRoutingConfig,
typeform: TypeformRoutingConfig,
};
function getRoute(appName: string, pages: string[]) {
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record<string, AppPageType>;
if (!routingConfig) {
return {
notFound: true,
} as NotFound;
}
const mainPage = pages[0];
const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType);
if (!appPage) {
return {
notFound: true,
} as NotFound;
}
return { notFound: false, Component: appPage.default, ...appPage } as Found;
}
const paramsSchema = z.object({
slug: z.string(),
pages: z.array(z.string()),
});
export async function getServerSideProps(
context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<any>> {
const { params, req } = context;
if (!params) {
return {
notFound: true,
};
}
const parsedParams = paramsSchema.safeParse(params);
if (!parsedParams.success) {
return {
notFound: true,
};
}
const appName = parsedParams.data.slug;
const pages = parsedParams.data.pages;
const route = getRoute(appName, pages);
if (route.notFound) {
return { notFound: true };
}
if (route.getServerSideProps) {
// TODO: Document somewhere that right now it is just a convention that filename should have appPages in it's name.
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
params.appPages = pages.slice(1);
const session = await getServerSession({ req });
const user = session?.user;
const app = await getAppWithMetadata({ slug: appName });
if (!app) {
return {
notFound: true,
};
}
const result = await route.getServerSideProps(
context as GetServerSidePropsContext<{
slug: string;
pages: string[];
appPages: string[];
}>,
prisma,
user,
ssrInit
);
if (result.notFound) {
return { notFound: true };
}
if (result.redirect) {
return { redirect: result.redirect };
}
return {
props: {
appName,
appUrl: app.simplePath || `/apps/${appName}`,
...result.props,
},
};
} else {
return {
props: {
appName,
},
};
}
}
@@ -0,0 +1,62 @@
import type { GetServerSidePropsResult, GetServerSidePropsContext } from "next";
import { z } from "zod";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import { routingServerSidePropsConfig } from "@calcom/app-store/routing-forms/pages/app-routing.config";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import type { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps";
import { ssrInit } from "@server/lib/ssr";
const paramsSchema = z.object({
pages: z.array(z.string()),
});
export async function getServerSideProps(
context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<any>> {
const { params, req } = context;
const parsedParams = paramsSchema.safeParse(params);
if (!parsedParams.success) {
return {
notFound: true,
};
}
const pages = parsedParams.data.pages;
const mainPage = pages[0];
const getServerSideProps = routingServerSidePropsConfig[mainPage];
if (!getServerSideProps) {
return { notFound: true };
}
const session = await getServerSession({ req });
const user = session?.user;
const app = await getAppWithMetadata({ slug: "routing-forms" });
if (!app) {
return {
notFound: true,
};
}
const result = await getServerSideProps(context as AppGetServerSidePropsContext, prisma, user, ssrInit);
if (result.notFound) {
return { notFound: true };
}
if (result.redirect) {
return { redirect: result.redirect };
}
return {
props: {
appUrl: app.simplePath || `/apps/routing-forms`,
...result.props,
},
};
}
+8 -3
View File
@@ -4,9 +4,14 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
export default function useIsBookingPage(): boolean {
const pathname = usePathname();
const isBookingPage = ["/booking", "/cancel", "/reschedule", "/d"].some((route) =>
pathname?.startsWith(route)
);
const isBookingPage = [
"/booking",
"/cancel",
"/reschedule",
"/d", // Private Link of booking page
"/apps/routing-forms/routing-link", // Routing Form page
"/forms/", // Rewrites to /apps/routing-forms/routing-link
].some((route) => pathname?.startsWith(route));
const searchParams = useCompatSearchParams();
const userParam = Boolean(searchParams?.get("user"));
+1 -12
View File
@@ -155,22 +155,11 @@ export const config = {
"/api/auth/signup",
"/api/trpc/:path*",
"/login",
"/apps/:path*",
"/auth/:path*",
/**
* Paths required by routingForms.handle
*/
"/apps/routing_forms/:path*",
"/event-types/:path*",
"/apps/installed/:category/",
"/apps/installation/:path*",
"/apps/:slug/",
"/apps/:slug/setup/",
"/apps/categories/",
"/apps/categories/:category/",
"/workflows/:path*",
"/getting-started/:path*",
"/apps",
"/bookings/:path*",
"/video/:path*",
"/teams/:path*",
@@ -1,102 +0,0 @@
"use client";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import type { AppProps } from "@lib/app-providers";
import type { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps";
type AppPageType = {
getServerSideProps: AppGetServerSideProps;
// A component than can accept any properties
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: ((props: any) => JSX.Element) &
Pick<AppProps["Component"], "isBookingPage" | "getLayout" | "PageWrapper">;
};
type Found = {
notFound: false;
Component: AppPageType["default"];
getServerSideProps: AppPageType["getServerSideProps"];
};
type NotFound = {
notFound: true;
};
// TODO: It is a candidate for apps.*.generated.*
const AppsRouting = {
"routing-forms": RoutingFormsRoutingConfig,
typeform: TypeformRoutingConfig,
};
function getRoute(appName: string, pages: string[]) {
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as unknown as Record<
string,
AppPageType
>;
if (!routingConfig) {
return {
notFound: true,
} as NotFound;
}
const mainPage = pages[0];
const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType);
if (!appPage) {
return {
notFound: true,
} as NotFound;
}
return { notFound: false, Component: appPage.default, ...appPage } as Found;
}
export type PageProps = inferSSRProps<typeof getServerSideProps>;
const AppPage: AppPageType["default"] = function AppPage(props: PageProps) {
const appName = props.appName;
const params = useParamsWithFallback();
const pages = Array.isArray(params.pages) ? params.pages : params.pages?.split("/") ?? [];
const route = getRoute(appName, pages);
const componentProps = {
...props,
pages: pages.slice(1),
};
if (!route || route.notFound) {
throw new Error("Route can't be undefined");
}
return <route.Component {...componentProps} />;
};
AppPage.isBookingPage = ({ router }) => {
const route = getRoute(router.query.slug as string, router.query.pages as string[]);
if (route.notFound) {
return false;
}
const isBookingPage = route.Component.isBookingPage;
if (typeof isBookingPage === "function") {
return isBookingPage({ router });
}
return !!isBookingPage;
};
export const getLayout: NonNullable<(typeof AppPage)["getLayout"]> = (page) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { slug, pages } = useParamsWithFallback();
const route = getRoute(slug as string, pages as string[]);
if (route.notFound) {
return null;
}
if (!route.Component.getLayout) {
return page;
}
return route.Component.getLayout(page);
};
export default AppPage;
-15
View File
@@ -1,15 +0,0 @@
import { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps";
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/apps/[slug]/[...pages]/pages-view";
import PagesView, { getLayout } from "~/apps/[slug]/[...pages]/pages-view";
const Page = (props: PageProps) => <PagesView {...props} />;
Page.PageWrapper = PageWrapper;
Page.getLayout = getLayout;
export { getServerSideProps };
export default Page;
@@ -770,7 +770,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF
</div>
}
subtitle={form.description || ""}
backPath={`/${appUrl}/forms`}
backPath={`${appUrl}/forms`}
CTA={<Actions form={form} mutation={mutation} />}>
<div className="-mx-4 mt-4 px-4 sm:px-6 md:-mx-8 md:mt-0 md:px-8">
<div className="flex flex-col items-center items-baseline md:flex-row md:items-start">
@@ -30,8 +30,9 @@ export const getServerSidePropsForSingleFormView = async function getServerSideP
notFound: true,
};
}
const formId = params.appPages[0];
if (!formId || params.appPages.length > 1) {
const appPages = params.pages.slice(1);
const formId = appPages[0];
if (!formId || appPages.length > 1) {
return {
notFound: true,
};
@@ -7,7 +7,6 @@ import * as forms from "./forms/[...appPages]";
// extracts getServerSideProps function from the client component
import { getServerSideProps as getServerSidePropsForms } from "./forms/getServerSideProps";
import * as IncompleteBooking from "./incomplete-booking/[...appPages]";
import * as LayoutHandler from "./layout-handler/[...appPages]";
import * as Reporting from "./reporting/[...appPages]";
import * as RouteBuilder from "./route-builder/[...appPages]";
import * as RoutingLink from "./routing-link/[...appPages]";
@@ -19,11 +18,10 @@ const routingConfig = {
forms: forms,
"routing-link": RoutingLink,
reporting: Reporting,
layoutHandler: LayoutHandler,
"incomplete-booking": IncompleteBooking,
};
export const serverSidePropsConfig: Record<string, AppGetServerSideProps> = {
export const routingServerSidePropsConfig: Record<string, AppGetServerSideProps> = {
forms: getServerSidePropsForms,
"form-edit": getServerSidePropsSingleForm,
"route-builder": getServerSidePropsSingleForm,
@@ -6,7 +6,6 @@ import type { UseFormReturn } from "react-hook-form";
import { Controller, useFieldArray, useWatch } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import Shell from "@calcom/features/shell/Shell";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import {
@@ -479,11 +478,3 @@ export default function FormEditPage({
/>
);
}
FormEditPage.getLayout = (page: React.ReactElement) => {
return (
<Shell backPath="/apps/routing-forms/forms" withoutMain={true}>
{page}
</Shell>
);
};
@@ -9,7 +9,7 @@ import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/Skeleto
import { FilterResults } from "@calcom/features/filters/components/FilterResults";
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import Shell, { ShellMain } from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { WEBAPP_URL } from "@calcom/lib/constants";
import useApp from "@calcom/lib/hooks/useApp";
@@ -346,18 +346,4 @@ export default function RoutingForms({
);
}
const ShellContainer = ({ page }: { page: React.ReactElement }) => {
const { t } = useLocale();
return (
<Shell title={t("routing_forms")} description={t("routing_forms_description")} withoutMain={true}>
{page}
</Shell>
);
};
RoutingForms.getLayout = (page: React.ReactElement) => {
return <ShellContainer page={page} />;
};
export { getServerSideProps };
@@ -6,7 +6,6 @@ import type z from "zod";
import { WhenToWriteToRecord, SalesforceFieldType } from "@calcom/app-store/salesforce/lib/enums";
import type { writeToRecordDataSchema as salesforceWriteToRecordDataSchema } from "@calcom/app-store/salesforce/zod";
import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { IncompleteBookingActionType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
@@ -320,12 +319,4 @@ export default function IncompleteBookingPage({
);
}
IncompleteBookingPage.getLayout = (page: React.ReactElement) => {
return (
<Shell backPath="/apps/routing-forms/forms" withoutMain={true}>
{page}
</Shell>
);
};
export { getServerSideProps };
@@ -35,26 +35,3 @@ export default function LayoutHandler(props: { [key: string]: unknown }) {
</FormProvider>
);
}
LayoutHandler.getLayout = (page: React.ReactElement) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const params = useParamsWithFallback();
const pageKey = Array.isArray(params.pages)
? params.pages[0]
: params.pages?.split("/")[0] ?? DEFAULT_ROUTE;
const component = getComponent(pageKey).default;
if (component && "getLayout" in component) {
return component.getLayout?.(page);
} else {
return page;
}
};
export async function getServerSideProps(
context: GetServerSidePropsContext,
...rest: GetServerSidePropsRestArgs
) {
const component = getComponent(context.params?.pages?.[0] || "");
return component.getServerSideProps?.(context, ...rest) || { props: {} };
}
@@ -10,7 +10,6 @@ import type {
} from "react-awesome-query-builder";
import { Builder, Query, Utils as QbUtils } from "react-awesome-query-builder";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { downloadAsCsv, sanitizeValue } from "@calcom/lib/csvUtils";
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
@@ -291,11 +290,3 @@ export default function ReporterWrapper({
/>
);
}
ReporterWrapper.getLayout = (page: React.ReactElement) => {
return (
<Shell backPath="/apps/routing-forms/forms" withoutMain={true}>
{page}
</Shell>
);
};
@@ -9,7 +9,6 @@ import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-bu
import type { JsonTree } from "react-awesome-query-builder";
import type { UseFormReturn } from "react-hook-form";
import Shell from "@calcom/features/shell/Shell";
import { areTheySiblingEntitites } from "@calcom/lib/entityPermissionUtils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { buildEmptyQueryValue, raqbQueryValueUtils } from "@calcom/lib/raqb/raqbUtils";
@@ -1243,12 +1242,4 @@ export default function RouteBuilder({
);
}
RouteBuilder.getLayout = (page: React.ReactElement) => {
return (
<Shell backPath="/apps/routing-forms/forms" withoutMain={true}>
{page}
</Shell>
);
};
export { getServerSideProps };
@@ -1,6 +1,5 @@
"use client";
import Head from "next/head";
import { useRouter } from "next/navigation";
import type { FormEvent } from "react";
import { useEffect, useRef, useState } from "react";
@@ -163,9 +162,6 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
<div>
{!customPageMessage ? (
<>
<Head>
<title>{`${form.name} | Cal.com Forms`}</title>
</Head>
<div className={classNames("mx-auto my-0 max-w-3xl", isEmbed ? "" : "md:my-24")}>
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
<div className="main border-booker md:border-booker-width dark:bg-muted bg-default mx-0 rounded-md p-4 py-6 sm:-mx-4 sm:px-8 ">
@@ -213,8 +209,6 @@ export default function RoutingLink(props: inferSSRProps<typeof getServerSidePro
return <RoutingForm {...props} />;
}
RoutingLink.isBookingPage = true;
export { getServerSideProps };
const usePrefilledResponse = (form: Props["form"]) => {
@@ -15,18 +15,16 @@ export const getServerSideProps = async function getServerSideProps(
notFound: true,
};
}
const formId = params.appPages[0];
if (!formId || params.appPages.length > 2) {
const appPages = params.pages.slice(1);
const formId = appPages[0];
if (!formId || appPages.length > 2) {
return {
notFound: true,
};
}
const { currentOrgDomain } = orgDomainConfig(context.req);
const isEmbed = params.appPages[1] === "embed";
if (context.query["flag.coep"] === "true") {
context.res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
}
const isEmbed = appPages[1] === "embed";
const form = await prisma.app_RoutingForms_Form.findFirst({
where: {
@@ -1,10 +0,0 @@
"use client";
//TODO: Generate this file automatically so that like in Next.js file based routing can work automatically
import * as HowToUse from "./how-to-use/[...appPages]";
const routingConfig = {
"how-to-use": HowToUse,
};
export default routingConfig;
+3 -12
View File
@@ -152,18 +152,9 @@ export function ShellMain(props: LayoutProps) {
variant="icon"
size="sm"
color="minimal"
onClick={() => {
if (typeof props.backPath === "string") {
// Prevents weird crash when navigating from /routing to /routing/forms
if (props.backPath.startsWith("/routing")) {
window.location.href = props.backPath;
} else {
router.push(props.backPath as string);
}
} else {
router.back();
}
}}
onClick={() =>
typeof props.backPath === "string" ? router.push(props.backPath as string) : router.back()
}
StartIcon="arrow-left"
aria-label="Go Back"
className="rounded-md ltr:mr-2 rtl:ml-2"
@@ -57,13 +57,6 @@ export const NavigationItem: React.FC<{
<Link
data-test-id={item.name}
href={item.href}
onClick={(e) => {
// Prevents weird crash when navigating from /routing to /routing/forms
if (item.href.startsWith("/routing")) {
e.preventDefault();
window.location.href = item.href;
}
}}
aria-label={t(item.name)}
target={item.target}
className={classNames(
+1 -1
View File
@@ -8,7 +8,7 @@ import type { ssrInit } from "@server/lib/ssr";
export type AppUser = CalendsoSessionUser | undefined;
export type AppPrisma = typeof prisma;
export type AppGetServerSidePropsContext = GetServerSidePropsContext<{
appPages: string[];
pages: string[];
}>;
export type AppSsrInit = ssrInit;
export type AppGetServerSideProps = (