From 9d772ee189e20de5bdf08df08b93238cb47980d7 Mon Sep 17 00:00:00 2001 From: Abhishekfm Date: Fri, 6 Nov 2026 13:14:02 +0530 Subject: [PATCH] Refactor AI model and dataset components to utilize separate query files for improved organization and maintainability. Update routing to handle permanent redirects based on public URLs and slugs, enhancing user experience and SEO. Remove deprecated canonical redirect logic. --- .../aimodels/[modelId]/AIModelDetailsPage.tsx | 84 +----------- .../(user)/aimodels/[modelId]/page.tsx | 31 ++--- .../(user)/aimodels/[modelId]/queries.ts | 74 +++++++++++ .../DatasetDetailsPage.tsx | 66 +--------- .../components/DatasetTabs/DatasetTabs.tsx | 4 +- .../components/Details/index.tsx | 9 +- .../datasets/[datasetIdentifier]/page.tsx | 41 ++---- .../datasets/[datasetIdentifier]/queries.ts | 65 +++++++++ .../(user)/events/[eventSlug]/page.tsx | 45 ++++--- .../[useCaseSlug]/UsecaseDetailsClient.tsx | 119 +---------------- .../(user)/usecases/[useCaseSlug]/page.tsx | 123 +++++++----------- .../(user)/usecases/[useCaseSlug]/queries.ts | 98 ++++++++++++++ lib/canonical-redirect.ts | 28 ---- 13 files changed, 347 insertions(+), 440 deletions(-) create mode 100644 app/[locale]/(user)/aimodels/[modelId]/queries.ts create mode 100644 app/[locale]/(user)/datasets/[datasetIdentifier]/queries.ts create mode 100644 app/[locale]/(user)/usecases/[useCaseSlug]/queries.ts delete mode 100644 lib/canonical-redirect.ts diff --git a/app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx b/app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx index 4269055c..3d994490 100644 --- a/app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx +++ b/app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx @@ -1,6 +1,5 @@ 'use client'; -import { graphql } from '@/gql'; import { useQuery } from '@tanstack/react-query'; import { Spinner, Text } from 'opub-ui'; @@ -12,81 +11,7 @@ import { generateJsonLd } from '@/lib/utils'; import Metadata from './components/Metadata'; import PrimaryData from './components/PrimaryData'; import Versions from './components/Versions'; - -const aiModelQuery = graphql(` - query getAIModel($modelId: Int, $publicId: String) { - getAiModel(modelId: $modelId, publicId: $publicId) { - id - urlSlug - name - displayName - description - modelType - domain - metadata - status - isPublic - isActive - createdAt - updatedAt - tags { - id - value - } - sectors { - id - name - } - geographies { - id - name - } - organization { - id - name - logo { - url - } - } - user { - id - fullName - profilePicture { - url - } - } - versions { - id - version - versionNotes - lifecycleStage - isLatest - supportsStreaming - maxTokens - supportedLanguages - status - createdAt - updatedAt - providers { - id - provider - providerModelId - isPrimary - isActive - # API Configuration - apiEndpointUrl - apiHttpMethod - apiTimeoutSeconds - apiAuthType - # HuggingFace Configuration - hfUsePipeline - hfModelClass - framework - } - } - } - } -`); +import { aiModelQuery } from './queries'; export default function AIModelDetailsPage({ modelId, @@ -96,12 +21,7 @@ export default function AIModelDetailsPage({ const { data, isLoading, error } = useQuery( [`aimodel_details_${modelId}`], () => GraphQL(aiModelQuery, {}, aiModelLookup(modelId)), - { - retry: false, - onError: (err) => { - console.error('Error fetching AI model:', err); - }, - } + { retry: false } ); const modelData = data?.getAiModel; diff --git a/app/[locale]/(user)/aimodels/[modelId]/page.tsx b/app/[locale]/(user)/aimodels/[modelId]/page.tsx index 19a06915..24cc91f9 100644 --- a/app/[locale]/(user)/aimodels/[modelId]/page.tsx +++ b/app/[locale]/(user)/aimodels/[modelId]/page.tsx @@ -1,11 +1,13 @@ import { graphql } from '@/gql'; +import { permanentRedirect } from 'next/navigation'; import { GraphQL } from '@/lib/api'; import { - isCanonicalRedirect, - redirectToCanonical, -} from '@/lib/canonical-redirect'; -import { aiModelLookup, publicPageUrl } from '@/lib/public-link'; + aiModelLookup, + isIntegerId, + publicHref, + publicPageUrl, +} from '@/lib/public-link'; import { generatePageMetadata } from '@/lib/utils'; import AIModelDetailsPage from './AIModelDetailsPage'; @@ -29,13 +31,11 @@ export async function generateMetadata({ try { const res = await GraphQL(aiModelMetaQuery, {}, aiModelLookup(modelId)); const model = res?.getAiModel; - await redirectToCanonical('aimodels', modelId, model?.urlSlug); const title = model?.displayName || model?.name || 'AI Model Details'; const description = model?.description || 'Explore AI model details, capabilities, and specifications'; const segment = model?.urlSlug || modelId; - return generatePageMetadata({ title: `${title} | CivicDataSpace`, description, @@ -50,9 +50,8 @@ export async function generateMetadata({ image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`, }, }); - } catch (e) { - if (isCanonicalRedirect(e)) throw e; - console.error('Metadata fetch error', e); + } catch (error) { + console.error('Metadata fetch error', error); return generatePageMetadata({ title: 'AI Model Details' }); } } @@ -64,12 +63,14 @@ export default async function Page({ }) { const { modelId } = await params; - try { - const res = await GraphQL(aiModelMetaQuery, {}, aiModelLookup(modelId)); - await redirectToCanonical('aimodels', modelId, res?.getAiModel?.urlSlug); - } catch (error) { - if (isCanonicalRedirect(error)) throw error; - console.error('AI model lookup error', error); + if (isIntegerId(modelId)) { + const res = await GraphQL(aiModelMetaQuery, {}, { + modelId: Number.parseInt(modelId, 10), + }).catch(() => null); + const urlSlug = res?.getAiModel?.urlSlug; + if (urlSlug && urlSlug !== modelId) { + permanentRedirect(publicHref('aimodels', urlSlug, urlSlug)); + } } return ; diff --git a/app/[locale]/(user)/aimodels/[modelId]/queries.ts b/app/[locale]/(user)/aimodels/[modelId]/queries.ts new file mode 100644 index 00000000..d7b2c7b6 --- /dev/null +++ b/app/[locale]/(user)/aimodels/[modelId]/queries.ts @@ -0,0 +1,74 @@ +import { graphql } from '@/gql'; + +export const aiModelQuery = graphql(` + query getAIModel($modelId: Int, $publicId: String) { + getAiModel(modelId: $modelId, publicId: $publicId) { + id + urlSlug + name + displayName + description + modelType + domain + metadata + status + isPublic + isActive + createdAt + updatedAt + tags { + id + value + } + sectors { + id + name + } + geographies { + id + name + } + organization { + id + name + logo { + url + } + } + user { + id + fullName + profilePicture { + url + } + } + versions { + id + version + versionNotes + lifecycleStage + isLatest + supportsStreaming + maxTokens + supportedLanguages + status + createdAt + updatedAt + providers { + id + provider + providerModelId + isPrimary + isActive + apiEndpointUrl + apiHttpMethod + apiTimeoutSeconds + apiAuthType + hfUsePipeline + hfModelClass + framework + } + } + } + } +`); diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx b/app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx index b62266ca..76f51899 100644 --- a/app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx @@ -24,70 +24,7 @@ import { } from './dataset-cover'; import { DatasetCover } from './components/DatasetCover/DatasetCover'; import { DatasetTabs } from './components/DatasetTabs/DatasetTabs'; - -const datasetQuery = graphql(` - query getDataset($datasetId: UUID, $publicId: String) { - getDataset(datasetId: $datasetId, publicId: $publicId) { - id - urlSlug - title - description - downloadCount - created - modified - isIndividualDataset - license - formats - user { - fullName - id - bio - location - profilePicture { - url - } - } - tags { - id - value - } - accessType - metadata { - metadataItem { - id - label - dataType - } - value - } - source { - sourceHomepage - sourceUrl - platform - } - resources { - id - noOfEntries - } - organization { - name - description - location - logo { - url - } - slug - id - } - sectors { - name - } - geographies { - name - } - } - } -`); +import { datasetQuery } from './queries'; const chartCountQuery = graphql(` query datasetVisualisationCount($datasetId: UUID!) { @@ -135,7 +72,6 @@ export default function DatasetDetailsPage({ useTourTrigger(true, 1500); const { trackDataset } = useAnalytics(); - // Load this dataset. Old UUID links and the current urlSlug both resolve. const datasetQueryResult = useQuery(['dataset-cover', datasetId], () => GraphQL(datasetQuery, {}, datasetLookup(datasetId)) ); diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/DatasetTabs/DatasetTabs.tsx b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/DatasetTabs/DatasetTabs.tsx index 14fb90f3..ec8f3172 100644 --- a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/DatasetTabs/DatasetTabs.tsx +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/DatasetTabs/DatasetTabs.tsx @@ -121,7 +121,9 @@ export function DatasetTabs({

Charts and maps created from this dataset.

- {tab === 'visualisations' ?
: null} + {tab === 'visualisations' ? ( +
+ ) : null}
{ - const params = useParams(); - - const datasetId = Array.isArray(params.datasetIdentifier) - ? params.datasetIdentifier[0] - : params.datasetIdentifier; - +const Details = ({ datasetId }: { datasetId: string }) => { const { data, isLoading } = useQuery( [`chartDetails_${datasetId}`], () => GraphQL(DetailsQuery, {}, { datasetId }), diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/page.tsx b/app/[locale]/(user)/datasets/[datasetIdentifier]/page.tsx index dcb794d9..34753d57 100644 --- a/app/[locale]/(user)/datasets/[datasetIdentifier]/page.tsx +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/page.tsx @@ -1,12 +1,9 @@ import { graphql } from '@/gql'; import { ClientError } from 'graphql-request'; +import { permanentRedirect } from 'next/navigation'; import { GraphQL } from '@/lib/api'; -import { - isCanonicalRedirect, - redirectToCanonical, -} from '@/lib/canonical-redirect'; -import { datasetLookup, publicPageUrl } from '@/lib/public-link'; +import { datasetLookup, isUuid, publicHref, publicPageUrl } from '@/lib/public-link'; import { generatePageMetadata } from '@/lib/utils'; import DatasetDetailsPage from './DatasetDetailsPage'; @@ -15,7 +12,6 @@ const datasetMetaQuery = graphql(` getDataset(datasetId: $datasetId, publicId: $publicId) { title description - id urlSlug tags { id @@ -50,13 +46,7 @@ export async function generateMetadata({ {}, datasetLookup(datasetIdentifier) ); - const dataset = res?.getDataset; - await redirectToCanonical( - 'datasets', - datasetIdentifier, - dataset?.urlSlug - ); const segment = dataset?.urlSlug || datasetIdentifier; return generatePageMetadata({ title: `${dataset?.title ?? ''} | Dataset | CivicDataSpace`, @@ -72,10 +62,9 @@ export async function generateMetadata({ image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`, }, }); - } catch (e) { - if (isCanonicalRedirect(e)) throw e; - if (!isUnpublishedDatasetError(e)) { - console.error('Metadata fetch error', e); + } catch (error) { + if (!isUnpublishedDatasetError(error)) { + console.error('Metadata fetch error', error); } return generatePageMetadata({ title: 'Dataset Details' }); } @@ -88,21 +77,13 @@ export default async function Page({ }) { const { datasetIdentifier } = await params; - try { - const res = await GraphQL( - datasetMetaQuery, - {}, - datasetLookup(datasetIdentifier) + if (isUuid(datasetIdentifier)) { + const res = await GraphQL(datasetMetaQuery, {}, { datasetId: datasetIdentifier }).catch( + () => null ); - await redirectToCanonical( - 'datasets', - datasetIdentifier, - res?.getDataset?.urlSlug - ); - } catch (error) { - if (isCanonicalRedirect(error)) throw error; - if (!isUnpublishedDatasetError(error)) { - console.error('Dataset lookup error', error); + const urlSlug = res?.getDataset?.urlSlug; + if (urlSlug && urlSlug !== datasetIdentifier) { + permanentRedirect(publicHref('datasets', urlSlug, urlSlug)); } } diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/queries.ts b/app/[locale]/(user)/datasets/[datasetIdentifier]/queries.ts new file mode 100644 index 00000000..be06a72c --- /dev/null +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/queries.ts @@ -0,0 +1,65 @@ +import { graphql } from '@/gql'; + +export const datasetQuery = graphql(` + query getDataset($datasetId: UUID, $publicId: String) { + getDataset(datasetId: $datasetId, publicId: $publicId) { + id + urlSlug + title + description + downloadCount + created + modified + isIndividualDataset + license + formats + user { + fullName + id + bio + location + profilePicture { + url + } + } + tags { + id + value + } + accessType + metadata { + metadataItem { + id + label + dataType + } + value + } + source { + sourceHomepage + sourceUrl + platform + } + resources { + id + noOfEntries + } + organization { + name + description + location + logo { + url + } + slug + id + } + sectors { + name + } + geographies { + name + } + } + } +`); diff --git a/app/[locale]/(user)/events/[eventSlug]/page.tsx b/app/[locale]/(user)/events/[eventSlug]/page.tsx index 07156fe9..6b561895 100644 --- a/app/[locale]/(user)/events/[eventSlug]/page.tsx +++ b/app/[locale]/(user)/events/[eventSlug]/page.tsx @@ -1,13 +1,9 @@ -import { notFound } from 'next/navigation'; +import { notFound, permanentRedirect } from 'next/navigation'; import { Text } from 'opub-ui'; import { GraphQLPublic } from '@/lib/api'; import { lumaCover } from '@/lib/luma-cover'; -import { - isCanonicalRedirect, - redirectToCanonical, -} from '@/lib/canonical-redirect'; -import { publicPageUrl } from '@/lib/public-link'; +import { publicHref, publicPageUrl } from '@/lib/public-link'; import { generatePageMetadata } from '@/lib/utils'; import BreadCrumbs from '@/components/BreadCrumbs'; import { @@ -31,25 +27,26 @@ export async function generateMetadata({ { publicId: eventSlug } ); const event = result.eventBySlug; - await redirectToCanonical('events', eventSlug, event?.urlSlug); const title = event?.name?.trim() || 'Event'; const segment = event?.urlSlug || eventSlug; - return generatePageMetadata({ - title: `${title} | Events | CivicDataSpace`, - description: event?.location?.trim() || title, - keywords: ['Events', 'CivicDataSpace'], - openGraph: { - type: 'article', - locale: 'en_US', - url: publicPageUrl('events', segment), - title, + return { + ...generatePageMetadata({ + title: `${title} | Events | CivicDataSpace`, description: event?.location?.trim() || title, - siteName: 'CivicDataSpace', - image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`, - }, - }); - } catch (error) { - if (isCanonicalRedirect(error)) throw error; + keywords: ['Events', 'CivicDataSpace'], + openGraph: { + type: 'article', + locale: 'en_US', + url: publicPageUrl('events', segment), + title, + description: event?.location?.trim() || title, + siteName: 'CivicDataSpace', + image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`, + }, + }), + alternates: { canonical: publicPageUrl('events', segment) }, + }; + } catch { return generatePageMetadata({ title: 'Event | CivicDataSpace', description: 'An event on CivicDataSpace.', @@ -80,7 +77,9 @@ export default async function EventDetailPage({ ).catch(() => null); const event = result?.eventBySlug; if (!event) notFound(); - await redirectToCanonical('events', eventSlug, event.urlSlug); + if (event.urlSlug && event.urlSlug !== eventSlug) { + permanentRedirect(publicHref('events', event.urlSlug, event.urlSlug)); + } const name = event.name?.trim() || 'Untitled event'; const cover = await lumaCover(event.lumaEmbed?.src); diff --git a/app/[locale]/(user)/usecases/[useCaseSlug]/UsecaseDetailsClient.tsx b/app/[locale]/(user)/usecases/[useCaseSlug]/UsecaseDetailsClient.tsx index 401ab23c..7fe48a68 100644 --- a/app/[locale]/(user)/usecases/[useCaseSlug]/UsecaseDetailsClient.tsx +++ b/app/[locale]/(user)/usecases/[useCaseSlug]/UsecaseDetailsClient.tsx @@ -5,11 +5,7 @@ import { useEffect } from 'react'; import { useParams } from 'next/navigation'; import { getUseCaseSummaryExcerpt } from '@/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/content-document'; -import { graphql } from '@/gql'; -import { - UseCasedetailsDocument, - type UseCasedetailsQuery, -} from '@/gql/generated/graphql'; +import { type UseCasedetailsQuery } from '@/gql/generated/graphql'; import { useAnalytics } from '@/hooks/use-analytics'; import { useQuery } from '@tanstack/react-query'; import { Text } from 'opub-ui'; @@ -37,107 +33,13 @@ import { } from '../components/usecase-rail'; import { UseCaseStory } from '../components/UseCaseStory'; import Dashboards from './Dashboards'; - -void graphql(` - query UseCasedetails($publicId: String!) { - useCaseBySlug(publicId: $publicId) { - id - urlSlug - title - summary - created - isIndividualUsecase - user { - fullName - id - profilePicture { - url - } - } - organization { - name - slug - id - logo { - url - } - } - sectors { - id - name - } - geographies { - name - } - sdgs { - id - code - name - } - tags { - id - value - } - logo { - url - } - datasets { - title - id - urlSlug - isIndividualDataset - user { - fullName - profilePicture { - url - } - } - downloadCount - description - organization { - name - logo { - url - } - } - sectors { - name - } - geographies { - name - } - modified - } - contributors { - id - fullName - profilePicture { - url - } - } - supportingOrganizations { - id - slug - name - logo { - url - } - } - partnerOrganizations { - id - slug - name - logo { - url - } - } - } - } -`); +import { useCaseDetailsQuery } from './queries'; const UseCaseDetailClient = () => { const params = useParams(); const { trackUsecase } = useAnalytics(); + const requested = + typeof params.useCaseSlug === 'string' ? params.useCaseSlug : ''; const { data: UseCaseDetails, @@ -146,21 +48,12 @@ const UseCaseDetailClient = () => { } = useQuery( [`fetch_UsecaseDetails_${params.useCaseSlug}`], async () => { - const segment = - typeof params.useCaseSlug === 'string' ? params.useCaseSlug : ''; const result = await GraphQLPublic( - UseCasedetailsDocument, + useCaseDetailsQuery, {}, - { publicId: segment } + { publicId: requested } ); return result; - }, - { - refetchOnMount: true, - refetchOnReconnect: true, - retry: (failureCount) => { - return failureCount < 3; - }, } ); diff --git a/app/[locale]/(user)/usecases/[useCaseSlug]/page.tsx b/app/[locale]/(user)/usecases/[useCaseSlug]/page.tsx index 77292bf5..bf1e79ce 100644 --- a/app/[locale]/(user)/usecases/[useCaseSlug]/page.tsx +++ b/app/[locale]/(user)/usecases/[useCaseSlug]/page.tsx @@ -1,26 +1,19 @@ import { Metadata } from 'next'; import { graphql } from '@/gql'; +import { permanentRedirect } from 'next/navigation'; import { GraphQLPublic } from '@/lib/api'; -import { - isCanonicalRedirect, - redirectToCanonical, -} from '@/lib/canonical-redirect'; -import { isIntegerId, publicPageUrl } from '@/lib/public-link'; +import { isIntegerId, publicHref, publicPageUrl } from '@/lib/public-link'; import { generatePageMetadata } from '@/lib/utils'; import { getUseCaseSummaryExcerpt } from '@/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/content-document'; import UseCaseDetailClient from './UsecaseDetailsClient'; -const UseCaseInfoQuery = graphql(` +const useCaseMetaQuery = graphql(` query UseCaseInfo($publicId: String!) { useCaseBySlug(publicId: $publicId) { - id title summary urlSlug - logo { - url - } tags { id value @@ -29,16 +22,12 @@ const UseCaseInfoQuery = graphql(` } `); -const UseCaseLegacyInfoQuery = graphql(` +const useCaseLegacyMetaQuery = graphql(` query UseCaseLegacyInfo($pk: ID!) { useCase(pk: $pk) { - id title summary urlSlug - logo { - url - } tags { id value @@ -47,67 +36,47 @@ const UseCaseLegacyInfoQuery = graphql(` } `); -async function loadUseCase(segment: string) { - if (isIntegerId(segment)) { - const data = await GraphQLPublic( - UseCaseLegacyInfoQuery, - {}, - { pk: segment } - ); - return data.useCase; - } - - const data = await GraphQLPublic(UseCaseInfoQuery, {}, { publicId: segment }); - return data.useCaseBySlug; -} - export async function generateMetadata({ params, }: { params: Promise<{ useCaseSlug: string }>; }): Promise { const { useCaseSlug } = await params; - try { - const UseCase = await loadUseCase(useCaseSlug); - await redirectToCanonical('usecases', useCaseSlug, UseCase?.urlSlug); - const segment = UseCase?.urlSlug || useCaseSlug; + const useCase = isIntegerId(useCaseSlug) + ? ( + await GraphQLPublic(useCaseLegacyMetaQuery, {}, { pk: useCaseSlug }).catch( + () => null + ) + )?.useCase + : ( + await GraphQLPublic(useCaseMetaQuery, {}, { publicId: useCaseSlug }).catch( + () => null + ) + )?.useCaseBySlug; + const segment = useCase?.urlSlug || useCaseSlug; + const description = + getUseCaseSummaryExcerpt(useCase?.summary) || + `Explore open data and curated datasets in the ${useCase?.title ?? 'this'} sector.`; - return generatePageMetadata({ - title: `${UseCase?.title} | Sector Data | CivicDataSpace`, - description: - getUseCaseSummaryExcerpt(UseCase?.summary) || - `Explore open data and curated datasets in the ${UseCase?.title} sector.`, - keywords: UseCase?.tags?.map((tag) => tag.value) || [], - openGraph: { - type: 'article', - locale: 'en_US', - url: publicPageUrl('usecases', segment), - title: `${UseCase?.title} | Sector Data | CivicDataSpace`, - description: - getUseCaseSummaryExcerpt(UseCase?.summary) || - `Explore open data and curated datasets in the ${UseCase?.title} sector.`, - siteName: 'CivicDataSpace', - image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`, - }, - }); - } catch (error) { - if (isCanonicalRedirect(error)) throw error; - console.error('Error fetching use case info:', error); - return generatePageMetadata({ - title: `Use Case Details | CivicDataSpace`, - description: `Explore open data and curated datasets in this use case.`, - keywords: ['usecase', 'data', 'civic', 'open data'], - openGraph: { - type: 'article', - locale: 'en_US', - url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/usecases/${useCaseSlug}`, - title: `Use Case Details | CivicDataSpace`, - description: `Explore open data and curated datasets in this use case.`, - siteName: 'CivicDataSpace', - image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`, - }, - }); - } + return generatePageMetadata({ + title: `${useCase?.title ?? 'Use Case Details'} | Sector Data | CivicDataSpace`, + description, + keywords: useCase?.tags?.map((tag) => tag.value) || [ + 'usecase', + 'data', + 'civic', + 'open data', + ], + openGraph: { + type: 'article', + locale: 'en_US', + url: publicPageUrl('usecases', segment), + title: `${useCase?.title ?? 'Use Case Details'} | Sector Data | CivicDataSpace`, + description, + siteName: 'CivicDataSpace', + image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`, + }, + }); } export default async function Page({ @@ -117,12 +86,16 @@ export default async function Page({ }) { const { useCaseSlug } = await params; - try { - const useCase = await loadUseCase(useCaseSlug); - await redirectToCanonical('usecases', useCaseSlug, useCase?.urlSlug); - } catch (error) { - if (isCanonicalRedirect(error)) throw error; - console.error('Use case lookup error', error); + if (isIntegerId(useCaseSlug)) { + const data = await GraphQLPublic( + useCaseLegacyMetaQuery, + {}, + { pk: useCaseSlug } + ).catch(() => null); + const urlSlug = data?.useCase?.urlSlug; + if (urlSlug && urlSlug !== useCaseSlug) { + permanentRedirect(publicHref('usecases', urlSlug, urlSlug)); + } } return ; diff --git a/app/[locale]/(user)/usecases/[useCaseSlug]/queries.ts b/app/[locale]/(user)/usecases/[useCaseSlug]/queries.ts new file mode 100644 index 00000000..4a2503b1 --- /dev/null +++ b/app/[locale]/(user)/usecases/[useCaseSlug]/queries.ts @@ -0,0 +1,98 @@ +import { graphql } from '@/gql'; + +export const useCaseDetailsQuery = graphql(` + query UseCasedetails($publicId: String!) { + useCaseBySlug(publicId: $publicId) { + id + urlSlug + title + summary + created + isIndividualUsecase + user { + fullName + id + profilePicture { + url + } + } + organization { + name + slug + id + logo { + url + } + } + sectors { + id + name + } + geographies { + name + } + sdgs { + id + code + name + } + tags { + id + value + } + logo { + url + } + datasets { + title + id + urlSlug + isIndividualDataset + user { + fullName + profilePicture { + url + } + } + downloadCount + description + organization { + name + logo { + url + } + } + sectors { + name + } + geographies { + name + } + modified + } + contributors { + id + fullName + profilePicture { + url + } + } + supportingOrganizations { + id + slug + name + logo { + url + } + } + partnerOrganizations { + id + slug + name + logo { + url + } + } + } + } +`); diff --git a/lib/canonical-redirect.ts b/lib/canonical-redirect.ts deleted file mode 100644 index 8e23e4b5..00000000 --- a/lib/canonical-redirect.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getLocale } from 'next-intl/server'; -import { permanentRedirect } from 'next/navigation'; - -import { publicHref, type PublicSection } from '@/lib/public-link'; - -/** Next.js implements redirects by throwing. Callers must rethrow this error. */ -export function isCanonicalRedirect(error: unknown): boolean { - return ( - typeof error === 'object' && - error !== null && - 'digest' in error && - String((error as { digest?: unknown }).digest).startsWith('NEXT_REDIRECT') - ); -} - -/** 308 to the current urlSlug when the address is an old id or a renamed link. */ -export async function redirectToCanonical( - section: PublicSection, - requested: string, - urlSlug: string | null | undefined -): Promise { - const canonical = urlSlug?.trim(); - if (!canonical || canonical === requested) return; - - const locale = await getLocale(); - const prefix = locale === 'en' ? '' : `/${locale}`; - permanentRedirect(`${prefix}${publicHref(section, canonical, canonical)}`); -}