diff --git a/icons/payment_link.svg b/icons/payment_link.svg index 35374b787d..f97128fff6 100644 --- a/icons/payment_link.svg +++ b/icons/payment_link.svg @@ -1,4 +1,4 @@ - - + + diff --git a/icons/swap.svg b/icons/swap.svg index a4f2178f1c..c1566be5fc 100644 --- a/icons/swap.svg +++ b/icons/swap.svg @@ -1,3 +1,3 @@ - + diff --git a/lib/metadata/__snapshots__/generate.test.ts.snap b/lib/metadata/__snapshots__/generate.test.ts.snap index de88a039ef..a664df8d20 100644 --- a/lib/metadata/__snapshots__/generate.test.ts.snap +++ b/lib/metadata/__snapshots__/generate.test.ts.snap @@ -2,6 +2,7 @@ exports[`generates correct metadata for: dynamic route 1`] = ` { + "canonical": undefined, "description": "View transaction 0x12345 on Blockscout (Blockscout) Explorer", "opengraph": { "description": "", @@ -14,6 +15,7 @@ exports[`generates correct metadata for: dynamic route 1`] = ` exports[`generates correct metadata for: dynamic route with API data 1`] = ` { + "canonical": undefined, "description": "0x12345, balances and analytics on the Blockscout (Blockscout) Explorer", "opengraph": { "description": "", @@ -26,12 +28,13 @@ exports[`generates correct metadata for: dynamic route with API data 1`] = ` exports[`generates correct metadata for: static route 1`] = ` { + "canonical": "http://localhost:3000/txs", "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.", "opengraph": { "description": "", "imageUrl": "http://localhost:3000/static/og_placeholder.png", - "title": "Blockscout blocks | Blockscout", + "title": "Blockscout transactions - Blockscout explorer | Blockscout", }, - "title": "Blockscout blocks | Blockscout", + "title": "Blockscout transactions - Blockscout explorer | Blockscout", } `; diff --git a/lib/metadata/generate.test.ts b/lib/metadata/generate.test.ts index b9cbe2f806..f2cd05dcc1 100644 --- a/lib/metadata/generate.test.ts +++ b/lib/metadata/generate.test.ts @@ -17,9 +17,9 @@ const TEST_CASES = [ { title: 'static route', route: { - pathname: '/blocks', + pathname: '/txs', }, - } as TestCase<'/blocks'>, + } as TestCase<'/txs'>, { title: 'dynamic route', route: { diff --git a/lib/metadata/generate.ts b/lib/metadata/generate.ts index 3c16903583..9282da7fe7 100644 --- a/lib/metadata/generate.ts +++ b/lib/metadata/generate.ts @@ -7,6 +7,7 @@ import config from 'configs/app'; import getNetworkTitle from 'lib/networks/getNetworkTitle'; import compileValue from './compileValue'; +import getCanonicalUrl from './getCanonicalUrl'; import getPageOgType from './getPageOgType'; import * as templates from './templates'; @@ -18,8 +19,7 @@ export default function generate(route: Rout network_title: getNetworkTitle(), }; - const compiledTitle = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params); - const title = compiledTitle ? compiledTitle + (config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : '') : ''; + const title = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params); const description = compileValue(templates.description.make(route.pathname), params); const pageOgType = getPageOgType(route.pathname); @@ -32,5 +32,6 @@ export default function generate(route: Rout description: pageOgType !== 'Regular page' ? config.meta.og.description : '', imageUrl: pageOgType !== 'Regular page' ? config.meta.og.imageUrl : '', }, + canonical: getCanonicalUrl(route.pathname), }; } diff --git a/lib/metadata/getCanonicalUrl.ts b/lib/metadata/getCanonicalUrl.ts new file mode 100644 index 0000000000..2a868419a8 --- /dev/null +++ b/lib/metadata/getCanonicalUrl.ts @@ -0,0 +1,24 @@ +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; + +const CANONICAL_ROUTES: Array = [ + '/', + '/txs', + '/ops', + '/verified-contracts', + '/name-domains', + '/withdrawals', + '/tokens', + '/stats', + '/api-docs', + '/graphiql', + '/gas-tracker', + '/apps', +]; + +export default function getCanonicalUrl(pathname: Route['pathname']) { + if (CANONICAL_ROUTES.includes(pathname)) { + return config.app.baseUrl + pathname; + } +} diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index a28c559686..b004af7f7d 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -1,71 +1,74 @@ import type { Route } from 'nextjs-routes'; +import config from 'configs/app'; + const TEMPLATE_MAP: Record = { - '/': 'blockchain explorer', - '/txs': 'transactions', - '/txs/kettle/[hash]': 'kettle %hash% transactions', - '/tx/[hash]': 'transaction %hash%', - '/blocks': 'blocks', - '/block/[height_or_hash]': 'block %height_or_hash%', - '/accounts': 'top accounts', - '/address/[hash]': 'address details for %hash%', - '/verified-contracts': 'verified contracts', - '/contract-verification': 'verify contract', - '/address/[hash]/contract-verification': 'contract verification for %hash%', - '/tokens': 'tokens', - '/token/[hash]': 'token details', - '/token/[hash]/instance/[id]': 'NFT instance', - '/apps': 'apps marketplace', - '/apps/[id]': 'marketplace app', - '/stats': 'statistics', - '/api-docs': 'REST API', - '/graphiql': 'GraphQL', - '/search-results': 'search result for %q%', - '/auth/profile': '- my profile', - '/account/watchlist': '- watchlist', - '/account/api-key': '- API keys', - '/account/custom-abi': '- custom ABI', - '/account/tag-address': '- private tags', - '/account/verified-addresses': '- my verified addresses', - '/public-tags/submit': 'submit public tag', - '/withdrawals': 'withdrawals', - '/visualize/sol2uml': 'Solidity UML diagram', - '/csv-export': 'export data to CSV', - '/deposits': 'deposits (L1 > L2)', - '/output-roots': 'output roots', - '/dispute-games': 'dispute games', - '/batches': 'tx batches (L2 blocks)', - '/batches/[number]': 'L2 tx batch %number%', - '/blobs/[hash]': 'blob %hash% details', - '/ops': 'user operations', - '/op/[hash]': 'user operation %hash%', - '/404': 'error - page not found', - '/name-domains': 'domains search and resolve', - '/name-domains/[name]': '%name% domain details', - '/validators': 'validators list', - '/gas-tracker': 'gas tracker', + '/': '%network_name% blockchain explorer - View %network_name% stats', + '/txs': '%network_name% transactions - %network_name% explorer', + '/txs/kettle/[hash]': '%network_name% kettle %hash% transactions', + '/tx/[hash]': '%network_name% transaction %hash%', + '/blocks': '%network_name% blocks', + '/block/[height_or_hash]': '%network_name% block %height_or_hash%', + '/accounts': '%network_name% top accounts', + '/address/[hash]': '%network_name% address details for %hash%', + '/verified-contracts': 'Verified %network_name% contracts lookup - %network_name% explorer', + '/contract-verification': '%network_name% verify contract', + '/address/[hash]/contract-verification': '%network_name% contract verification for %hash%', + '/tokens': 'Tokens list - %network_name% explorer', + '/token/[hash]': '%network_name% token details', + '/token/[hash]/instance/[id]': '%network_name% NFT instance', + '/apps': '%network_name% DApps - Explore top apps', + '/apps/[id]': '%network_name% marketplace app', + '/stats': '%network_name% stats - %network_name% network insights', + '/api-docs': '%network_name% API docs - %network_name% developer tools', + '/graphiql': 'GraphQL for %network_name% - %network_name% data query', + '/search-results': '%network_name% search result for %q%', + '/auth/profile': '%network_name% - my profile', + '/account/watchlist': '%network_name% - watchlist', + '/account/api-key': '%network_name% - API keys', + '/account/custom-abi': '%network_name% - custom ABI', + '/account/tag-address': '%network_name% - private tags', + '/account/verified-addresses': '%network_name% - my verified addresses', + '/public-tags/submit': '%network_name% - public tag requests', + '/withdrawals': '%network_name% withdrawals - track on %network_name% explorer', + '/visualize/sol2uml': '%network_name% Solidity UML diagram', + '/csv-export': '%network_name% export data to CSV', + '/deposits': '%network_name% deposits (L1 > L2)', + '/output-roots': '%network_name% output roots', + '/dispute-games': '%network_name% dispute games', + '/batches': '%network_name% tx batches (L2 blocks)', + '/batches/[number]': '%network_name% L2 tx batch %number%', + '/blobs/[hash]': '%network_name% blob %hash% details', + '/ops': 'User operations on %network_name% - %network_name% explorer', + '/op/[hash]': '%network_name% user operation %hash%', + '/404': '%network_name% error - page not found', + '/name-domains': '%network_name% name domains - %network_name% explorer', + '/name-domains/[name]': '%network_name% %name% domain details', + '/validators': '%network_name% validators list', + '/gas-tracker': '%network_name% gas tracker - Current gas fees', // service routes, added only to make typescript happy - '/login': 'login', - '/api/metrics': 'node API prometheus metrics', - '/api/log': 'node API request log', - '/api/media-type': 'node API media type', - '/api/proxy': 'node API proxy', - '/api/csrf': 'node API CSRF token', - '/api/healthz': 'node API health check', - '/auth/auth0': 'authentication', - '/auth/unverified-email': 'unverified email', + '/login': '%network_name% login', + '/api/metrics': '%network_name% node API prometheus metrics', + '/api/log': '%network_name% node API request log', + '/api/media-type': '%network_name% node API media type', + '/api/proxy': '%network_name% node API proxy', + '/api/csrf': '%network_name% node API CSRF token', + '/api/healthz': '%network_name% node API health check', + '/auth/auth0': '%network_name% authentication', + '/auth/unverified-email': '%network_name% unverified email', }; const TEMPLATE_MAP_ENHANCED: Partial> = { - '/token/[hash]': '%symbol% token details', - '/token/[hash]/instance/[id]': 'token instance for %symbol%', - '/apps/[id]': '- %app_name%', - '/address/[hash]': 'address details for %domain_name%', + '/token/[hash]': '%network_name% %symbol% token details', + '/token/[hash]/instance/[id]': '%network_name% token instance for %symbol%', + '/apps/[id]': '%network_name% - %app_name%', + '/address/[hash]': '%network_name% address details for %domain_name%', }; export function make(pathname: Route['pathname'], isEnriched = false) { const template = (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname]; + const postfix = config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : ''; - return `%network_name% ${ template }`; + return (template + postfix).trim(); } diff --git a/lib/metadata/types.ts b/lib/metadata/types.ts index b7e7547717..fda74301ba 100644 --- a/lib/metadata/types.ts +++ b/lib/metadata/types.ts @@ -20,4 +20,5 @@ export interface Metadata { description?: string; imageUrl?: string; }; + canonical: string | undefined; } diff --git a/nextjs/PageNextJs.tsx b/nextjs/PageNextJs.tsx index fe99868cb7..89e066ca6d 100644 --- a/nextjs/PageNextJs.tsx +++ b/nextjs/PageNextJs.tsx @@ -21,7 +21,7 @@ interface Props { initSentry(); const PageNextJs = (props: Props) => { - const { title, description, opengraph } = metadata.generate(props, props.apiData); + const { title, description, opengraph, canonical } = metadata.generate(props, props.apiData); useGetCsrfToken(); useAdblockDetect(); @@ -34,6 +34,7 @@ const PageNextJs = (props: Props) { title } + { canonical && } { /* OG TAGS */ } diff --git a/pages/api-docs.tsx b/pages/api-docs.tsx index 54d678c2d6..64148d64e2 100644 --- a/pages/api-docs.tsx +++ b/pages/api-docs.tsx @@ -3,13 +3,16 @@ import React from 'react'; import PageNextJs from 'nextjs/PageNextJs'; +import config from 'configs/app'; import SwaggerUI from 'ui/apiDocs/SwaggerUI'; import PageTitle from 'ui/shared/Page/PageTitle'; const Page: NextPage = () => { return ( - + ); diff --git a/pages/graphiql.tsx b/pages/graphiql.tsx index 63092129b6..2521af804a 100644 --- a/pages/graphiql.tsx +++ b/pages/graphiql.tsx @@ -4,6 +4,7 @@ import React from 'react'; import PageNextJs from 'nextjs/PageNextJs'; +import config from 'configs/app'; import ContentLoader from 'ui/shared/ContentLoader'; import PageTitle from 'ui/shared/Page/PageTitle'; @@ -16,7 +17,9 @@ const Page: NextPage = () => { return ( - + ); diff --git a/ui/pages/BeaconChainWithdrawals.tsx b/ui/pages/BeaconChainWithdrawals.tsx index c7de0fdf51..c953a5aa39 100644 --- a/ui/pages/BeaconChainWithdrawals.tsx +++ b/ui/pages/BeaconChainWithdrawals.tsx @@ -82,7 +82,10 @@ const Withdrawals = () => { return ( <> - + { return ( <> diff --git a/ui/pages/Home.tsx b/ui/pages/Home.tsx index 6dd6d3b598..12ecd34b97 100644 --- a/ui/pages/Home.tsx +++ b/ui/pages/Home.tsx @@ -34,7 +34,11 @@ const Home = () => { fontWeight={ 600 } color={ config.UI.homepage.plate.textColor } > - { config.chain.name } explorer + { + config.meta.seo.enhancedDataEnabled ? + `${ config.chain.name } blockchain explorer` : + `${ config.chain.name } explorer` + } { config.features.account.isEnabled && } diff --git a/ui/pages/NameDomains.tsx b/ui/pages/NameDomains.tsx index 4c52c067d7..30a5c4fdb3 100644 --- a/ui/pages/NameDomains.tsx +++ b/ui/pages/NameDomains.tsx @@ -193,7 +193,10 @@ const NameDomains = () => { return ( <> - + { return ( <> - + diff --git a/ui/pages/Tokens.tsx b/ui/pages/Tokens.tsx index 2db3e0d8c7..fe9fdcda4b 100644 --- a/ui/pages/Tokens.tsx +++ b/ui/pages/Tokens.tsx @@ -172,7 +172,10 @@ const Tokens = () => { return ( <> - + { tabs.length === 1 && !isMobile && actionBar } { return ( <> - + { return ( <> - + ); diff --git a/ui/pages/VerifiedContracts.tsx b/ui/pages/VerifiedContracts.tsx index 9003c9a181..bb75f1755d 100644 --- a/ui/pages/VerifiedContracts.tsx +++ b/ui/pages/VerifiedContracts.tsx @@ -5,6 +5,7 @@ import React from 'react'; import type { VerifiedContractsFilters } from 'types/api/contracts'; import type { VerifiedContractsSorting, VerifiedContractsSortingField, VerifiedContractsSortingValue } from 'types/api/verifiedContracts'; +import config from 'configs/app'; import useDebounce from 'lib/hooks/useDebounce'; import useIsMobile from 'lib/hooks/useIsMobile'; import { apos } from 'lib/html-entities'; @@ -128,7 +129,10 @@ const VerifiedContracts = () => { return ( - +