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 React from 'react';
import { SECOND } from 'toolkit/utils/consts'; import { SECOND } from 'toolkit/utils/consts';
import { initGrowthBook } from './init'; export default function useLoadFeatures(growthBook: GrowthBook | undefined) {
export default function useLoadFeatures(uuid: string) {
const growthBook = initGrowthBook(uuid);
React.useEffect(() => { React.useEffect(() => {
growthBook?.setAttributes({ if (!growthBook) {
return;
}
growthBook.setAttributes({
...growthBook.getAttributes(), ...growthBook.getAttributes(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: window.navigator.language, language: window.navigator.language,
}); });
growthBook?.loadFeatures({ timeout: SECOND }); growthBook.loadFeatures({ timeout: SECOND });
}, [ growthBook ]); }, [ growthBook ]);
} }
...@@ -39,6 +39,7 @@ const moduleExports = { ...@@ -39,6 +39,7 @@ const moduleExports = {
headers, headers,
output: 'standalone', output: 'standalone',
productionBrowserSourceMaps: true, productionBrowserSourceMaps: true,
serverExternalPackages: ["@opentelemetry/sdk-node", "@opentelemetry/auto-instrumentations-node"],
experimental: { experimental: {
staleTimes: { staleTimes: {
dynamic: 30, 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 React from 'react';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
import type { Props as PageProps } from 'nextjs/getServerSideProps'; 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 useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; 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'; import * as mixpanel from 'lib/mixpanel';
interface Props<Pathname extends Route['pathname']> { interface Props<Pathname extends Route['pathname']> {
...@@ -18,35 +18,19 @@ 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 PageNextJs = <Pathname extends Route['pathname']>(props: Props<Pathname>) => {
const { title, description, opengraph, canonical } = metadata.generate(props, props.apiData); const isMounted = useIsMounted();
useGetCsrfToken(); useGetCsrfToken();
useAdblockDetect(); useAdblockDetect();
useNotifyOnNavigation();
const isMixpanelInited = mixpanel.useInit(); const isMixpanelInited = mixpanel.useInit();
mixpanel.useLogPageView(isMixpanelInited); mixpanel.useLogPageView(isMixpanelInited);
return ( return (
<> <>
<Head> <PageMetadata pathname={ props.pathname } query={ props.query } apiData={ props.apiData }/>
<title>{ title }</title> { isMounted ? props.children : null }
<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 }
</> </>
); );
}; };
......
...@@ -16,7 +16,6 @@ import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; ...@@ -16,7 +16,6 @@ import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { SettingsContextProvider } from 'lib/contexts/settings'; import { SettingsContextProvider } from 'lib/contexts/settings';
import { initGrowthBook } from 'lib/growthbook/init'; import { initGrowthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
import { clientConfig as rollbarConfig, Provider as RollbarProvider } from 'lib/rollbar'; import { clientConfig as rollbarConfig, Provider as RollbarProvider } from 'lib/rollbar';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import { Provider as ChakraProvider } from 'toolkit/chakra/provider'; import { Provider as ChakraProvider } from 'toolkit/chakra/provider';
...@@ -49,26 +48,28 @@ const ERROR_SCREEN_STYLES: HTMLChakraProps<'div'> = { ...@@ -49,26 +48,28 @@ const ERROR_SCREEN_STYLES: HTMLChakraProps<'div'> = {
}; };
function MyApp({ Component, pageProps }: AppPropsWithLayout) { 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); const growthBook = initGrowthBook(pageProps.uuid);
useLoadFeatures(growthBook);
const queryClient = useQueryClientConfig(); const queryClient = useQueryClientConfig();
if (!mounted) { const content = (() => {
return null; 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 ( return (
<ChakraProvider> <ChakraProvider>
...@@ -86,14 +87,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -86,14 +87,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<RewardsContextProvider> <RewardsContextProvider>
<MarketplaceContextProvider> <MarketplaceContextProvider>
<SettingsContextProvider> <SettingsContextProvider>
{ getLayout(<Component { ...pageProps }/>) } { content }
<Toaster/>
{ config.features.rewards.isEnabled && (
<>
<RewardsLoginModal/>
<RewardsActivityTracker/>
</>
) }
</SettingsContextProvider> </SettingsContextProvider>
</MarketplaceContextProvider> </MarketplaceContextProvider>
</RewardsContextProvider> </RewardsContextProvider>
......
...@@ -11,24 +11,26 @@ import * as Layout from './components'; ...@@ -11,24 +11,26 @@ import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => { const LayoutDefault = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Root content={ children }>
<Layout.TopRow/> <Layout.Container>
<Layout.NavBar/> <Layout.TopRow/>
<HeaderMobile/> <Layout.NavBar/>
<Layout.MainArea> <HeaderMobile/>
<Layout.SideBar/> <Layout.MainArea>
<Layout.MainColumn> <Layout.SideBar/>
<HeaderAlert/> <Layout.MainColumn>
<HeaderDesktop/> <HeaderAlert/>
<AppErrorBoundary> <HeaderDesktop/>
<Layout.Content> <AppErrorBoundary>
{ children } <Layout.Content>
</Layout.Content> { children }
</AppErrorBoundary> </Layout.Content>
</Layout.MainColumn> </AppErrorBoundary>
</Layout.MainArea> </Layout.MainColumn>
<Layout.Footer/> </Layout.MainArea>
</Layout.Container> <Layout.Footer/>
</Layout.Container>
</Layout.Root>
); );
}; };
......
...@@ -12,34 +12,36 @@ const HEADER_HEIGHT_MOBILE = 56; ...@@ -12,34 +12,36 @@ const HEADER_HEIGHT_MOBILE = 56;
const LayoutDefault = ({ children }: Props) => { const LayoutDefault = ({ children }: Props) => {
return ( return (
<Layout.Container <Layout.Root content={ children }>
overflowY="hidden" <Layout.Container
height="$100vh" overflowY="hidden"
display="flex" height="$100vh"
flexDirection="column" 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.MainColumn <Layout.TopRow/>
paddingTop={{ base: 0, lg: 0 }} <HeaderMobile hideSearchBar/>
paddingBottom={ 0 } <Layout.MainArea
paddingX={{ base: 4, lg: 6 }} minH={{
base: `calc(100dvh - ${ TOP_BAR_HEIGHT + HEADER_HEIGHT_MOBILE }px)`,
lg: `calc(100dvh - ${ TOP_BAR_HEIGHT }px)`,
}}
flex={ 1 }
> >
<AppErrorBoundary> <Layout.MainColumn
<Layout.Content pt={{ base: 0, lg: 2 }} flexGrow={ 1 }> paddingTop={{ base: 0, lg: 0 }}
{ children } paddingBottom={ 0 }
</Layout.Content> paddingX={{ base: 4, lg: 6 }}
</AppErrorBoundary> >
</Layout.MainColumn> <AppErrorBoundary>
</Layout.MainArea> <Layout.Content pt={{ base: 0, lg: 2 }} flexGrow={ 1 }>
</Layout.Container> { children }
</Layout.Content>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
</Layout.Container>
</Layout.Root>
); );
}; };
......
...@@ -11,24 +11,26 @@ import * as Layout from './components'; ...@@ -11,24 +11,26 @@ import * as Layout from './components';
const LayoutError = ({ children }: Props) => { const LayoutError = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Root content={ children }>
<Layout.TopRow/> <Layout.Container>
<Layout.NavBar/> <Layout.TopRow/>
<HeaderMobile/> <Layout.NavBar/>
<Layout.MainArea> <HeaderMobile/>
<Layout.SideBar/> <Layout.MainArea>
<Layout.MainColumn> <Layout.SideBar/>
<HeaderAlert/> <Layout.MainColumn>
<HeaderDesktop/> <HeaderAlert/>
<AppErrorBoundary> <HeaderDesktop/>
<main> <AppErrorBoundary>
{ children } <main>
</main> { children }
</AppErrorBoundary> </main>
</Layout.MainColumn> </AppErrorBoundary>
</Layout.MainArea> </Layout.MainColumn>
<Layout.Footer/> </Layout.MainArea>
</Layout.Container> <Layout.Footer/>
</Layout.Container>
</Layout.Root>
); );
}; };
......
...@@ -10,23 +10,25 @@ import * as Layout from './components'; ...@@ -10,23 +10,25 @@ import * as Layout from './components';
const LayoutHome = ({ children }: Props) => { const LayoutHome = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Root content={ children }>
<Layout.TopRow/> <Layout.Container>
<Layout.NavBar/> <Layout.TopRow/>
<HeaderMobile hideSearchBar/> <Layout.NavBar/>
<Layout.MainArea> <HeaderMobile hideSearchBar/>
<Layout.SideBar/> <Layout.MainArea>
<Layout.MainColumn <Layout.SideBar/>
paddingTop={{ base: 3, lg: 6 }} <Layout.MainColumn
> paddingTop={{ base: 3, lg: 6 }}
<HeaderAlert/> >
<AppErrorBoundary> <HeaderAlert/>
{ children } <AppErrorBoundary>
</AppErrorBoundary> { children }
</Layout.MainColumn> </AppErrorBoundary>
</Layout.MainArea> </Layout.MainColumn>
<Layout.Footer/> </Layout.MainArea>
</Layout.Container> <Layout.Footer/>
</Layout.Container>
</Layout.Root>
); );
}; };
......
...@@ -7,11 +7,13 @@ import * as Layout from './components'; ...@@ -7,11 +7,13 @@ import * as Layout from './components';
const LayoutSearchResults = ({ children }: Props) => { const LayoutSearchResults = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Root content={ children }>
<Layout.TopRow/> <Layout.Container>
<Layout.NavBar/> <Layout.TopRow/>
{ children } <Layout.NavBar/>
</Layout.Container> { 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'; ...@@ -6,9 +6,11 @@ import Content from './Content';
import MainArea from './MainArea'; import MainArea from './MainArea';
import MainColumn from './MainColumn'; import MainColumn from './MainColumn';
import NavBar from './NavBar'; import NavBar from './NavBar';
import Root from './Root';
import SideBar from './SideBar'; import SideBar from './SideBar';
export { export {
Root,
Container, Container,
Content, Content,
MainArea, MainArea,
...@@ -19,7 +21,8 @@ export { ...@@ -19,7 +21,8 @@ export {
TopRow, TopRow,
}; };
// Container // Root
// Container
// TopRow // TopRow
// MainArea // MainArea
// SideBar // 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