Skip to content

Commit

Permalink
SEO: add canonicals and change meta titles (#1960)
Browse files Browse the repository at this point in the history
* add canonical link tag

* change title templates

* adjust title for dapps category pages

* change h1 titles

* Revert "adjust title for dapps category pages"

This reverts commit 88ec522.

* fix unit test
  • Loading branch information
tom2drum authored May 29, 2024
1 parent e45a58c commit 2e9728e
Show file tree
Hide file tree
Showing 20 changed files with 143 additions and 78 deletions.
4 changes: 2 additions & 2 deletions icons/payment_link.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion icons/swap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions lib/metadata/__snapshots__/generate.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`generates correct metadata for: dynamic route 1`] = `
{
"canonical": undefined,
"description": "View transaction 0x12345 on Blockscout (Blockscout) Explorer",
"opengraph": {
"description": "",
Expand All @@ -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": "",
Expand All @@ -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",
}
`;
4 changes: 2 additions & 2 deletions lib/metadata/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
5 changes: 3 additions & 2 deletions lib/metadata/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -18,8 +19,7 @@ export default function generate<Pathname extends Route['pathname']>(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);
Expand All @@ -32,5 +32,6 @@ export default function generate<Pathname extends Route['pathname']>(route: Rout
description: pageOgType !== 'Regular page' ? config.meta.og.description : '',
imageUrl: pageOgType !== 'Regular page' ? config.meta.og.imageUrl : '',
},
canonical: getCanonicalUrl(route.pathname),
};
}
24 changes: 24 additions & 0 deletions lib/metadata/getCanonicalUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Route } from 'nextjs-routes';

import config from 'configs/app';

const CANONICAL_ROUTES: Array<Route['pathname']> = [
'/',
'/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;
}
}
117 changes: 60 additions & 57 deletions lib/metadata/templates/title.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,74 @@
import type { Route } from 'nextjs-routes';

import config from 'configs/app';

const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/': '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<Record<Route['pathname'], string>> = {
'/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();
}
1 change: 1 addition & 0 deletions lib/metadata/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export interface Metadata {
description?: string;
imageUrl?: string;
};
canonical: string | undefined;
}
3 changes: 2 additions & 1 deletion nextjs/PageNextJs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface Props<Pathname extends Route['pathname']> {
initSentry();

const PageNextJs = <Pathname extends Route['pathname']>(props: Props<Pathname>) => {
const { title, description, opengraph } = metadata.generate(props, props.apiData);
const { title, description, opengraph, canonical } = metadata.generate(props, props.apiData);

useGetCsrfToken();
useAdblockDetect();
Expand All @@ -34,6 +34,7 @@ const PageNextJs = <Pathname extends Route['pathname']>(props: Props<Pathname>)
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
{ canonical && <link rel="canonical" href={ canonical }/> }

{ /* OG TAGS */ }
<meta property="og:title" content={ opengraph.title }/>
Expand Down
5 changes: 4 additions & 1 deletion pages/api-docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<PageNextJs pathname="/api-docs">
<PageTitle title="API Documentation"/>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } API documentation` : 'API documentation' }
/>
<SwaggerUI/>
</PageNextJs>
);
Expand Down
5 changes: 4 additions & 1 deletion pages/graphiql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -16,7 +17,9 @@ const Page: NextPage = () => {

return (
<PageNextJs pathname="/graphiql">
<PageTitle title="GraphQL playground"/>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `GraphiQL ${ config.chain.name } interface` : 'GraphQL playground' }
/>
<GraphQL/>
</PageNextJs>
);
Expand Down
5 changes: 4 additions & 1 deletion ui/pages/BeaconChainWithdrawals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ const Withdrawals = () => {

return (
<>
<PageTitle title="Withdrawals" withTextAd/>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } withdrawals` : 'Withdrawals' }
withTextAd
/>
<DataListDisplay
isError={ isError }
items={ data?.items }
Expand Down
2 changes: 1 addition & 1 deletion ui/pages/GasTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const GasTracker = () => {
return (
<>
<PageTitle
title="Gas tracker"
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } gas tracker` : 'Gas tracker' }
secondRow={ titleSecondRow }
withTextAd
/>
Expand Down
6 changes: 5 additions & 1 deletion ui/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
}
</Box>
<Box display={{ base: 'none', lg: 'flex' }}>
{ config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> }
Expand Down
5 changes: 4 additions & 1 deletion ui/pages/NameDomains.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,10 @@ const NameDomains = () => {

return (
<>
<PageTitle title="Name services lookup" withTextAd/>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } name domains` : 'Name services lookup' }
withTextAd
/>
<DataListDisplay
isError={ isError }
items={ data?.items }
Expand Down
4 changes: 3 additions & 1 deletion ui/pages/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const Stats = () => {

return (
<>
<PageTitle title={ `${ config.chain.name } stats` }/>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } statistic & data` : `${ config.chain.name } stats` }
/>

<Box mb={{ base: 6, sm: 8 }}>
<NumberWidgetsList/>
Expand Down
Loading

0 comments on commit 2e9728e

Please sign in to comment.