Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SEO: add canonicals and change meta titles #1960

Merged
merged 8 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
115 changes: 59 additions & 56 deletions lib/metadata/templates/title.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,73 @@
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',
'/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',
'/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
5 changes: 4 additions & 1 deletion ui/pages/Tokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ const Tokens = () => {

return (
<>
<PageTitle title="Tokens" withTextAd/>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `Tokens on ${ config.chain.name }` : 'Tokens' }
withTextAd
/>
{ tabs.length === 1 && !isMobile && actionBar }
<RoutedTabs
tabs={ tabs }
Expand Down
5 changes: 4 additions & 1 deletion ui/pages/Transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ const Transactions = () => {

return (
<>
<PageTitle title="Transactions" withTextAd/>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } transactions` : 'Transactions' }
withTextAd
/>
<TxsStats/>
<RoutedTabs
tabs={ tabs }
Expand Down
6 changes: 5 additions & 1 deletion ui/pages/UserOps.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';

import config from 'configs/app';
import { USER_OPS_ITEM } from 'stubs/userOps';
import { generateListStub } from 'stubs/utils';
import PageTitle from 'ui/shared/Page/PageTitle';
Expand All @@ -19,7 +20,10 @@ const UserOps = () => {

return (
<>
<PageTitle title="User operations" withTextAd/>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } user operations` : 'User operations' }
withTextAd
/>
<UserOpsContent query={ query }/>
</>
);
Expand Down
Loading