Commit 32c13634 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Enhance OG tags with API data (#1806)

* simple fetch data for token instance page

* generate enhanced metadata

* group query params in page props

* make base gSSP function generic

* add metric for search engine bots

* measure api request duration

* get API data for token, address and dapp pages

* improve typings for apiData prop

* add ENV variable

* tweak logs for /media-type requests

* [skip ci] add prefix to default metrics
parent 79b02e11
...@@ -8,6 +8,7 @@ const meta = Object.freeze({ ...@@ -8,6 +8,7 @@ const meta = Object.freeze({
og: { og: {
description: getEnvValue('NEXT_PUBLIC_OG_DESCRIPTION') || '', description: getEnvValue('NEXT_PUBLIC_OG_DESCRIPTION') || '',
imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl), imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl),
enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED') === 'true',
}, },
}); });
......
...@@ -601,6 +601,7 @@ const schema = yup ...@@ -601,6 +601,7 @@ const schema = yup
NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(), NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(),
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest), NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: yup.boolean(),
NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest), NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(), NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
......
...@@ -40,6 +40,7 @@ NEXT_PUBLIC_NETWORK_SHORT_NAME=Test ...@@ -40,6 +40,7 @@ NEXT_PUBLIC_NETWORK_SHORT_NAME=Test
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_OG_DESCRIPTION='Hello world!' NEXT_PUBLIC_OG_DESCRIPTION='Hello world!'
NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://blockscout.com','text':'Blockscout'}] NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://blockscout.com','text':'Blockscout'}]
NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE=true NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE=true
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
......
...@@ -85,7 +85,7 @@ frontend: ...@@ -85,7 +85,7 @@ frontend:
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]" NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
NEXT_PUBLIC_HAS_USER_OPS: true NEXT_PUBLIC_HAS_USER_OPS: true
NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]"
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: noves NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: blockscout
NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true
NEXT_PUBLIC_AD_BANNER_PROVIDER: getit NEXT_PUBLIC_AD_BANNER_PROVIDER: getit
...@@ -93,7 +93,7 @@ frontend: ...@@ -93,7 +93,7 @@ frontend:
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: "{ \"id\": \"632019\", \"width\": \"728\", \"height\": \"90\" }" NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: "{ \"id\": \"632019\", \"width\": \"728\", \"height\": \"90\" }"
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: "{ \"id\": \"632018\", \"width\": \"320\", \"height\": \"100\" }" NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: "{ \"id\": \"632018\", \"width\": \"320\", \"height\": \"100\" }"
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true
PROMETHEUS_METRICS_ENABLED: true NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true
envFromSecret: envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
......
...@@ -176,6 +176,7 @@ Settings for meta tags and OG tags ...@@ -176,6 +176,7 @@ Settings for meta tags and OG tags
| NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE | `boolean` | Set to `true` to promote Blockscout in meta and OG titles | - | `true` | `true` | | NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE | `boolean` | Set to `true` to promote Blockscout in meta and OG titles | - | `true` | `true` |
| NEXT_PUBLIC_OG_DESCRIPTION | `string` | Custom OG description | - | - | `Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.` | | NEXT_PUBLIC_OG_DESCRIPTION | `string` | Custom OG description | - | - | `Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.` |
| NEXT_PUBLIC_OG_IMAGE_URL | `string` | OG image url. Minimum image size is 200 x 20 pixels (recommended: 1200 x 600); maximum supported file size is 8 MB; 2:1 aspect ratio; supported formats: image/jpeg, image/gif, image/png | - | `static/og_placeholder.png` | `https://placekitten.com/1200/600` | | NEXT_PUBLIC_OG_IMAGE_URL | `string` | OG image url. Minimum image size is 200 x 20 pixels (recommended: 1200 x 600); maximum supported file size is 8 MB; 2:1 aspect ratio; supported formats: image/jpeg, image/gif, image/png | - | `static/og_placeholder.png` | `https://placekitten.com/1200/600` |
| NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to populate OG tags (title, description) with API data for social preview robot requests | - | `false` | `true` |
   
