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 (
-
+