Skip to content

Commit

Permalink
feat: support sub-page blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-guoba committed Feb 22, 2024
1 parent fdd8afd commit d15164d
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 73 deletions.
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,32 @@ Blog build on [Next.js](https://nextjs.org/) and [Notion Public API](https://www
3. Full support for dark mode
4. Support [Static Site Generation](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation)

## Setup
## ✨ Getting Started

1. Fork / clone this repo
2. Duplicate [this Notion template](https://gelco.notion.site/910de43a0db24ebc9a34209ffab613a7?v=7f2614c5918f4a5bab3a6637b12a19f9&pvs=4), and edit your blog content.
3. follow Notions [getting started guide](https://developers.notion.com/docs/getting-started) to get a NOTION_TOKEN and a NOTION_DATABASE_ID, then add them to a file called .env.local.
### Prerequisites

```
NOTION_TOKEN=
NOTION_DATABASE_ID=
```

4. install the dependences:
1. Duplicate [this Notion template](https://gelco.notion.site/910de43a0db24ebc9a34209ffab613a7?v=7f2614c5918f4a5bab3a6637b12a19f9&pvs=4), and edit your blog.
2. Follow Notions [getting started guide](https://developers.notion.com/docs/getting-started) to get a `NOTION_TOKEN` and a `NOTION_DATABASE_ID`.

### Development
1. Setup: Star and Fork the repository
2. install the dependences:
```bash
npm install
```

5. run locally
3. Set up you `env.local` with `NOTION_TOKEN` and `NOTION_DATABASE_ID`.
```
NOTION_TOKEN=
NOTION_DATABASE_ID=
```
4. run locally
```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.


## Supported Blocks

Most common block types are supported. But some blocks information not supported in Public API, So we give an abbreviated rendering.
Expand All @@ -49,15 +51,15 @@ Most common block types are supported. But some blocks information not supported
| Bulleted List | ✅ Yes | |
| Callout | ✅ Yes | |
| Child Databases | ❌ Missing | Not planned. |
| Child page | ❌ Missing | |
| Child page | ✅ Yes | |
| Code | ✅ Yes | Use [prismjs](https://prismjs.com/) |
| Column list and column | ✅ Yes | |
| Divider | ❌ Missing | |
| Embed | ❌ Missing | |
| Equation | ✅ Yes | Use [katex ](https://katex.org/) |
| File | ✅ Yes | |
| Image | ✅ Yes | No position、size info in API |
| Link Preview | ❌ Missing | |
| Link Preview | ✅ Yes | |
| Mention | ✅ Yes | Only Date |
| Numbered List | ✅ Yes | |
| PDF | ✅ Yes | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@ import { Fragment } from "react";
import { Metadata, ResolvingMetadata } from "next";

import { renderBlock } from "@/app/notion/render";
import { QueryDatabase, getPageFromSlug, getBlocks } from "@/app/api/notion";
import {
QueryDatabase,
queryPageBySlug,
retrieveBlockChildren,
retrievePage,
} from "@/app/api/notion";
import Shell from "@/components/shells/shell";
import React from "react";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeftIcon } from "@radix-ui/react-icons";
import { formatDate } from "@/lib/utils";
import { PageHeader, PageHeaderDescription, PageHeaderHeading } from "@/components/page-header";
import {
PageHeader,
PageHeaderDescription,
PageHeaderHeading,
} from "@/components/page-header";
import { Separator } from "@/components/ui/separator";

// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate
Expand All @@ -25,7 +34,7 @@ export const revalidate =
export async function generateStaticParams() {
const database = await QueryDatabase();
return database.map((page: any) => {
const slug = page.properties.Slug?.rich_text[0].plain_text;
const slug = [page.properties.Slug?.rich_text[0].plain_text];
return { slug };
});
}
Expand All @@ -47,23 +56,46 @@ export async function generateMetadata(
};
}

export default async function Page({ params }: { params: { slug: string } }) {
export default async function Page({ params }: { params: { slug: string[] } }) {
console.log(params, "environment variables:", revalidate);
const page: any = await getPageFromSlug(params.slug);
const blocks = page && (await getBlocks(page.id));
console.log(params, "page params:", params);

if (!page || !blocks) {
return <div />;
let pageID, lastEditTime, title;

// retrieve page meta info by page ID
if (params.slug.length > 1) {
pageID = params.slug[1];

const page: any = await retrievePage(pageID);
if (!page) {
return <div />;
}
lastEditTime = page?.last_edited_time;
title = page?.properties?.title?.title[0].plain_text;
} else {
// query from database by slug
const page: any = await queryPageBySlug(params.slug[0]);
if (!page) {
return <div />;
}
pageID = page.id;
title = page.properties?.Title.title[0].plain_text;
lastEditTime = page.last_edited_time;
}

const title = page.properties?.Title.title[0].text.content;
const blocks = await retrieveBlockChildren(pageID);
if (!blocks) {
return <div />;
}

return (
<Shell as="article" className="relative flex min-h-screen flex-col">
<PageHeader>
<PageHeaderDescription variant="sm">{formatDate(page.last_edited_time)}</PageHeaderDescription>
<PageHeaderDescription variant="sm">
{formatDate(lastEditTime)}
</PageHeaderDescription>
<PageHeaderHeading>{title}</PageHeaderHeading>
</PageHeader>
</PageHeader>
<Separator className="mb-2.5" />

<section className="w-full">
Expand Down
25 changes: 13 additions & 12 deletions app/api/notion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@ export const QueryDatabase = async (): Promise<TypePostList> => {
//
// 读取一个page的基础数据(page object)
// API: https://developers.notion.com/reference/retrieve-a-page
export const getPage = async (pageId: any) => {
export const retrievePage = cache(async (pageId: any) => {
const start = new Date().getTime();
const response = await notion.pages.retrieve({ page_id: pageId });
const end = new Date().getTime();
console.log('[getPage]', `${end - start}ms`);
return response;
};
});

//
// slug:(计算机)处理后的标题(用于构建固定链接)
// 根据标题取db中的page列表数据,限制一条
export const getPageFromSlug = async (slug: string) => {
export const queryPageBySlug = cache(async (slug: string) => {
const start = new Date().getTime();

const response = await notion.databases.query({
Expand All @@ -71,36 +71,37 @@ export const getPageFromSlug = async (slug: string) => {
if (response?.results?.length) {
return response?.results?.[0];
}
};
});

//
// 取page/block的children列表数据 (blocks)
export const getBlocks = cache(async (blockID: string): Promise<any> => {
// Returns a paginated array of child block objects contained in the block using the ID specified.
// see: https://developers.notion.com/reference/get-block-children
export const retrieveBlockChildren = cache(async (blockID: string): Promise<any> => {
const start = new Date().getTime();

const blockId = blockID.replaceAll('-', '');

// 只读取了100条,超过100时没有取完
// TODO: only 100, not finished.
const { results } = await notion.blocks.children.list({
block_id: blockId,
page_size: 100,
});

// Fetches all child blocks recursively
// be mindful of rate limits if you have large amounts of nested blocks
// be mindful of rate limits if you have large amounts of nested blocks !!
// See https://developers.notion.com/docs/working-with-page-content#reading-nested-blocks
// 递归取child blocks,嵌套block量大时会有风险。
const childBlocks = results.map(async (block: any) => {
if (block.has_children) {
const children = await getBlocks(block.id);
// ignore child pages
if (block.has_children && block.type != 'child_page') {
const children = await retrieveBlockChildren(block.id);
return { ...block, children };
}

return block;
});

const end = new Date().getTime();
console.log('[getBlocks]', `${end - start}ms`);
console.log('[retrieveBlockChildren]', `${end - start}ms`);

return Promise.all(childBlocks).then((blocks) => blocks.reduce((acc, curr) => {
// special conversation for list(bullet、number):convert to children array and add uniqed IDs inn each item
Expand Down
2 changes: 1 addition & 1 deletion app/notion/_components/bookmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function BookmarkRender({ block, className }: BookmarkBlockProps) {

return (
<div className={cn(className, "inline-flex w-full items-center flex-wrap")} title="bookmark">
<Icons.bookmark className="h-6 w-6 p-[0.5]"/>
<Icons.bookmark className="h-6 w-6 p-[0.8] text-gray-600"/>
<a
id={id}
href={url}
Expand Down
2 changes: 1 addition & 1 deletion app/notion/_components/file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function FileRender({ block, className }: FileBlockProps) {

return (
<div className={cn(className, "inline-flex w-full items-center flex-wrap")}>
<Icons.fileblock className="h-6 w-6 p-[0.5]" />
<Icons.fileblock className="h-6 w-6 p-[0.8] text-gray-600" />
<a
id={id}
href={url}
Expand Down
2 changes: 1 addition & 1 deletion app/notion/_components/link_preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function LinkPreviewRender({ block, className }: LinkPreviewBlockProps) {

return (
<div className={cn(className, "inline-flex w-full items-center flex-wrap")} title="link preview">
<Icons.linkpreview className="h-6 w-6 p-[0.5]"/>
<Icons.linkpreview className="h-6 w-6 p-[0.8] text-gray-600"/>
<a
id={id}
href={url}
Expand Down
4 changes: 2 additions & 2 deletions app/notion/_components/pdf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function PDFRender({ block, className }: PDFBlockProps) {
}

return (
<div key={id} className={cn(className, "w-full max-w-ful")}>
<div key={id} className={cn(className, "w-full max-w-ful min-w-full")}>
<Nav pageNumber={pageNumber} numPages={numPages} title={title} />
<div
hidden={loading}
Expand Down Expand Up @@ -112,7 +112,7 @@ export function PDFRender({ block, className }: PDFBlockProps) {
renderTextLayer={false}
onLoadSuccess={onPageLoadSuccess}
onRenderError={() => setLoading(false)}
width={Math.max(pageWidth * 0.75, 320)}
// width={Math.max(pageWidth * 0.75, 320)}
/>
</Document>
</div>
Expand Down
33 changes: 33 additions & 0 deletions app/notion/_components/sub_page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { cn } from "@/lib/utils";
import RichText from "../text";
import { Icons } from "@/components/icons";
import Link from "next/link";


interface SubPageProps {
block: any;
className?: string | undefined;
}

export function SubPageRender({ block, className }: SubPageProps) {
const {
id,
child_page: { title },
} = block;

// Subpage not in blog list. page ID shouldn't be empty
const slug = `/article/${title}/${id}`

return (
<div className={cn(className, "inline-flex w-full items-center flex-wrap")}>
<Icons.textFile className="h-6 w-6 p-[0.8] text-gray-400" />
<Link
key={id}
href={slug}
className="whitespace-pre-wrap break-words font-semibold truncate underline decoration-slate-300"
>
{title}
</Link>
</div>
);
}
39 changes: 11 additions & 28 deletions app/notion/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TableRender } from './_components/table';
import { EquationRender } from './_components/equation';
import { LinkPreviewRender } from './_components/link_preview';
import { PDFRender } from './_components/pdf';
import { SubPageRender } from './_components/sub_page';


export function renderBlock(block: any, level: number = 1) {
Expand Down Expand Up @@ -105,12 +106,14 @@ export function renderBlock(block: any, level: number = 1) {
);

case 'child_page':
return (
<div className={styles.childPage}>
<strong>{value?.title}</strong>
{block.children.map((child: any) => renderBlock(child, level + 1))}
</div>
);
return <SubPageRender block={block} className='mt-1.5'></SubPageRender>

// return (
// <div className={styles.childPage}>
// <strong>{value?.title}</strong>
// {block.children.map((child: any) => renderBlock(child, level + 1))}
// </div>
// );

case 'image':
return <ImageRender block={block} className='mt-1.5'></ImageRender>
Expand Down Expand Up @@ -140,29 +143,9 @@ export function renderBlock(block: any, level: number = 1) {
case 'link_preview':
return <LinkPreviewRender block={block} className='mt-1.5'></LinkPreviewRender>;

case 'table': {
case 'table':
return <TableRender block={block} className='mt-1.5'></TableRender>;

// return (
// <table className={styles.table}>
// <tbody>
// {block.children?.map((child: any, index: number) => {
// const RowElement = value.has_column_header && index === 0 ? 'th' : 'td';
// return (
// <tr key={child.id}>
// {child.table_row?.cells?.map((cell: { plain_text: any; }, i: any) => (
// // eslint-disable-next-line react/no-array-index-key
// <RowElement key={`${cell.plain_text}-${i}`}>
// <RichText title={cell} />
// </RowElement>
// ))}
// </tr>
// );
// })}
// </tbody>
// </table>
// );
}

case 'column_list':
return <ColumnListRender block={block} className="mt-1.5" level={level+1}></ColumnListRender>

Expand Down
Loading

0 comments on commit d15164d

Please sign in to comment.