Merge pull request #991 from datopian/feature/seo-improvements-ii

SEO Improvements (II) - JSON-LD
This commit is contained in:
João Demenech 2023-07-27 13:10:33 -03:00 committed by GitHub
commit d88a23c922
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 220 additions and 150 deletions

View File

@ -0,0 +1,65 @@
import { ArticleJsonLd } from 'next-seo';
import { useRouter } from 'next/router';
export default function JSONLD({
meta,
source,
}: {
meta: any;
source: string;
}): JSX.Element {
if (!source) {
return <></>;
}
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://portaljs.org';
const pageUrl = `${baseUrl}/${meta.urlPath}`;
const imageMatches = source.match(
/(?<=src: ")(.*)\.((png)|(jpg)|(jpeg))(?=")/g
);
let images = [];
if (imageMatches) {
images = [...imageMatches];
images = images.map((img) =>
img.startsWith('http')
? img
: `${baseUrl}${img.startsWith('/') ? '' : '/'}${img}`
);
}
let Component: JSX.Element;
const isBlog: boolean =
/^blog\/.*/.test(meta.urlPath) || meta.filetype === 'blog';
const isDoc: boolean = /^((docs)|(howtos\/)|(guide\/)).*/.test(meta.urlPath);
if (isBlog) {
Component = (
<ArticleJsonLd
type="BlogPosting"
url={pageUrl}
title={meta.title}
datePublished={meta.date}
dateModified={meta.date}
authorName={meta.authors.length ? meta.authors[0].name : 'PortalJS'}
description={meta.description}
images={images}
/>
);
} else if (isDoc) {
Component = (
<ArticleJsonLd
url={pageUrl}
title={meta.title}
images={images}
datePublished={meta.date}
dateModified={meta.date}
authorName={meta.authors.length ? meta.authors[0].name : 'PortalJS'}
description={meta.description}
/>
);
}
return Component;
}

View File

