使用Notion作为您的CMS

在本文中,我将向您展示如何将此平台与Notion集成,以将其用作我在Dealo上的文章的CMS。Notion在组织和撰写内容方面是一个很好的工具。

所有这里展示的代码都将使用Next.js和Notion JS客户端。

创建您的Notion集成

这个标题可能听起来有些令人困惑,但这就是Notion称呼它的方式。一个Notion集成是访问您的内容的门户,它将为您提供连接所需的API密钥。用他们自己的话来说:

“集成定义了公共API如何与您的Notion工作区进行程序化交互。它们需要被授权(即,获得明确的许可)才能对您的工作区进行任何更改。”

因此,您的第一步是创建一个集成。您可以阅读Li 现在让我们来玩一下 Notion。首先,您需要一种方法来组织您的文章。我建议使用表格模板,这将允许您为文章添加属性,如状态、类别、关键词、标语(简短描述)等,然后您可以用于过滤、元数据等。Notion 结构的最大优点之一是,您仍然可以为任何行写一篇完整的文章。

创建表格后,您需要连接刚刚创建的集成,方法如下: https://developers.notion.com/docs/create-a-notion-integration# (opens new window)。对于这种用例,集成将是内部的,只需要读取权限。根据用户权限,我在本文中不会使用任何与用户相关的信息。如果您想要显示文章的作者,您将需要访问它。 创建页面

现在进入编码部分。 第一步是创建列出所有文章的根页面。

这是我们将使用的文件夹结构(我正在使用应用程序路由器):

app / └── blog / ├── [slug] ├── layout.tsx └── page.tsx

正如您所看到的,这是一个非常简单的结构。 如果您想要添加层次结构,比如按类别分组等,结构仍然会类似,尽管层次更多。

我不会详细介绍layout.tsx组件的细节,因为页面的外观取决于您。 我们将首先关注page.tsx

由于这是关于博客的内容,页面不需要是动态的,将在构建时生成。 这将有助于 SEO 和页面加载时间。

现在让我们看一些代码。 这是根页面的代码:

import { Client } from '@notionhq/client';

const NOTION_API_KEY = 'your api key'; const NOTION_BLOG_DATABASE = 'the table id';

const notion = new Client({ auth: NOTION_API_KEY });

