Dev how-to pages (#890)
Related to #878 ## Changes How-to docs pages: - /howto/index: home page for guides - /howto/analytics: "How to add Google Analytics?" - /howto/seo: "How to customize page metadata for SEO?" - /howto/sitemap: "How to build a sitemap?" - /howto/blog: "How to add a simple blog?" - blog index page - blog layout - comments -> link to the page below - /howto/comments: "How to add user comments?" - /howto/markdown: "How to add markdown-based content pages?" - /howto/drd: "How to create data-rich documents with charts and tables?"
This commit is contained in:
parent
adb6d1bb0e
commit
a62addbfbb
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,6 +16,7 @@ node_modules
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
.obsidian
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
|
||||
BIN
site/content/assets/howto/catalog-example.png
Normal file
BIN
site/content/assets/howto/catalog-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
site/content/assets/howto/catalog-facets-example.png
Normal file
BIN
site/content/assets/howto/catalog-facets-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
site/content/assets/howto/comments-example.png
Normal file
BIN
site/content/assets/howto/comments-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
BIN
site/content/assets/howto/linechart-example.png
Normal file
BIN
site/content/assets/howto/linechart-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
site/content/assets/howto/table-example.png
Normal file
BIN
site/content/assets/howto/table-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
77
site/content/howto/analytics.md
Normal file
77
site/content/howto/analytics.md
Normal file
@ -0,0 +1,77 @@
|
||||
# How to add Google Analytics?
|
||||
|
||||
>[!todo] Prerequisites
|
||||
>- [Google Analytics account](https://support.google.com/analytics/answer/9304153?hl=en)
|
||||
>- [Google tag ID](https://support.google.com/analytics/answer/12270356?hl=en#:~:text=A%20Measurement%20ID%20is%20an,same%20as%20your%20destination%20ID.)
|
||||
|
||||
The following piece of code adds pageview tracking functionality to the custom Next.js App in `/pages/_app.tsx`, but you can use it only in specific pages if you want.
|
||||
|
||||
```tsx
|
||||
// /pages/_app.tsx
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import Script from "next/script";
|
||||
|
||||
// Step 0:
|
||||
// Add your Google tag ID to your local .env.local file, e.g. under GA_TRACKING_ID
|
||||
|
||||
// Step 1: create the following pageview function
|
||||
const pageview = ({
|
||||
url,
|
||||
analyticsID,
|
||||
}: {
|
||||
url: string;
|
||||
analyticsID: string;
|
||||
}) => {
|
||||
if (typeof window.gtag !== undefined) {
|
||||
window.gtag("config", analyticsID, {
|
||||
page_path: url,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
const router = useRouter();
|
||||
|
||||
// ... your code
|
||||
|
||||
// Step 3: Add the following useEffect to trigger pageview on each route change
|
||||
useEffect(() => {
|
||||
const handleRouteChange = (url) => {
|
||||
pageview(url, GA_TRACKING_ID);
|
||||
};
|
||||
router.events.on("routeChangeComplete", handleRouteChange);
|
||||
return () => {
|
||||
router.events.off("routeChangeComplete", handleRouteChange);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
return (
|
||||
{/* Step 4: Install Google Analytics tag */}
|
||||
<>
|
||||
<Script
|
||||
strategy="afterInteractive"
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
|
||||
/>
|
||||
<Script
|
||||
id="gtag-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${GA_TRACKING_ID}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
>[!info]
|
||||
> For more info on measuring pageviews with Google Analytics see [Google Analytics documentation](https://developers.google.com/analytics/devguides/collection/gtagjs/pages).
|
||||
75
site/content/howto/blog.md
Normal file
75
site/content/howto/blog.md
Normal file
@ -0,0 +1,75 @@
|
||||
# How to add a simple blog?
|
||||
|
||||
## Setup
|
||||
|
||||
The following example uses components imported from the [`@flowershow/core` package](https://github.com/datopian/flowershow). If you want to follow along install it too:
|
||||
|
||||
```sh
|
||||
npm i @flowershow/core
|
||||
```
|
||||
|
||||
## Create home page for your blogs
|
||||
|
||||
Add the following code to the Next.js page that is going to be your blog home page, e.g. to `/pages/blog/index.tsx`:
|
||||
|
||||
```tsx
|
||||
import { BlogsList, SimpleLayout } from "@flowershow/core";
|
||||
|
||||
// pass a list of blogs, home page title and home page description, e.g. from `getStaticProps`
|
||||
export default function BlogIndex({ blogs, title, description }) {
|
||||
return (
|
||||
<SimpleLayout title={title} description={description}>
|
||||
<BlogsList blogs={blogs} />
|
||||
</SimpleLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`BlogsList` component has the following API:
|
||||
|
||||
```ts
|
||||
interface BlogsListProps {
|
||||
blogs: Blog;
|
||||
}
|
||||
|
||||
interface Blog {
|
||||
title: string;
|
||||
date: string;
|
||||
urlPath: string;
|
||||
description?: string;
|
||||
authors?: Array<string>;
|
||||
tags?: Array<string>;
|
||||
}
|
||||
```
|
||||
|
||||
## Create blog post pages
|
||||
|
||||
Add the following code to your blog pages, e.g. to `/pages/blog/[...slug].tsx`:
|
||||
|
||||
```tsx
|
||||
import { BlogLayout } from "@flowershow/core";
|
||||
|
||||
export default BlogPost({ content, title, date, authors }) {
|
||||
return (
|
||||
<BlogLayout title={title} date={date} authors={authors}
|
||||
{content}
|
||||
</BlogLayout>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`BlogLayout` component has the following API:
|
||||
|
||||
```ts
|
||||
interface BlogLayoutProps {
|
||||
title?: string;
|
||||
date?: string;
|
||||
authors?: Array<Author>;
|
||||
}
|
||||
|
||||
interface Author {
|
||||
name: string;
|
||||
avatar: string; // avatar image path
|
||||
urlPath?: string; // author page
|
||||
}
|
||||
```
|
||||
106
site/content/howto/comments.md
Normal file
106
site/content/howto/comments.md
Normal file
@ -0,0 +1,106 @@
|
||||
# How to add user comments?
|
||||
|
||||
![[comments-example.png]]
|
||||
|
||||
## Setup
|
||||
|
||||
Page comments can be setup with any one of the following supported providers:
|
||||
|
||||
1. [giscus](https://giscus.app/)
|
||||
2. [utterances](https://utteranc.es/)
|
||||
3. [disqus](https://disqus.com/)
|
||||
|
||||
Each provider has it's own configuration options that you should add to your `.env` file.
|
||||
|
||||
>[!Info]
|
||||
>If you are hosting your website on hosting providers like Netlify, Vercel or Cloudflare, you will also need to add the environment variables there.
|
||||
|
||||
### Giscus
|
||||
|
||||
[Giscus](https://giscus.app/) uses your github repo's discussions feature to store your comments and display them on your site.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
1. You have a public [github repository](https://docs.github.com/en/get-started/quickstart/create-a-repo) with the [discussions](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/enabling-or-disabling-github-discussions-for-a-repository) feature activated which will be used to store your comments.
|
||||
2. The [giscus app](https://github.com/apps/giscus) installed in your repo by following their configuration setup at [https://giscus.app/](https://giscus.app/)
|
||||
|
||||
Once the above steps are completed, head over to [https://giscus.app](https://giscus.app/) and follow the steps there by filling out the fields to get your config values.
|
||||
|
||||
>[!important]
|
||||
> Make sure to choose `pathname` under page discussions mapping.
|
||||
|
||||
After filling out the fields, you will be provided with a script tag that contains your config values. Add them to your `.env` file, like so:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_GISCUS_REPO=
|
||||
NEXT_PUBLIC_GISCUS_REPOSITORY_ID=
|
||||
NEXT_PUBLIC_GISCUS_CATEGORY=
|
||||
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
|
||||
```
|
||||
|
||||
When a user visits a web page, Giscus searches for a discussion whose title contains the page's pathname URL component. If there are no matches (i.e., no comments have been made about that page), then a new discussion is created for that page.
|
||||
|
||||
By using the pathname for mapping with a page, Giscus ensures that each discussion is unique to a particular page and that comments made on that page are kept together.
|
||||
|
||||
More info on how this connection is achieved can be found on their '**how it works**' section in their repo - [https://github.com/giscus/giscus#how-it-works](https://github.com/giscus/giscus#how-it-works) and also in the '**Page Discussions Mapping**' section at [https://giscus.app/](https://giscus.app/)
|
||||
|
||||
### Utterances
|
||||
|
||||
[Utterances](https://utteranc.es/) uses your github repo's issues to store comments for your pages and display them on your site. To setup comments with utterances, you would need to do the following:
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
1. You have a public [github repository](https://docs.github.com/en/get-started/quickstart/create-a-repo)
|
||||
2. The utterances app installed in your repo by following their configuration setup at [https://utteranc.es/](https://utteranc.es/)
|
||||
|
||||
Once installed fill in the required fields and you will see a script tag with your repo value which you can add to your `.env` file, like so:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_UTTERANCES_REPO=
|
||||
```
|
||||
|
||||
### Disqus
|
||||
|
||||
[Disqus](https://disqus.com/) is a feature rich provider which can be used to add comments to your site. You can configure flowershow to use disqus by creating an account on their site and following their process. You will be asked to enter a shortname for your site. Once completed, we can use this shortname in our comments configuration as below.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
1. Create an account on [Disqus](https://disqus.com/) and follow their setup process.
|
||||
|
||||
You will be asked to enter a shortname for your site. Add it to your `.env` file as well, like so:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_DISQUS_SHORTNAME=
|
||||
```
|
||||
|
||||
## Add comments to your page layout
|
||||
|
||||
You can use `@flowershow/core` to import the comments section component. Install it with:
|
||||
|
||||
```sh
|
||||
npm i @flowershow/core
|
||||
```
|
||||
|
||||
Then, add the following to your custom layout (or directly to your pages):
|
||||
|
||||
```tsx
|
||||
import Navbar from './navbar';
|
||||
import Footer from './footer';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main>{children}</main>
|
||||
<div>
|
||||
<Comments commentsConfig={commentsConfig} slug={urlPath} />
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Add user comments
|
||||
|
||||
Learn how to add support for user comments in [[comments|this guide]].
|
||||
334
site/content/howto/drd.md
Normal file
334
site/content/howto/drd.md
Normal file
@ -0,0 +1,334 @@
|
||||
# How to create data-rich documents with charts and tables?
|
||||
|
||||
> [!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.
|
||||
|
||||
PortalJS comes with a library of components that provides essential pieces for your data portal. The best way to explore the components is to look at our [Storybook](https://storybook.portaljs.org/) that contains all the details on how to use them. Below is an overview of available components.
|
||||
|
||||
You can install the library with:
|
||||
|
||||
```sh
|
||||
npm i @portaljs/components
|
||||
```
|
||||
|
||||
Now, in order to use these components in your markdown files, we need to pass them to our `MDXRemote` component (see [[markdown#Import, parse and render your markdown files|this section in our markdown guide]]):
|
||||
|
||||
```tsx
|
||||
// e.g. /blog/[[...slug]].tsx
|
||||
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";
|
||||
|
||||
const components = {
|
||||
Table: Table,
|
||||
Vega: Vega,
|
||||
VegaLite: VegaLite,
|
||||
LineChart: LineChart,
|
||||
};
|
||||
|
||||
|
||||
export default function Page({ source }) {
|
||||
source = JSON.parse(source);
|
||||
|
||||
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 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", {});
|
||||
|
||||
return {
|
||||
props: {
|
||||
source: JSON.stringify(mdxSource),
|
||||
// frontMatter
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
You can now use these components in your markdown, like so:
|
||||
|
||||
```md
|
||||
### My Data Rich Note
|
||||
|
||||
Some markdown here.
|
||||
|
||||
<Table csv={`
|
||||
Year,Temp Anomaly
|
||||
1850,-0.418
|
||||
2020,0.923
|
||||
`} />
|
||||
|
||||
Some markdown here.
|
||||
```
|
||||
|
||||
## PortalJS components
|
||||
|
||||
### Table
|
||||
|
||||
An easy-to-use table component with built-in pagination, search, and sorting. It can be used with raw data, raw CSV strings or with URLs to hosted CSV files.
|
||||
|
||||
![[table-example.png]]
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
<Table
|
||||
cols={[
|
||||
{
|
||||
key: 'id',
|
||||
name: 'ID'
|
||||
},
|
||||
{
|
||||
key: 'firstName',
|
||||
name: 'First name'
|
||||
},
|
||||
{
|
||||
key: 'lastName',
|
||||
name: 'Last name'
|
||||
},
|
||||
{
|
||||
key: 'age',
|
||||
name: 'Age'
|
||||
}
|
||||
]}
|
||||
data={[
|
||||
{
|
||||
age: 35,
|
||||
firstName: 'Jon',
|
||||
id: 1,
|
||||
lastName: 'Snow'
|
||||
},
|
||||
{
|
||||
age: 42,
|
||||
firstName: 'Cersei',
|
||||
id: 2,
|
||||
lastName: 'Lannister'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
>[!info]
|
||||
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-table--docs)
|
||||
|
||||
### Linechart
|
||||
|
||||
A simple component that allows the creation of an opinionated line chart without the need to go deep into charting standards. It can be used with raw data, raw CSV strings or with URLs to hosted CSV files.
|
||||
|
||||
![[linechart-example.png]]
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
<LineChart
|
||||
data={[
|
||||
[
|
||||
'1850',
|
||||
-0.41765878
|
||||
],
|
||||
[
|
||||
'1851',
|
||||
-0.2333498
|
||||
],
|
||||
[
|
||||
'1852',
|
||||
-0.22939907
|
||||
],
|
||||
[
|
||||
'1853',
|
||||
-0.27035445
|
||||
],
|
||||
[
|
||||
'1854',
|
||||
-0.29163003
|
||||
]
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
>[!info]
|
||||
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-linechart--docs)
|
||||
|
||||
### Vega chart
|
||||
|
||||
A wrapper around the [Vega specification](https://vega.github.io/vega/) that allows you to build pretty much any kind of chart imaginable.
|
||||
|
||||
Example usage:
|
||||

|
||||
|
||||
```js
|
||||
<Vega
|
||||
data={{
|
||||
table: [
|
||||
{
|
||||
x: 1850,
|
||||
y: -0.418
|
||||
},
|
||||
{
|
||||
x: 2020,
|
||||
y: 0.923
|
||||
}
|
||||
]
|
||||
}}
|
||||
spec={{
|
||||
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
|
||||
data: {
|
||||
name: 'table'
|
||||
},
|
||||
encoding: {
|
||||
x: {
|
||||
field: 'x',
|
||||
type: 'ordinal'
|
||||
},
|
||||
y: {
|
||||
field: 'y',
|
||||
type: 'quantitative'
|
||||
}
|
||||
},
|
||||
mark: 'bar'
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
> [!info]
|
||||
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-vega--docs)
|
||||
|
||||
### VegaLite chart
|
||||
|
||||
A wrapper around the [Vega Lite specification](https://vega.github.io/vega-lite/) which allows for a more concise grammar than Vega around the building of charts.
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
<VegaLite
|
||||
data={{
|
||||
table: [
|
||||
{
|
||||
x: 1850,
|
||||
y: -0.418
|
||||
},
|
||||
{
|
||||
x: 2020,
|
||||
y: 0.923
|
||||
}
|
||||
]
|
||||
}}
|
||||
spec={{
|
||||
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
|
||||
data: {
|
||||
name: 'table'
|
||||
},
|
||||
encoding: {
|
||||
x: {
|
||||
field: 'x',
|
||||
type: 'ordinal'
|
||||
},
|
||||
y: {
|
||||
field: 'y',
|
||||
type: 'quantitative'
|
||||
}
|
||||
},
|
||||
mark: 'bar'
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
>[!info]
|
||||
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-vegalite--docs)
|
||||
|
||||
### Catalog
|
||||
|
||||
A searchable catalog that will index a list of datasets and allow for contextual searching + filters.
|
||||
|
||||
![[catalog-example.png]]
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
<Catalog
|
||||
datasets={[
|
||||
{
|
||||
_id: '07026b22d49916754df1dc8ffb9ccd1c31878aae',
|
||||
metadata: {
|
||||
'details-of-task': 'Detect and categorise abusive language in social media data',
|
||||
language: 'Albanian',
|
||||
'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'
|
||||
],
|
||||
'percentage-abusive': 13.2,
|
||||
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'
|
||||
},
|
||||
url_path: 'dataset-4'
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
You can also add facets that are going to act as filters for your metadata.
|
||||
|
||||
![[catalog-facets-example.png]]
|
||||
|
||||
```js
|
||||
<Catalog
|
||||
datasets={[
|
||||
{
|
||||
_id: '07026b22d49916754df1dc8ffb9ccd1c31878aae',
|
||||
metadata: {
|
||||
'details-of-task': 'Detect and categorise abusive language in social media data',
|
||||
language: 'Albanian',
|
||||
'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'
|
||||
],
|
||||
'percentage-abusive': 13.2,
|
||||
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'
|
||||
},
|
||||
url_path: 'dataset-4'
|
||||
},
|
||||
]}
|
||||
facets={['platform', 'language']}
|
||||
/>
|
||||
```
|
||||
|
||||
>[!info]
|
||||
> More info on the [storybook page](https://storybook.portaljs.org/?path=/docs/components-catalog--docs)
|
||||
12
site/content/howto/index.md
Normal file
12
site/content/howto/index.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Guides and tutorials
|
||||
|
||||
- [[howto/analytics|How to add web analytics?]]
|
||||
- [[howto/seo|How to customize page metadata for SEO?]]
|
||||
- [[howto/sitemap|How to build a sitemap?]]
|
||||
- [[howto/markdown|How to add markdown-based content pages?]]
|
||||
- [[howto/blog|How to add a simple blog?]]
|
||||
- [[howto/drd|How to create data-rich documents with charts and tables?]]
|
||||
- [[howto/comments|How to add user comments?]]
|
||||
|
||||
If you have questions about anything related to PortalJS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portaljs/discussions) or on [our chat channel on Discord](https://discord.gg/EeyfGrGu4U).
|
||||
|
||||
169
site/content/howto/markdown.md
Normal file
169
site/content/howto/markdown.md
Normal file
@ -0,0 +1,169 @@
|
||||
# How to add markdown-based content pages?
|
||||
|
||||
## Add content layer to your app
|
||||
|
||||
Create a folder where you'll keep your markdown files and add some markdown files to it.
|
||||
|
||||
```sh
|
||||
cd my-portaljs-project
|
||||
mkdir content
|
||||
# touch content/index.md ...
|
||||
```
|
||||
|
||||
Install [MarkdownDB](https://github.com/datopian/markdowndb) package:
|
||||
|
||||
```
|
||||
npm i @flowershow/markdowndb
|
||||
```
|
||||
|
||||
And add the following to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"mddb": "mddb <path-to-your-content-folder>",
|
||||
"prebuild": "npm run mddb"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
You can give it a go by running `npm run mddb`. You should see a `markdown.db` file created in the root of your project. You can inspect it with any SQLite viewer or in the command line. In the `files` table you should see all your markdown files from your content folder.
|
||||
|
||||
Now, once the data is in the database, you can add the following script to your project (e.g. in `/lib` folder). It will allow you to establish a single connection to the database and use it across your app.
|
||||
|
||||
```ts
|
||||
// lib/mddb.ts
|
||||
import { MarkdownDB } from "@flowershow/markdowndb";
|
||||
|
||||
// path to the markdown.db file created by the mddb script
|
||||
const dbPath = "markdown.db";
|
||||
|
||||
const client = new MarkdownDB({
|
||||
client: "sqlite3",
|
||||
connection: {
|
||||
filename: dbPath,
|
||||
},
|
||||
});
|
||||
|
||||
const clientPromise = client.init();
|
||||
|
||||
export default clientPromise;
|
||||
```
|
||||
|
||||
Now you can import it across your project to query the database, e.g.:
|
||||
|
||||
```ts
|
||||
import clientPromise from "@/lib/mddb";
|
||||
|
||||
const mddb = await clientPromise;
|
||||
const blogs = await mddb.getFiles({
|
||||
folder: "blog",
|
||||
extensions: ["md", "mdx"],
|
||||
});
|
||||
```
|
||||
|
||||
## Write a markdown parser
|
||||
|
||||
Install [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) package, which we'll first use to parse markdown files and then to render them in Next.js app.
|
||||
|
||||
```sh
|
||||
npm i next-mdx-remote
|
||||
```
|
||||
|
||||
Create the following basic parser for your markdown files, e.g. in `/lib/markdown.ts`:
|
||||
|
||||
```ts
|
||||
import matter from "gray-matter";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
|
||||
|
||||
const parse = async function (source) {
|
||||
const { content } = matter(source);
|
||||
|
||||
const mdxSource = await serialize(
|
||||
{ value: content },
|
||||
{
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
// ... your remark plugins
|
||||
],
|
||||
rehypePlugins: [
|
||||
// ... your plugins
|
||||
],
|
||||
format,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
mdxSource
|
||||
};
|
||||
};
|
||||
|
||||
export default parse;
|
||||
|
||||
```
|
||||
|
||||
## Import, parse and render your markdown files
|
||||
|
||||
Create a page in the `/pages` folder that will render your markdown content, e.g. `pages/blog/[[...slug]].tsx`:
|
||||
|
||||
```tsx
|
||||
import fs from "fs";
|
||||
|
||||
import { MdxRemote } from "next-mdx-remote";
|
||||
import clientPromise from "@/lib/mddb.mjs";
|
||||
import parse from "@/lib/markdown";
|
||||
|
||||
|
||||
export default function Page({ source }) {
|
||||
source = JSON.parse(source);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MdxRemote source={source} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 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", {});
|
||||
|
||||
return {
|
||||
props: {
|
||||
source: JSON.stringify(mdxSource),
|
||||
// frontMatter
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// Import metadata of your markdown files from MarkdownDB and return a list of static paths
|
||||
export const getStaticPaths = async () => {
|
||||
const mddb = await clientPromise;
|
||||
const allDocuments = await mddb.getFiles({ extensions: ["md", "mdx"] });
|
||||
const paths = allDocuments
|
||||
.map((page) => {
|
||||
const url = decodeURI(page.url_path);
|
||||
const parts = url.split("/");
|
||||
return { params: { slug: parts } };
|
||||
});
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
79
site/content/howto/seo.md
Normal file
79
site/content/howto/seo.md
Normal file
@ -0,0 +1,79 @@
|
||||
# How to customize page metadata for SEO?
|
||||
|
||||
>[!info]
|
||||
>See [`next-seo` documentation](https://github.com/garmeeh/next-seo) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Install `next-seo` package:
|
||||
|
||||
```sh
|
||||
npm i next-seo
|
||||
```
|
||||
|
||||
## Default SEO configuration
|
||||
|
||||
Create `next-seo.config` file (e.g. in the root of your project) and add default meta tags values you want to be set up for your pages. For example:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_IE',
|
||||
url: 'https://www.my.portaljs.app/',
|
||||
siteName: 'MyPortalJSApp',
|
||||
},
|
||||
twitter: {
|
||||
handle: '@handle',
|
||||
site: '@site',
|
||||
cardType: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Then, in your custom Next.js App, add the following:
|
||||
|
||||
```tsx
|
||||
// /pages/_app.tsx
|
||||
import { DefaultSeo } from "next-seo";
|
||||
|
||||
// import your default seo configuration
|
||||
import SEO from '../next-seo.config';
|
||||
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
// ... your code
|
||||
return (
|
||||
<>
|
||||
<DefaultSeo {...SEO} />
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
>[!info]
|
||||
>To learn more on default SEO configuration with `next-seo`, see [this docs section](https://github.com/garmeeh/next-seo#default-seo-configuration).
|
||||
|
||||
## Per-page SEO configuration
|
||||
|
||||
To add page specific meta tag values:
|
||||
|
||||
```tsx
|
||||
import { NextSeo } from 'next-seo';
|
||||
|
||||
export default function Page() (
|
||||
// ...
|
||||
<>
|
||||
<NextSeo
|
||||
title="My Portal JS page"
|
||||
description="A short description."
|
||||
/>
|
||||
<p>My Portal JS page</p>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
>[!info]
|
||||
> To learn more on per-page SEO configuration with `next-seo`, see [this docs section](https://github.com/garmeeh/next-seo#add-seo-to-page)
|
||||
94
site/content/howto/sitemap.md
Normal file
94
site/content/howto/sitemap.md
Normal file
@ -0,0 +1,94 @@
|
||||
# How to build a sitemap?
|
||||
|
||||
## Setup
|
||||
|
||||
Install the following packages:
|
||||
|
||||
```sh
|
||||
npm i globby
|
||||
npm i -D prettier
|
||||
```
|
||||
|
||||
## Add sitemap script
|
||||
|
||||
>[!info]
|
||||
>The following example assumes files list for dynamic Next.js routes is imported from the database created with [MarkdownDB](https://github.com/datopian/markdowndb) package. You need to adjust it to your specific use case.
|
||||
|
||||
Add the following script to e.g. `/scripts/sitemap.mjs` and adjust it to work with your app:
|
||||
|
||||
```js
|
||||
// /scripts/sitemap.mjs
|
||||
import { writeFileSync } from "fs";
|
||||
import { globby } from "globby";
|
||||
import prettier from "prettier";
|
||||
|
||||
// Change this to import data from your content layer (if needed)
|
||||
import clientPromise from "../lib/mddb.mjs";
|
||||
|
||||
export default async function sitemap() {
|
||||
const prettierConfig = await prettier.resolveConfig("path-to-your-prettier-config");
|
||||
|
||||
// Change this to create a list of page paths
|
||||
const mddb = await clientPromise;
|
||||
const allFiles = await mddb.getFiles({ extensions: ["mdx", "md"] });
|
||||
const contentPages = allFiles
|
||||
.filter((x) => !x.metadata?.isDraft)
|
||||
.map((x) => `/${x.url_path}`);
|
||||
const pages = await globby([
|
||||
"pages/*.(js|tsx)",
|
||||
"!pages/_*.(js|tsx)",
|
||||
"!pages/api",
|
||||
"!pages/404.(js|tsx)",
|
||||
"!pages/**/\\[\\[*\\]\\].(js|tsx)", // pages/[[...slug]].tsx
|
||||
]);
|
||||
const allPages = pages.concat(contentPages);
|
||||
// end
|
||||
|
||||
// Replace this with your site domain
|
||||
const siteDomain = "your-site-domain";
|
||||
|
||||
const sitemap = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${allPages
|
||||
.map((page) => {
|
||||
const path = page
|
||||
.replace("pages/", "/")
|
||||
.replace("public/", "/")
|
||||
.replace(/\.js|.tsx|.mdx|.md[^\/.]+$/, "")
|
||||
.replace("/feed.xml", "");
|
||||
const route = path === "/index" ? "" : path;
|
||||
return `
|
||||
<url>
|
||||
<loc>${siteDomain}${route}</loc>
|
||||
</url>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</urlset>
|
||||
`;
|
||||
|
||||
const formatted = prettier.format(sitemap, {
|
||||
...prettierConfig,
|
||||
parser: "html",
|
||||
});
|
||||
|
||||
if (siteUrl) {
|
||||
writeFileSync("public/sitemap.xml", formatted);
|
||||
console.log("Sitemap generated...");
|
||||
}
|
||||
}
|
||||
|
||||
await sitemap();
|
||||
process.exit();
|
||||
```
|
||||
|
||||
## Execute script before each build
|
||||
|
||||
Add this to the scripts section in your `package.json` to regenerate the sitemap before each build.
|
||||
|
||||
```json
|
||||
|
||||
"prebuild": "node ./scripts/sitemap.mjs",
|
||||
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user