Skip to content

Commit

Permalink
feat: add type, pubdate, status field for blog database; add filter &…
Browse files Browse the repository at this point in the history
… sort function for query database
  • Loading branch information
alex-guoba committed Mar 14, 2024
1 parent d82ca7e commit bcdf657
Show file tree
Hide file tree
Showing 19 changed files with 283 additions and 73 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Next-Blogger built on [Next.js 14+](https://nextjs.org/) and [Tailwind CSS](http
2. Utilizes the [Notion Public API](https://developers.notion.com/).
3. Supports caching Notion data using Prisma to reduce API calls and improve overall performance.
4. Includes a dark mode option.
5. Supports [Static Site Generation](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation).
5. SEO friendly with RSS feed, sitemaps and more!
6. Includes load testing scripts, see [load-testing](./scripts/load-testing/).
7. SEO friendly with RSS feed, sitemaps and more!
7. Supports [Server Rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default) and [Dynamic Rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default).

## Tech Stack

Expand Down
33 changes: 15 additions & 18 deletions app/(blog)/article/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// import Head from "next/head";
import Link from "next/link";
// import { Fragment } from "react";
import { Metadata } from "next";

import { RenderBlock } from "@/app/notion/render";
Expand All @@ -14,22 +12,23 @@ import { formatDate } from "@/lib/utils";
import { PageHeader, PageHeaderDescription, PageHeaderHeading } from "@/components/page-header";
import { Separator } from "@/components/ui/separator";

// import { env } from "@/env.mjs";
import { siteMeta } from "@/config/meta";
import { rawText } from "@/app/notion/block-parse";
import { pagePublished, rawText } from "@/app/notion/block-parse";
import { notFound } from "next/navigation";

// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate
export const revalidate = parseInt(process.env.NEXT_REVALIDATE_PAGES || "", 10) || 300; // revalidate the data interval
// export const dynamic = 'force-dynamic';
// export const revalidate = 0;

// export const dynamicParams = true; // true | false,

// Return a list of `params` to populate the [slug] dynamic segment
// export const revalidate = parseInt(process.env.NEXT_REVALIDATE_PAGES || "", 10) || 60; // revalidate the data interval
// // export const dynamic = 'force-dynamic';
// // export const revalidate = 0;
// // export const dynamicParams = true; // true | false,

/**
* If you want to enable static rendering, uncommment the following function
*/
// export async function generateStaticParams() {
// const database = await QueryDatabase(env.NOTION_DATABASE_ID);
// return database.map((page: any) => {
// const slug = [page.properties.Slug?.rich_text[0].plain_text];
// const slug = [page.id];
// return { slug };
// });
// }
Expand All @@ -40,9 +39,7 @@ type Props = {
};

// Generate metadata for this page.
// TODO: add more fields and site name
export async function generateMetadata({ params }: Props): Promise<Metadata> {
// read route params
const pageInfo = await parseSlug(params.slug);

return {
Expand All @@ -69,11 +66,11 @@ async function parseSlug(slug: string[]) {
pageID = slug[0];

const page: any = await retrievePage(pageID);
if (page) {
lastEditTime = page?.last_edited_time;
if (page && pagePublished(page)) {
summary = rawText(page?.properties?.Summary?.rich_text);
// May be linked from child-page not in database list which still have a default title property
title = rawText(page?.properties?.Title?.title || page?.properties?.title?.title);
lastEditTime = page?.properties?.PublishDate?.date?.start || page?.last_edited_time;
}
}
return { pageID, lastEditTime, title, summary };
Expand All @@ -83,8 +80,8 @@ export default async function Page({ params }: { params: { slug: string[] } }) {
// retrieve page meta info by page ID
const { pageID, lastEditTime, title } = await parseSlug(params.slug);
if (!pageID || !title) {
console.log("empty page", pageID, title);
return <div />;
console.log("Post not found or unpublished", pageID, title);
return notFound();
}

const blocks = await retrieveBlockChildren(pageID);
Expand Down
4 changes: 3 additions & 1 deletion app/(blog)/db/[...row]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { QueryDatabase, RetrieveDatabase } from "@/app/notion/api";
import { DatabaseRenderer } from "@/app/notion/_components/db/database";
import { IconRender } from "@/app/notion/_components/emoji";
import RichText from "@/app/notion/text";
import { filterBase } from "@/app/notion/block-parse";

export default async function Page({ params }: { params: { row: string[] } }) {
const dbID = params.row[0];
Expand All @@ -17,7 +18,8 @@ export default async function Page({ params }: { params: { row: string[] } }) {
return null;
}

const data = await QueryDatabase(dbID);
const queryParam = filterBase(dbID);
const data = await QueryDatabase(dbID, queryParam);

const { icon, title, description } = columns;
return (
Expand Down
6 changes: 6 additions & 0 deletions app/(blog)/page/[...slug]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// import Script from "next/script"
import React from "react";

export default async function LobyLayout({ children }: { children: React.ReactNode }) {
return <div className="relative flex min-h-screen flex-col">{children}</div>;
}
136 changes: 136 additions & 0 deletions app/(blog)/page/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import Link from "next/link";
import { Metadata } from "next";

import { RenderBlock } from "@/app/notion/render";
import { QueryDatabase, retrieveBlockChildren } from "@/app/notion/api";
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 { PageHeader, PageHeaderDescription, PageHeaderHeading } from "@/components/page-header";
import { Separator } from "@/components/ui/separator";

import { siteMeta } from "@/config/meta";
import { filterBase, filterSelect, filterText, rawText } from "@/app/notion/block-parse";
import { notFound } from "next/navigation";
import { env } from "@/env.mjs";

// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate
// export const revalidate = parseInt(process.env.NEXT_REVALIDATE_PAGES || "", 10) || 60; // revalidate the data interval
// // export const dynamic = 'force-dynamic';
// // export const revalidate = 0;
// // export const dynamicParams = true; // true | false,
/**
* If you want to enable static rendering, uncommment the following function
*/
// export async function generateStaticParams() {
// const database = await QueryDatabase(env.NOTION_DATABASE_ID);
// return database.map((page: any) => {
// const slug = [page.id];
// return { slug };
// });
// }

type Props = {
params: { slug: string[] };
searchParams: { [key: string]: string | string[] | undefined };
};

// Generate metadata for this page.
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const pageInfo = await filterPageBySlug(params.slug);

return {
title: pageInfo.title,
description: pageInfo.summary,

openGraph: {
description: pageInfo.summary,
type: "article",
url: siteMeta.siteUrl + "/" + params.slug.join("/"),
// image: pageInfo.cover,
},
twitter: {
card: "summary_large_image",
title: pageInfo.title,
description: pageInfo.summary,
},
};
}

function slugParam(slug: string) {
const defaultParam = filterBase(env.NOTION_DATABASE_ID);
const filters = {
filter: {
and: [
filterSelect("Status", "Published").filter,
filterSelect("Type", "Page").filter,
filterText("Slug", slug).filter,
],
},
};

return { ...defaultParam, ...filters };
}

async function filterPageBySlug(slug: string[]) {
const info = {
pageID: "",
lastEditTime: "",
title: "",
summary: "",
};

if (slug.length >= 1) {
const params = slugParam(slug[0]);

const posts = await QueryDatabase(env.NOTION_DATABASE_ID, params);
if (posts.length == 0) {
return info;
}
const post: any = posts[0];

info.pageID = post?.id;
info.summary = rawText(post.properties?.Summary?.rich_text);
info.title = rawText(post?.properties?.Title?.title || post?.properties?.title?.title);
info.lastEditTime = post?.properties?.PublishDate?.date?.start || post?.last_edited_time;
}
return info;
}

export default async function Page({ params }: { params: { slug: string[] } }) {
// retrieve page meta info by page ID
const { pageID, title, summary } = await filterPageBySlug(params.slug);
if (!pageID || !title) {
console.log("page not found or unpublished", pageID, title);
return notFound();
}

const blocks = await retrieveBlockChildren(pageID);
if (!blocks) {
return <div />;
}

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

<section className="flex w-full flex-col gap-y-0.5">
{blocks.map((block: any) => (
<RenderBlock key={block.id} block={block}></RenderBlock>
))}
</section>

<Link href="/" className={cn(buttonVariants({ variant: "ghost", className: "mx-auto mt-4 w-fit" }))}>
<ChevronLeftIcon className="mr-2 h-4 w-4" aria-hidden="true" />
See all posts
<span className="sr-only">See all posts</span>
</Link>
</Shell>
);
}
19 changes: 16 additions & 3 deletions app/(lobby)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@ import { PageHeader, PageHeaderHeading, PageHeaderDescription } from "@/componen
import { Separator } from "@/components/ui/separator";
import React from "react";
import { PostCard, PostCardSkeleton } from "@/components/post-card";
import { extractFileUrl, rawText } from "@/app/notion/block-parse";
import { extractFileUrl, filterBase, filterSelect, rawText, sorterProperties } from "@/app/notion/block-parse";
import { env } from "@/env.mjs";

function dbParams() {
const defaultParam = filterBase(env.NOTION_DATABASE_ID);
const filters = {
filter: {
and: [filterSelect("Status", "Published").filter, filterSelect("Type", "Post").filter],
},
};
const sorter = sorterProperties([{ property: "PublishDate", direction: "descending" }]);

return { ...defaultParam, ...filters, ...sorter };
}

export default async function Home() {
const posts = await QueryDatabase(env.NOTION_DATABASE_ID);
const queryParams = dbParams();
const posts = await QueryDatabase(env.NOTION_DATABASE_ID, queryParams);

return (
<Shell className="md:pb-10">
Expand All @@ -29,7 +42,7 @@ export default async function Home() {
{posts.map((post: any, i) => {
const title = rawText(post.properties?.Title?.title);
const slug = post.id; // + "/" + post.properties?.Summary?.rich_text[0]?.plain_text;
const edit_time = post.last_edited_time;
const edit_time = post?.properties?.PublishDate?.date?.start || post?.last_edited_time;
const image = extractFileUrl(post.cover);
const desc = rawText(post.properties?.Summary?.rich_text);

Expand Down
2 changes: 1 addition & 1 deletion app/api/unfurl/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async function queryIframely(url: string) {
headers: {
"Content-Type": "application/json",
},
next: { revalidate: 1800 }, // cache time
next: { revalidate: 300 }, // cache time
});
const res = await result.json();
return {
Expand Down
13 changes: 5 additions & 8 deletions app/notion/_components/bookmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,16 @@ function UnfurledBookmarkPreview({
const icon = data?.favicon;

return (
<div key={id} className={cn(className, "space-y-1 mt-1.5")}>
<div key={id} className={cn(className, "mt-1.5 space-y-1")}>
<Link href={url} target="_blank">
<div className="flex w-full max-w-full overflow-hidden rounded-md border border-gray-200 hover:bg-slate-200 dark:hover:bg-stone-500">
{/* <span className="sr-only">{url}</span> */}
<div className="flex-[100%] lg:flex-[65%] p-4 flex flex-col space-y-2">

<div className="flex flex-[100%] flex-col space-y-2 p-4 lg:flex-[65%]">
<CardHeader className="p-0">
<CardContent className="line-clamp-1 p-0 text-sm font-normal">{title}</CardContent>
</CardHeader>

{desc ? (
<CardDescription className="line-clamp-2 text-xs">{desc}</CardDescription>
) : null}
{desc ? <CardDescription className="line-clamp-2 text-xs">{desc}</CardDescription> : null}

<div className="mt-auto flex w-full max-w-full overflow-hidden">
{icon ? (
Expand All @@ -58,8 +55,8 @@ function UnfurledBookmarkPreview({
<CardContent className="line-clamp-1 p-0 text-xs">{url}</CardContent>
</div>
</div>
<div className="flex-[0%] lg:flex-[35%] my-0 max-h-32 flex justify-center items-center">

<div className="my-0 flex max-h-32 flex-[0%] items-center justify-center lg:flex-[35%]">
{image && (
<div>
{/* eslint-disable-next-line @next/next/no-img-element */}
Expand Down
4 changes: 3 additions & 1 deletion app/notion/_components/child-database.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils";
import RichText from "../text";
import { IconRender } from "./emoji";
import { DatabaseRenderer } from "./db/database";
import { filterBase } from "../block-parse";

interface ChildDatabaseBlockProps {
block: any;
Expand Down Expand Up @@ -35,7 +36,8 @@ export async function ChildDatabaseRenderer({ block, className }: ChildDatabaseB
);
}

const data = await QueryDatabase(id);
const queryParam = filterBase(id);
const data = await QueryDatabase(id, queryParam);
return (
<div>
<div className="text-lg font-bold">
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 @@ -38,7 +38,7 @@ function UnfurledLinkPreview({
<div className="flex w-full max-w-full overflow-hidden rounded-md border border-gray-200 hover:bg-slate-200 dark:hover:bg-stone-500">
<span className="sr-only">{url}</span>
{icon ? (
<div className="max-w-12 my-auto p-2 flex justify-center text-center">
<div className="my-auto flex max-w-12 justify-center p-2 text-center">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={icon} alt={url} />
</div>
Expand Down
16 changes: 8 additions & 8 deletions app/notion/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { GetDatabaseResponse, QueryDatabaseResponse } from "@notionhq/client/build/src/api-endpoints";
import {
GetDatabaseResponse,
QueryDatabaseParameters,
QueryDatabaseResponse,
} from "@notionhq/client/build/src/api-endpoints";
import { proxyListBlockChildren, proxyQueryDatabases, proxyRetrieveDatabase, proxyRetrievePage } from "./proxy/proxy";

/**
Expand All @@ -23,14 +27,13 @@ export const RetrieveDatabase = async (database_id: string): Promise<GetDatabase
return proxyRetrieveDatabase(database_id);
};

// Get pages from database
// Get rows(pages) from database
// API: https://developers.notion.com/reference/post-database-query
// export type TypePostItem = QueryDatabaseResponse["results"][0];
export type TypePostList = QueryDatabaseResponse["results"];
export const QueryDatabase = async (database_id: string): Promise<TypePostList> => {
export const QueryDatabase = async (database_id: string, params: QueryDatabaseParameters): Promise<TypePostList> => {
const start = new Date().getTime();

const response = await proxyQueryDatabases(database_id);
const response = await proxyQueryDatabases(database_id, params);
const end = new Date().getTime();
console.log("[QueryDatabase]", `${end - start}ms`);
return response.results;
Expand All @@ -49,9 +52,6 @@ export const retrievePage = async (pageId: any) => {
};

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

Expand Down
Loading

0 comments on commit bcdf657

Please sign in to comment.