diff --git a/.storybook/preview.js b/.storybook/preview.js index 7ac86f816..3af2903b3 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,30 +1,30 @@ -import { Amplitude, AmplitudeProvider } from '@amplitude/react-amplitude'; -import { ThemeProvider } from '@emotion/react'; -import '@storybook/addon-console'; // Automatically forwards all logs in the "Actions" panel - See https://github.com/storybookjs/storybook-addon-console -import { withTests } from '@storybook/addon-jest'; -import { addDecorator } from '@storybook/react'; -import { themes } from '@storybook/theming'; -import find from 'lodash.find'; -import React from 'react'; -import { withNextRouter } from 'storybook-addon-next-router'; -import { withPerformance } from 'storybook-addon-performance'; import '@/app/components/MultiversalGlobalExternalStyles'; // Import the same 3rd party libraries global styles as the pages/_app.tsx (for UI consistency) import MultiversalGlobalStyles from '@/app/components/MultiversalGlobalStyles'; -import { defaultLocale, getLangFromLocale, supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import '@/common/utils/ignoreNoisyWarningsHacks'; +import { getCustomer } from '@/modules/core/airtable/getAirtableDataset'; +import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitude'; import amplitudeContext from '@/modules/core/amplitude/context/amplitudeContext'; import customerContext from '@/modules/core/data/contexts/customerContext'; -import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; import datasetContext from '@/modules/core/data/contexts/datasetContext'; +import '@/modules/core/fontAwesome/fontAwesome'; import i18nContext from '@/modules/core/i18n/contexts/i18nContext'; +import { defaultLocale, getLangFromLocale, supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import i18nextLocize from '@/modules/core/i18n/i18nextLocize'; import previewModeContext from '@/modules/core/previewMode/contexts/previewModeContext'; import quickPreviewContext from '@/modules/core/quickPreview/contexts/quickPreviewContext'; +import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; +import { initCustomerTheme } from '@/modules/core/theming/theme'; import userConsentContext from '@/modules/core/userConsent/contexts/userConsentContext'; import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; -import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitude'; -import '@/common/utils/ignoreNoisyWarningsHacks'; -import { initCustomerTheme } from '@/modules/core/theming/theme'; -import i18nextLocize from '@/modules/core/i18n/i18nextLocize'; -import '@/modules/core/fontAwesome/fontAwesome'; +import { Amplitude, AmplitudeProvider } from '@amplitude/react-amplitude'; +import { ThemeProvider } from '@emotion/react'; +import '@storybook/addon-console'; // Automatically forwards all logs in the "Actions" panel - See https://github.com/storybookjs/storybook-addon-console +import { withTests } from '@storybook/addon-jest'; +import { addDecorator } from '@storybook/react'; +import { themes } from '@storybook/theming'; +import React from 'react'; +import { withNextRouter } from 'storybook-addon-next-router'; +import { withPerformance } from 'storybook-addon-performance'; import dataset from './mock/sb-dataset'; // Loads translations from local file cache (Locize) @@ -179,10 +179,8 @@ export const decorators = [ // Although, they are configured in the same way as the Next.js app during development mode i18nextLocize(lang, i18nTranslations); - const customer = find(dataset, { __typename: 'Customer' }); + const customer = getCustomer(dataset); const customerTheme = initCustomerTheme(customer); - // console.log('customer', customer) - // console.log('customerTheme', customerTheme) const customerRef = 'storybook'; // Fake customer ref const amplitudeApiKey = ''; // Use invalid amplitude tracking key to force disable all amplitude analytics const userConsent = { @@ -290,6 +288,6 @@ try { }), ); } catch (e) { - console.log(`Couldn't find ../.jest-test-results.json, Jest tests might not work properly.`) + console.log(`Couldn't find ../.jest-test-results.json, Jest tests might not work properly.`); } diff --git a/next.config.js b/next.config.js index ac0be5f09..eb33f205e 100644 --- a/next.config.js +++ b/next.config.js @@ -1,8 +1,10 @@ const bundleAnalyzer = require('@next/bundle-analyzer'); const nextSourceMaps = require('@zeit/next-source-maps'); +const createNextPluginPreval = require('next-plugin-preval/config'); const packageJson = require('./package'); const i18nConfig = require('./src/modules/core/i18n/i18nConfig'); +const withNextPluginPreval = createNextPluginPreval(); const withSourceMaps = nextSourceMaps(); const withBundleAnalyzer = bundleAnalyzer({ // Run with "yarn analyse:bundle" - See https://www.npmjs.com/package/@next/bundle-analyzer enabled: process.env.ANALYZE_BUNDLE === 'true', @@ -40,7 +42,7 @@ console.debug(`Release version resolved from tags: "${APP_RELEASE_TAG}" (matchin * * @see https://nextjs.org/docs/api-reference/next.config.js/introduction */ -module.exports = withBundleAnalyzer(withSourceMaps({ +module.exports = withNextPluginPreval(withBundleAnalyzer(withSourceMaps({ // basepath: '', // If you want Next.js to cover only a subsection of the domain. See https://nextjs.org/docs/api-reference/next.config.js/basepath // target: 'serverless', // Automatically enabled on Vercel, you may need to manually opt-in if you're not using Vercel. See https://nextjs.org/docs/api-reference/next.config.js/build-target#serverless-target // trailingSlash: false, // By default Next.js will redirect urls with trailing slashes to their counterpart without a trailing slash. See https://nextjs.org/docs/api-reference/next.config.js/trailing-slash @@ -319,4 +321,4 @@ module.exports = withBundleAnalyzer(withSourceMaps({ // }, poweredByHeader: false, // See https://nextjs.org/docs/api-reference/next.config.js/disabling-x-powered-by -})); +}))); diff --git a/package.json b/package.json index 3c39691e7..b766d27b8 100644 --- a/package.json +++ b/package.json @@ -156,10 +156,12 @@ "lodash.size": "4.2.0", "lodash.some": "4.6.0", "lodash.startswith": "4.2.1", + "lodash.uniq": "4.5.0", "lodash.xorby": "4.7.0", "markdown-to-jsx": "7.1.2", "next": "10.2.0", "next-cookies": "2.0.3", + "next-plugin-preval": "1.0.1", "prop-types": "15.7.2", "rc-tooltip": "5.1.1", "react": "17.0.2", @@ -210,6 +212,7 @@ "@types/lodash.size": "4.2.6", "@types/lodash.some": "4.6.6", "@types/lodash.startswith": "4.2.6", + "@types/lodash.uniq": "4.5.6", "@types/lodash.xorby": "4.7.6", "@types/popper.js": "1.11.0", "@types/react": "17.0.5", diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx index 536896548..bcb96b4b0 100644 --- a/src/app/components/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -1,6 +1,7 @@ import Loader from '@/components/animations/Loader'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; +import { getCustomer } from '@/modules/core/airtable/getAirtableDataset'; import customerContext from '@/modules/core/data/contexts/customerContext'; import datasetContext from '@/modules/core/data/contexts/datasetContext'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; @@ -31,7 +32,6 @@ import { ThemeProvider } from '@emotion/react'; import * as Sentry from '@sentry/node'; import { isBrowser } from '@unly/utils'; import { i18n } from 'i18next'; -import find from 'lodash.find'; import isEmpty from 'lodash.isempty'; import size from 'lodash.size'; import React, { useState } from 'react'; @@ -200,7 +200,7 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } } - if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production' && !process.env.IS_SERVER_INITIAL_BUILD) { // XXX It's too cumbersome to do proper typings when type changes // The "customer" was forwarded as a JSON-ish string (using Flatten) in order to avoid circular dependencies issues (SSG/SSR) // It now being converted back into an object to be actually usable on all pages @@ -210,7 +210,7 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } const dataset: SanitizedAirtableDataset = deserializeSafe(serializedDataset); - const customer: AirtableRecord = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const customer: AirtableRecord = getCustomer(dataset); let availableLanguages: string[] = customer?.availableLanguages; if (isEmpty(availableLanguages)) { @@ -275,12 +275,12 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele if (process.env.NEXT_PUBLIC_CUSTOMER_REF !== customer?.ref) { error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? `Fatal error - An error happened, the page cannot be displayed. (customer doesn't match)` : - `Fatal error when bootstrapping the app. The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}".`, + `Fatal error when bootstrapping the app ("${props?.Component?.name}"). The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}").`, ); } else { error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? `Fatal error - An error happened, the page cannot be displayed.` : - `Fatal error when bootstrapping the app. It might happen when lang/locale/translations couldn't be resolved.`, + `Fatal error when bootstrapping the app ("${props?.Component?.name}"). It might happen when lang/locale/translations couldn't be resolved.`, ); } } else { diff --git a/src/layouts/core/coreLayoutSSG.ts b/src/layouts/core/coreLayoutSSG.ts index 85271b58c..378fe15e8 100644 --- a/src/layouts/core/coreLayoutSSG.ts +++ b/src/layouts/core/coreLayoutSSG.ts @@ -5,8 +5,14 @@ import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; +import fetchAirtableDataset from '@/modules/core/airtable/fetchAirtableDataset'; +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getAirtableDataset'; +import prepareAndSanitizeAirtableDataset from '@/modules/core/airtable/prepareAndSanitizeAirtableDataset'; import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { RawAirtableRecordsSet } from '@/modules/core/airtable/types/RawAirtableRecordsSet'; import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; @@ -24,9 +30,8 @@ import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; -import find from 'lodash.find'; -import includes from 'lodash.includes'; import map from 'lodash.map'; +import uniq from 'lodash.uniq'; import { GetStaticPaths, GetStaticPathsContext, @@ -56,14 +61,15 @@ const logger = createLogger({ * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation */ export const getCoreStaticPaths: GetStaticPaths = async (context: GetStaticPathsContext): Promise => { - // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, - // to only generate the pages for the locales the customer has explicitly enabled - // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption - // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too - const paths: StaticPath[] = map(supportedLocales, (supportedLocale: I18nLocale): StaticPath => { + const preferredLocalesOrLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(preferredLocalesOrLanguages); + const customer: AirtableRecord = 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: supportedLocale.name, + locale: availableLanguage, }, }; }); @@ -100,18 +106,18 @@ export const getCoreStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + let dataset: SanitizedAirtableDataset; - // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") - if (!includes(customer?.availableLanguages, locale)) { - logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); + if (preview) { + // When preview mode is enabled, we want to make real-time API requests to get up-to-date data + const airtableSchema: AirtableSchema = getAirtableSchema(); + const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDataset(airtableSchema, bestCountryCodes); + const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, bestCountryCodes); - return { - notFound: true, - }; + dataset = consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); + } else { + // When preview mode is not enabled, we fallback to the app-wide shared/static data (stale) + dataset = await getSharedAirtableDataset(bestCountryCodes); } return { diff --git a/src/layouts/core/coreLayoutSSR.ts b/src/layouts/core/coreLayoutSSR.ts index cdf8b45d2..92f6ac094 100644 --- a/src/layouts/core/coreLayoutSSR.ts +++ b/src/layouts/core/coreLayoutSSR.ts @@ -1,13 +1,12 @@ import { CommonServerSideParams } from '@/app/types/CommonServerSideParams'; import { PublicHeaders } from '@/layouts/core/types/PublicHeaders'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; -import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; -import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; -import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getAirtableDataset'; import { Cookies } from '@/modules/core/cookiesManager/types/Cookies'; import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; -import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { GenericObject } from '@/modules/core/data/types/GenericObject'; @@ -29,7 +28,6 @@ import * as Sentry from '@sentry/node'; import universalLanguageDetect from '@unly/universal-language-detector'; import { ERROR_LEVELS } from '@unly/universal-language-detector/lib/utils/error'; import { IncomingMessage } from 'http'; -import find from 'lodash.find'; import includes from 'lodash.includes'; import { GetServerSideProps, @@ -104,10 +102,8 @@ export const getCoreServerSideProps: GetServerSideProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(bestCountryCodes); + const customer: AirtableRecord = getCustomer(dataset); // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") if (!includes(customer?.availableLanguages, locale)) { diff --git a/src/layouts/demo/components/IntroductionSection.tsx b/src/layouts/demo/components/IntroductionSection.tsx index 8c52c4f46..70f24beea 100644 --- a/src/layouts/demo/components/IntroductionSection.tsx +++ b/src/layouts/demo/components/IntroductionSection.tsx @@ -71,7 +71,7 @@ const IntroductionSection: React.FunctionComponent = (props): JSX.Element Nav/Footer component are localised, as well as dynamic content and i18n examples.

