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

Merged
merged 27 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
974340e
[seo][lg]: central SEO config file created
Datopian-Shreyas Jun 17, 2025
b779116
[seo][xs]: modified seo-config-file
Datopian-Shreyas Jun 17, 2025
92a0ae8
[seo][md]: added jsonLd for homepage
Datopian-Shreyas Jun 19, 2025
417d2eb
[seo][md]: added jsonLd for organization page
Datopian-Shreyas Jun 19, 2025
3ebfdd6
[seo][md]: added jsonLd for search page
Datopian-Shreyas Jun 19, 2025
61bf865
[seo][md]: added jsonLd for org individual page
Datopian-Shreyas Jun 19, 2025
3abf4c4
[seo][md]: added jsonLd for dataset page
Datopian-Shreyas Jun 19, 2025
7104cc2
[seo][md]: added jsonLd for groups page
Datopian-Shreyas Jun 19, 2025
e38930e
[seo][xs]: added jsonLd for groups page
Datopian-Shreyas Jun 19, 2025
41f9399
[seo][lg]: added jsonLd for groups page and resource page
Datopian-Shreyas Jun 19, 2025
a49b231
[seo][lg]: breadcrumbLD fixes
Datopian-Shreyas Jun 19, 2025
634245b
[seo][xs]: small fix
Datopian-Shreyas Jun 19, 2025
7ca5ed8
[seo][xxl]: improved and fixed the nextseo component
Datopian-Shreyas Jun 24, 2025
555341c
[seo][sm]:Handled undefined resource properties in JSON-LD.
Datopian-Shreyas Jun 24, 2025
0791451
[seo][xs]: small fix
Datopian-Shreyas Jun 24, 2025
dcf5c54
[seo][xs]: manifest icon fixes
Datopian-Shreyas Jun 26, 2025
1f13583
[seo][xs]: indidual org page fixes
Datopian-Shreyas Jun 26, 2025
67b66d0
[seo][xs]: added types for structured data
Datopian-Shreyas Jun 26, 2025
6968b97
[seo][xs]: Added null safety for organization property access and Add…
Datopian-Shreyas Jun 26, 2025
7f6f34b
[seo][xs]: updated next-seo
Datopian-Shreyas Jun 26, 2025
ec474a4
[seo][xs]: updated next-seo
Datopian-Shreyas Jun 26, 2025
15f6084
[seo][xs]: updated next-seo
Datopian-Shreyas Jun 26, 2025
b86d9f3
[seo][xs]: updated next-seo
Datopian-Shreyas Jun 26, 2025
7bdc681
[seo][xs]: title updation on org and groups
Datopian-Shreyas Jun 26, 2025
655456a
[seo][xs]: small fix
Datopian-Shreyas Jun 26, 2025
4227069
[seo][xs]: search page title changed
Datopian-Shreyas Jun 26, 2025
143c491
[seo][xs]: added default title
Datopian-Shreyas Jun 26, 2025
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
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, SiteLinksSearchBoxJsonLd } from "next-seo";

export function OrganizationIndividualPageStructuredData({ org }) {
const title = org.name || org.title
const description = org.notes || "Organizations page of " + org
return (
<>
<LogoJsonLd
url={`${url}/@${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",
},
]
};
27 changes: 18 additions & 9 deletions pages/[org]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getOrganization } from "@/lib/queries/orgs";
import { getDataset } from "@/lib/queries/dataset";

import HeroSection from "@/components/_shared/HeroSection";
import { OrganizationIndividualPageStructuredData } from "@/components/schema/OrganizationIndividualPageStructuredData";

export const getServerSideProps: GetServerSideProps = async (context) => {
const DMS = process.env.NEXT_PUBLIC_DMS;
Expand All @@ -27,14 +28,26 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
name: orgName as string,
include_datasets: true,
});

if (org.packages) {
const packagesWithResources = await Promise.all(
org.packages.map(
async (dataset) => await getDataset({ name: dataset.name })
)
org.packages.map(async (dataset) => {
try {
const fullDataset = await getDataset({ name: dataset.name });
return fullDataset || null;
} catch (err) {
console.error(`Failed to fetch dataset: ${dataset.name}`, err);
return null;
}
})
);
org = { ...org, packages: packagesWithResources };

org = {
...org,
packages: packagesWithResources.filter(Boolean),
};
}

const activityStream = await ckan.getOrgActivityStream(org._name);
if (!org) {
return {
Expand Down Expand Up @@ -72,11 +85,7 @@ export default function OrgPage({ org }): JSX.Element {
];
return (
<>
<Head>
<title>{org.title || org.name + " - Organization Page"}</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<OrganizationIndividualPageStructuredData org={org} />
{org && (
<Layout>
<HeroSection title={org.title} cols="6" />
Expand Down
7 changes: 2 additions & 5 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { searchDatasets } from "@/lib/queries/dataset";
import { getAllGroups } from "@/lib/queries/groups";
import { getAllOrganizations } from "@/lib/queries/orgs";
import HeroSectionLight from "@/components/home/heroSectionLight";
import { HomePageStructuredData } from "@/components/schema/HomePageStructuredData";

export async function getServerSideProps() {
const datasets = await searchDatasets({
Expand Down Expand Up @@ -39,11 +40,7 @@ export default function Home({
}: InferGetServerSidePropsType<typeof getServerSideProps>): JSX.Element {
return (
<>
<Head>
<title>Open Data Portal Demo</title>
<meta name="description" content="Open Data Portal Demo" />
<link rel="icon" href="/favicon.ico" />
</Head>
<HomePageStructuredData />
<HeroSectionLight stats={stats} />
<MainSection groups={groups} datasets={datasets} />
</>
Expand Down
11 changes: 2 additions & 9 deletions pages/organizations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ListOfOrgs from "../components/organization/ListOfOrganizations";
import Layout from "../components/_shared/Layout";
import { Organization } from "@portaljs/ckan";
import { getAllOrganizations } from "@/lib/queries/orgs";
import { OrganizationPageStructuredData } from "@/components/schema/OrganizationPageStructuredData";

export async function getServerSideProps() {
const orgs = await getAllOrganizations({ detailed: true });
Expand All @@ -24,11 +25,7 @@ export default function OrgsPage({ orgs }): JSX.Element {
miniSearch.addAll(orgs);
return (
<>
<Head>
<title>Ckan Homepage</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<OrganizationPageStructuredData />
<Main miniSearch={miniSearch} orgs={orgs} />
</>
);
Expand All @@ -44,10 +41,6 @@ function Main({
const [searchString, setSearchString] = useState("");
return (
<>
<Head>
<title>Organizations</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Layout>
<SearchHero
title="Organizations"
Expand Down
Loading