diff --git a/configs/app/index.ts b/configs/app/index.ts index f2253ee8fe..a946604593 100644 --- a/configs/app/index.ts +++ b/configs/app/index.ts @@ -2,6 +2,7 @@ import api from './api'; import app from './app'; import chain from './chain'; import * as features from './features'; +import meta from './meta'; import services from './services'; import UI from './ui'; @@ -12,6 +13,7 @@ const config = Object.freeze({ UI, features, services, + meta, }); export default config; diff --git a/configs/app/meta.ts b/configs/app/meta.ts new file mode 100644 index 0000000000..db601cd25e --- /dev/null +++ b/configs/app/meta.ts @@ -0,0 +1,14 @@ +import app from './app'; +import { getEnvValue, getExternalAssetFilePath } from './utils'; + +const defaultImageUrl = app.baseUrl + '/static/og_placeholder.png'; + +const meta = Object.freeze({ + promoteBlockscoutInTitle: getEnvValue(process.env.NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE) || 'true', + og: { + description: getEnvValue(process.env.NEXT_PUBLIC_OG_DESCRIPTION) || '', + imageUrl: getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL', process.env.NEXT_PUBLIC_OG_IMAGE_URL) || defaultImageUrl, + }, +}); + +export default meta; diff --git a/configs/envs/.env.eth b/configs/envs/.env.eth index 97cdd23a0b..475da9a39b 100644 --- a/configs/envs/.env.eth +++ b/configs/envs/.env.eth @@ -41,3 +41,6 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com + +#meta +NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true diff --git a/configs/envs/.env.eth_goerli b/configs/envs/.env.eth_goerli index ba7f48949e..b6f1d9ab6c 100644 --- a/configs/envs/.env.eth_goerli +++ b/configs/envs/.env.eth_goerli @@ -46,3 +46,6 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] + +#meta +NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true diff --git a/configs/envs/.env.polygon b/configs/envs/.env.polygon index 8a53ff00f9..874444ee2b 100644 --- a/configs/envs/.env.polygon +++ b/configs/envs/.env.polygon @@ -40,4 +40,7 @@ NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_HAS_BEACON_CHAIN=false # NEXT_PUBLIC_STATS_API_HOST=https://stats-rsk-testnet.k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com -NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] \ No newline at end of file +NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] + +#meta +NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/polygon-mainnet.png?raw=true diff --git a/configs/envs/.env.rootstock b/configs/envs/.env.rootstock index 097fc852ee..cf7c0153d5 100644 --- a/configs/envs/.env.rootstock +++ b/configs/envs/.env.rootstock @@ -41,3 +41,6 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97fa753626b8d44011d0b9f9a947c735f20b6e895efde NEXT_PUBLIC_HAS_BEACON_CHAIN=false NEXT_PUBLIC_STATS_API_HOST=https://stats-rsk-testnet.k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com + +#meta +NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/rootstock-testnet.png?raw=true diff --git a/deploy/scripts/download_assets.sh b/deploy/scripts/download_assets.sh index 179062e2d5..c1c82d5b50 100755 --- a/deploy/scripts/download_assets.sh +++ b/deploy/scripts/download_assets.sh @@ -21,6 +21,7 @@ ASSETS_ENVS=( "NEXT_PUBLIC_NETWORK_LOGO_DARK" "NEXT_PUBLIC_NETWORK_ICON" "NEXT_PUBLIC_NETWORK_ICON_DARK" + "NEXT_PUBLIC_OG_IMAGE_URL" ) # Create the assets directory if it doesn't exist diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index db477a77d1..2ebb0ba851 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -325,6 +325,9 @@ const schema = yup }), NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: yup.boolean(), NEXT_PUBLIC_AD_TEXT_PROVIDER: yup.string().oneOf(SUPPORTED_AD_TEXT_PROVIDERS), + NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(), + NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), + NEXT_PUBLIC_OG_IMAGE_URL: yup.string().url(), // 6. External services envs NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), diff --git a/deploy/values/review-l2/values.yaml.gotmpl b/deploy/values/review-l2/values.yaml.gotmpl index f1b3f13708..5d544b71f6 100644 --- a/deploy/values/review-l2/values.yaml.gotmpl +++ b/deploy/values/review-l2/values.yaml.gotmpl @@ -144,3 +144,5 @@ frontend: _default: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY: _default: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY + NEXT_PUBLIC_OG_IMAGE_URL: + _default: https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/base-goerli.png?raw=true diff --git a/docs/ENVS.md b/docs/ENVS.md index 87c5051b62..fff0582c37 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -13,6 +13,7 @@ The app instance could be customized by passing following variables to NodeJS en - [Sidebar](ENVS.md#sidebar) - [Footer](ENVS.md#footer) - [Favicon](ENVS.md#favicon) + - [Meta](ENVS.md#meta) - [Views](ENVS.md#views) - [Block](ENVS.md#block-views) - [Misc](ENVS.md#misc) @@ -145,6 +146,18 @@ By default, the app has generic favicon. You can override this behavior by provi   +### Meta + +Settings for meta tags and OG tags + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| 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_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` | + +  + ### Views #### Block views diff --git a/lib/metadata/__snapshots__/generate.test.ts.snap b/lib/metadata/__snapshots__/generate.test.ts.snap index 362d8d2fd2..de88a039ef 100644 --- a/lib/metadata/__snapshots__/generate.test.ts.snap +++ b/lib/metadata/__snapshots__/generate.test.ts.snap @@ -3,6 +3,11 @@ exports[`generates correct metadata for: dynamic route 1`] = ` { "description": "View transaction 0x12345 on Blockscout (Blockscout) Explorer", + "opengraph": { + "description": "", + "imageUrl": "", + "title": "Blockscout transaction 0x12345 | Blockscout", + }, "title": "Blockscout transaction 0x12345 | Blockscout", } `; @@ -10,6 +15,11 @@ exports[`generates correct metadata for: dynamic route 1`] = ` exports[`generates correct metadata for: dynamic route with API data 1`] = ` { "description": "0x12345, balances and analytics on the Blockscout (Blockscout) Explorer", + "opengraph": { + "description": "", + "imageUrl": "", + "title": "Blockscout USDT token details | Blockscout", + }, "title": "Blockscout USDT token details | Blockscout", } `; @@ -17,6 +27,11 @@ exports[`generates correct metadata for: dynamic route with API data 1`] = ` exports[`generates correct metadata for: static route 1`] = ` { "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 blocks | Blockscout", } `; diff --git a/lib/metadata/generate.ts b/lib/metadata/generate.ts index f688bfaa52..8d24fd1606 100644 --- a/lib/metadata/generate.ts +++ b/lib/metadata/generate.ts @@ -6,6 +6,7 @@ import config from 'configs/app'; import getNetworkTitle from 'lib/networks/getNetworkTitle'; import compileValue from './compileValue'; +import getPageOgType from './getPageOgType'; import * as templates from './templates'; export default function generate(route: R, apiData?: ApiData): Metadata { @@ -16,11 +17,19 @@ export default function generate(route: R, apiData?: ApiData network_title: getNetworkTitle(), }; - const title = compileValue(templates.title.make(route.pathname), params); + const compiledTitle = compileValue(templates.title.make(route.pathname), params); + const title = compiledTitle ? compiledTitle + (config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : '') : ''; const description = compileValue(templates.description.make(route.pathname), params); + const pageOgType = getPageOgType(route.pathname); + return { - title, + title: title, description, + opengraph: { + title: title, + description: pageOgType !== 'Regular page' ? config.meta.og.description : '', + imageUrl: pageOgType !== 'Regular page' ? config.meta.og.imageUrl : '', + }, }; } diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts new file mode 100644 index 0000000000..e8b4d0b002 --- /dev/null +++ b/lib/metadata/getPageOgType.ts @@ -0,0 +1,52 @@ +import type { Route } from 'nextjs-routes'; + +type OGPageType = 'Homepage' | 'Root page' | 'Regular page'; + +const OG_TYPE_DICT: Record = { + '/': 'Homepage', + '/txs': 'Root page', + '/tx/[hash]': 'Regular page', + '/blocks': 'Root page', + '/block/[height_or_hash]': 'Regular page', + '/accounts': 'Root page', + '/address/[hash]': 'Regular page', + '/verified-contracts': 'Root page', + '/address/[hash]/contract-verification': 'Regular page', + '/tokens': 'Root page', + '/token/[hash]': 'Regular page', + '/token/[hash]/instance/[id]': 'Regular page', + '/apps': 'Root page', + '/apps/[id]': 'Regular page', + '/stats': 'Root page', + '/api-docs': 'Regular page', + '/graphiql': 'Regular page', + '/search-results': 'Regular page', + '/auth/profile': 'Root page', + '/account/watchlist': 'Regular page', + '/account/api-key': 'Regular page', + '/account/custom-abi': 'Regular page', + '/account/public-tags-request': 'Regular page', + '/account/tag-address': 'Regular page', + '/account/verified-addresses': 'Root page', + '/withdrawals': 'Root page', + '/visualize/sol2uml': 'Regular page', + '/csv-export': 'Regular page', + '/l2-deposits': 'Root page', + '/l2-output-roots': 'Root page', + '/l2-txn-batches': 'Root page', + '/l2-withdrawals': 'Root page', + '/404': 'Regular page', + + // service routes, added only to make typescript happy + '/login': 'Regular page', + '/api/media-type': 'Regular page', + '/api/proxy': 'Regular page', + '/api/csrf': 'Regular page', + '/api/healthz': 'Regular page', + '/auth/auth0': 'Regular page', + '/auth/unverified-email': 'Regular page', +}; + +export default function getPageOgType(pathname: Route['pathname']) { + return OG_TYPE_DICT[pathname]; +} diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index f7e0abc81d..dc37e8164c 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -48,5 +48,5 @@ const TEMPLATE_MAP: Record = { export function make(pathname: Route['pathname']) { const template = TEMPLATE_MAP[pathname]; - return `%network_name% ${ template } | Blockscout`; + return `%network_name% ${ template }`; } diff --git a/lib/metadata/types.ts b/lib/metadata/types.ts index 5eefdc0c70..252dbc29cf 100644 --- a/lib/metadata/types.ts +++ b/lib/metadata/types.ts @@ -10,4 +10,9 @@ never; export interface Metadata { title: string; description: string; + opengraph: { + title: string; + description?: string; + imageUrl?: string; + }; } diff --git a/nextjs/PageNextJs.tsx b/nextjs/PageNextJs.tsx index 0a26964a9e..738fbaca5e 100644 --- a/nextjs/PageNextJs.tsx +++ b/nextjs/PageNextJs.tsx @@ -14,7 +14,7 @@ type Props = Route & { } const PageNextJs = (props: Props) => { - const { title, description } = metadata.generate(props); + const { title, description, opengraph } = metadata.generate(props); useGetCsrfToken(); useAdblockDetect(); @@ -28,6 +28,12 @@ const PageNextJs = (props: Props) => { { title } + + { /* OG TAGS */ } + + { opengraph.description && } + + { props.children } diff --git a/pages/_document.tsx b/pages/_document.tsx index e6c0e263ae..325d2d3e46 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -5,7 +5,6 @@ import React from 'react'; import * as serverTiming from 'nextjs/utils/serverTiming'; -import config from 'configs/app'; import theme from 'theme'; class MyDocument extends Document { @@ -46,19 +45,6 @@ class MyDocument extends Document { - - { /* OG TAGS */ } - - - - - - - diff --git a/public/static/og.png b/public/static/og.png deleted file mode 100644 index 5d35a97b4c..0000000000 Binary files a/public/static/og.png and /dev/null differ diff --git a/public/static/og_placeholder.png b/public/static/og_placeholder.png new file mode 100644 index 0000000000..1babd9e522 Binary files /dev/null and b/public/static/og_placeholder.png differ