You can switch locale from the footer or by clicking on{' '} - fr-FR or en-US. + fr or en or en-US. diff --git a/src/layouts/demo/demoLayoutSSG.ts b/src/layouts/demo/demoLayoutSSG.ts index 8b72ab827..175d693f1 100644 --- a/src/layouts/demo/demoLayoutSSG.ts +++ b/src/layouts/demo/demoLayoutSSG.ts @@ -5,8 +5,14 @@ import { StaticPropsInput } from '@/app/types/StaticPropsInput'; import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; +import fetchAirtableDataset from '@/modules/core/airtable/fetchAirtableDataset'; +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getAirtableDataset'; +import prepareAndSanitizeAirtableDataset from '@/modules/core/airtable/prepareAndSanitizeAirtableDataset'; import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { RawAirtableRecordsSet } from '@/modules/core/airtable/types/RawAirtableRecordsSet'; import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; @@ -24,9 +30,8 @@ import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; -import find from 'lodash.find'; -import includes from 'lodash.includes'; import map from 'lodash.map'; +import uniq from 'lodash.uniq'; import { GetStaticPaths, GetStaticPathsContext, @@ -56,14 +61,15 @@ const logger = createLogger({ * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation */ export const getDemoStaticPaths: GetStaticPaths = async (context: GetStaticPathsContext): Promise => { - // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, - // to only generate the pages for the locales the customer has explicitly enabled - // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption - // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too - const paths: StaticPath[] = map(supportedLocales, (supportedLocale: I18nLocale): StaticPath => { + const preferredLocalesOrLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(preferredLocalesOrLanguages); + const customer: AirtableRecord = 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: supportedLocale.name, + locale: availableLanguage, }, }; }); @@ -76,13 +82,13 @@ export const getDemoStaticPaths: GetStaticPaths = async /** * Only executed on the server side at build time. - * Computes all static props that should be available for all SSG pages + * 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 - * Can be overridden for per-page customisation (e.g: deepmerge) + * Meant to avoid code duplication. + * Can be overridden for per-page customisation (e.g: deepmerge). * * XXX Demo component, not meant to be modified. It's a copy of the coreLayoutSSG implementation, so the demo keep working even if you change the base implementation. @@ -100,18 +106,18 @@ export const getDemoStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + let dataset: SanitizedAirtableDataset; - // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") - if (!includes(customer?.availableLanguages, locale)) { - logger.warn(`Locale "${locale}" not enabled for this customer (allowed: "${customer?.availableLanguages}"), returning 404 page.`); + if (preview) { + // When preview mode is enabled, we want to make real-time API requests to get up-to-date data + const airtableSchema: AirtableSchema = getAirtableSchema(); + const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDataset(airtableSchema, bestCountryCodes); + const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, bestCountryCodes); - return { - notFound: true, - }; + dataset = consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); + } else { + // When preview mode is not enabled, we fallback to the app-wide shared/static data (stale) + dataset = await getSharedAirtableDataset(bestCountryCodes); } return { diff --git a/src/layouts/demo/demoLayoutSSR.ts b/src/layouts/demo/demoLayoutSSR.ts index 1cba58f65..080aec4e6 100644 --- a/src/layouts/demo/demoLayoutSSR.ts +++ b/src/layouts/demo/demoLayoutSSR.ts @@ -1,13 +1,12 @@ import { CommonServerSideParams } from '@/app/types/CommonServerSideParams'; import { PublicHeaders } from '@/layouts/core/types/PublicHeaders'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; -import { getAirtableSchema } from '@/modules/core/airtable/airtableSchema'; -import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; -import fetchAndSanitizeAirtableDatasets from '@/modules/core/airtable/fetchAndSanitizeAirtableDatasets'; -import { AirtableSchema } from '@/modules/core/airtable/types/AirtableSchema'; +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getAirtableDataset'; import { Cookies } from '@/modules/core/cookiesManager/types/Cookies'; import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; -import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; import { GenericObject } from '@/modules/core/data/types/GenericObject'; @@ -29,7 +28,6 @@ import * as Sentry from '@sentry/node'; import universalLanguageDetect from '@unly/universal-language-detector'; import { ERROR_LEVELS } from '@unly/universal-language-detector/lib/utils/error'; import { IncomingMessage } from 'http'; -import find from 'lodash.find'; import includes from 'lodash.includes'; import { GetServerSideProps, @@ -104,10 +102,8 @@ export const getDemoServerSideProps: GetServerSideProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(bestCountryCodes); + const customer: AirtableRecord = getCustomer(dataset); // Do not serve pages under locales the customer doesn't have enabled (even though they've been generated by "getDemoStaticPaths") if (!includes(customer?.availableLanguages, locale)) { diff --git a/src/layouts/public/pagePublicTemplateSSG.tsx b/src/layouts/public/pagePublicTemplateSSG.tsx index 88155d0d8..a05a5ae07 100644 --- a/src/layouts/public/pagePublicTemplateSSG.tsx +++ b/src/layouts/public/pagePublicTemplateSSG.tsx @@ -23,15 +23,15 @@ const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript- }); /** - * Only executed on the server side at build time - * Necessary when a page has dynamic routes and uses "getStaticProps" + * Only executed on the server side at build time. + * Necessary when a page has dynamic routes and uses "getStaticProps". */ export const getStaticPaths: GetStaticPaths = getPublicLayoutStaticPaths; /** * Only executed on the server side at build time. * - * @return Props (as "SSGPageProps") that will be passed to the Page component, as props + * @return Props (as "SSGPageProps") that will be passed to the Page component, as props. * * @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884 * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation @@ -39,12 +39,12 @@ export const getStaticPaths: GetStaticPaths = getPublicL export const getStaticProps: GetStaticProps = getPublicLayoutStaticProps; /** - * SSG pages are first rendered by the server (during static bundling) - * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps) - * Because this last case is the most common (server bundle only happens during development stage), we consider it a default + * SSG pages are first rendered by the server (during static bundling). + * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps). + * Because this last case is the most common (server bundle only happens during development stage), we consider it a d.efault. * To represent this behaviour, we use the native Partial TS keyword to make all OnlyBrowserPageProps optional * - * Beware props in OnlyBrowserPageProps are not available on the server + * Beware props in OnlyBrowserPageProps are not available on the server. */ type Props = {} & SSGPageProps>; diff --git a/src/layouts/public/publicLayoutSSG.ts b/src/layouts/public/publicLayoutSSG.ts index e50df44da..dec208e25 100644 --- a/src/layouts/public/publicLayoutSSG.ts +++ b/src/layouts/public/publicLayoutSSG.ts @@ -80,7 +80,8 @@ export const getPublicLayoutStaticProps: GetStaticProps { +export const getAirtableSchema = (props?: GetAirtableSchemaProps): AirtableSchema => { return { Customer: { diff --git a/src/modules/core/airtable/fetchAirtableDS.ts b/src/modules/core/airtable/fetchAirtableDataset.ts similarity index 97% rename from src/modules/core/airtable/fetchAirtableDS.ts rename to src/modules/core/airtable/fetchAirtableDataset.ts index 65f221aaf..fba43f9fd 100644 --- a/src/modules/core/airtable/fetchAirtableDS.ts +++ b/src/modules/core/airtable/fetchAirtableDataset.ts @@ -46,7 +46,7 @@ const VERCEL_DISK_CACHE_TTL = 180; // In seconds * Whether you use locales or languages is up to you, as it depends how you name your Airtable fields. * Tip: "Underscore" is recommended if using localized locales. (i.e: 'en_gb', not 'en-gb') */ -export const fetchAirtableDS = async (airtableSchema: AirtableSchema, localesOfLanguagesToFetch: string[]): Promise => { +export const fetchAirtableDataset = async (airtableSchema: AirtableSchema, localesOfLanguagesToFetch: string[]): Promise => { const promises: Promise[] = []; const rawAirtableRecordsSets: RawAirtableRecordsSet[] = []; const tableSchemaKeys: AirtableDBTable[] = Object.keys(airtableSchema) as AirtableDBTable[]; @@ -122,4 +122,4 @@ export const fetchAirtableDS = async (airtableSchema: AirtableSchema, localesOfL return rawAirtableRecordsSets; }; -export default fetchAirtableDS; +export default fetchAirtableDataset; diff --git a/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts b/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts new file mode 100644 index 000000000..20bf846b9 --- /dev/null +++ b/src/modules/core/airtable/fetchRawAirtableDataset.preval.ts @@ -0,0 +1,36 @@ +import fetchRawAirtableDataset from '@/modules/core/airtable/fetchRawAirtableDataset'; +import preval from 'next-plugin-preval'; + +/** + * Pre-fetches the Airtable dataset and stores the result in an cached internal JSON file. + * Overall, this approach allows us to have some static app-wide data that will never update, and have real-time data wherever we want. + * + * This is very useful to avoid fetching the same data for each page during the build step. + * By default, Next.js would call the Airtable API once per page built. + * This was a huge pain for many reasons, because our app uses mostly static pages and we don't want those static pages to be updated. + * + * Also, even considering built time only, it was very inefficient, because Next was triggering too many API calls: + * - More than 120 fetch attempts (locales * pages) + * - 3 locales (in supportedLocales) + * - lots of static pages (40+ demo pages) + * - Our in-memory cache was helping but wouldn't completely conceal the over-fetching caused by Next.js + * - Airtable API requires 1 API request per table (we fetch 9 tables for the demo) + * - We were generating pages for all supportedLocales, even if the customer hadn't enabled some languages (longer build + undesired pages leading to bad UX) + * - We weren't able to auto-redirect only to one of the enabled customer locales, not without fetching Airtable (which is slow and has strong rate limits) + * + * The shared/static dataset is accessible to: + * - All components + * - All pages (both getStaticProps and getStaticPaths, and even in getServerSideProps is you wish to!) + * - All API endpoints + * + * XXX The data are therefore STALE, they're not fetched in real-time. + * They won't update (the app won't display up-to-date data until the next deployment, for static pages). + * + * @example await import('@/modules/core/airtable/fetchRawAirtableDataset.preval') + * @example const rawAirtableRecordsSets: RawAirtableRecordsSet[] = (await import('@/modules/core/airtable/fetchRawAirtableDataset.preval')) as unknown as RawAirtableRecordsSet[]; + * + * @see https://github.com/ricokahler/next-plugin-preval + */ +export const dataset = preval(fetchRawAirtableDataset()); + +export default dataset; diff --git a/src/modules/core/airtable/fetchRawAirtableDataset.ts b/src/modules/core/airtable/fetchRawAirtableDataset.ts new file mode 100644 index 000000000..6cc9654f2 --- /dev/null +++ b/src/modules/core/airtable/fetchRawAirtableDataset.ts @@ -0,0 +1,34 @@ +import { + getAirtableSchema, + GetAirtableSchemaProps, +} from '@/modules/core/airtable/airtableSchema'; +import { supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; +import { createLogger } from '@/modules/core/logging/logger'; +import uniq from 'lodash.uniq'; +import fetchAirtableDataset from './fetchAirtableDataset'; +import { AirtableSchema } from './types/AirtableSchema'; +import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; + +const fileLabel = 'modules/core/airtable/fetchRawAirtableDataset'; +const logger = createLogger({ + fileLabel, +}); + +/** + * Fetches the airtable dataset. + * Invoked by fetchRawAirtableDataset.preval.ts file at build time (during Webpack bundling). + * + * XXX Must be a single export file otherwise it can cause issues - See https://github.com/ricokahler/next-plugin-preval/issues/19#issuecomment-848799473 + */ +export const fetchRawAirtableDataset = async (airtableSchemaProps?: GetAirtableSchemaProps): Promise => { + const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); + + // Resolves the languages we want to fetch the fields for (all supported languages configured in the app) + // We want to fetch all fields (for all language variations) during the initial dataset fetch + const localesOfLanguagesToFetch = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + + return await fetchAirtableDataset(airtableSchema, localesOfLanguagesToFetch); +}; + +export default fetchRawAirtableDataset; diff --git a/src/modules/core/airtable/getAirtableDataset.ts b/src/modules/core/airtable/getAirtableDataset.ts new file mode 100644 index 000000000..68669f8b9 --- /dev/null +++ b/src/modules/core/airtable/getAirtableDataset.ts @@ -0,0 +1,92 @@ +import { + getAirtableSchema, + GetAirtableSchemaProps, +} from '@/modules/core/airtable/airtableSchema'; +import consolidateSanitizedAirtableDataset from '@/modules/core/airtable/consolidateSanitizedAirtableDataset'; +import prepareAndSanitizeAirtableDataset from '@/modules/core/airtable/prepareAndSanitizeAirtableDataset'; +import { AirtableDatasets } from '@/modules/core/data/types/AirtableDatasets'; +import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; +import { createLogger } from '@/modules/core/logging/logger'; +import find from 'lodash.find'; +import { AirtableSchema } from './types/AirtableSchema'; +import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; + +const fileLabel = 'modules/core/airtable/getAirtableDataset'; +const logger = createLogger({ + fileLabel, +}); + +/** + * Finds the customer within the dataset. + * + * @param dataset + */ +export const getCustomer = (dataset: SanitizedAirtableDataset): AirtableRecord => { + return find(dataset, { __typename: 'Customer' }) as AirtableRecord; +}; + +/** + * Returns the whole dataset (raw), based on the app-wide static/shared/stale data fetched at build time. + * + * @example const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await getSharedRawAirtableDataset(); + */ +export const getSharedRawAirtableDataset = async (): Promise => { + return (await import('@/modules/core/airtable/fetchRawAirtableDataset.preval')) as unknown as RawAirtableRecordsSet[]; +}; + +/** + * Returns the whole dataset (sanitized), based on the app-wide static/shared/stale data fetched at build time. + * + * This dataset is STALE. It will not update, ever. + * The dataset is created at build time, using the "next-plugin-preval" webpack plugin. + * + * @example const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(bestCountryCodes); + * + * @param preferredLocalesOrLanguages + * @param airtableSchemaProps + */ +export const getSharedAirtableDataset = async (preferredLocalesOrLanguages: string[], airtableSchemaProps?: GetAirtableSchemaProps): Promise => { + const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await getSharedRawAirtableDataset(); + const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); + const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, preferredLocalesOrLanguages); + + return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); +}; + +/** + * WIP Not working + * + * @param preferredLocalesOrLanguages + * @param airtableSchemaProps + */ +export const getLiveAirtableDataset = async (preferredLocalesOrLanguages: string[], airtableSchemaProps?: GetAirtableSchemaProps): Promise => { + // const airtableSchema: AirtableSchema = getAirtableSchema(airtableSchemaProps); + // XXX Importing fetchAirtableDataset in the file causes a crash, while doing exactly the same from the Next.js page works fine (claiming "fs" module cannot be found) + // This is most likely related to the "next-plugin-preval" package, which messes up with the Webpack configuration + // const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDataset(airtableSchema, preferredLocalesOrLanguages); + // const datasets: AirtableDatasets = prepareAndSanitizeAirtableDataset(rawAirtableRecordsSets, airtableSchema, preferredLocalesOrLanguages); + // + // return consolidateSanitizedAirtableDataset(airtableSchema, datasets.sanitized); + return {}; +}; + +/** + * WIP not used because getLiveAirtableDataset isn't working + * + * Meant to make code more reusable and avoid bloating pages with too much logic + * + * @param isPreviewMode + * @param preferredLocalesOrLanguages + * @param airtableSchemaProps + */ +export const getAirtableDataset = async (isPreviewMode: boolean, preferredLocalesOrLanguages: string[], airtableSchemaProps?: GetAirtableSchemaProps): Promise => { + if (isPreviewMode) { + // When preview mode is enabled, we want to make real-time API requests to get up-to-date data + return await getLiveAirtableDataset(preferredLocalesOrLanguages, airtableSchemaProps); + } else { + // When preview mode is not enabled, we fallback to the app-wide shared/static data (stale) + return await getSharedAirtableDataset(preferredLocalesOrLanguages); + } +}; diff --git a/src/modules/core/airtable/prepareAirtableDS.ts b/src/modules/core/airtable/prepareAirtableDS.ts deleted file mode 100644 index b65e15121..000000000 --- a/src/modules/core/airtable/prepareAirtableDS.ts +++ /dev/null @@ -1,31 +0,0 @@ -import map from 'lodash.map'; -import { AirtableDatasets } from '../data/types/AirtableDatasets'; -import { RawAirtableRecord } from './types/RawAirtableRecord'; -import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; - -/** - * Prepare the raw and sanitized airtable datasets. - * - * Each RawAirtableRecordsSet is basically the result of a full table fetch (a list of records sharing the same data structure). - * - * @param rawAirtableRecordsSets - */ -export const prepareAirtableDS = (rawAirtableRecordsSets: RawAirtableRecordsSet[]): AirtableDatasets => { - const datasets: AirtableDatasets = { - raw: {}, - sanitized: {}, - }; - - map(rawAirtableRecordsSets, (rawRecordsSet: RawAirtableRecordsSet) => { - map(rawRecordsSet.records, (record: RawAirtableRecord) => { - datasets.raw[record.id] = { - ...record, - __typename: rawRecordsSet.__typename, - }; - }); - }); - - return datasets; -}; - -export default prepareAirtableDS; diff --git a/src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts b/src/modules/core/airtable/prepareAndSanitizeAirtableDataset.ts similarity index 50% rename from src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts rename to src/modules/core/airtable/prepareAndSanitizeAirtableDataset.ts index 18ec463d8..663b1e494 100644 --- a/src/modules/core/airtable/fetchAndSanitizeAirtableDatasets.ts +++ b/src/modules/core/airtable/prepareAndSanitizeAirtableDataset.ts @@ -1,53 +1,82 @@ +import { RawAirtableRecord } from '@/modules/core/airtable/types/RawAirtableRecord'; import { createLogger } from '@/modules/core/logging/logger'; import groupBy from 'lodash.groupby'; import map from 'lodash.map'; import size from 'lodash.size'; import { AirtableDatasets } from '../data/types/AirtableDatasets'; import { RawAirtableDataset } from '../data/types/RawAirtableDataset'; -import fetchAirtableDS from './fetchAirtableDS'; -import prepareAirtableDS from './prepareAirtableDS'; -import sanitizeRawAirtableDS from './sanitizeRawAirtableDS'; +import { sanitizeRawAirtableDS } from './sanitizeRawAirtableDS'; import { AirtableSchema } from './types/AirtableSchema'; import { RawAirtableRecordsSet } from './types/RawAirtableRecordsSet'; import { TableSchema } from './types/TableSchema'; -const fileLabel = 'modules/core/airtable/fetchAndSanitizeAirtableDatasets'; +const fileLabel = 'modules/core/airtable/prepareAndSanitizeAirtableDataset'; const logger = createLogger({ fileLabel, }); +/** + * Prints stats metadata about the dataset. + * Useful for debugging. + * + * @param airtableDataset + * @param label + */ const printAirtableDatasetStatistics = (airtableDataset: RawAirtableDataset, label: string): void => { const rawDatasetGroupedByTable = groupBy(airtableDataset, '__typename'); - logger.log(label, // eslint-disable-line no-console + logger.info(label, `size: ${size(airtableDataset)}`, map(rawDatasetGroupedByTable, (tableSchemas: TableSchema[], tableName) => `${tableName}: ${size(tableSchemas)}`), ); }; /** - * Fetches the Airtable API (once per table) - * Main entry point of the Airtable Dataset consolidation. + * Prepare the raw and sanitized airtable datasets. * + * Each RawAirtableRecordsSet is basically the result of a full table fetch (a list of records sharing the same data structure). + * + * @param rawAirtableRecordsSets + */ +export const prepareAirtableDS = (rawAirtableRecordsSets: RawAirtableRecordsSet[]): AirtableDatasets => { + const datasets: AirtableDatasets = { + raw: {}, + sanitized: {}, + }; + + map(rawAirtableRecordsSets, (rawRecordsSet: RawAirtableRecordsSet) => { + map(rawRecordsSet.records, (record: RawAirtableRecord) => { + datasets.raw[record.id] = { + ...record, + __typename: rawRecordsSet.__typename, + }; + }); + }); + + return datasets; +}; + +/** * Performs the consolidation in the following order: - * 1) Fetches all tables and their records as described in the schema. - * 2) Prepare the datasets (raw and sanitized). - * 3) Sanitize the raw dataset. + * 1) Prepare the datasets (raw and sanitized). + * 2) Sanitize the raw dataset. * + * @param rawAirtableRecordsSets * @param airtableSchema * @param preferredLocalesOrLanguages Those locales will be used to fetch i18n variant fields and resolve the missing translations * @param filterByPredicate Optional lodash.filter predicate to filter the whole raw dataset (after it's been fetched). * Different than per-table filterByFormula which is applied directly by the Airtable API). * See https://lodash.com/docs/4.17.15#filter */ -export const fetchAndSanitizeAirtableDatasets = async (airtableSchema: AirtableSchema, preferredLocalesOrLanguages: string[], filterByPredicate?: any): Promise => { - const rawAirtableRecordsSets: RawAirtableRecordsSet[] = await fetchAirtableDS(airtableSchema, preferredLocalesOrLanguages); +export const prepareAndSanitizeAirtableDataset = (rawAirtableRecordsSets: RawAirtableRecordsSet[], airtableSchema: AirtableSchema, preferredLocalesOrLanguages: string[], filterByPredicate?: any): AirtableDatasets => { const airtableDatasets: AirtableDatasets = prepareAirtableDS(rawAirtableRecordsSets); printAirtableDatasetStatistics(airtableDatasets.raw, 'Raw dataset metadata:'); + // Sanitizes the dataset, during sanitization we will fallback (when a record isn't translated) + // using lang/locale depending on the order defined in preferredLocalesOrLanguages airtableDatasets.sanitized = sanitizeRawAirtableDS(airtableSchema, airtableDatasets, preferredLocalesOrLanguages, filterByPredicate); return airtableDatasets; }; -export default fetchAndSanitizeAirtableDatasets; +export default prepareAndSanitizeAirtableDataset; diff --git a/src/modules/core/i18n/middlewares/localeMiddleware.ts b/src/modules/core/i18n/middlewares/localeMiddleware.ts index 939ca2040..65a06d9a4 100644 --- a/src/modules/core/i18n/middlewares/localeMiddleware.ts +++ b/src/modules/core/i18n/middlewares/localeMiddleware.ts @@ -1,6 +1,16 @@ +import { + getCustomer, + getSharedAirtableDataset, +} from '@/modules/core/airtable/getAirtableDataset'; +import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { SanitizedAirtableDataset } from '@/modules/core/data/types/SanitizedAirtableDataset'; +import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; import { createLogger } from '@/modules/core/logging/logger'; import redirect from '@/utils/redirect'; +import includes from 'lodash.includes'; import size from 'lodash.size'; +import uniq from 'lodash.uniq'; import { NextApiRequest, NextApiResponse, @@ -30,21 +40,16 @@ export const localeMiddleware = async (req: NextApiRequest, res: NextApiResponse logger.debug('Detecting browser locale...'); const detections: string[] = acceptLanguageHeaderLookup(req) || []; let localeFound; // Will contain the most preferred browser locale (e.g: fr-FR, fr, en-US, en, etc.) + const preferredLocalesOrLanguages = uniq(supportedLocales.map((supportedLocale: I18nLocale) => supportedLocale.lang)); + const dataset: SanitizedAirtableDataset = await getSharedAirtableDataset(preferredLocalesOrLanguages); + const customer: AirtableRecord = getCustomer(dataset); if (detections && !!size(detections)) { detections.forEach((language) => { if (localeFound || typeof language !== 'string') return; - // TODO We shouldn't use "supportedLocales" but "customer?.availableLanguages" instead, - // to only redirect the pages for the locales the customer has explicitly enabled - // I haven't found a nice way to do that yet, because if we're fetching Airtable here too, it will increase our API rate consumption - // It'd be better to fetch the Airtable data ahead (at webpack level) so they're available when building pages, it'd make the build faster and lower the API usage too - const lookedUpLocale = supportedLocales.find( - (allowedLocale) => allowedLocale.name === language, - ); - - if (lookedUpLocale) { - localeFound = lookedUpLocale.lang; + if (includes(customer?.availableLanguages, language)) { + localeFound = language; } }); @@ -54,7 +59,7 @@ export const localeMiddleware = async (req: NextApiRequest, res: NextApiResponse } if (!localeFound) { - localeFound = DEFAULT_LOCALE; + localeFound = customer?.availableLanguages?.[0] || DEFAULT_LOCALE; } logger.debug(`Locale applied: "${localeFound}", for url "${req.url}"`); diff --git a/src/modules/core/logging/logger.ts b/src/modules/core/logging/logger.ts index 551a5b6e6..612391972 100644 --- a/src/modules/core/logging/logger.ts +++ b/src/modules/core/logging/logger.ts @@ -1,9 +1,12 @@ import createSimpleLogger, { SimpleLogger } from '@unly/simple-logger'; +import { isBrowser } from '@unly/utils'; /** * Custom logger proxy. * - * Customize the @unly/simple-logger library by providing app-wide default behavior. + * Customizes the @unly/simple-logger library by providing app-wide default behavior. + * + * Optimized to avoid logging in the browser in production, to reduce the noise and to avoid leaking debug information publicly. * * @param fileLabel */ @@ -16,7 +19,14 @@ export const createLogger = ({ fileLabel }: { fileLabel: string }): SimpleLogger return createSimpleLogger({ prefix: fileLabel, shouldPrint: (mode) => { - return process.env.NEXT_PUBLIC_APP_STAGE !== 'production'; + // When bundling with Webpack, only print errors/warnings to avoid printing too much noise on Vercel + // IS_SERVER_INITIAL_BUILD is always "1" during development, and is being ignored + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'development' && process.env.IS_SERVER_INITIAL_BUILD === '1') { + return mode === 'error' || mode === 'warn' || mode === 'debug'; + } + + // Otherwise, only hide browser errors in production + return !(process.env.NEXT_PUBLIC_APP_STAGE === 'production' && isBrowser()); }, }); }; diff --git a/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx b/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx index 0339a85ac..6d9f20cb7 100644 --- a/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx +++ b/src/pages/[locale]/demo/native-features/example-with-ssg-and-revalidate.tsx @@ -11,6 +11,7 @@ import { getDemoStaticPaths, getDemoStaticProps, } from '@/layouts/demo/demoLayoutSSG'; +import { getCustomer } from '@/modules/core/airtable/getAirtableDataset'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; @@ -63,7 +64,7 @@ export const getStaticProps: GetStaticProps = find(dataset, { __typename: 'Customer' }) as AirtableRecord; + const customer: AirtableRecord = getCustomer(dataset); return deepmerge(commonStaticProps, { props: { diff --git a/src/pages/[locale]/demo/privacy.tsx b/src/pages/[locale]/demo/privacy.tsx index 048c10d53..febc7f4cd 100644 --- a/src/pages/[locale]/demo/privacy.tsx +++ b/src/pages/[locale]/demo/privacy.tsx @@ -63,8 +63,6 @@ const PrivacyPage: NextPage = (props): JSX.Element => { serviceLabel, } = customer || {}; - console.log('customer', customer) - // Replace dynamic values (like "{customerLabel}") by their actual value const privacy = replaceAllOccurrences(privacyDescription, { serviceLabel: `**${serviceLabel}**`, diff --git a/src/pages/[locale]/demo/terms.tsx b/src/pages/[locale]/demo/terms.tsx index 3cff34f86..290aa72a2 100644 --- a/src/pages/[locale]/demo/terms.tsx +++ b/src/pages/[locale]/demo/terms.tsx @@ -58,7 +58,6 @@ type Props = {} & SSGPageProps>; */ const TermsPage: NextPage = (props): JSX.Element => { const customer: Customer = useCustomer(); - console.log('customer', customer); const { termsDescription, serviceLabel, diff --git a/src/pages/[locale]/public/index.tsx b/src/pages/[locale]/public/index.tsx index 09dc1638c..9af7c2eec 100644 --- a/src/pages/[locale]/public/index.tsx +++ b/src/pages/[locale]/public/index.tsx @@ -24,15 +24,15 @@ const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript- }); /** - * Only executed on the server side at build time - * Necessary when a page has dynamic routes and uses "getStaticProps" + * Only executed on the server side at build time. + * Necessary when a page has dynamic routes and uses "getStaticProps". */ export const getStaticPaths: GetStaticPaths = getPublicLayoutStaticPaths; /** * Only executed on the server side at build time. * - * @return Props (as "SSGPageProps") that will be passed to the Page component, as props + * @return Props (as "SSGPageProps") that will be passed to the Page component, as props. * * @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884 * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation @@ -40,19 +40,19 @@ export const getStaticPaths: GetStaticPaths = getPublicL export const getStaticProps: GetStaticProps = getPublicLayoutStaticProps; /** - * SSG pages are first rendered by the server (during static bundling) - * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps) - * Because this last case is the most common (server bundle only happens during development stage), we consider it a default - * To represent this behaviour, we use the native Partial TS keyword to make all OnlyBrowserPageProps optional + * SSG pages are first rendered by the server (during static bundling). + * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps). + * Because this last case is the most common (server bundle only happens during development stage), we consider it a default. + * To represent this behaviour, we use the native Partial TS keyword to make all OnlyBrowserPageProps optional. * - * Beware props in OnlyBrowserPageProps are not available on the server + * Beware props in OnlyBrowserPageProps are not available on the server. */ type Props = {} & SSGPageProps>; /** - * Public template for SSG pages + * Example of a public page. */ -const PagePublicTemplateSSG: NextPage = (props): JSX.Element => { +const ExamplePublicPage: NextPage = (props): JSX.Element => { const customer: Customer = useCustomer(); return ( @@ -83,4 +83,4 @@ const PagePublicTemplateSSG: NextPage = (props): JSX.Element => { ); }; -export default PagePublicTemplateSSG; +export default ExamplePublicPage; diff --git a/yarn.lock b/yarn.lock index 0c0353203..7ec54bbb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4546,6 +4546,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -4684,6 +4689,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.uniq@4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.uniq/-/lodash.uniq-4.5.6.tgz#adb052f6c7eeb38b920c13166e7a972dd960b4c5" + integrity sha512-XHNMXBtiwsWZstZMyxOYjr0e8YYWv0RgPlzIHblTuwBBiWo2MzWVaTBihtBpslb5BglgAWIeBv69qt1+RTRW1A== + dependencies: + "@types/lodash" "*" + "@types/lodash.xorby@4.7.6": version "4.7.6" resolved "https://registry.yarnpkg.com/@types/lodash.xorby/-/lodash.xorby-4.7.6.tgz#709c3d6994cc95fc0a2892cb60c7cbee5259f8d0" @@ -6198,6 +6210,17 @@ babel-plugin-macros@^3.0.1: cosmiconfig "^7.0.0" resolve "^1.19.0" +babel-plugin-module-resolver@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.1.0.tgz#22a4f32f7441727ec1fbf4967b863e1e3e9f33e2" + integrity sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA== + dependencies: + find-babel-config "^1.2.0" + glob "^7.1.6" + pkg-up "^3.1.0" + reselect "^4.0.0" + resolve "^1.13.1" + babel-plugin-named-asset-import@^0.3.1: version "0.3.7" resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz#156cd55d3f1228a5765774340937afc8398067dd" @@ -9804,6 +9827,14 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +find-babel-config@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.2.0.tgz#a9b7b317eb5b9860cda9d54740a8c8337a2283a2" + integrity sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA== + dependencies: + json5 "^0.5.1" + path-exists "^3.0.0" + find-cache-dir@3.3.1, find-cache-dir@^3.2.0, find-cache-dir@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" @@ -12416,6 +12447,11 @@ json5@2.x, json5@^2.1.0: dependencies: minimist "^1.2.0" +json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -13676,6 +13712,21 @@ next-cookies@2.0.3: dependencies: universal-cookie "^4.0.2" +next-plugin-preval@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/next-plugin-preval/-/next-plugin-preval-1.0.1.tgz#b3329d6aa22c23bcad7269d97da22950b1c56f1a" + integrity sha512-jKxueEzov+qR7vLMbIykAvCi2DczbBgtUp0QNV1wmagL7wKeRXgvc+f3gGXncefkHRHnZ3Ll6G8R19hZ6Bk7dA== + dependencies: + "@babel/core" "^7.12.10" + "@babel/preset-env" "^7.12.11" + babel-plugin-module-resolver "^4.1.0" + loader-utils "^2.0.0" + lodash "^4.17.20" + pirates "^4.0.1" + regenerator-runtime "^0.13.7" + require-from-string "^2.0.2" + tsconfig-paths "^3.9.0" + next-unused@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/next-unused/-/next-unused-0.0.6.tgz#dbefa300bf5586e33d5bfde909130fb19ab04a64" @@ -14688,7 +14739,7 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -pkg-up@3.1.0: +pkg-up@3.1.0, pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== @@ -16230,6 +16281,11 @@ requirejs@^2.3.5: resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -16287,7 +16343,7 @@ resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.8.1: dependencies: path-parse "^1.0.6" -resolve@^1.14.2, resolve@^1.19.0: +resolve@^1.13.1, resolve@^1.14.2, resolve@^1.19.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -17380,6 +17436,11 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" @@ -18044,6 +18105,16 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.11.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.0.tgz#f1f3528301621a53220d58373ae510ff747a66bc"