Skip to content

Commit

Permalink
Merge pull request #1397 from nickgros/PORTALS-3195-canonical-urls
Browse files Browse the repository at this point in the history
  • Loading branch information
nickgros authored Nov 20, 2024
2 parents cf94528 + a87f4e1 commit 2afe008
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 14 deletions.
1 change: 1 addition & 0 deletions apps/portals/nf/src/pages/DatasetDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default function DatasetDetailsPage() {
ContainerProps={{
maxWidth: 'xl',
}}
resourcePrimaryKey={['id']}
>
<DetailsPageContent
content={[
Expand Down
1 change: 1 addition & 0 deletions apps/portals/nf/src/pages/InitiativeDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function InitiativeDetailsPage() {
ContainerProps={{
maxWidth: 'xl',
}}
resourcePrimaryKey={['initiative']}
>
<DetailsPageContent
content={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ function OrganizationDetailsPage() {
isHeader={true}
searchParams={searchParams}
/>
<DetailsPage sql={fundersSql} ContainerProps={{ maxWidth: 'xl' }}>
<DetailsPage
sql={fundersSql}
ContainerProps={{ maxWidth: 'xl' }}
resourcePrimaryKey={['abbreviation']}
>
<DetailsPageTabs tabConfig={tabConfig} />
<Outlet />
</DetailsPage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { DetailsPageProps } from '../../types/portal-util-types'
import { useGetPortalComponentSearchParams } from '../../utils/UseGetPortalComponentSearchParams'
import { DetailsPageContextProvider } from './DetailsPageContext'
import { DetailsPageDocumentMetadata } from './DetailsPageDocumentMetadata'
import { useScrollOnMount } from './utils'

const goToExplorePage = () => {
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function DetailsPage(props: DetailsPageProps) {
additionalFiltersSessionStorageKey,
ContainerProps,
children = <Outlet />,
resourcePrimaryKey,
} = props

const searchParams = useGetPortalComponentSearchParams()
Expand Down Expand Up @@ -117,6 +119,7 @@ export default function DetailsPage(props: DetailsPageProps) {
rowData: row,
}}
>
<DetailsPageDocumentMetadata resourcePrimaryKey={resourcePrimaryKey} />
<Container
maxWidth={'lg'}
className="DetailsPage tab-layout"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Row, RowSet } from '@sage-bionetworks/synapse-types'
import React from 'react'
import { useLocation } from 'react-router-dom'
import { getColumnIndex } from 'synapse-react-client'
import { useSetCanonicalUrl } from '../../utils/useSetCanonicalUrl'
import { useDetailsPageContext } from './DetailsPageContext'

type DetailsPageDocumentMetadataProps = {
/** The set of column name(s) which define the main unique key of the column (used to define the canonical URL for SEO) */
resourcePrimaryKey?: string[]
}

function getCanonicalUrl(
pathname: string,
resourcePrimaryKey: string[],
rowSet: RowSet,
rowData: Row,
) {
try {
const canonicalUrl = new URL(pathname, window.location.origin)
resourcePrimaryKey.forEach(columnName => {
const columnIndex = getColumnIndex(resourcePrimaryKey[0], rowSet?.headers)
if (columnIndex == null) {
throw new Error(
'No column name in rowSet.headers matching: ' + columnName,
)
}
const value = rowData.values[columnIndex]
if (value == null) {
throw new Error(`Retrieved null value for column ${columnName}`)
}
canonicalUrl.searchParams.append(columnName, value)
})

return canonicalUrl.toString()
} catch (e) {
console.error('Error generating canonical URL', e)
return undefined
}
}

export function DetailsPageDocumentMetadata(
props: DetailsPageDocumentMetadataProps,
) {
const { resourcePrimaryKey } = props

const { pathname } = useLocation()
const {
context: { rowSet, rowData },
} = useDetailsPageContext()

const canonicalUrl =
resourcePrimaryKey != null && rowSet != null && rowData != null
? getCanonicalUrl(pathname, resourcePrimaryKey, rowSet, rowData)
: undefined

useSetCanonicalUrl(canonicalUrl)

return <></>
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ArrowDropDown, ArrowDropUp } from '@mui/icons-material'
import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { Outlet, useLocation, useMatch } from 'react-router-dom'
import { OrientationBanner } from 'synapse-react-client'
import {
NEGATIVE_RESPONSIVE_SIDE_MARGIN,
RESPONSIVE_SIDE_PADDING,
} from '../../utils'
import { useSetCanonicalUrl } from '../../utils/useSetCanonicalUrl'
import { ExplorePageRoute, ExploreWrapperProps } from './ExploreWrapperProps'
import { ExploreWrapperTabs } from './ExploreWrapperTabs'

Expand All @@ -21,8 +22,7 @@ function RouteMatchedOrientationBanner(props: { route: ExplorePageRoute }) {
}

/**
* RouteControl is the set of controls used on the /Explore page to navigate the
* different keys.
* The set of controls shared between Explore page to navigate the different Explore routes
*/
export default function ExploreWrapper(props: ExploreWrapperProps) {
const { explorePaths } = props
Expand All @@ -36,15 +36,22 @@ export default function ExploreWrapper(props: ExploreWrapperProps) {
const currentRoute = explorePaths.find(
route => encodeURI(route.path!) === currentExploreRoute,
)
if (currentRoute) {
const pageName =
currentRoute.displayName ?? currentRoute.path?.replaceAll('/', '')
const documentTitle = `${import.meta.env.VITE_PORTAL_NAME} - ${pageName}`
const newTitle: string = documentTitle
if (document.title !== newTitle) {
document.title = newTitle
const pageName =
currentRoute?.displayName ?? currentRoute?.path?.replaceAll('/', '')

useEffect(() => {
if (pageName) {
const newTitle: string = `${
import.meta.env.VITE_PORTAL_NAME
} - ${pageName}`
if (document.title !== newTitle) {
document.title = newTitle
}
}
}
}, [pathname, pageName])

// The canonical URL is the explore route with no searchParams
useSetCanonicalUrl(new URL(pathname, window.location.origin).toString())

return (
<>
Expand Down Expand Up @@ -76,7 +83,7 @@ export default function ExploreWrapper(props: ExploreWrapperProps) {
}}
onClick={() => setShowSubNav(showSubNav => !showSubNav)}
>
<Typography variant={'headline3'}>{currentExploreRoute}</Typography>
<Typography variant={'headline3'}>{pageName}</Typography>
{showSubNav ? (
<ArrowDropDown fontSize={'large'} />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export function ExploreWrapperTabs(props: ExploreWrapperProps) {
>
{explorePaths.map(({ path, displayName = path }) => {
path = `/Explore/${path}`
console.log('path', path, pathname)
return (
<Tab
key={path}
Expand Down
2 changes: 2 additions & 0 deletions apps/synapse-portal-framework/src/types/portal-util-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export type DetailsPageProps = React.PropsWithChildren<{
sqlOperator?: ColumnSingleValueFilterOperator | ColumnMultiValueFunction
additionalFiltersSessionStorageKey?: string
ContainerProps?: ContainerProps
/** The set of column name(s) which define the main unique key of the column (used to define the canonical URL for SEO) */
resourcePrimaryKey?: string[]
}>
32 changes: 32 additions & 0 deletions apps/synapse-portal-framework/src/utils/useSetCanonicalUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useEffect } from 'react'

/**
* Hook to set the canonical URL of the page.
*
* The canonical URL helps search crawlers understand which URL(s) to index when there are multiple URLs that point to
* the same content.
* @param canonicalUrl
*/
export function useSetCanonicalUrl(canonicalUrl?: string) {
useEffect(() => {
const previousCanonicalUrlTag = document.querySelector(
'link[rel="canonical"]',
)
if (canonicalUrl) {
if (previousCanonicalUrlTag) {
document.head.removeChild(previousCanonicalUrlTag)
}
const newCanonicalUrlTag = document.createElement('link')
newCanonicalUrlTag.setAttribute('rel', 'canonical')
newCanonicalUrlTag.setAttribute('href', canonicalUrl)
document.head.appendChild(newCanonicalUrlTag)
}

return () => {
document.querySelector('link[rel="canonical"]')?.remove()
if (previousCanonicalUrlTag) {
document.head.appendChild(previousCanonicalUrlTag)
}
}
}, [canonicalUrl])
}

0 comments on commit 2afe008

Please sign in to comment.