......
...@@ -15,13 +15,9 @@ import 'lib/setLocale'; ...@@ -15,13 +15,9 @@ import 'lib/setLocale';
const PAGE_PROPS = { const PAGE_PROPS = {
cookies: '', cookies: '',
referrer: '', referrer: '',
id: '', query: {},
height_or_hash: '', adBannerProvider: undefined,
hash: '', apiData: null,
number: '',
q: '',
name: '',
adBannerProvider: '',
}; };
const TestApp = ({ children }: {children: React.ReactNode}) => { const TestApp = ({ children }: {children: React.ReactNode}) => {
......
...@@ -10,13 +10,9 @@ type Props = { ...@@ -10,13 +10,9 @@ type Props = {
const AppContext = createContext<PageProps>({ const AppContext = createContext<PageProps>({
cookies: '', cookies: '',
referrer: '', referrer: '',
id: '', query: {},
height_or_hash: '', adBannerProvider: undefined,
hash: '', apiData: null,
number: '',
q: '',
name: '',
adBannerProvider: '',
}); });
export function AppContextProvider({ children, pageProps }: Props) { export function AppContextProvider({ children, pageProps }: Props) {
......
...@@ -4,26 +4,29 @@ import type { Route } from 'nextjs-routes'; ...@@ -4,26 +4,29 @@ import type { Route } from 'nextjs-routes';
import generate from './generate'; import generate from './generate';
interface TestCase<R extends Route> { interface TestCase<Pathname extends Route['pathname']> {
title: string; title: string;
route: R; route: {
apiData?: ApiData<R>; pathname: Pathname;
query?: Route['query'];
};
apiData?: ApiData<Pathname>;
} }
const TEST_CASES: Array<TestCase<Route>> = [ const TEST_CASES = [
{ {
title: 'static route', title: 'static route',
route: { route: {
pathname: '/blocks', pathname: '/blocks',
}, },
}, } as TestCase<'/blocks'>,
{ {
title: 'dynamic route', title: 'dynamic route',
route: { route: {
pathname: '/tx/[hash]', pathname: '/tx/[hash]',
query: { hash: '0x12345' }, query: { hash: '0x12345' },
}, },
}, } as TestCase<'/tx/[hash]'>,
{ {
title: 'dynamic route with API data', title: 'dynamic route with API data',
route: { route: {
...@@ -31,7 +34,7 @@ const TEST_CASES: Array<TestCase<Route>> = [ ...@@ -31,7 +34,7 @@ const TEST_CASES: Array<TestCase<Route>> = [
query: { hash: '0x12345' }, query: { hash: '0x12345' },
}, },
apiData: { symbol: 'USDT' }, apiData: { symbol: 'USDT' },
} as TestCase<{ pathname: '/token/[hash]'; query: { hash: string }}>, } as TestCase<'/token/[hash]'>,
]; ];
describe('generates correct metadata for:', () => { describe('generates correct metadata for:', () => {
......
import type { ApiData, Metadata } from './types'; import type { ApiData, Metadata } from './types';
import type { RouteParams } from 'nextjs/types';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
...@@ -9,7 +10,7 @@ import compileValue from './compileValue'; ...@@ -9,7 +10,7 @@ import compileValue from './compileValue';
import getPageOgType from './getPageOgType'; import getPageOgType from './getPageOgType';
import * as templates from './templates'; import * as templates from './templates';
export default function generate<R extends Route>(route: R, apiData?: ApiData<R>): Metadata { export default function generate<Pathname extends Route['pathname']>(route: RouteParams<Pathname>, apiData: ApiData<Pathname> = null): Metadata {
const params = { const params = {
...route.query, ...route.query,
...apiData, ...apiData,
...@@ -17,7 +18,7 @@ export default function generate<R extends Route>(route: R, apiData?: ApiData<R> ...@@ -17,7 +18,7 @@ export default function generate<R extends Route>(route: R, apiData?: ApiData<R>
network_title: getNetworkTitle(), network_title: getNetworkTitle(),
}; };
const compiledTitle = compileValue(templates.title.make(route.pathname), params); const compiledTitle = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params);
const title = compiledTitle ? compiledTitle + (config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : '') : ''; const title = compiledTitle ? compiledTitle + (config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : '') : '';
const description = compileValue(templates.description.make(route.pathname), params); const description = compileValue(templates.description.make(route.pathname), params);
......
import type { IncomingMessage, ServerResponse } from 'http';
import { httpLogger } from 'nextjs/utils/logger';
import metrics from 'lib/monitoring/metrics';
export default async function getApiDataForSocialPreview(req: IncomingMessage | undefined, res: ServerResponse<IncomingMessage> | undefined, pathname: string) {
if (!req || !res || !metrics) {
return;
}
const userAgent = req.headers['user-agent'];
if (!userAgent) {
return;
}
if (userAgent.toLowerCase().includes('twitter')) {
httpLogger(req, res);
metrics.requestCounter.inc({ route: pathname, bot: 'twitter' });
}
if (userAgent.toLowerCase().includes('facebook')) {
httpLogger(req, res);
metrics.requestCounter.inc({ route: pathname, bot: 'facebook' });
}
if (userAgent.toLowerCase().includes('telegram')) {
httpLogger(req, res);
metrics.requestCounter.inc({ route: pathname, bot: 'telegram' });
}
}
export { default as generate } from './generate'; export { default as generate } from './generate';
export { default as update } from './update'; export { default as update } from './update';
export * from './types';
...@@ -13,10 +13,10 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -13,10 +13,10 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/contract-verification': 'verify contract', '/contract-verification': 'verify contract',
'/address/[hash]/contract-verification': 'contract verification for %hash%', '/address/[hash]/contract-verification': 'contract verification for %hash%',
'/tokens': 'tokens', '/tokens': 'tokens',
'/token/[hash]': '%symbol% token details', '/token/[hash]': 'token details',
'/token/[hash]/instance/[id]': 'NFT instance', '/token/[hash]/instance/[id]': 'NFT instance',
'/apps': 'apps marketplace', '/apps': 'apps marketplace',
'/apps/[id]': '- %app_name%', '/apps/[id]': 'marketplace app',
'/stats': 'statistics', '/stats': 'statistics',
'/api-docs': 'REST API', '/api-docs': 'REST API',
'/graphiql': 'GraphQL', '/graphiql': 'GraphQL',
...@@ -56,8 +56,15 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -56,8 +56,15 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/auth/unverified-email': 'unverified email', '/auth/unverified-email': 'unverified email',
}; };
export function make(pathname: Route['pathname']) { const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
const template = TEMPLATE_MAP[pathname]; '/token/[hash]': '%symbol% token details',
'/token/[hash]/instance/[id]': 'token instance for %symbol%',
'/apps/[id]': '- %app_name%',
'/address/[hash]': 'address details for %domain_name%',
};
export function make(pathname: Route['pathname'], isEnriched = false) {
const template = (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname];
return `%network_name% ${ template }`; return `%network_name% ${ template }`;
} }
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
/* eslint-disable @typescript-eslint/indent */ /* eslint-disable @typescript-eslint/indent */
export type ApiData<R extends Route> = export type ApiData<Pathname extends Route['pathname']> =
R['pathname'] extends '/token/[hash]' ? { symbol: string } : (
R['pathname'] extends '/token/[hash]/instance/[id]' ? { symbol: string } : Pathname extends '/address/[hash]' ? { domain_name: string } :
R['pathname'] extends '/apps/[id]' ? { app_name: string } : Pathname extends '/token/[hash]' ? { symbol: string } :
never; Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } :
Pathname extends '/apps/[id]' ? { app_name: string } :
never
) | null;
export interface Metadata { export interface Metadata {
title: string; title: string;
......
import type { ApiData } from './types'; import type { ApiData } from './types';
import type { RouteParams } from 'nextjs/types';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
import generate from './generate'; import generate from './generate';
export default function update<R extends Route>(route: R, apiData: ApiData<R>) { export default function update<Pathname extends Route['pathname']>(route: RouteParams<Pathname>, apiData: ApiData<Pathname>) {
const { title, description } = generate(route, apiData); const { title, description } = generate(route, apiData);
window.document.title = title; window.document.title = title;
......
...@@ -8,13 +8,26 @@ const metrics = (() => { ...@@ -8,13 +8,26 @@ const metrics = (() => {
promClient.register.clear(); promClient.register.clear();
const requestCounter = new promClient.Counter({ const socialPreviewBotRequests = new promClient.Counter({
name: 'request_counter', name: 'social_preview_bot_requests_total',
help: 'Number of incoming requests', help: 'Number of incoming requests from social preview bots',
labelNames: [ 'route', 'bot' ] as const, labelNames: [ 'route', 'bot' ] as const,
}); });
return { requestCounter }; const searchEngineBotRequests = new promClient.Counter({
name: 'search_engine_bot_requests_total',
help: 'Number of incoming requests from search engine bots',
labelNames: [ 'route', 'bot' ] as const,
});
const apiRequestDuration = new promClient.Histogram({
name: 'api_request_duration_seconds',
help: 'Duration of requests to API in seconds',
labelNames: [ 'route', 'code' ],
buckets: [ 0.2, 0.5, 1, 3, 10 ],
});
return { socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration };
})(); })();
export default metrics; export default metrics;
...@@ -2,6 +2,7 @@ import Head from 'next/head'; ...@@ -2,6 +2,7 @@ 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 config from 'configs/app'; import config from 'configs/app';
import useAdblockDetect from 'lib/hooks/useAdblockDetect'; import useAdblockDetect from 'lib/hooks/useAdblockDetect';
...@@ -10,14 +11,17 @@ import * as metadata from 'lib/metadata'; ...@@ -10,14 +11,17 @@ import * as metadata from 'lib/metadata';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import { init as initSentry } from 'lib/sentry/config'; import { init as initSentry } from 'lib/sentry/config';
type Props = Route & { interface Props<Pathname extends Route['pathname']> {
pathname: Pathname;
children: React.ReactNode; children: React.ReactNode;
query?: PageProps<Pathname>['query'];
apiData?: PageProps<Pathname>['apiData'];
} }
initSentry(); initSentry();
const PageNextJs = (props: Props) => { const PageNextJs = <Pathname extends Route['pathname']>(props: Props<Pathname>) => {
const { title, description, opengraph } = metadata.generate(props); const { title, description, opengraph } = metadata.generate(props, props.apiData);
useGetCsrfToken(); useGetCsrfToken();
useAdblockDetect(); useAdblockDetect();
......
import type { GetServerSideProps } from 'next'; import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
import type { AdBannerProviders } from 'types/client/adProviders';
import type { Route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import isNeedProxy from 'lib/api/isNeedProxy'; import isNeedProxy from 'lib/api/isNeedProxy';
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
const adBannerFeature = config.features.adsBanner; const adBannerFeature = config.features.adsBanner;
import type * as metadata from 'lib/metadata';
export type Props = { export interface Props<Pathname extends Route['pathname'] = never> {
query: Route['query'];
cookies: string; cookies: string;
referrer: string; referrer: string;
id: string; adBannerProvider: AdBannerProviders | undefined;
height_or_hash: string; // if apiData is undefined, Next.js will complain that it is not serializable
hash: string; // so we force it to be always present in the props but it can be null
number: string; apiData: metadata.ApiData<Pathname> | null;
q: string;
name: string;
adBannerProvider: string;
} }
export const base: GetServerSideProps<Props> = async({ req, query }) => { export const base = async <Pathname extends Route['pathname'] = never>({ req, query }: GetServerSidePropsContext):
Promise<GetServerSidePropsResult<Props<Pathname>>> => {
const adBannerProvider = (() => { const adBannerProvider = (() => {
if (adBannerFeature.isEnabled) { if (adBannerFeature.isEnabled) {
if ('additionalProvider' in adBannerFeature && adBannerFeature.additionalProvider) { if ('additionalProvider' in adBannerFeature && adBannerFeature.additionalProvider) {
...@@ -28,20 +32,16 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => { ...@@ -28,20 +32,16 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => {
return adBannerFeature.provider; return adBannerFeature.provider;
} }
} }
return ''; return;
})(); })();
return { return {
props: { props: {
query,
cookies: req.headers.cookie || '', cookies: req.headers.cookie || '',
referrer: req.headers.referer || '', referrer: req.headers.referer || '',
id: query.id?.toString() || '',
hash: query.hash?.toString() || '',
height_or_hash: query.height_or_hash?.toString() || '',
number: query.number?.toString() || '',
q: query.q?.toString() || '',
name: query.name?.toString() || '',
adBannerProvider, adBannerProvider,
apiData: null,
}, },
}; };
}; };
...@@ -119,14 +119,15 @@ export const batch: GetServerSideProps<Props> = async(context) => { ...@@ -119,14 +119,15 @@ export const batch: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const marketplace: GetServerSideProps<Props> = async(context) => { export const marketplace = async <Pathname extends Route['pathname'] = never>(context: GetServerSidePropsContext):
Promise<GetServerSidePropsResult<Props<Pathname>>> => {
if (!config.features.marketplace.isEnabled) { if (!config.features.marketplace.isEnabled) {
return { return {
notFound: true, notFound: true,
}; };
} }
return base(context); return base<Pathname>(context);
}; };
export const apiDocs: GetServerSideProps<Props> = async(context) => { export const apiDocs: GetServerSideProps<Props> = async(context) => {
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import type { Route } from 'nextjs-routes';
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & { export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: React.ReactElement) => React.ReactNode; getLayout?: (page: React.ReactElement) => React.ReactNode;
} }
export interface RouteParams<Pathname extends Route['pathname']> {
pathname: Pathname;
query?: Route['query'];
}
import type { IncomingMessage } from 'http';
type SocialPreviewBot = 'twitter' | 'facebook' | 'telegram' | 'slack';
type SearchEngineBot = 'google' | 'bing' | 'yahoo' | 'duckduckgo';
type ReturnType = {
type: 'social_preview';
bot: SocialPreviewBot;
} | {
type: 'search_engine';
bot: SearchEngineBot;
} | undefined
export default function detectBotRequest(req: IncomingMessage): ReturnType {
const userAgent = req.headers['user-agent'];
if (!userAgent) {
return;
}
if (userAgent.toLowerCase().includes('twitter')) {
return { type: 'social_preview', bot: 'twitter' };
}
if (userAgent.toLowerCase().includes('facebook')) {
return { type: 'social_preview', bot: 'facebook' };
}
if (userAgent.toLowerCase().includes('telegram')) {
return { type: 'social_preview', bot: 'telegram' };
}
if (userAgent.toLowerCase().includes('slack')) {
return { type: 'social_preview', bot: 'slack' };
}
if (userAgent.toLowerCase().includes('googlebot')) {
return { type: 'search_engine', bot: 'google' };
}
if (userAgent.toLowerCase().includes('bingbot')) {
return { type: 'search_engine', bot: 'bing' };
}
if (userAgent.toLowerCase().includes('yahoo')) {
return { type: 'search_engine', bot: 'yahoo' };
}
if (userAgent.toLowerCase().includes('duckduck')) {
return { type: 'search_engine', bot: 'duckduckgo' };
}
}
import fetch, { AbortError } from 'node-fetch';
import buildUrl from 'nextjs/utils/buildUrl';
import { httpLogger } from 'nextjs/utils/logger';
import { RESOURCES } from 'lib/api/resources';
import type { ResourceName, ResourcePathParams, ResourcePayload } from 'lib/api/resources';
import { SECOND } from 'lib/consts';
import metrics from 'lib/monitoring/metrics';
type Params<R extends ResourceName> = (
{
resource: R;
pathParams?: ResourcePathParams<R>;
} | {
url: string;
route: string;
}
) & {
timeout?: number;
}
export default async function fetchApi<R extends ResourceName = never, S = ResourcePayload<R>>(params: Params<R>): Promise<S | undefined> {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, params.timeout || SECOND);
const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams);
const route = 'route' in params ? params.route : RESOURCES[params.resource]['path'];
const end = metrics?.apiRequestDuration.startTimer();
try {
const response = await fetch(url, { signal: controller.signal });
const duration = end?.({ route, code: response.status });
if (response.status === 200) {
httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration });
} else {
httpLogger.logger.error({ message: 'API fetch', url, code: response.status, duration });
}
return await response.json() as Promise<S>;
} catch (error) {
const code = error instanceof AbortError ? 504 : 500;
const duration = end?.({ route, code });
httpLogger.logger.error({ message: 'API fetch', url, code, duration });
} finally {
clearTimeout(timeout);
}
}
...@@ -30,14 +30,9 @@ export default function fetchFactory( ...@@ -30,14 +30,9 @@ export default function fetchFactory(
}; };
httpLogger.logger.info({ httpLogger.logger.info({
message: 'Trying to call API', message: 'API fetch via Next.js proxy',
url, url,
req: _req, // headers,
});
httpLogger.logger.info({
message: 'API request headers',
headers,
}); });
const body = (() => { const body = (() => {
......
import type { IncomingMessage, ServerResponse } from 'http';
import metrics from 'lib/monitoring/metrics';
import detectBotRequest from './detectBotRequest';
export default async function logRequestFromBot(req: IncomingMessage | undefined, res: ServerResponse<IncomingMessage> | undefined, pathname: string) {
if (!req || !res || !metrics) {
return;
}
const botInfo = detectBotRequest(req);
if (!botInfo) {
return;
}
switch (botInfo.type) {
case 'search_engine': {
metrics.searchEngineBotRequests.inc({ route: pathname, bot: botInfo.bot });
return;
}
case 'social_preview': {
metrics.socialPreviewBotRequests.inc({ route: pathname, bot: botInfo.bot });
return;
}
}
}
...@@ -3,9 +3,9 @@ import type { DocumentContext } from 'next/document'; ...@@ -3,9 +3,9 @@ import type { DocumentContext } from 'next/document';
import Document, { Html, Head, Main, NextScript } from 'next/document'; import Document, { Html, Head, Main, NextScript } from 'next/document';
import React from 'react'; import React from 'react';
import logRequestFromBot from 'nextjs/utils/logRequestFromBot';
import * as serverTiming from 'nextjs/utils/serverTiming'; import * as serverTiming from 'nextjs/utils/serverTiming';
import getApiDataForSocialPreview from 'lib/metadata/getApiDataForSocialPreview';
import theme from 'theme'; import theme from 'theme';
import * as svgSprite from 'ui/shared/IconSvg'; import * as svgSprite from 'ui/shared/IconSvg';
...@@ -22,7 +22,7 @@ class MyDocument extends Document { ...@@ -22,7 +22,7 @@ class MyDocument extends Document {
return result; return result;
}; };
await getApiDataForSocialPreview(ctx.req, ctx.res, ctx.pathname); await logRequestFromBot(ctx.req, ctx.res, ctx.pathname);
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx);
......
...@@ -8,7 +8,7 @@ import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddr ...@@ -8,7 +8,7 @@ import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddr
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/address/[hash]/contract-verification" query={ props }> <PageNextJs pathname="/address/[hash]/contract-verification" query={ props.query }>
<ContractVerificationForAddress/> <ContractVerificationForAddress/>
</PageNextJs> </PageNextJs>
); );
......
import type { NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import type { Route } from 'nextjs-routes';
import type { Props } from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps';
import * as gSSP from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
import detectBotRequest from 'nextjs/utils/detectBotRequest';
import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString';
const Address = dynamic(() => import('ui/pages/Address'), { ssr: false }); const Address = dynamic(() => import('ui/pages/Address'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const pathname: Route['pathname'] = '/address/[hash]';
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
return ( return (
<PageNextJs pathname="/address/[hash]" query={ props }> <PageNextJs pathname="/address/[hash]" query={ props.query } apiData={ props.apiData }>
<Address/> <Address/>
</PageNextJs> </PageNextJs>
); );
...@@ -17,4 +25,24 @@ const Page: NextPage<Props> = (props: Props) => { ...@@ -17,4 +25,24 @@ const Page: NextPage<Props> = (props: Props) => {
export default Page; export default Page;
export { base as getServerSideProps } from 'nextjs/getServerSideProps'; export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.base<typeof pathname>(ctx);
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) {
const botInfo = detectBotRequest(ctx.req);
if (botInfo?.type === 'social_preview') {
const addressData = await fetchApi({
resource: 'address',
pathParams: { hash: getQueryParamString(ctx.query.hash) },
timeout: 1_000,
});
(await baseResponse.props).apiData = addressData && addressData.ens_domain_name ? {
domain_name: addressData.ens_domain_name,
} : null;
}
}
return baseResponse;
};
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import buildUrl from 'nextjs/utils/buildUrl'; import buildUrl from 'nextjs/utils/buildUrl';
import fetchFactory from 'nextjs/utils/fetch'; import fetchFactory from 'nextjs/utils/fetchProxy';
import { httpLogger } from 'nextjs/utils/logger'; import { httpLogger } from 'nextjs/utils/logger';
export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) { export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) {
......
...@@ -3,16 +3,20 @@ import nodeFetch from 'node-fetch'; ...@@ -3,16 +3,20 @@ import nodeFetch from 'node-fetch';
import { httpLogger } from 'nextjs/utils/logger'; import { httpLogger } from 'nextjs/utils/logger';
import metrics from 'lib/monitoring/metrics';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
export default async function mediaTypeHandler(req: NextApiRequest, res: NextApiResponse) { export default async function mediaTypeHandler(req: NextApiRequest, res: NextApiResponse) {
httpLogger(req, res);
try { try {
const url = getQueryParamString(req.query.url); const url = getQueryParamString(req.query.url);
const end = metrics?.apiRequestDuration.startTimer();
const response = await nodeFetch(url, { method: 'HEAD' }); const response = await nodeFetch(url, { method: 'HEAD' });
const duration = end?.({ route: '/media-type', code: response.status });
if (response.status !== 200) { if (response.status !== 200) {
httpLogger.logger.error({ message: 'API fetch', url, code: response.status, duration });
throw new Error(); throw new Error();
} }
...@@ -30,6 +34,8 @@ export default async function mediaTypeHandler(req: NextApiRequest, res: NextApi ...@@ -30,6 +34,8 @@ export default async function mediaTypeHandler(req: NextApiRequest, res: NextApi
return 'html'; return 'html';
} }
})(); })();
httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration });
res.status(200).json({ type: mediaType }); res.status(200).json({ type: mediaType });
} catch (error) { } catch (error) {
res.status(200).json({ type: undefined }); res.status(200).json({ type: undefined });
......
...@@ -4,7 +4,7 @@ import * as promClient from 'prom-client'; ...@@ -4,7 +4,7 @@ import * as promClient from 'prom-client';
// eslint-disable-next-line no-restricted-properties // eslint-disable-next-line no-restricted-properties
const isEnabled = process.env.PROMETHEUS_METRICS_ENABLED === 'true'; const isEnabled = process.env.PROMETHEUS_METRICS_ENABLED === 'true';
isEnabled && promClient.collectDefaultMetrics(); isEnabled && promClient.collectDefaultMetrics({ prefix: 'frontend_' });
export default async function metricsHandler(req: NextApiRequest, res: NextApiResponse) { export default async function metricsHandler(req: NextApiRequest, res: NextApiResponse) {
const metrics = await promClient.register.metrics(); const metrics = await promClient.register.metrics();
......
...@@ -2,7 +2,7 @@ import _pick from 'lodash/pick'; ...@@ -2,7 +2,7 @@ import _pick from 'lodash/pick';
import _pickBy from 'lodash/pickBy'; import _pickBy from 'lodash/pickBy';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'nextjs/utils/fetch'; import fetchFactory from 'nextjs/utils/fetchProxy';
import appConfig from 'configs/app'; import appConfig from 'configs/app';
......
import type { GetServerSideProps } from 'next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import type { NextPageWithLayout } from 'nextjs/types'; import type { NextPageWithLayout } from 'nextjs/types';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import type { Route } from 'nextjs-routes';
import * as gSSP from 'nextjs/getServerSideProps';
import type { Props } from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
import detectBotRequest from 'nextjs/utils/detectBotRequest';
import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString';
import LayoutApp from 'ui/shared/layout/LayoutApp'; import LayoutApp from 'ui/shared/layout/LayoutApp';
const MarketplaceApp = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false }); const MarketplaceApp = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false });
const Page: NextPageWithLayout<Props> = (props: Props) => { const pathname: Route['pathname'] = '/apps/[id]';
const feature = config.features.marketplace;
const Page: NextPageWithLayout<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
return ( return (
<PageNextJs pathname="/apps/[id]" query={ props }> <PageNextJs pathname="/apps/[id]" query={ props.query } apiData={ props.apiData }>
<MarketplaceApp/> <MarketplaceApp/>
</PageNextJs> </PageNextJs>
); );
...@@ -28,4 +39,40 @@ Page.getLayout = function getLayout(page: React.ReactElement) { ...@@ -28,4 +39,40 @@ Page.getLayout = function getLayout(page: React.ReactElement) {
export default Page; export default Page;
export { marketplace as getServerSideProps } from 'nextjs/getServerSideProps'; export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.marketplace<typeof pathname>(ctx);
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse && feature.isEnabled) {
const botInfo = detectBotRequest(ctx.req);
if (botInfo?.type === 'social_preview') {
const appData = await(async() => {
if ('configUrl' in feature) {
const appList = await fetchApi<never, Array<MarketplaceAppOverview>>({
url: config.app.baseUrl + feature.configUrl,
route: '/marketplace_config',
timeout: 1_000,
});
if (appList && Array.isArray(appList)) {
return appList.find(app => app.id === getQueryParamString(ctx.query.id));
}
} else {
return await fetchApi({
resource: 'marketplace_dapp',
pathParams: { dappId: getQueryParamString(ctx.query.id), chainId: config.chain.id },
timeout: 1_000,
});
}
})();
(await baseResponse.props).apiData = appData && appData.title ? {
app_name: appData.title,
} : null;
}
}
return baseResponse;
};
...@@ -25,7 +25,7 @@ const Batch = dynamic(() => { ...@@ -25,7 +25,7 @@ const Batch = dynamic(() => {
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/batches/[number]" query={ props }> <PageNextJs pathname="/batches/[number]" query={ props.query }>
<Batch/> <Batch/>
</PageNextJs> </PageNextJs>
); );
......
...@@ -9,7 +9,7 @@ const Blob = dynamic(() => import('ui/pages/Blob'), { ssr: false }); ...@@ -9,7 +9,7 @@ const Blob = dynamic(() => import('ui/pages/Blob'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/blobs/[hash]" query={ props }> <PageNextJs pathname="/blobs/[hash]" query={ props.query }>
<Blob/> <Blob/>
</PageNextJs> </PageNextJs>
); );
......
...@@ -9,7 +9,7 @@ const Block = dynamic(() => import('ui/pages/Block'), { ssr: false }); ...@@ -9,7 +9,7 @@ const Block = dynamic(() => import('ui/pages/Block'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/block/[height_or_hash]" query={ props }> <PageNextJs pathname="/block/[height_or_hash]" query={ props.query }>
<Block/> <Block/>
</PageNextJs> </PageNextJs>
); );
......
...@@ -8,7 +8,7 @@ import ContractVerification from 'ui/pages/ContractVerification'; ...@@ -8,7 +8,7 @@ import ContractVerification from 'ui/pages/ContractVerification';
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/contract-verification" query={ props }> <PageNextJs pathname="/contract-verification" query={ props.query }>
<ContractVerification/> <ContractVerification/>
</PageNextJs> </PageNextJs>
); );
......
...@@ -9,7 +9,7 @@ const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false }); ...@@ -9,7 +9,7 @@ const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/name-domains/[name]" query={ props }> <PageNextJs pathname="/name-domains/[name]" query={ props.query }>
<NameDomain/> <NameDomain/>
</PageNextJs> </PageNextJs>
); );
......
...@@ -9,7 +9,7 @@ const UserOp = dynamic(() => import('ui/pages/UserOp'), { ssr: false }); ...@@ -9,7 +9,7 @@ const UserOp = dynamic(() => import('ui/pages/UserOp'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/op/[hash]" query={ props }> <PageNextJs pathname="/op/[hash]" query={ props.query }>
<UserOp/> <UserOp/>
</PageNextJs> </PageNextJs>
); );
......
...@@ -12,7 +12,7 @@ const SearchResults = dynamic(() => import('ui/pages/SearchResults'), { ssr: fal ...@@ -12,7 +12,7 @@ const SearchResults = dynamic(() => import('ui/pages/SearchResults'), { ssr: fal
const Page: NextPageWithLayout<Props> = (props: Props) => { const Page: NextPageWithLayout<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/search-results" query={ props }> <PageNextJs pathname="/search-results" query={ props.query }>
<SearchResults/> <SearchResults/>
</PageNextJs> </PageNextJs>
); );
......
import type { NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import type { Route } from 'nextjs-routes';
import type { Props } from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps';
import * as gSSP from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
import detectBotRequest from 'nextjs/utils/detectBotRequest';
import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString';
const Token = dynamic(() => import('ui/pages/Token'), { ssr: false }); const Token = dynamic(() => import('ui/pages/Token'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const pathname: Route['pathname'] = '/token/[hash]';
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
return ( return (
<PageNextJs pathname="/token/[hash]" query={ props }> <PageNextJs pathname="/token/[hash]" query={ props.query } apiData={ props.apiData }>
<Token/> <Token/>
</PageNextJs> </PageNextJs>
); );
...@@ -17,4 +26,24 @@ const Page: NextPage<Props> = (props: Props) => { ...@@ -17,4 +26,24 @@ const Page: NextPage<Props> = (props: Props) => {
export default Page; export default Page;
export { base as getServerSideProps } from 'nextjs/getServerSideProps'; export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.base<typeof pathname>(ctx);
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) {
const botInfo = detectBotRequest(ctx.req);
if (botInfo?.type === 'social_preview') {
const tokenData = await fetchApi({
resource: 'token',
pathParams: { hash: getQueryParamString(ctx.query.hash) },
timeout: 1_000,
});
(await baseResponse.props).apiData = tokenData && tokenData.symbol ? {
symbol: tokenData.symbol,
} : null;
}
}
return baseResponse;
};
import type { NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import type { Route } from 'nextjs-routes';
import type { Props } from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps';
import * as gSSP from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
import detectBotRequest from 'nextjs/utils/detectBotRequest';
import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString';
const TokenInstance = dynamic(() => import('ui/pages/TokenInstance'), { ssr: false }); const TokenInstance = dynamic(() => import('ui/pages/TokenInstance'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const pathname: Route['pathname'] = '/token/[hash]/instance/[id]';
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
return ( return (
<PageNextJs pathname="/token/[hash]/instance/[id]" query={ props }> <PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
<TokenInstance/> <TokenInstance/>
</PageNextJs> </PageNextJs>
); );
...@@ -17,4 +26,24 @@ const Page: NextPage<Props> = (props: Props) => { ...@@ -17,4 +26,24 @@ const Page: NextPage<Props> = (props: Props) => {
export default Page; export default Page;
export { base as getServerSideProps } from 'nextjs/getServerSideProps'; export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.base<typeof pathname>(ctx);
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) {
const botInfo = detectBotRequest(ctx.req);
if (botInfo?.type === 'social_preview') {
const tokenData = await fetchApi({
resource: 'token',
pathParams: { hash: getQueryParamString(ctx.query.hash) },
timeout: 1_000,
});
(await baseResponse.props).apiData = tokenData && tokenData.symbol ? {
symbol: tokenData.symbol,
} : null;
}
}
return baseResponse;
};
...@@ -9,7 +9,7 @@ const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false } ...@@ -9,7 +9,7 @@ const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false }
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/tx/[hash]" query={ props }> <PageNextJs pathname="/tx/[hash]" query={ props.query }>
<Transaction/> <Transaction/>
</PageNextJs> </PageNextJs>
); );
......
...@@ -9,7 +9,7 @@ const KettleTxs = dynamic(() => import('ui/pages/KettleTxs'), { ssr: false }); ...@@ -9,7 +9,7 @@ const KettleTxs = dynamic(() => import('ui/pages/KettleTxs'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => { const Page: NextPage<Props> = (props: Props) => {
return ( return (
<PageNextJs pathname="/txs/kettle/[hash]" query={ props }> <PageNextJs pathname="/txs/kettle/[hash]" query={ props.query }>
<KettleTxs/> <KettleTxs/>
</PageNextJs> </PageNextJs>
); );
......
...@@ -24,13 +24,9 @@ const defaultAppContext = { ...@@ -24,13 +24,9 @@ const defaultAppContext = {
pageProps: { pageProps: {
cookies: '', cookies: '',
referrer: '', referrer: '',
id: '', query: {},
height_or_hash: '', adBannerProvider: 'slise' as const,
hash: '', apiData: null,
number: '',
q: '',
name: '',
adBannerProvider: 'slise',
}, },
}; };
......
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