@ -54,12 +54,14 @@ function useTableOfContents(tableOfContents) {
export default function Layout({
children,
title,
description,
tableOfContents = [],
isHomePage = false,
sidebarTree = [],
}: {
children;
title?: string;
description?: string;
tableOfContents?;
urlPath?: string;
sidebarTree?: [];
@ -82,7 +84,7 @@ export default function Layout({
return (
<>
{title && <NextSeo title={title} />}
{title && <NextSeo title={title} description={description} />}
<Nav />
<div className="mx-auto p-6 bg-background dark:bg-background-dark">
{isHomePage && <Hero />}

View File

@ -264,7 +264,7 @@ The above script will output the following to the terminal:
## Done!
That's it! We've just created a simple catalog of our GitHub projects using markdown files and the MarkdownDB package. You can find the full code for this tutorial [here](https://github.com/datopian/markdowndb/tree/main/examples/basic-example).
That's it! We've just created a simple catalog of our GitHub projects using markdown files and the MarkdownDB package. You can find the [full code for this tutorial here](https://github.com/datopian/markdowndb/tree/main/examples/basic-example).
We look forward to seeing the amazing applications you'll build with this tool!

View File

@ -1,7 +1,7 @@
const config = {
title: 'PortalJS - The JavaScript framework for data portals.',
description:
'PortalJS is a framework for rapidly building rich data portal frontends using a modern frontend approach.',
'PortalJS is a JavaScript framework for rapidly building rich data portal frontends using a modern frontend approach.',
theme: {
default: 'dark',
toggleIcon: '/images/theme-button.svg',

View File

@ -1,6 +1,7 @@
<NextSeo title="Creating new datasets - PortalJS" />
# Creating new datasets
---
title: 'Creating new datasets'
description: 'PortalJS Tutorial II - Learn how to create new datasets on a data portal'
---
So far, the PortalJS app we created only has a single page displaying a dataset. Data catalogs and data portals generally showcase many different datasets.
@ -42,7 +43,7 @@ Year,Population (mi)
Note that pages are associated with a route based on their pathname, so, to see the new data page, access http://localhost:3000/my-incredible-dataset from the browser. You should see the following:
<img src="/assets/docs/my-incredible-dataset.png" />
<img src="/assets/docs/my-incredible-dataset.png" alt="Page of a new dataset created on a PortalJS data portal" />
> [!tip]
> In this tutorial we opted for storing content as markdown files and data as CSV files in the app, but PortalJS can have metadata, data and content stored anywhere.
@ -58,12 +59,11 @@ List of available datasets:
- [My Awesome Dataset](/my-awesome-dataset)
- [My Incredible Dataset](/my-incredible-dataset)
```
From the browser, access http://localhost:3000. You should see the following:
<img src="/assets/docs/datasets-index-page.png" />
<img src="/assets/docs/datasets-index-page.png" alt="PortalJS data portal with multiple datasets" />
At this point, the app has multiple datasets, and users can find and navigate to any dataset they want. In the next lesson, you are going to learn how to improve this experience with search.

View File

@ -1,6 +1,7 @@
<NextSeo title="Getting Started - PortalJS" />
# Getting Started
---
title: Getting Started
description: 'Getting started guide and tutorial about data portal-building with PortalJS'
---
Welcome to the PortalJS documentation!
@ -38,7 +39,7 @@ Let's check it's working and what we have! Open http://localhost:3000 from your
You should see a page like this when you access http://localhost:3000. This is the starter template page which shows the most simple data portal you could have: a simple README plus csv file.
<img src="/assets/examples/basic-example.png" alt="Basic example" />
<img src="/assets/examples/basic-example.png" alt="Initial state of the PortalJS tutorial project" />
### Editing the Page
@ -51,7 +52,7 @@ Lets try editing the starter page.
After refreshing the page, you should see the new text:
<img src="/assets/docs/editing-the-page-1.png" alt="Editing base example" />
<img src="/assets/docs/editing-the-page-1.png" alt="PortalJS tutorial project after a simple change is made by a user" />
Congratulations! The app is up and running and you learned how to edit a page. In the next lesson, you are going to learn how to create new datasets.

View File

@ -26,7 +26,7 @@ This example makes use of the [markdowndb](https://github.com/datopian/markdownd
From the browser, access http://localhost:3000. You should see the following, you now have a searchable automatic list of your datasets:
![](https://i.imgur.com/9HfSPIx.png)
![Simple data catalog built with PortalJS](https://i.imgur.com/9HfSPIx.png)
To make this catalog look even better, we can change the text that is being displayed for each dataset to a title. Let's do that by adding the "title" [frontmatter field](https://daily-dev-tips.com/posts/what-exactly-is-frontmatter/) to the first dataset in the list. Change `content/my-awesome-dataset/index.md` to the following:
@ -46,7 +46,7 @@ Built with PortalJS
Rerun `npm run mddb` and, from the browser, access http://localhost:3000. You should see the title appearing instead of the folder name:
![](https://i.imgur.com/nvmSnJ5.png)
![Example of a newly added dataset on a data catalog built with PortalJS](https://i.imgur.com/nvmSnJ5.png)
Any frontmatter attribute that you add will automatically get indexed and be usable in the search box.
@ -102,7 +102,7 @@ List of available datasets:
You now have a filter in your page with all possible values automatically added to it.
![](https://i.imgur.com/p2miSdg.png)
![Data catalog with facets built with PortalJS](https://i.imgur.com/p2miSdg.png)
In the next lesson, you are going to learn how to display metadata on the dataset page.

View File

@ -4,7 +4,7 @@
If you go now to `http://localhost:3000/my-awesome-dataset`, you will see that we now have two titles on the page. That's because `title` is one of the default metadata fields supported by PortalJS.
![](https://i.imgur.com/O145uuc.png)
![Example of a page displaying the title metadata twice](https://i.imgur.com/O145uuc.png)
Change the content inside `/content/my-awesome-dataset/index.md` to this.
@ -27,7 +27,7 @@ Built with PortalJS
Once you refresh the page at `http://localhost:3000/my-awesome-dataset` you should see something like this at the top:
![](https://i.imgur.com/nvDYJQT.png)
![Example of a dataset page displaying metadata](https://i.imgur.com/nvDYJQT.png)
These are the standard metadata fields that will be shown at the top of the page if you add them.

View File

@ -3,6 +3,7 @@ showToc: false
showSidebar: false
title: "Markdown-based Websites Guide"
disableTitle: true
description: Create markdown-based websites and data portals, update it, add collaborators and discover markdown superpowers with Flowershow and PortalJS
---
<Hero title="Markdown-based Websites" subtitle="Create markdown-based website, update it, add collaborators and discover markdown superpowers" />

View File

@ -1,4 +1,7 @@
# How to add Google Analytics?
---
title: How to add Google Analytics?
description: Learn to implement Google Analytics on PortalJS data portals
---
>[!todo] Prerequisites
>- [Google Analytics account](https://support.google.com/analytics/answer/9304153?hl=en)

View File

@ -1,4 +1,7 @@
# How to add a simple blog?
---
title: How to add a simple blog?
description: How to add a simple blog on a PortalJS data portal
---
## Setup

View File

@ -1,4 +1,7 @@
# How to add user comments?
---
title: How to add user comments?
description: Learn how to add user comments on a PortalJS data portal
---
![[comments-example.png]]

View File

@ -1,4 +1,7 @@
# How to create data-rich documents with charts and tables?
---
title: How to create data-rich documents with charts and tables?
description: Learn how to create a data-rich document with charts and tables on a PortalJS data portal
---
> [!info] Prerequisites
> If you want to enrich your markdown content with charts and tables, you first need to add support for rendering markdown files in your PortalJS app. Follow [[markdown|this guide]] to learn how to do this.
@ -15,12 +18,12 @@ Now, in order to use these components in your markdown files, we need to pass th
```tsx
// e.g. /blog/[[...slug]].tsx
import fs from "fs";
import { LineChart, Table, Catalog, Vega, VegaLite } from "@portaljs/components";
import fs from 'fs';
import { LineChart, Table, Catalog, Vega, VegaLite } from '@portaljs/components';
import { MdxRemote } from "next-mdx-remote";
import clientPromise from "@/lib/mddb.mjs";
import parse from "@/lib/markdown";
import { MdxRemote } from 'next-mdx-remote';
import clientPromise from '@/lib/mddb.mjs';
import parse from '@/lib/markdown';
const components = {
Table: Table,
@ -29,37 +32,35 @@ const components = {
LineChart: LineChart,
};
export default function Page({ source }) {
source = JSON.parse(source);
source = JSON.parse(source);
return (
<>
<MdxRemote source={source} components={components} />
</>
);
return (
<>
<MdxRemote source={source} components={components} />
</>
);
}
// Import metadata of a file matching the static path and return its parsed source and frontmatter object
export const getStaticProps = async ({ params }) => {
const urlPath = params?.slug ? (params.slug as string[]).join("/") : "/";
const urlPath = params?.slug ? (params.slug as string[]).join('/') : '/';
const mddb = await clientPromise;
const dbFile = await mddb.getFileByUrl(urlPath);
const filePath = dbFile!.file_path;
// const frontMatter = dbFile!.metadata ?? {};
const mddb = await clientPromise;
const dbFile = await mddb.getFileByUrl(urlPath);
const filePath = dbFile!.file_path;
// const frontMatter = dbFile!.metadata ?? {};
const source = fs.readFileSync(filePath, { encoding: "utf-8" });
const { mdxSource } = await parse(source, "mdx", {});
const source = fs.readFileSync(filePath, { encoding: 'utf-8' });
const { mdxSource } = await parse(source, 'mdx', {});
return {
props: {
source: JSON.stringify(mdxSource),
// frontMatter
},
};
return {
props: {
source: JSON.stringify(mdxSource),
// frontMatter
},
};
};
```
You can now use these components in your markdown, like so:
@ -93,39 +94,39 @@ Example usage:
cols={[
{
key: 'id',
name: 'ID'
name: 'ID',
},
{
key: 'firstName',
name: 'First name'
name: 'First name',
},
{
key: 'lastName',
name: 'Last name'
name: 'Last name',
},
{
key: 'age',
name: 'Age'
}
name: 'Age',
},
]}
data={[
{
age: 35,
firstName: 'Jon',
id: 1,
lastName: 'Snow'
lastName: 'Snow',
},
{
age: 42,
firstName: 'Cersei',
id: 2,
lastName: 'Lannister'
}
lastName: 'Lannister',
},
]}
/>
```
>[!info]
> [!info]
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-table--docs)
### Linechart
@ -139,31 +140,16 @@ Example usage:
```js
<LineChart
data={[
[
'1850',
-0.41765878
],
[
'1851',
-0.2333498
],
[
'1852',
-0.22939907
],
[
'1853',
-0.27035445
],
[
'1854',
-0.29163003
]
['1850', -0.41765878],
['1851', -0.2333498],
['1852', -0.22939907],
['1853', -0.27035445],
['1854', -0.29163003],
]}
/>
/>
```
>[!info]
> [!info]
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-linechart--docs)
### Vega chart
@ -179,30 +165,30 @@ Example usage:
table: [
{
x: 1850,
y: -0.418
y: -0.418,
},
{
x: 2020,
y: 0.923
}
]
y: 0.923,
},
],
}}
spec={{
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
data: {
name: 'table'
name: 'table',
},
encoding: {
x: {
field: 'x',
type: 'ordinal'
type: 'ordinal',
},
y: {
field: 'y',
type: 'quantitative'
}
type: 'quantitative',
},
},
mark: 'bar'
mark: 'bar',
}}
/>
```
@ -222,35 +208,35 @@ Example usage:
table: [
{
x: 1850,
y: -0.418
y: -0.418,
},
{
x: 2020,
y: 0.923
}
]
y: 0.923,
},
],
}}
spec={{
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
data: {
name: 'table'
name: 'table',
},
encoding: {
x: {
field: 'x',
type: 'ordinal'
type: 'ordinal',
},
y: {
field: 'y',
type: 'quantitative'
}
type: 'quantitative',
},
},
mark: 'bar'
mark: 'bar',
}}
/>
```
>[!info]
> [!info]
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-vegalite--docs)
### Catalog
@ -269,28 +255,21 @@ Example usage:
metadata: {
'details-of-task': 'Detect and categorise abusive language in social media data',
language: 'Albanian',
'level-of-annotation': [
'Posts'
],
'level-of-annotation': ['Posts'],
'link-to-data': 'https://doi.org/10.6084/m9.figshare.19333298.v1',
'link-to-publication': 'https://arxiv.org/abs/2107.13592',
medium: [
'Text'
],
medium: ['Text'],
'percentage-abusive': 13.2,
platform: [
'Instagram',
'Youtube'
],
platform: ['Instagram', 'Youtube'],
reference: 'Nurce, E., Keci, J., Derczynski, L., 2021. Detecting Abusive Albanian. arXiv:2107.13592',
'size-of-dataset': 11874,
'task-description': 'Hierarchical (offensive/not; untargeted/targeted; person/group/other)',
title: 'Detecting Abusive Albanian'
title: 'Detecting Abusive Albanian',
},
url_path: 'dataset-4'
url_path: 'dataset-4',
},
]}
/>
/>
```
You can also add facets that are going to act as filters for your metadata.
@ -305,30 +284,23 @@ You can also add facets that are going to act as filters for your metadata.
metadata: {
'details-of-task': 'Detect and categorise abusive language in social media data',
language: 'Albanian',
'level-of-annotation': [
'Posts'
],
'level-of-annotation': ['Posts'],
'link-to-data': 'https://doi.org/10.6084/m9.figshare.19333298.v1',
'link-to-publication': 'https://arxiv.org/abs/2107.13592',
medium: [
'Text'
],
medium: ['Text'],
'percentage-abusive': 13.2,
platform: [
'Instagram',
'Youtube'
],
platform: ['Instagram', 'Youtube'],
reference: 'Nurce, E., Keci, J., Derczynski, L., 2021. Detecting Abusive Albanian. arXiv:2107.13592',
'size-of-dataset': 11874,
'task-description': 'Hierarchical (offensive/not; untargeted/targeted; person/group/other)',
title: 'Detecting Abusive Albanian'
title: 'Detecting Abusive Albanian',
},
url_path: 'dataset-4'
url_path: 'dataset-4',
},
]}
facets={['platform', 'language']}
/>
/>
```
>[!info]
> [!info]
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-catalog--docs)

View File

@ -1,5 +1,6 @@
---
title: Guides and Tutorials
description: Learn more about how you can achieve different data portal features with PortalJS
---
- [[howtos/analytics|How to add web analytics?]]

View File

@ -1,4 +1,7 @@
# How to add markdown-based content pages?
---
title: How to add markdown-based content pages?
description: Learn how to add markdown-based content pages on PortalJS data portals
---
## Add content layer to your app

View File

@ -1,4 +1,7 @@
# How to customize page metadata for SEO?
---
title: How to customize page metadata for SEO?
description: Learn to customize page metadata for SEO on data portals with PortalJS
---
>[!info]
>See [`next-seo` documentation](https://github.com/garmeeh/next-seo) to learn more.

View File

@ -1,4 +1,7 @@
# How to build a sitemap?
---
title: How to build a sitemap?
description: Learn how to build a sitemap for a data portal with PortalJS
---
## Setup

View File

@ -12,6 +12,7 @@ import { GetStaticProps, GetStaticPropsResult } from 'next';
import { CustomAppProps } from './_app.jsx';
import computeFields from '@/lib/computeFields';
import { getAuthorsDetails } from '@/lib/getAuthorsDetails';
import JSONLD from '@/components/JSONLD';
export default function Page({ source, meta, sidebarTree }) {
source = JSON.parse(source);
@ -29,14 +30,18 @@ export default function Page({ source, meta, sidebarTree }) {
}, [router.asPath]); // update table of contents on route change with next/link
return (
<Layout
tableOfContents={tableOfContents}
title={meta.title}
sidebarTree={sidebarTree}
urlPath={meta.urlPath}
>
<MDXPage source={source} frontMatter={meta} />
</Layout>
<>
<JSONLD meta={meta} source={source.compiledSource} />
<Layout
tableOfContents={tableOfContents}
title={meta.title}
description={meta.description}
sidebarTree={sidebarTree}
urlPath={meta.urlPath}
>
<MDXPage source={source} frontMatter={meta} />
</Layout>
</>
);
}

View File

@ -3,12 +3,15 @@ import computeFields from '@/lib/computeFields';
import clientPromise from '@/lib/mddb';
import { BlogsList, SimpleLayout } from '@portaljs/core';
import * as fs from 'fs';
import {NextSeo} from 'next-seo';
import { NextSeo } from 'next-seo';
export default function Blog({ blogs }) {
return (
<>
<NextSeo title="Blog posts" />
<NextSeo
title="Blog posts"
description="Find news and more information about rapidly building rich data portals using a modern frontend framework in the PortalJS blog"
/>
<Layout>
<SimpleLayout title="Blog posts">
<BlogsList blogs={blogs} />
@ -50,12 +53,9 @@ export async function getStaticProps() {
const blogList = await Promise.all(blogsWithComputedFields);
const blogsSorted = blogList.sort(
(a, b) =>
new Date(b?.date).getTime() -
new Date(a?.date).getTime()
(a, b) => new Date(b?.date).getTime() - new Date(a?.date).getTime()
);
return {
props: {
blogs: blogsSorted,

View File

@ -8,6 +8,7 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { collectHeadings } from '@portaljs/core';
import Head from 'next/head';
import { LogoJsonLd } from 'next-seo';
export default function Home({ sidebarTree }) {
const router = useRouter();
@ -24,6 +25,10 @@ export default function Home({ sidebarTree }) {
return (
<>
<LogoJsonLd
url="https://portaljs.org"
logo="https://portaljs.org/icon.png"
/>
<Layout
isHomePage={true}
tableOfContents={tableOfContents}