Commit 61e791fa authored by tom goriunov's avatar tom goriunov Committed by GitHub

Page meta tags are not generated on the server (#2731)

* Page meta tags are not generated on the server

Fixes #2730

* suppress warnings about critical dependencies in Next.js logs
parent 53d02c51
import type { GrowthBook } from '@growthbook/growthbook-react';
import React from 'react';
import { SECOND } from 'toolkit/utils/consts';
import { initGrowthBook } from './init';
export default function useLoadFeatures(uuid: string) {
const growthBook = initGrowthBook(uuid);
export default function useLoadFeatures(growthBook: GrowthBook | undefined) {
React.useEffect(() => {
growthBook?.setAttributes({
if (!growthBook) {
return;
}
growthBook.setAttributes({
...growthBook.getAttributes(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: window.navigator.language,
});
growthBook?.loadFeatures({ timeout: SECOND });
growthBook.loadFeatures({ timeout: SECOND });
}, [ growthBook ]);
}
......@@ -39,6 +39,7 @@ const moduleExports = {
headers,
output: 'standalone',
productionBrowserSourceMaps: true,
serverExternalPackages: ["@opentelemetry/sdk-node", "@opentelemetry/auto-instrumentations-node"],
experimental: {
staleTimes: {
dynamic: 30,
......
import Head from 'next/head';
import React from 'react';
import type { Route } from 'nextjs-routes';
import type { Props as PageProps } from 'nextjs/getServerSideProps';
import config from 'configs/app';
import * as metadata from 'lib/metadata';
interface Props<Pathname extends Route['pathname']> {
pathname: Pathname;
query?: PageProps<Pathname>['query'];
apiData?: PageProps<Pathname>['apiData'];
}
const PageMetadata = <Pathname extends Route['pathname']>(props: Props<Pathname>) => {
const { title, description, opengraph, canonical } = metadata.generate(props, props.apiData);
return (
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
{ canonical && <link rel="canonical" href={ canonical }/> }
{ /* OG TAGS */ }
<meta property="og:title" content={ opengraph.title }/>
{ opengraph.description && <meta property="og:description" content={ opengraph.description }/> }
<meta property="og:image" content={ opengraph.imageUrl }/>
<meta property="og:type" content="website"/>
{ /* Twitter Meta Tags */ }
<meta name="twitter:card" content="summary_large_image"/>
<meta property="twitter:domain" content={ config.app.host }/>
<meta name="twitter:title" content={ opengraph.title }/>
{ opengraph.description && <meta name="twitter:description" content={ opengraph.description }/> }
<meta property="twitter:image" content={ opengraph.imageUrl }/>
</Head>
);
};
export default PageMetadata;
import Head from 'next/head';
import React from 'react';
import type { Route } from 'nextjs-routes';
import type { Props as PageProps } from 'nextjs/getServerSideProps';
import PageMetadata from 'nextjs/PageMetadata';
import config from 'configs/app';
import useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as metadata from 'lib/metadata';
import useIsMounted from 'lib/hooks/useIsMounted';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
import * as mixpanel from 'lib/mixpanel';
interface Props<Pathname extends Route['pathname']> {
......@@ -18,35 +18,19 @@ interface Props<Pathname extends Route['pathname']> {
}
const PageNextJs = <Pathname extends Route['pathname']>(props: Props<Pathname>) => {
const { title, description, opengraph, canonical } = metadata.generate(props, props.apiData);
const isMounted = useIsMounted();
useGetCsrfToken();
useAdblockDetect();
useNotifyOnNavigation();
const isMixpanelInited = mixpanel.useInit();
mixpanel.useLogPageView(isMixpanelInited);
return (
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
{ canonical && <link rel="canonical" href={ canonical }/> }
{ /* OG TAGS */ }
<meta property="og:title" content={ opengraph.title }/>
{ opengraph.description && <meta property="og:description" content={ opengraph.description }/> }
<meta property="og:image" content={ opengraph.imageUrl }/>
<meta property="og:type" content="website"/>
{ /* Twitter Meta Tags */ }
<meta name="twitter:card" content="summary_large_image"/>
<meta property="twitter:domain" content={ config.app.host }/>
<meta name="twitter:title" content={ opengraph.title }/>
{ opengraph.description && <meta name="twitter:description" content={ opengraph.description }/> }
<meta property="twitter:image" content={ opengraph.imageUrl }/>
</Head>
{ props.children }
<PageMetadata pathname={ props.pathname } query={ props.query } apiData={ props.apiData }/>
{ isMounted ? props.children : null }
</>
);
};
......
......@@ -16,7 +16,6 @@ import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { SettingsContextProvider } from 'lib/contexts/settings';
import { initGrowthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
import { clientConfig as rollbarConfig, Provider as RollbarProvider } from 'lib/rollbar';
import { SocketProvider } from 'lib/socket/context';
import { Provider as ChakraProvider } from 'toolkit/chakra/provider';
......@@ -49,26 +48,28 @@ const ERROR_SCREEN_STYLES: HTMLChakraProps<'div'> = {
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// to avoid hydration mismatch between server and client
// we have to render the app only on client (when it is mounted)
// https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch
const [ mounted, setMounted ] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
useLoadFeatures(pageProps.uuid);
useNotifyOnNavigation();
const growthBook = initGrowthBook(pageProps.uuid);
useLoadFeatures(growthBook);
const queryClient = useQueryClientConfig();
if (!mounted) {
return null;
}
const content = (() => {
const getLayout = Component.getLayout ?? ((page) => <Layout>{ page }</Layout>);
const getLayout = Component.getLayout ?? ((page) => <Layout>{ page }</Layout>);
return (
<>
{ getLayout(<Component { ...pageProps }/>) }
<Toaster/>
{ config.features.rewards.isEnabled && (
<>
<RewardsLoginModal/>
<RewardsActivityTracker/>
</>
) }
</>
);
})();
return (
<ChakraProvider>
......@@ -86,14 +87,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<RewardsContextProvider>
<MarketplaceContextProvider>
<SettingsContextProvider>
{ getLayout(<Component { ...pageProps }/>) }
<Toaster/>
{ config.features.rewards.isEnabled && (
<>
<RewardsLoginModal/>
<RewardsActivityTracker/>
</>
) }
{ content }
</SettingsContextProvider>
</MarketplaceContextProvider>
</RewardsContextProvider>
......
......@@ -11,24 +11,26 @@ import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => {
return (
<Layout.Container>
<Layout.TopRow/>
<Layout.NavBar/>
<HeaderMobile/>
<Layout.MainArea>
<Layout.SideBar/>
<Layout.MainColumn>
<HeaderAlert/>
<HeaderDesktop/>
<AppErrorBoundary>
<Layout.Content>
{ children }
</Layout.Content>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</Layout.Container>
<Layout.Root content={ children }>
<Layout.Container>
<Layout.TopRow/>
<Layout.NavBar/>
<HeaderMobile/>
<Layout.MainArea>
<Layout.SideBar/>
<Layout.MainColumn>
<HeaderAlert/>
<HeaderDesktop/>
<AppErrorBoundary>
<Layout.Content>
{ children }
</Layout.Content>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</Layout.Container>
</Layout.Root>
);
};
......
......@@ -12,34 +12,36 @@ const HEADER_HEIGHT_MOBILE = 56;
const LayoutDefault = ({ children }: Props) => {
return (
<Layout.Container
overflowY="hidden"
height="$100vh"
display="flex"
flexDirection="column"
>
<Layout.TopRow/>
<HeaderMobile hideSearchBar/>
<Layout.MainArea
minH={{
base: `calc(100dvh - ${ TOP_BAR_HEIGHT + HEADER_HEIGHT_MOBILE }px)`,
lg: `calc(100dvh - ${ TOP_BAR_HEIGHT }px)`,
}}
flex={ 1 }
<Layout.Root content={ children }>
<Layout.Container
overflowY="hidden"
height="$100vh"
display="flex"
flexDirection="column"
>
<Layout.MainColumn
paddingTop={{ base: 0, lg: 0 }}
paddingBottom={ 0 }
paddingX={{ base: 4, lg: 6 }}
<Layout.TopRow/>
<HeaderMobile hideSearchBar/>
<Layout.MainArea
minH={{
base: `calc(100dvh - ${ TOP_BAR_HEIGHT + HEADER_HEIGHT_MOBILE }px)`,
lg: `calc(100dvh - ${ TOP_BAR_HEIGHT }px)`,
}}
flex={ 1 }
>
<AppErrorBoundary>
<Layout.Content pt={{ base: 0, lg: 2 }} flexGrow={ 1 }>
{ children }
</Layout.Content>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
</Layout.Container>
<Layout.MainColumn
paddingTop={{ base: 0, lg: 0 }}
paddingBottom={ 0 }
paddingX={{ base: 4, lg: 6 }}
>
<AppErrorBoundary>
<Layout.Content pt={{ base: 0, lg: 2 }} flexGrow={ 1 }>
{ children }
</Layout.Content>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
</Layout.Container>
</Layout.Root>
);
};
......
......@@ -11,24 +11,26 @@ import * as Layout from './components';
const LayoutError = ({ children }: Props) => {
return (
<Layout.Container>
<Layout.TopRow/>
<Layout.NavBar/>
<HeaderMobile/>
<Layout.MainArea>
<Layout.SideBar/>
<Layout.MainColumn>
<HeaderAlert/>
<HeaderDesktop/>
<AppErrorBoundary>
<main>
{ children }
</main>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</Layout.Container>
<Layout.Root content={ children }>
<Layout.Container>
<Layout.TopRow/>
<Layout.NavBar/>
<HeaderMobile/>
<Layout.MainArea>
<Layout.SideBar/>
<Layout.MainColumn>
<HeaderAlert/>
<HeaderDesktop/>
<AppErrorBoundary>
<main>
{ children }
</main>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</Layout.Container>
</Layout.Root>
);
};
......
......@@ -10,23 +10,25 @@ import * as Layout from './components';
const LayoutHome = ({ children }: Props) => {
return (
<Layout.Container>
<Layout.TopRow/>
<Layout.NavBar/>
<HeaderMobile hideSearchBar/>
<Layout.MainArea>
<Layout.SideBar/>
<Layout.MainColumn
paddingTop={{ base: 3, lg: 6 }}
>
<HeaderAlert/>
<AppErrorBoundary>
{ children }
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</Layout.Container>
<Layout.Root content={ children }>
<Layout.Container>
<Layout.TopRow/>
<Layout.NavBar/>
<HeaderMobile hideSearchBar/>
<Layout.MainArea>
<Layout.SideBar/>
<Layout.MainColumn
paddingTop={{ base: 3, lg: 6 }}
>
<HeaderAlert/>
<AppErrorBoundary>
{ children }
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</Layout.Container>
</Layout.Root>
);
};
......
......@@ -7,11 +7,13 @@ import * as Layout from './components';
const LayoutSearchResults = ({ children }: Props) => {
return (
<Layout.Container>
<Layout.TopRow/>
<Layout.NavBar/>
{ children }
</Layout.Container>
<Layout.Root content={ children }>
<Layout.Container>
<Layout.TopRow/>
<Layout.NavBar/>
{ children }
</Layout.Container>
</Layout.Root>
);
};
......
import React from 'react';
import useIsMounted from 'lib/hooks/useIsMounted';
interface Props {
children: React.ReactNode;
content: React.ReactNode;
}
const Root = ({ children, content }: Props) => {
const isMounted = useIsMounted();
if (!isMounted) {
return content;
}
return children;
};
export default React.memo(Root);
......@@ -6,9 +6,11 @@ import Content from './Content';
import MainArea from './MainArea';
import MainColumn from './MainColumn';
import NavBar from './NavBar';
import Root from './Root';
import SideBar from './SideBar';
export {
Root,
Container,
Content,
MainArea,
......@@ -19,7 +21,8 @@ export {
TopRow,
};
// Container
// Root
// Container
// TopRow
// MainArea
// SideBar
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment