feat: react helmet implementation (#223)

* feat: dynamic page metadata implementation

* cleanup: unused imports

* cleanup: more unused imports
This commit is contained in:
Yusuf Seyrek 2023-05-24 16:05:19 +03:00 committed by GitHub
parent e38b8be496
commit 999d936f85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 179 additions and 27 deletions

View File

@ -0,0 +1,32 @@
import { render } from '@testing-library/react'
import * as rrd from 'react-router-dom'
import PageMetadata from 'components/PageMetadata'
import PAGE_METADATA from 'constants/pageMetadata'
jest.mock('react-router-dom')
const mockedUseLocation = rrd.useLocation as jest.Mock
describe('<PageMetadata />', () => {
afterAll(() => {
jest.clearAllMocks()
})
Object.keys(PAGE_METADATA).forEach((page) => {
it(`should render correct ${page} metadata`, () => {
const pageKey = page as keyof typeof PAGE_METADATA
const pageMetadata = PAGE_METADATA[pageKey]
mockedUseLocation.mockReturnValue({ pathname: pageKey })
const { container } = render(<PageMetadata />)
const titleElement = container.querySelector('title')
const descriptionElement = container.querySelector('meta[name="description"]')
const keywordsElement = container.querySelector('meta[name="keywords"]')
expect(titleElement).toHaveTextContent(pageMetadata.title)
expect(descriptionElement).toHaveAttribute('content', pageMetadata.description)
expect(keywordsElement).toHaveAttribute('content', pageMetadata.keywords)
})
})
})

View File

@ -1 +1,19 @@
import '@testing-library/jest-dom/extend-expect'
jest.mock('react-helmet', () => {
const React = require('react')
const plugin = jest.requireActual('react-helmet')
const mockHelmet = ({ children, ...props }) =>
React.createElement(
'div',
{
...props,
className: 'mock-helmet',
},
children,
)
return {
...plugin,
Helmet: jest.fn().mockImplementation(mockHelmet),
}
})

View File

@ -27,6 +27,7 @@
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.5",
"react-helmet": "^6.1.0",
"react-router-dom": "^6.11.2",
"react-spring": "^9.7.1",
"react-toastify": "^9.1.2",
@ -43,6 +44,7 @@
"@types/node": "^20.2.3",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"@types/react-helmet": "^6.1.6",
"autoprefixer": "^10.4.14",
"babel-jest": "^29.5.0",
"dotenv": "^16.0.3",

View File

@ -0,0 +1,25 @@
import Head from 'next/head'
function DefaultPageHead() {
return (
<Head>
<title>Mars Protocol V2</title>
<meta charSet='utf-8' />
<link href='/favicon.svg' rel='icon' />
<link href='/apple-touch-icon.png' rel='apple-touch-icon' sizes='180x180' />
<link href='/site.webmanifest' rel='manifest' />
<link color='#dd5b65' href='/safari-pinned-tab.svg' rel='mask-icon' />
<meta content='index,follow' name='robots' />
<meta content='summary_large_image' name='twitter:card' />
<meta content='@mars_protocol' name='twitter:site' />
<meta content='@mars_protocol' name='twitter:creator' />
<meta content='https://osmosis.marsprotocol.io' property='og:url' />
<meta content='https://osmosis.marsprotocol.io/banner.png' property='og:image' />
<meta content='Mars Protocol V2' property='og:site_name' />
<meta content='#ffffff' name='msapplication-TileColor' />
<meta content='#ffffff' name='theme-color' />
</Head>
)
}
export default DefaultPageHead

View File

@ -0,0 +1,25 @@
import { useLocation } from 'react-router-dom'
import { Helmet } from 'react-helmet'
import { useMemo } from 'react'
import PAGE_METADATA from 'constants/pageMetadata'
function PageMetadata() {
const location = useLocation()
const metadata = useMemo(() => {
const route = location.pathname.split('/').reverse()[0] as keyof typeof PAGE_METADATA
return PAGE_METADATA[route] || PAGE_METADATA['trade']
}, [location])
return (
<Helmet>
<title>{metadata.title}</title>
<meta content={metadata.title} property='og:title' />
<meta name='description' content={metadata.description} property='og:description' />
<meta name='keywords' content={metadata.keywords} property='og:keywords' />
</Helmet>
)
}
export default PageMetadata

View File

@ -0,0 +1,45 @@
const PAGE_METADATA = {
trade: {
title: 'Mars Protocol V2 - Trade',
description:
'Create a Rover credit account to begin spot and margin trading crypto assets with cross-collateralized positions and a single liquidation point.',
keywords:
'crypto trading, spot trading, margin trading, decentralized trading, defi, cross-collateralization, cosmos tokens',
},
farm: {
title: 'Mars Protocol V2 - Farm',
description:
"Lend your crypto assets or deploy advanced defi strategies including leveraged yield farming and leveraged staking in Mars' Farm vaults.",
keywords:
'leveraged yield farming, crypto yield, leveraged staking, mars farm vaults, crypto lending, cosmos lending',
},
lend: {
title: 'Mars Protocol V2 - Lend',
description:
"Lend your crypto assets or deploy advanced defi strategies including leveraged yield farming and leveraged staking in Mars' Farm vaults.",
keywords:
'leveraged yield farming, crypto yield, leveraged staking, mars farm vaults, crypto lending, cosmos lending',
},
borrow: {
title: 'Mars Protocol V2 - Borrow',
description:
"Use your positions on Mars as collateral to borrow other assets. Then, use them for trading or put them to work in Mars' Farm Vaults.",
keywords:
'borrow crypto, defi, cosmos credit protocol, cosmos lending, decentralized finance, crypto collateral, mars farm vaults, crypto lending',
},
portfolio: {
title: 'Mars Protocol V2 - Portfolio',
description:
'Review all of your positions on Mars at a glance and make adjustments to keep your Rover credit accounts healthy and productive.',
keywords:
'rover credit accounts, mars credit accounts, mars, defi, crypto lending, crypto yield',
},
council: {
title: 'Mars Protocol V2 - Council',
description:
'Stake MARS token to ascend to the Martian Council and help govern key changes to the protocol.',
keywords: 'martian council, mars governance, cosmos governance, mars voting, mars staking',
},
}
export default PAGE_METADATA

View File

@ -1,7 +1,8 @@
import Head from 'next/head'
import { AppProps } from 'next/app'
import { useEffect, useState } from 'react'
import DefaultPageHead from 'components/DefaultPageHead'
import 'react-toastify/dist/ReactToastify.min.css'
import 'styles/globals.css'
@ -15,32 +16,7 @@ export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>Mars Protocol V2</title>
<meta charSet='utf-8' />
<link href='/favicon.svg' rel='icon' />
<link href='/apple-touch-icon.png' rel='apple-touch-icon' sizes='180x180' />
<link href='/site.webmanifest' rel='manifest' />
<link color='#dd5b65' href='/safari-pinned-tab.svg' rel='mask-icon' />
<meta content='index,follow' name='robots' />
<meta
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies."
name='description'
/>
<meta content='summary_large_image' name='twitter:card' />
<meta content='@mars_protocol' name='twitter:site' />
<meta content='@mars_protocol' name='twitter:creator' />
<meta content='https://osmosis.marsprotocol.io' property='og:url' />
<meta content='Mars Protocol V2 - Powered by Osmosis' property='og:title' />
<meta
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies."
property='og:description'
/>
<meta content='https://osmosis.marsprotocol.io/banner.png' property='og:image' />
<meta content='Mars Protocol V2' property='og:site_name' />
<meta content='#ffffff' name='msapplication-TileColor' />
<meta content='#ffffff' name='theme-color' />
</Head>
<DefaultPageHead />
<div suppressHydrationWarning>
{typeof window === 'undefined' ? null : <PageComponent {...pageProps} />}
</div>

View File

@ -5,11 +5,13 @@ import Background from 'components/Background'
import Footer from 'components/Footer'
import DesktopHeader from 'components/Header/DesktopHeader'
import ModalsContainer from 'components/Modals/ModalsContainer'
import PageMetadata from 'components/PageMetadata'
import Toaster from 'components/Toaster'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<PageMetadata />
<Background />
<DesktopHeader />
<main

View File

@ -3524,6 +3524,13 @@
dependencies:
"@types/react" "*"
"@types/react-helmet@^6.1.6":
version "6.1.6"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.6.tgz#7d1afd8cbf099616894e8240e9ef70e3c6d7506d"
integrity sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A==
dependencies:
"@types/react" "*"
"@types/react@*":
version "18.0.26"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.26.tgz#8ad59fc01fef8eaf5c74f4ea392621749f0b7917"
@ -7852,6 +7859,21 @@ react-draggable@^4.4.5:
clsx "^1.1.1"
prop-types "^15.8.1"
react-fast-compare@^3.1.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
react-helmet@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==
dependencies:
object-assign "^4.1.1"
prop-types "^15.7.2"
react-fast-compare "^3.1.1"
react-side-effect "^2.1.0"
react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
@ -7912,6 +7934,11 @@ react-router@6.11.2:
dependencies:
"@remix-run/router" "1.6.2"
react-side-effect@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a"
integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==
react-smooth@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.2.tgz#0ef24213628cb13bf4305194a050e1db4302a3a1"