Skip to content

Commit

Permalink
Fix 404 page generation + refactor all layouts SSG/SSR (#346)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadorequest authored May 28, 2021
1 parent 5fe1519 commit c7b0165
Show file tree
Hide file tree
Showing 55 changed files with 918 additions and 538 deletions.
182 changes: 109 additions & 73 deletions src/layouts/core/coreLayoutSSG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { CommonServerSideParams } from '@/app/types/CommonServerSideParams';
import { StaticPath } from '@/app/types/StaticPath';
import { StaticPathsOutput } from '@/app/types/StaticPathsOutput';
import { StaticPropsInput } from '@/app/types/StaticPropsInput';
import {
GetCoreLayoutStaticPaths,
GetCoreLayoutStaticPathsOptions,
} from '@/layouts/core/types/GetCoreLayoutStaticPaths';
import {
GetCoreLayoutStaticProps,
GetCoreLayoutStaticPropsOptions,
} from '@/layouts/core/types/GetCoreLayoutStaticProps';
import { SSGPageProps } from '@/layouts/core/types/SSGPageProps';
import { getCustomer } from '@/modules/core/airtable/dataset';
import { getAirtableDataset } from '@/modules/core/airtable/getAirtableDataset';
Expand Down Expand Up @@ -29,99 +37,127 @@ import {
GetStaticPropsResult,
} from 'next';

const fileLabel = 'layouts/demo/demoLayoutSSG';
const fileLabel = 'layouts/core/coreLayoutSSG';
const logger = createLogger({
fileLabel,
});

/**
* Only executed on the server side at build time.
* Computes all static paths that should be available for all SSG pages.
* Necessary when a page has dynamic routes and uses "getStaticProps", in order to build the HTML pages.
*
* You can use "fallback" option to avoid building all page variants and allow runtime fallback.
*
* Meant to avoid code duplication between pages sharing the same layout.
* Can be overridden for per-page customisation (e.g: deepmerge).
*
* XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly.
* Returns a "getStaticPaths" function.
*
* @return Static paths that will be used by "getCoreStaticProps" to generate pages
*
* @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation
* @param options
*/
export const getCoreStaticPaths: GetStaticPaths<CommonServerSideParams> = async (context: GetStaticPathsContext): Promise<StaticPathsOutput> => {
const preferredLocalesOrLanguages = uniq<string>(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang));
const dataset: SanitizedAirtableDataset = await getAirtableDataset(preferredLocalesOrLanguages);
const customer: AirtableRecord<Customer> = getCustomer(dataset);
export const getCoreLayoutStaticPaths: GetCoreLayoutStaticPaths = (options?: GetCoreLayoutStaticPathsOptions) => {
const {
fallback = false,
} = options || {};

/**
* Only executed on the server side at build time.
* Computes all static paths that should be available for all SSG pages.
* Necessary when a page has dynamic routes and uses "getStaticProps", in order to build the HTML pages.
*
* You can use "fallback" option to avoid building all page variants and allow runtime fallback.
*
* Meant to avoid code duplication between pages sharing the same layout.
* Can be overridden for per-page customisation (e.g: deepmerge).
*
* XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly.
*
* @return Static paths that will be used by "getCoreLayoutStaticProps" to generate pages
*
* @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation
*/
const getStaticPaths: GetStaticPaths<CommonServerSideParams> = async (context: GetStaticPathsContext): Promise<StaticPathsOutput> => {
const preferredLocalesOrLanguages = uniq<string>(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang));
const dataset: SanitizedAirtableDataset = await getAirtableDataset(preferredLocalesOrLanguages);
const customer: AirtableRecord<Customer> = getCustomer(dataset);

// Generate only pages for languages that have been allowed by the customer
const paths: StaticPath[] = map(customer?.availableLanguages, (availableLanguage: string): StaticPath => {
return {
params: {
locale: availableLanguage,
},
};
});

// Generate only pages for languages that have been allowed by the customer
const paths: StaticPath[] = map(customer?.availableLanguages, (availableLanguage: string): StaticPath => {
return {
params: {
locale: availableLanguage,
},
fallback,
paths,
};
});

return {
fallback: false,
paths,
};

return getStaticPaths;
};

/**
* Only executed on the server side at build time.
* Computes all static props that should be available for all SSG pages.
*
* Note that when a page uses "getStaticProps", then "_app:getInitialProps" is executed (if defined) but not actually used by the page,
* only the results from getStaticProps are actually injected into the page (as "SSGPageProps").
*
* Meant to avoid code duplication between pages sharing the same layout.
* Can be overridden for per-page customisation (e.g: deepmerge).
* Returns a "getStaticProps" function.
*
* XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly.
* Disables redirecting to the 404 page when building the 404 page.
*
* @return Props (as "SSGPageProps") that will be passed to the Page component, as props (known as "pageProps" in _app).
*
* @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884
* @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation
* @param options
*/
export const getCoreStaticProps: GetStaticProps<SSGPageProps, CommonServerSideParams> = async (props: StaticPropsInput): Promise<GetStaticPropsResult<SSGPageProps>> => {
const customerRef: string = process.env.NEXT_PUBLIC_CUSTOMER_REF;
const preview: boolean = props?.preview || false;
const previewData: PreviewData = props?.previewData || null;
const hasLocaleFromUrl = !!props?.params?.locale;
const locale: string = hasLocaleFromUrl ? props?.params?.locale : DEFAULT_LOCALE; // If the locale isn't found (e.g: 404 page)
const lang: string = locale.split('-')?.[0];
const bestCountryCodes: string[] = [lang, resolveFallbackLanguage(lang)];
const i18nTranslations: I18nextResources = await getLocizeTranslations(lang);
const dataset: SanitizedAirtableDataset = await getAirtableDataset(bestCountryCodes, preview);
const customer: AirtableRecord<Customer> = getCustomer(dataset);
export const getCoreLayoutStaticProps: GetCoreLayoutStaticProps = (options?: GetCoreLayoutStaticPropsOptions): GetStaticProps<SSGPageProps, CommonServerSideParams> => {
const {
enable404Redirect = true,
} = options || {};

/**
* Only executed on the server side at build time.
* Computes all static props that should be available for all SSG pages.
*
* Note that when a page uses "getStaticProps", then "_app:getInitialProps" is executed (if defined) but not actually used by the page,
* only the results from getStaticProps are actually injected into the page (as "SSGPageProps").
*
* Meant to avoid code duplication between pages sharing the same layout.
* Can be overridden for per-page customisation (e.g: deepmerge).
*
* XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly.
*
* @return Props (as "SSGPageProps") that will be passed to the Page component, as props (known as "pageProps" in _app).
*
* @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884
* @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation
*/
const getStaticProps: GetStaticProps<SSGPageProps, CommonServerSideParams> = async (props: StaticPropsInput): Promise<GetStaticPropsResult<SSGPageProps>> => {
const customerRef: string = process.env.NEXT_PUBLIC_CUSTOMER_REF;
const preview: boolean = props?.preview || false;
const previewData: PreviewData = props?.previewData || null;
const hasLocaleFromUrl = !!props?.params?.locale;
const locale: string = hasLocaleFromUrl ? props?.params?.locale : DEFAULT_LOCALE; // If the locale isn't found (e.g: 404 page)
const lang: string = locale.split('-')?.[0];
const bestCountryCodes: string[] = [lang, resolveFallbackLanguage(lang)];
const i18nTranslations: I18nextResources = await getLocizeTranslations(lang);
const dataset: SanitizedAirtableDataset = await getAirtableDataset(bestCountryCodes, preview);
const customer: AirtableRecord<Customer> = getCustomer(dataset);

// Do not serve pages using locales the customer doesn't have enabled (useful during preview mode and in development env)
if (!includes(customer?.availableLanguages, locale)) {
logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`);
// Do not serve pages using locales the customer doesn't have enabled (useful during preview mode and in development env)
if (enable404Redirect && !includes(customer?.availableLanguages, locale)) {
logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`);

return {
notFound: true,
};
}

return {
notFound: true,
// Props returned here will be available as page properties (pageProps)
props: {
bestCountryCodes,
serializedDataset: serializeSafe(dataset),
customerRef,
i18nTranslations,
hasLocaleFromUrl,
isReadyToRender: true,
isStaticRendering: true,
lang,
locale,
preview,
previewData,
},
};
}

return {
// Props returned here will be available as page properties (pageProps)
props: {
bestCountryCodes,
serializedDataset: serializeSafe(dataset),
customerRef,
i18nTranslations,
hasLocaleFromUrl,
isReadyToRender: true,
isStaticRendering: true,
lang,
locale,
preview,
previewData,
},
};

return getStaticProps;
};
141 changes: 80 additions & 61 deletions src/layouts/core/coreLayoutSSR.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { CommonServerSideParams } from '@/app/types/CommonServerSideParams';
import {
GetCoreLayoutServerSideProps,
GetCoreServerSidePropsOptions,
} from '@/layouts/core/types/GetCoreLayoutServerSideProps';
import { PublicHeaders } from '@/layouts/core/types/PublicHeaders';
import { SSRPageProps } from '@/layouts/core/types/SSRPageProps';
import { getCustomer } from '@/modules/core/airtable/dataset';
Expand Down Expand Up @@ -27,85 +31,100 @@ import {
} from 'next';
import NextCookies from 'next-cookies';

const fileLabel = 'layouts/demo/demoLayoutSSR';
const fileLabel = 'layouts/core/coreLayoutSSR';
const logger = createLogger({
fileLabel,
});

/**
* "getCoreServerSideProps" returns only part of the props expected in SSRPageProps.
* "getCoreLayoutServerSideProps" returns only part of the props expected in SSRPageProps.
* To avoid TS errors, we omit those that we don't return, and add those necessary to the "getServerSideProps" function.
*/
export type GetCoreServerSidePropsResults = SSRPageProps & {
export type GetCoreLayoutServerSidePropsResults = SSRPageProps & {
headers: PublicHeaders;
}

/**
* Only executed on the server side, for every request.
* Computes some dynamic props that should be available for all SSR pages that use getServerSideProps.
*
* Because the exact GQL query will depend on the consumer (AKA "caller"), this helper doesn't run any query by itself, but rather return all necessary props to allow the consumer to perform its own queries.
* This improves performances, by only running one GQL query instead of many (consumer's choice).
*
* Meant to avoid code duplication between pages sharing the same layout.
* Returns a "getServerSideProps" function.
*
* XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly.
* Disables redirecting to the 404 page when building the 404 page.
*
* @see https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering
* @param options
*/
export const getCoreServerSideProps: GetServerSideProps<GetCoreServerSidePropsResults, CommonServerSideParams> = async (context: GetServerSidePropsContext<CommonServerSideParams>): Promise<GetServerSidePropsResult<GetCoreServerSidePropsResults>> => {
export const getCoreLayoutServerSideProps: GetCoreLayoutServerSideProps = (options?: GetCoreServerSidePropsOptions) => {
const {
query,
params,
req,
res,
...rest
} = context;
const isQuickPreviewPage: boolean = isQuickPreviewRequest(req);
const customerRef: string = process.env.NEXT_PUBLIC_CUSTOMER_REF;
const readonlyCookies: Cookies = NextCookies(context); // Parses Next.js cookies in a universal way (server + client)
const cookiesManager: UniversalCookiesManager = new UniversalCookiesManager(req, res); // Cannot be forwarded as pageProps, because contains circular refs
const userSession: UserSemiPersistentSession = cookiesManager.getUserData();
const { headers }: IncomingMessage = req;
const publicHeaders: PublicHeaders = {
'accept-language': headers?.['accept-language'],
'user-agent': headers?.['user-agent'],
'host': headers?.host,
};
const hasLocaleFromUrl = !!query?.locale;
const locale: string = resolveSSRLocale(query, req, readonlyCookies);
const lang: string = locale.split('-')?.[0];
const bestCountryCodes: string[] = [lang, resolveFallbackLanguage(lang)];
const i18nTranslations: I18nextResources = await getLocizeTranslations(lang);
const dataset: SanitizedAirtableDataset = await getAirtableDataset(bestCountryCodes, true);
const customer: AirtableRecord<Customer> = getCustomer(dataset);
enable404Redirect = true,
} = options || {};

// Do not serve pages using locales the customer doesn't have enabled
if (!includes(customer?.availableLanguages, locale)) {
logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`);
/**
* Only executed on the server side, for every request.
* Computes some dynamic props that should be available for all SSR pages that use getServerSideProps.
*
* Because the exact GQL query will depend on the consumer (AKA "caller"), this helper doesn't run any query by itself, but rather return all necessary props to allow the consumer to perform its own queries.
* This improves performances, by only running one GQL query instead of many (consumer's choice).
*
* Meant to avoid code duplication between pages sharing the same layout.
*
* XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly.
*
* @see https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering
*/
const getServerSideProps: GetServerSideProps<GetCoreLayoutServerSidePropsResults, CommonServerSideParams> = async (context: GetServerSidePropsContext<CommonServerSideParams>): Promise<GetServerSidePropsResult<GetCoreLayoutServerSidePropsResults>> => {
const {
query,
params,
req,
res,
...rest
} = context;
const isQuickPreviewPage: boolean = isQuickPreviewRequest(req);
const customerRef: string = process.env.NEXT_PUBLIC_CUSTOMER_REF;
const readonlyCookies: Cookies = NextCookies(context); // Parses Next.js cookies in a universal way (server + client)
const cookiesManager: UniversalCookiesManager = new UniversalCookiesManager(req, res); // Cannot be forwarded as pageProps, because contains circular refs
const userSession: UserSemiPersistentSession = cookiesManager.getUserData();
const { headers }: IncomingMessage = req;
const publicHeaders: PublicHeaders = {
'accept-language': headers?.['accept-language'],
'user-agent': headers?.['user-agent'],
'host': headers?.host,
};
const hasLocaleFromUrl = !!query?.locale;
const locale: string = resolveSSRLocale(query, req, readonlyCookies);
const lang: string = locale.split('-')?.[0];
const bestCountryCodes: string[] = [lang, resolveFallbackLanguage(lang)];
const i18nTranslations: I18nextResources = await getLocizeTranslations(lang);
const dataset: SanitizedAirtableDataset = await getAirtableDataset(bestCountryCodes, true);
const customer: AirtableRecord<Customer> = getCustomer(dataset);

// Do not serve pages using locales the customer doesn't have enabled
if (enable404Redirect && !includes(customer?.availableLanguages, locale)) {
logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`);

return {
notFound: true,
};
}

// Most props returned here will be necessary for the app to work properly (see "SSRPageProps")
// Some props are meant to be helpful to the consumer and won't be passed down to the _app.render (e.g: apolloClient, layoutQueryOptions)
return {
notFound: true,
props: {
bestCountryCodes,
serializedDataset: serializeSafe(dataset),
customerRef,
i18nTranslations,
headers: publicHeaders,
hasLocaleFromUrl,
isReadyToRender: true,
isServerRendering: true,
lang,
locale,
readonlyCookies,
userSession,
isQuickPreviewPage,
},
};
}

// Most props returned here will be necessary for the app to work properly (see "SSRPageProps")
// Some props are meant to be helpful to the consumer and won't be passed down to the _app.render (e.g: apolloClient, layoutQueryOptions)
return {
props: {
bestCountryCodes,
serializedDataset: serializeSafe(dataset),
customerRef,
i18nTranslations,
headers: publicHeaders,
hasLocaleFromUrl,
isReadyToRender: true,
isServerRendering: true,
lang,
locale,
readonlyCookies,
userSession,
isQuickPreviewPage,
},
};

return getServerSideProps;
};
Loading

0 comments on commit c7b0165

Please sign in to comment.