export default async function RootBlogPage() { const blogs = ( await notion.databases.query({ database_id: env.NOTION_BLOG_DATABASE, filter: { property: 'status', status: { equals: 'Done' } }, sorts: [ { property: 'created_at', direction: 'ascending', }, ], }) ).results as DatabaseObjectResponse[]; //...

这是如何从表中列出文章。注意过滤和排序;这是我提到使用表方法的好处之一,因为页面有更多属性可供使用。

要获取NOTION_BLOG_DATABASE id,您可以从表中复制可共享链接并从中获取。链接看起来像这样:

https://www.notion.so/467bef11d2bf4877928f9be8447104a3?v=77da2764f80c46d590017de202c226a4&pv 下面,您将以您喜欢的任何布局显示文章。我使用了一个简单的两列网格和带有最少信息的卡片。

```jsx
<main className="max-w-[980px] mx-auto">
  <div className="grid grid-cols-2 gap-24">
    {blogs.map((blog: any) => (
      <Link key={blog.id} href={`/blog/${(buildSlug(blog))}`}>
        <div
          key={blog.id}
          className={cn(
            'flex flex-col p-6 h-full rounded border border-neutral-200 dark:border-slate-800',
            'hover:border-emerald-600 dark:hover:border-emerald-500 transition-border duration-200',
          )}
        >
          <h2 className="text-2xl">{blog.properties.Name?.title[0].plain_text}</h2>
          <div className="flex flex-row gap-2 mt-1">
            {blog.properties.keywords.multi_select.map((tag: any) => (
              <Code key={tag.id}>{tag.name}</Code>
            ))}
          </div>
        </div>
      </Link>
    ))}
  </div>
</main>
``` ```jsx
<main>
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
    {blogs.map((blog) => (
      <Link href={`/blog/${buildSlug(blog)}`} key={blog.id}>
        <div className="border border-gray-200 p-4 rounded-lg">
          <h2 className="text-lg font-semibold">{blog.properties.Name.title[0].plain_text}</h2>
          <span className="mt-2 mb-4">{formatDate(blog.properties.created_at.created_time)}</span>
          <p className="text-neutral-600 dark:text-neutral-300">{blog.properties.tagline.rich_text[0].plain_text}</p>
        </div>
      </Link>
    ))}
  </div>
</main>

构建 buildSlug 函数我用来规范化链接的格式。您希望有人类可读的链接,但同时也需要文章的ID,以便实现自我修复的URL。自我修复的URL通过修复损坏的链接并保持页面与内容的最新状态来帮助SEO,例如:如果标题更改了。您可以在这篇文章 (opens new window)中阅读更多。

这是函数:

export function buildSlug(blog: any) {
  const slug: string = blog.properties.Name?.title[0].plain_text.toLowerCase().replace(/ /g, '-');
  const cleanId = blog.id.replace(/-/g, '');
  return `${slug}-${cleanId}`;
}

渲染文章 #

现在让我们渲染文章。 让我们看看将呈现每篇文章的页面。这是一个更复杂的页面,因为它需要处理将Notion块转换为HTML标签,以及添加元数据。

这是处理元数据的函数。它需要是一个函数,而不是一个简单的const导出,因为它需要获取每篇文章的特定数据。这是一个简单的实现,仍然缺少关键的元数据,如开放图形和Twitter相关标签。

export async function generateMetadata(props: Props): Promise<Metadata> {
  const { params: { slug } } = props;
  const articleId = slug.split('-').pop();

  if (!articleId) {
    return {};
  }

  const blog: any = await notion.pages.retrieve({
    page_id: articleId,
  });
  const title = blog.properties.Name.title[0].plain_text;

  return {
    title,
    description: blog.properties.tagline.rich_text[0].plain_text,
    keywords: blog.properties.keywords.multi_select.map((tag: any) => tag.name),
  };
}

接下来是generateStaticParams。这个将 ```javascript export async function 生成静态参数() { const 博客文章 = ( await notion.databases.query({ database_id: env.NOTION_BLOG_DATABASE, filter: { property: 'status', status: { equals: 'Done' } }, sorts: [ { property: 'created_at', direction: 'ascending', }, ], }) ).results as DatabaseObjectResponse[];

return 博客文章.map((博客) => ({ slug: 构建Slug(博客), })) }


现在,要渲染实际文章,我们将创建一个页面,查询Notion页面信息以及构成文章本身的块。

```javascript
export default async function 博客详情页面(props: Props) {
  const { params: { slug } } = props;
  const 文章Id = slug.split('-').pop();

  if (!文章Id) {
    return notFound();
  }

  co
``` const page = await notion.pages.retrieve({
    page_id: articleId,
  });

  const blocks = [];
  let cursor = undefined;

  do {
    const response = await notion.blocks.children.list({
      block_id: articleId,
      start_cursor: cursor,
    });
    blocks.push(...response.results);
    cursor = response.next_cursor;
  } while (cursor);

  const elements = await processNotionBlocks(blocks as BlockObjectResponse[]);

  return (
    <main className="max-w-[780px] mx-auto">
      <div className="mb-24">
        <Link href="/blog">
          <Button variant="outline">
            <IconArrowLeft size={14} className="mr-2"/>
            返回文章列表
          </Button>
        </Link>
      </div>
      <h1 className="text-5xl mb-6 mt-4">
        {page.properties.Name.title[0].plain_text}
      </h1>
      <div className="flex flex-row gap-2 mb-3">
        {page.properties.keywords.multi_select.map((tag: any) => (
          <Code key={tag.id}>{tag.name}</Code>
        ))}
      </div> ```javascript
async function processNotionBlocks(blocks: BlockObjectResponse[]) {
  const elements = [];

  for (let i = 0; i < blocks.length; i++) {
    const block = blocks[i] as BlockObjectResponse;

    if (block.type === 'heading_1') {
      elements.push(
        <h1 key={block.id} className="text-4xl mt-8 mb-8">
          {block.heading_1.rich_text.map((text) => text.plain_text).join('')}
        </h1>
      );
    }

    if (block.type === 'heading_2') {
      elements.push(
        <h2 key={block.id} className="text-3xl mt-8 mb-8">
          {block.heading_2.rich_text.map((text) => text.plain_text).join('')}
        </h2>
      );
    }

    if (block.type === 'paragraph') {
      elements.push(
        <p key={block.id} className="mb-4">
          {block.paragraph.text.map((text) => text.plain_text).join('')}
        </p>
      );
    }

    if (block.type === 'bulleted_list_item') {
      elements.push(
        <li key={block.id} className="list-disc ml-4">
          {block.bulleted_list_item.text.map((text) => text.plain_text).join('')}
        </li>
      );
    }

    if (block.type === 'numbered_list_item') {
      elements.push(
        <li key={block.id} className="list-decimal ml-4">
          {block.numbered_list_item.text.map((text) => text.plain_text).join('')}
        </li>
      );
    }

    if (block.type === 'to_do') {
      elements.push(
        <div key={block.id} className="flex items-center mb-2">
          <input type="checkbox" className="mr-2" defaultChecked={block.to_do.checked} />
          <p className={block.to_do.checked ? 'line-through mb-4' : 'mb-4'}>
            {block.to_do.text.map((text) => text.plain_text).join('')}
          </p>
        </div>
      );
    }

    if (block.type === 'image') {
      elements.push(
        <img key={block.id} src={block.image.file.url} alt={block.image.caption[0].plain_text} className="w-full mt-4 mb-4 object-cover" />
      );
    }

    if (block.type === 'divider') {
      elements.push(<hr key={block.id} className="border-t border-gray-300 my-4" />);
    }
  }

  return elements;
}
``` ```javascript
if (block.type === 'heading_3') {
  elements.push(
    <h3 key={block.id} className="text-2xl mt-4 mb-4">
      {block.heading_3.rich_text.map((text) => text.plain_text).join('')}
    </h3>
  );
}

if (block.type === 'paragraph') {
  if (block.paragraph.rich_text.length === 0) {
    elements.push(<br key={block.id}/>);
    continue;
  }

  elements.push(
    <p key={block.id} className="mb-2">
      {block.paragraph.rich_text.map((text, index) => {
        if (text.annotations.code) {
          return (
            <code
              key={`${block.id}-${index}`}
              className={cn(
                'bg-emerald-200 dark:bg-emerald-800 rounded-sm px-1',
                {
                  'font-bold': text.annotations.bold,
                  italic: text.annotations.italic,
                  'line-through': text.annotations.strikethrough,
                  underline: text.annotations.underline,
``` 以前,我会在工作时经常陷入混乱,感觉无法控制自己的时间和任务。但是,通过学习时间管理技巧和使用工具,我现在能更好地组织自己的工作和生活。下面是我分享的一些时间管理技巧:

1. 制定计划:每天清晨或工作开始前,列出当天需要完成的任务和目标。将它们按重要性和紧急程度排序,然后制定一个计划,逐步完成任务。

2. 使用时间管理工具:利用时间管理工具,如番茄工作法、日程表或任务管理应用程序,帮助你更好地管理时间和任务。

3. 集中注意力:避免分心和多任务处理,集中注意力完成一项任务,然后再处理下一项任务。

4. 设定目标:设定短期和长期目标,为自己制定计划和时间表,以实现这些目标。

5. 学会拒绝:学会拒绝那些会浪费你时间的事情,保持专注于重要任务。

6. 休息调整:合理安排工作和休息时间,保持身心健康,提高工作效率。

7. 反思总结:每天结束前,反思一天的工作,总结完成的任务和需要改进的地方,为明天的工作做准备。

通过这些时间管理技巧,我现在能更高效地工作和生活,实现自己的目标和梦想。希望这些技巧也能帮助你更好地管理时间和任务,提高工作效率。 如果block.type是'quote',则执行以下代码:
const quoteChildren = await notion.blocks.children.list({
  block_id: block.id,
});
const quoteElements = await processNotionBlocks(quoteChildren.results as BlockObjectResponse[]);
elements.push(
  <blockquote key={block.id} className="my-6 pl-4 border-l-4 border-emerald-500 dark:border-emerald-700">
    {block.quote.rich_text.map((text) => text.plain_text).join('')}
    {quoteElements}
  </blockquote>
);

如果block.type是'code',则执行以下代码:
elements.push(
  <pre key={block.id} data-el="code-block" className="group p-6 my-6 rounded bg-neutral-50 dark:bg-slate-900 overflow-x-auto">
    <code className="text-sm font-mono py-[1px] px-1 rounded-sm bg-emerald-200 dark:bg-emerald-800 group-data-[el=code-block]:bg-neutral-50 group-data-[el=code-block]:dark:bg-slate-900">
      {block.code.rich_text.map((text) => text.plain_text).join('')}
    </code>
  </pre>
); 如果块的类型是'paragraph',则将该块添加到元素数组中。
如果块的类型是'bulleted_list_item',则将该块及其后续相同类型的块添加到元素数组中。
如果块的类型是'numbered_list_item',则将该块及其后续相同类型的块添加到元素数组中。

将元素数组渲染为相应的HTML元素并返回。 将以下的Markdown翻译成中文,并删除第一级标题,同时删除图片链接,尽量消除Markdown格式错误和一些无用段落,重新装饰整篇文章,使之读起来更加自然:

imal list-outside pl-6 mb-2">
          {bulletItems.map((item) => (
            <li key={item.id}>
              {item.numbered_list_item.rich_text.map((text) => text.plain_text).join('')}
            </li>
          ))}
        </ol>
      );
    }
  }

  return elements;
}

你可以(应该)完善这个函数,使其更加简单,因为可能会有更多的块类型需要支持,你可以看到它可能会变得难以控制,但作为初始步骤,这样做是可以的。

结语 #

这就是如何将Notion设置为你的内容管理系统。值得一提的是,你可能已经注意到,我没有提供任何API来接收Notion中新文章创建或更新时的通知。这是因为他们没有webhooks API。

这意味着只有在项目中进行构建时,你的文章才会被添加或更新。希望这不会成为一个严重问题,但值得注意。

感谢您一直坚持到最后。

祝好,编程愉快。