Skip to content

Commit 1003889

Browse files
authored
Merge pull request #349 from kleros/feat/improve-seo-with-registry-and-entry-specific-title
feat: improve seo with registry and entry specific title and descriptions
2 parents 2d32a93 + 34dc95e commit 1003889

File tree

3 files changed

+144
-58
lines changed

3 files changed

+144
-58
lines changed

src/pages/light-item-details/index.js

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import styled from 'styled-components'
33
import { Layout, Breadcrumb } from 'antd'
44
import { useParams } from 'react-router'
55
import { Link } from 'react-router-dom'
6+
import { Helmet } from 'react-helmet'
67
import qs from 'qs'
78
import { abi as _IArbitrator } from '@kleros/erc-792/build/contracts/IArbitrator.json'
89
import { ethers } from 'ethers'
@@ -21,6 +22,8 @@ import { LIGHT_ITEM_DETAILS_QUERY } from 'utils/graphql'
2122
import { useQuery } from '@apollo/client'
2223
import SearchBar from 'components/light-search-bar'
2324
import { parseIpfs } from 'utils/ipfs-parse'
25+
import { itemToStatusCode, STATUS_CODE } from 'utils/item-status'
26+
import { truncateAtWord } from 'utils/truncate-at-word'
2427
import { fetchMetaEvidence } from 'hooks/tcr-view'
2528

2629
export const ITEM_TOUR_DISMISSED = 'ITEM_TOUR_DISMISSED'
@@ -77,7 +80,9 @@ const ItemDetails = ({ itemID, search }) => {
7780
const [ipfsItemData, setIpfsItemData] = useState()
7881
const { timestamp } = useContext(WalletContext)
7982
const [modalOpen, setModalOpen] = useState()
80-
const { tcrError, metaEvidence } = useContext(LightTCRViewContext)
83+
const { tcrError, metaEvidence, challengePeriodDuration } = useContext(
84+
LightTCRViewContext
85+
)
8186
const [appealCost, setAppealCost] = useState()
8287

8388
// subgraph item entities have id "<itemID>@<listaddress>"
@@ -113,7 +118,7 @@ const ItemDetails = ({ itemID, search }) => {
113118
return ordered
114119
}
115120

116-
const result = {
121+
return {
117122
...item, // Spread to convert from array to object.
118123
errors: [],
119124
columns: metaEvidence.metadata.columns,
@@ -122,11 +127,62 @@ const ItemDetails = ({ itemID, search }) => {
122127
ipfsItemData.values
123128
)
124129
}
125-
return result
126130
}, [item, metaEvidence, ipfsItemData])
127131

128132
const { metadata } = metaEvidence || {}
129133
const { decodedData } = decodedItem || {}
134+
const { tcrTitle, itemName } = metadata || {}
135+
136+
const statusCode = useMemo(() => {
137+
if (!item || !timestamp || !challengePeriodDuration) return null
138+
return itemToStatusCode(item, timestamp, challengePeriodDuration)
139+
}, [item, timestamp, challengePeriodDuration])
140+
141+
const getStatusPhrase = statusCode => {
142+
switch (statusCode) {
143+
case STATUS_CODE.REGISTERED:
144+
return 'is verified to be safe'
145+
case STATUS_CODE.SUBMITTED:
146+
return 'is pending verification'
147+
case STATUS_CODE.REMOVAL_REQUESTED:
148+
return 'has removal requested'
149+
case STATUS_CODE.CHALLENGED:
150+
return 'is under challenge'
151+
case STATUS_CODE.CROWDFUNDING:
152+
return 'is crowdfunding appeal'
153+
case STATUS_CODE.CROWDFUNDING_WINNER:
154+
return 'won crowdfunding appeal'
155+
case STATUS_CODE.PENDING_SUBMISSION:
156+
return 'awaits submission'
157+
case STATUS_CODE.PENDING_REMOVAL:
158+
return 'awaits removal'
159+
case STATUS_CODE.WAITING_ARBITRATOR:
160+
return 'awaits arbitrator ruling'
161+
case STATUS_CODE.ABSENT:
162+
return 'is not listed'
163+
default:
164+
return 'has unknown status'
165+
}
166+
}
167+
168+
const capitalizeFirst = s => s?.charAt(0).toUpperCase() + s?.slice(1)
169+
170+
const fullSeoTitle =
171+
decodedItem && metadata
172+
? `${capitalizeFirst(itemName)} - ${tcrTitle} - Kleros · Curate`
173+
: 'Kleros · Curate'
174+
const truncatedSeoTitle = truncateAtWord(fullSeoTitle, 160)
175+
176+
const fullSeoMetaDescription =
177+
decodedItem && metadata && statusCode !== null
178+
? `${decodedData.join(' ')} - ${getStatusPhrase(statusCode)} on ${
179+
metadata.tcrTitle
180+
} in Kleros Curate`
181+
: 'View item details on Kleros Curate.'
182+
const truncatedSeoMetaDescription = truncateAtWord(
183+
fullSeoMetaDescription,
184+
160
185+
)
130186

