-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Changes from 7 commits
974340e
b779116
92a0ae8
417d2eb
3ebfdd6
61bf865
3abf4c4
7104cc2
e38930e
41f9399
a49b231
634245b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 }) { | ||
const title = dataset.title || dataset.name | ||
const description = dataset.notes || "Dataset page of " + title | ||
const owner_org = dataset.organization.name || "" | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ( | ||
<> | ||
<LogoJsonLd | ||
url={`${url}/@${owner_org}/${title}`} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implement URL encoding for title to prevent invalid URLs. The + 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
|
||
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}` | ||
}, | ||
]} | ||
/> | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<DatasetJsonLd | ||
id={`${url}/@${owner_org}/${title}#webpage`} | ||
url={`${url}/@${owner_org}/${title}`} | ||
name={title} | ||
description={description} | ||
/> | ||
</> | ||
); | ||
} |
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", | ||
}, | ||
]} | ||
/> | ||
</> | ||
); | ||
} |
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 }) { | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add TypeScript types for better type safety. The 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||
const title = org.name || org.title | ||||||||||||||||||||
const description = org.notes || "Organizations page of " + title | ||||||||||||||||||||
return ( | ||||||||||||||||||||
<> | ||||||||||||||||||||
<LogoJsonLd | ||||||||||||||||||||
url={`${url}/@${title}`} | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. URL construction needs proper encoding and correction. Multiple issues with URL construction:
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
|
||||||||||||||||||||
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} | ||||||||||||||||||||
/> | ||||||||||||||||||||
</> | ||||||||||||||||||||
); | ||||||||||||||||||||
} |
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" | ||
}, | ||
]} | ||
/> | ||
</> | ||
); | ||
} |
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" | ||
}, | ||
]} | ||
/> | ||
</> | ||
); | ||
} |
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", | ||
}, | ||
] | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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); | ||||||||||||||||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add null safety for organization property access. The validation assumes - if ("@" + dataset.organization.name !== orgName) {
+ if (!dataset.organization || "@" + dataset.organization.name !== orgName) {
return {
notFound: true,
};
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||
return { | ||||||||||||||||||||||
props: { | ||||||||||||||||||||||
dataset, | ||||||||||||||||||||||
|
@@ -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 | ||||||||||||||||||||||
|
There was a problem hiding this comment.
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.📝 Committable suggestion
🤖 Prompt for AI Agents