Skip to content

Improve PortalJS template's SEO features #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions components/schema/DatasetPageStructuredData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import nextSeoConfig, { url } from "@/next-seo.config";
import { BreadcrumbJsonLd, LogoJsonLd, NextSeo, WebPageJsonLd, SiteLinksSearchBoxJsonLd, DatasetJsonLd } from "next-seo";

export function DatasetPageStructuredData({ dataset }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add TypeScript interface for better type safety.

The component lacks proper TypeScript typing for the dataset prop, which could lead to runtime errors if the expected properties are missing.

+interface Dataset {
+  title?: string;
+  name: string;
+  notes?: string;
+  organization: {
+    name: string;
+  };
+}
+
+interface DatasetPageStructuredDataProps {
+  dataset: Dataset;
+}
+
-export function DatasetPageStructuredData({ dataset }) {
+export function DatasetPageStructuredData({ dataset }: DatasetPageStructuredDataProps) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function DatasetPageStructuredData({ dataset }) {
interface Dataset {
title?: string;
name: string;
notes?: string;
organization: {
name: string;
};
}
interface DatasetPageStructuredDataProps {
dataset: Dataset;
}
-export function DatasetPageStructuredData({ dataset }) {
+export function DatasetPageStructuredData({ dataset }: DatasetPageStructuredDataProps) {
// …rest of component
}
🤖 Prompt for AI Agents
In components/schema/DatasetPageStructuredData.tsx at line 4, the
DatasetPageStructuredData component lacks TypeScript typing for its dataset
prop. Define a TypeScript interface describing the expected shape of the dataset
object with all required properties, then update the component's props to use
this interface for type safety and to prevent runtime errors.

const title = dataset.title || dataset.name
const description = dataset.notes || "Dataset page of " + title
const owner_org = dataset.organization.name || ""
return (
<>
<LogoJsonLd
url={`${url}/@${owner_org}/${title}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement URL encoding for title to prevent invalid URLs.

The title variable is directly used in URL construction without encoding, which could create invalid URLs if the title contains special characters or spaces.

+  const encodedTitle = encodeURIComponent(title);
+  const encodedOwnerOrg = encodeURIComponent(owner_org);
+  const datasetUrl = `${url}/@${encodedOwnerOrg}/${encodedTitle}`;

-        url={`${url}/@${owner_org}/${title}`}
+        url={datasetUrl}
-        canonical={`${url}/@${owner_org}/${title}`}
+        canonical={datasetUrl}
-            item: `${url}/@${owner_org}/${title}`
+            item: datasetUrl
-        id={`${url}/@${owner_org}/${title}#webpage`}
-        url={`${url}/@${owner_org}/${title}`}
+        id={`${datasetUrl}#webpage`}
+        url={datasetUrl}

Also applies to: 15-15, 30-30, 35-36

🤖 Prompt for AI Agents
In components/schema/DatasetPageStructuredData.tsx at lines 11, 15, 30, and
35-36, the title variable is used directly in URL construction without encoding,
which can cause invalid URLs if the title contains special characters or spaces.
Fix this by applying URL encoding (e.g., using encodeURIComponent) to the title
variable wherever it is included in the URL string to ensure the URLs are valid
and safe.

logo={`${url}/favicon.ico`}
/>
<NextSeo
canonical={`${url}/@${owner_org}/${title}`}
title={title}
description={description}
{...nextSeoConfig}
/>
<BreadcrumbJsonLd
itemListElements={[
{
position: 1,
name: 'Home',
item: url,
},
{
position: 2,
name: 'Organizations',
item: `${url}/@${owner_org}/${title}`
},
]}
/>
<DatasetJsonLd
id={`${url}/@${owner_org}/${title}#webpage`}
url={`${url}/@${owner_org}/${title}`}
name={title}
description={description}
/>
</>
);
}
40 changes: 40 additions & 0 deletions components/schema/HomePageStructuredData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import nextSeoConfig, { url } from "@/next-seo.config";
import { BreadcrumbJsonLd, LogoJsonLd, NextSeo, WebPageJsonLd, SiteLinksSearchBoxJsonLd } from "next-seo";

export function HomePageStructuredData() {
return (
<>
<LogoJsonLd
url={url}
logo={`${url}/favicon.ico`}
/>
<NextSeo
{...nextSeoConfig}
/>
<BreadcrumbJsonLd
itemListElements={[
{
position: 1,
name: 'Home',
item: url,
},
]}
/>
<WebPageJsonLd
id={`${url}#webpage`}
url={url}
name={nextSeoConfig.title}
description={nextSeoConfig.description}
/>
<SiteLinksSearchBoxJsonLd
url={url}
potentialActions={[
{
target: `${url}/search?q={search_term_string}`,
queryInput: "required name=search_term_string",
},
]}
/>
</>
);
}
41 changes: 41 additions & 0 deletions components/schema/OrganizationIndividualPageStructuredData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import nextSeoConfig, { url } from "@/next-seo.config";
import { BreadcrumbJsonLd, LogoJsonLd, NextSeo, WebPageJsonLd } from "next-seo";

export function OrganizationIndividualPageStructuredData({ org }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add TypeScript types for better type safety.

The org parameter lacks type definitions, which reduces type safety and IDE support.

Apply this diff to add proper typing:

-export function OrganizationIndividualPageStructuredData({ org }) {
+interface Organization {
+  name?: string;
+  title?: string;
+  notes?: string;
+  image_display_url?: string;
+}
+
+export function OrganizationIndividualPageStructuredData({ org }: { org: Organization }) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function OrganizationIndividualPageStructuredData({ org }) {
interface Organization {
name?: string;
title?: string;
notes?: string;
image_display_url?: string;
}
export function OrganizationIndividualPageStructuredData({ org }: { org: Organization }) {
🤖 Prompt for AI Agents
In components/schema/OrganizationIndividualPageStructuredData.tsx at line 4, the
org parameter lacks TypeScript type definitions, reducing type safety and IDE
support. Define an appropriate interface or type for the org parameter
reflecting its expected structure, then update the function signature to include
this type for org. This will improve type safety and developer experience.

const title = org.name || org.title
const description = org.notes || "Organizations page of " + title
return (
<>
<LogoJsonLd
url={`${url}/@${title}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

URL construction needs proper encoding and correction.

Multiple issues with URL construction:

  1. Using title directly in URLs without encoding can cause issues if the title contains special characters
  2. The breadcrumb Organizations URL is incorrect - should point to /organizations not /@${title}

Apply this diff to fix URL construction:

+  const encodedOrgName = encodeURIComponent(org.name || org.title || '')
+  const orgUrl = `${url}/@${encodedOrgName}`
+
   return (
     <>
       <LogoJsonLd
-        url={`${url}/@${title}`}
+        url={orgUrl}
         logo={org.image_display_url || `${url}/favicon.ico`}
       />
       <NextSeo
-        canonical={`${url}/@${title}`}
+        canonical={orgUrl}
         title={title}
         description={description}
         {...nextSeoConfig}
       />
       <BreadcrumbJsonLd
         itemListElements={[
           {
             position: 1,
             name: 'Home',
             item: url,
           },
           {
             position: 2,
             name: 'Organizations',
-            item: `${url}/@${title}`,
+            item: `${url}/organizations`,
           },
         ]}
       />
       <WebPageJsonLd
-        id={`${url}/@${title}#webpage`}
-        url={`${url}/@${title}`}
+        id={`${orgUrl}#webpage`}
+        url={orgUrl}
         name={title}
         description={description}
       />

Also applies to: 14-14, 29-29, 34-35

🤖 Prompt for AI Agents
In components/schema/OrganizationIndividualPageStructuredData.tsx at lines 10,
14, 29, and 34-35, the URL construction uses the raw title without encoding and
incorrectly sets the Organizations breadcrumb URL. Fix this by encoding the
title using encodeURIComponent before inserting it into URLs and correct the
Organizations breadcrumb URL to point to '/organizations' instead of using the
title.

logo={org.image_display_url || `${url}/favicon.ico`}
/>
<NextSeo
canonical={`${url}/@${title}`}
title={title}
description={description}
{...nextSeoConfig}
/>
<BreadcrumbJsonLd
itemListElements={[
{
position: 1,
name: 'Home',
item: url,
},
{
position: 2,
name: 'Organizations',
item: `${url}/@${title}`,
},
]}
/>
<WebPageJsonLd
id={`${url}/@${title}#webpage`}
url={`${url}/@${title}`}
name={title}
description={description}
/>
</>
);
}
50 changes: 50 additions & 0 deletions components/schema/OrganizationPageStructuredData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import nextSeoConfig, { url } from "@/next-seo.config";
import { BreadcrumbJsonLd, LogoJsonLd, NextSeo, WebPageJsonLd, SiteLinksSearchBoxJsonLd } from "next-seo";

export function OrganizationPageStructuredData() {
const title = "Organizations page"
const description = "Organizations page of " + title
return (
<>
<LogoJsonLd
url={`${url}/organizations`}
logo={`${url}/favicon.ico`}
/>
<NextSeo
canonical={`${url}/organizations`}
title={title}
description={description}
{...nextSeoConfig}
/>
<BreadcrumbJsonLd
itemListElements={[
{
position: 1,
name: 'Home',
item: url,
},
{
position: 2,
name: 'Organizations',
item: `${url}/organizations`,
},
]}
/>
<WebPageJsonLd
id={`${url}/organizations#webpage`}
url={`${url}/organizations`}
name={title}
description={description}
/>
<SiteLinksSearchBoxJsonLd
url={`${url}/organizations`}
potentialActions={[
{
target: `${url}/organizations`,
queryInput: "search_term_string"
},
]}
/>
</>
);
}
57 changes: 57 additions & 0 deletions components/schema/SearchPageStructuredData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import nextSeoConfig, { url } from "@/next-seo.config";
import { BreadcrumbJsonLd, LogoJsonLd, NextSeo, SiteLinksSearchBoxJsonLd } from "next-seo";
import Script from "next/script";

export function SearchPageStructuredData() {
const title = "Search page"
const description = "Browse through multiple datasets available on " + title
const jsonLd = {
"@context": "https://schema.org",
"@type": "DataCatalog",
"name": title,
"description": description,
"url": url + "/search",
};
return (
<>
<LogoJsonLd
url={`${url}/search`}
logo={`${url}/favicon.ico`}
/>
<NextSeo
canonical={`${url}/search`}
title={title}
description={description}
{...nextSeoConfig}
/>
<BreadcrumbJsonLd
itemListElements={[
{
position: 1,
name: 'Home',
item: url,
},
{
position: 2,
name: 'Search',
item: `${url}/search`,
},
]}
/>
<Script
id="datacatalog-jsonld"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<SiteLinksSearchBoxJsonLd
url={`${url}/search`}
potentialActions={[
{
target: `${url}/search?q={search_term_string}`,
queryInput: "search_term_string"
},
]}
/>
</>
);
}
77 changes: 63 additions & 14 deletions next-seo.config.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,74 @@
/* eslint-disable import/no-anonymous-default-export */

const title = "PortalJS Open Data Portal";
const description =
"Discover thousands of datasets, publish your own, and request data via Portal – an open data platform powered by PortalJS.";

export const url = "https://portaljs-cloud-frontend-template.vercel.app";
const imageUrl = `${url}/images/portaljs-frontend.png`;

export default {
titleTemplate: "%s | Ckan Homepage",
description: "Ckan Homepage",
canonical: "https://datahub-enterprise.vercel.app/",
title,
titleTemplate: "%s | PortalJS",
description,
canonical: url,
openGraph: {
title: "Ckan Homepage",
title,
description,
type: "website",
url: "https://datahub-enterprise.vercel.app/",
site_name: "Ckan Homepage",
locale: "en_US",
url,
site_name: title,
images: [
{
url: "https://datahub-enterprise.vercel.app/images/datahub_enterprise_frontend.png",
alt: "Ckan Homepage",
url: imageUrl,
alt: title,
width: 1200,
height: 627,
type: "image/jpg",
type: "image/png",
},
],
},
// twitter: {
// handle: "@datahubenterprise",
// site: "https://datahub-enterprise.vercel.app/",
// cardType: "summary_large_image",
// },
twitter: {
handle: "@datopian",
site: "@PortalJS_",
cardType: "summary_large_image",
},
additionalMetaTags: [
{
name: "keywords",
content: "PortalJS, open data, datasets, data portal, Portal, datopian, frontend template",
},
{
name: "author",
content: "Datopian / PortalJS",
},
{
property: "og:image:width",
content: "1200",
},
{
property: "og:image:height",
content: "627",
},
{
property: "og:locale",
content: "en_US",
},
],
additionalLinkTags: [
{
rel: "icon",
href: "/favicon.ico",
},
{
rel: "apple-touch-icon",
href: "/apple-touch-icon.png",
sizes: "180x180",
},
{
rel: "manifest",
href: "/site.webmanifest",
},
]
};
13 changes: 8 additions & 5 deletions pages/[org]/[dataset]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import styles from "styles/DatasetInfo.module.scss";
import { publicToPrivateDatasetName } from "@/lib/queries/utils";
import { getDataset } from "@/lib/queries/dataset";
import HeroSection from "@/components/_shared/HeroSection";
import { DatasetPageStructuredData } from "@/components/schema/DatasetPageStructuredData";

export const getServerSideProps: GetServerSideProps = async (context) => {
const ckan = new CKAN(process.env.NEXT_PUBLIC_DMS);
Expand Down Expand Up @@ -39,6 +40,12 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
...dataset,
activity_stream: activityStream,
};

if ("@" + dataset.organization.name !== orgName) {
return {
notFound: true,
};
}
Comment on lines +44 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null safety for organization property access.

The validation assumes dataset.organization.name exists without checking if dataset.organization is null or undefined, which could cause a runtime error.

-    if ("@" + dataset.organization.name !== orgName) {
+    if (!dataset.organization || "@" + dataset.organization.name !== orgName) {
       return {
         notFound: true,
       };
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ("@" + dataset.organization.name !== orgName) {
return {
notFound: true,
};
}
if (!dataset.organization || "@" + dataset.organization.name !== orgName) {
return {
notFound: true,
};
}
🤖 Prompt for AI Agents
In pages/[org]/[dataset]/index.tsx around lines 44 to 48, the code accesses
dataset.organization.name without verifying if dataset.organization is null or
undefined, risking a runtime error. Add a null check to ensure
dataset.organization exists before accessing its name property, and handle the
case where it does not by returning notFound or an appropriate fallback.

return {
props: {
dataset,
Expand Down Expand Up @@ -81,11 +88,7 @@ export default function DatasetPage({ dataset }): JSX.Element {
];
return (
<>
<Head>
<title>{`${dataset.title || dataset.name} - Dataset`}</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<DatasetPageStructuredData dataset={dataset} />
<Layout>
<HeroSection title={dataset.title} cols="6" />
<DatasetNavCrumbs
Expand Down
Loading