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)}`);
-}