131187
// If this is a TCR in a TCR of TCRs, we fetch its metadata as well
132188
// to build a better item details card.
@@ -196,9 +252,12 @@ const ItemDetails = ({ itemID, search }) => {
196252
/>
197253
)
198254

199-
const { tcrTitle, itemName } = metadata || {}
200255
return (
201256
<>
257+
<Helmet>
258+
<title>{truncatedSeoTitle}</title>
259+
<meta name="description" content={truncatedSeoMetaDescription} />
260+
</Helmet>
202261
<StyledBanner>
203262
<Breadcrumb separator=">">
204263
<StyledBreadcrumbItem>

src/pages/light-items/banner.js

Lines changed: 73 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import styled, { css } from 'styled-components'
33
import { smallScreenStyle } from 'styles/small-screen-style'
44
import { Link } from 'react-router-dom'
55
import React from 'react'
6+
import { Helmet } from 'react-helmet'
67
import PropTypes from 'prop-types'
78
import { ZERO_ADDRESS, capitalizeFirstLetter } from 'utils/string'
89
import { useWeb3Context } from 'web3-react'
910
import ContractExplorerUrl from 'components/contract-explorer-url'
1011
import { defaultTcrAddresses } from 'config/tcr-addresses'
1112
import { parseIpfs } from 'utils/ipfs-parse'
13+
import { truncateAtWord } from 'utils/truncate-at-word'
1214

1315
export const StyledBanner = styled.div`
1416
display: flex;
@@ -120,39 +122,56 @@ const Banner = ({
120122
: `${tcrDescription}.`
121123
: ''
122124

125+
const fullSeoTitle = tcrTitle
126+
? `${tcrTitle} - Kleros · Curate`
127+
: 'Kleros · Curate'
128+
const truncatedSeoTitle = truncateAtWord(fullSeoTitle, 60)
129+
130+
const fullSeoMetaDescription = metadata
131+
? `Explore the ${tcrTitle} list on Kleros Curate: ${normalizedDescription}`
132+
: 'Explore curated lists on Kleros Curate.'
133+
const truncatedSeoMetaDescription = truncateAtWord(
134+
fullSeoMetaDescription,
135+
160
136+
)
137+
123138
return (
124-
<StyledBanner>
125-
<TCRInfoColumn id="tcr-info-column">
126-
{metadata ? (
127-
<>
128-
<TitleContainer>
129-
<StyledTitle>{tcrTitle}</StyledTitle>
130-
{defaultTCRAddress && tcrAddress !== defaultTCRAddress && (
131-
<TCRLogo logoURI={logoURI} />
132-
)}
133-
<ContractExplorerUrl
134-
networkId={networkId}
135-
contractAddress={tcrAddress}
136-
/>
137-
</TitleContainer>
138-
<StyledDescription>
139-
{capitalizeFirstLetter(normalizedDescription)}
140-
</StyledDescription>
141-
</>
142-
) : (
143-
<>
144-
<Skeleton active paragraph={false} title={{ width: 100 }} />
145-
<Skeleton
146-
active
147-
paragraph={{ rows: 1, width: 150 }}
148-
title={false}
149-
/>
150-
</>
151-
)}
152-
{connectedTCRAddr &&
153-
connectedTCRAddr !== ZERO_ADDRESS &&
154-
!relTcrDisabled && (
139+
<>
140+
<Helmet>
141+
<title> {truncatedSeoTitle} </title>
142+
<meta name="description" content={truncatedSeoMetaDescription} />
143+
</Helmet>
144+
<StyledBanner>
145+
<TCRInfoColumn id="tcr-info-column">
146+
{metadata ? (
147+
<>
148+
<TitleContainer>
149+
<StyledTitle>{tcrTitle}</StyledTitle>
150+
{defaultTCRAddress && tcrAddress !== defaultTCRAddress && (
151+
<TCRLogo logoURI={logoURI} />
152+
)}
153+
<ContractExplorerUrl
154+
networkId={networkId}
155+
contractAddress={tcrAddress}
156+
/>
157+
</TitleContainer>
158+
<StyledDescription>
159+
{capitalizeFirstLetter(normalizedDescription)}
160+
</StyledDescription>
161+
</>
162+
) : (
155163
<>
164+
<Skeleton active paragraph={false} title={{ width: 100 }} />
165+
<Skeleton
166+
active
167+
paragraph={{ rows: 1, width: 150 }}
168+
title={false}
169+
/>
170+
</>
171+
)}
172+
{connectedTCRAddr &&
173+
connectedTCRAddr !== ZERO_ADDRESS &&
174+
!relTcrDisabled && (
156175
<Typography.Text
157176
ellipsis
158177
type="secondary"
@@ -162,29 +181,29 @@ const Banner = ({
162181
View Badges list
163182
</StyledLink>
164183
</Typography.Text>
165-
</>
166-
)}
167-
</TCRInfoColumn>
168-
<ActionCol>
169-
<StyledButton
170-
type="primary"
171-
size="large"
172-
onClick={() => requestWeb3Auth(() => setSubmissionFormOpen(true))}
173-
id="submit-item-button"
174-
>
175-
{`Submit ${capitalizeFirstLetter(itemName) || 'Item'}`}
176-
<Icon type="plus-circle" />
177-
</StyledButton>
178-
<StyledPolicyAnchor
179-
href={parseIpfs(fileURI || '')}
180-
target="_blank"
181-
rel="noopener noreferrer"
182-
id="policy-link"
183-
>
184-
View Listing Policies
185-
</StyledPolicyAnchor>
186-
</ActionCol>
187-
</StyledBanner>
184+
)}
185+
</TCRInfoColumn>
186+
<ActionCol>
187+
<StyledButton
188+
type="primary"
189+
size="large"
190+
onClick={() => requestWeb3Auth(() => setSubmissionFormOpen(true))}
191+
id="submit-item-button"
192+
>
193+
{`Submit ${capitalizeFirstLetter(itemName) || 'Item'}`}
194+
<Icon type="plus-circle" />
195+
</StyledButton>
196+
<StyledPolicyAnchor
197+
href={parseIpfs(fileURI || '')}
198+
target="_blank"
199+
rel="noopener noreferrer"
200+
id="policy-link"
201+
>
202+
View Listing Policies
203+
</StyledPolicyAnchor>
204+
</ActionCol>
205+
</StyledBanner>
206+
</>
188207
)
189208
}
190209

src/utils/truncate-at-word.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const truncateAtWord = (text, maxLength) => {
2+
if (text.length <= maxLength) return text
3+
const truncated = text.substring(0, maxLength)
4+
const lastSpace = truncated.lastIndexOf(' ')
5+
return lastSpace > 0
6+
? `${truncated.substring(0, lastSpace)}...`
7+
: `${truncated}...`
8+
}

0 commit comments

Comments
 (0)