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,27 +48,29 @@ const ERROR_SCREEN_STYLES: HTMLChakraProps<'div'> = { ...@@ -49,27 +48,29 @@ 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);
const queryClient = useQueryClientConfig(); useLoadFeatures(growthBook);
if (!mounted) { const queryClient = useQueryClientConfig();
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 ( return (
<ChakraProvider> <ChakraProvider>
<RollbarProvider config={ rollbarConfig }> <RollbarProvider config={ rollbarConfig }>
...@@ -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,6 +11,7 @@ import * as Layout from './components'; ...@@ -11,6 +11,7 @@ import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => { const LayoutDefault = ({ children }: Props) => {
return ( return (
<Layout.Root content={ children }>
<Layout.Container> <Layout.Container>
<Layout.TopRow/> <Layout.TopRow/>
<Layout.NavBar/> <Layout.NavBar/>
...@@ -29,6 +30,7 @@ const LayoutDefault = ({ children }: Props) => { ...@@ -29,6 +30,7 @@ const LayoutDefault = ({ children }: Props) => {
</Layout.MainArea> </Layout.MainArea>
<Layout.Footer/> <Layout.Footer/>
</Layout.Container> </Layout.Container>
</Layout.Root>
); );
}; };
......
...@@ -12,6 +12,7 @@ const HEADER_HEIGHT_MOBILE = 56; ...@@ -12,6 +12,7 @@ const HEADER_HEIGHT_MOBILE = 56;
const LayoutDefault = ({ children }: Props) => { const LayoutDefault = ({ children }: Props) => {
return ( return (
<Layout.Root content={ children }>
<Layout.Container <Layout.Container
overflowY="hidden" overflowY="hidden"
height="$100vh" height="$100vh"
...@@ -40,6 +41,7 @@ const LayoutDefault = ({ children }: Props) => { ...@@ -40,6 +41,7 @@ const LayoutDefault = ({ children }: Props) => {
</Layout.MainColumn> </Layout.MainColumn>
</Layout.MainArea> </Layout.MainArea>
</Layout.Container> </Layout.Container>
</Layout.Root>
); );
}; };
......
...@@ -11,6 +11,7 @@ import * as Layout from './components'; ...@@ -11,6 +11,7 @@ import * as Layout from './components';
const LayoutError = ({ children }: Props) => { const LayoutError = ({ children }: Props) => {
return ( return (
<Layout.Root content={ children }>
<Layout.Container> <Layout.Container>
<Layout.TopRow/> <Layout.TopRow/>
<Layout.NavBar/> <Layout.NavBar/>
...@@ -29,6 +30,7 @@ const LayoutError = ({ children }: Props) => { ...@@ -29,6 +30,7 @@ const LayoutError = ({ children }: Props) => {
</Layout.MainArea> </Layout.MainArea>
<Layout.Footer/> <Layout.Footer/>
</Layout.Container> </Layout.Container>
</Layout.Root>
); );
}; };
......
...@@ -10,6 +10,7 @@ import * as Layout from './components'; ...@@ -10,6 +10,7 @@ import * as Layout from './components';
const LayoutHome = ({ children }: Props) => { const LayoutHome = ({ children }: Props) => {
return ( return (
<Layout.Root content={ children }>
<Layout.Container> <Layout.Container>
<Layout.TopRow/> <Layout.TopRow/>
<Layout.NavBar/> <Layout.NavBar/>
...@@ -27,6 +28,7 @@ const LayoutHome = ({ children }: Props) => { ...@@ -27,6 +28,7 @@ const LayoutHome = ({ children }: Props) => {
</Layout.MainArea> </Layout.MainArea>
<Layout.Footer/> <Layout.Footer/>
</Layout.Container> </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.Root content={ children }>
<Layout.Container> <Layout.Container>
<Layout.TopRow/> <Layout.TopRow/>
<Layout.NavBar/> <Layout.NavBar/>
{ children } { children }
</Layout.Container> </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,6 +21,7 @@ export { ...@@ -19,6 +21,7 @@ export {
TopRow, TopRow,
}; };
// Root
// Container // Container
// TopRow // TopRow
// MainArea // MainArea
......
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