使用Next.js 13 App目录制作动态博客
#javascript #教程 #react #nextjs

本文最初发表在Cosmic

介绍

博客不仅仅是一种爱好;对于许多人来说,这是在特定行业中建立思想领导力的一种职业。拥有表演者和SEO友好的平台至关重要。在本指南中,我们将深入研究具有Next.js 13 App Router中的新功能的博客模板。

创建博客模板:

探索设计响应,高效和动态博客模板的步骤。本教程强调了利用Next的优势。JS13,Cosmic,以及针对顶级用户和开发人员体验的响应式设计工具包。

使用的关键工具:

  • next.js 13:著名的React框架的第一次迭代介绍了应用程序路由器,并包含增强功能以​​进行全堆栈开发。
  • 宇宙: headless CMS可以实现数据(内容)层的独立性,并使我们能够快速管理模板内容。在这种情况下,我们的博客文章,作者和标签。
  • tailwind CSS:可以直接在标记中组成的性能效用优先级CSS框架。
  • 打字稿:确保在我们的项目中键入安全性,如果您喜欢JavaScript,则可以将所有tsx文件重命名为jsx(将tsts命名为js)并解决错误。

教程故障

tl; dr

Install the template

View the live demo

View the code

设置Next.js 13 App目录

  • 初始化具有最新next.js功能的新应用程序。
  • 为项目安装必要的依赖项。
pnpx create-next-app@latest nextjs-developer-portfolio
# or
yarn create next-app nextjs-developer-portfolio
# or
npx create-next-app@latest nextjs-developer-portfolio

然后安装依赖项。

cd nextjs-developer-portfolio
pnpm install
# or
cd nextjs-developer-portfolio
yarn
# or
cd nextjs-developer-portfolio
npm install

让我们启动我们的申请!在下面运行命令后,您可以在浏览器中打开http://localhost:3000

pnpm run dev
# or
yarn dev
# or
npm run dev

克隆github的模板(推荐)

我们有一个非常简单的项目设置,该设置与我们的内容模型密切映射(稍后会详细介绍)。

|— app
  |— author
    |— [slug]
        page.tsx
  |— posts
    |— [slug]
        page.tsx
  layout.tsx
  page.tsx
|— components
|— fonts
|— lib

// the rest is typical Next.js structuring

配置宇宙

要开始集成无头内容,请用免费帐户注册Cosmic,然后安装Simple Next.js Blog的演示桶。创建帐户后,创建一个新项目。将提示您从一个空项目或模板开始。选择模板,然后选择我们在本教程中使用的简单next.js博客模板以及演示内容。

建立内容模型

如前所述,我们的内容模型非常紧密地映射到我们的应用结构。我们有针对作者和帖子的对象类型,并且我们的类别具有对象类型,这些类型被我们的明信片组件使用来渲染UI中的徽章。

如果您没有安装模板,则需要构建自定义内容模型。为了了解您需要哪些数据,您想参考lib/types文件,该文件将显示模型结构为打字稿类型。

请注意,在这种情况下,idslugtitle是当我们制作新对象类型时给我们提供的默认属性,而metadata对象内的所有内容都是基于我们想要集成的特定模型metafields。

// lib/types.ts

export interface GlobalData {
  metadata: {
    site_title: string;
    site_tag: string;
  };
}

export interface Post {
  id: string;
  slug: string;
  title: string;
  metadata: {
    published_date: string;
    content: string;
    hero?: {
      imgix_url?: string;
    };
    author?: {
      slug?: string;
      title?: string;
      metadata: {
        image?: {
          imgix_url?: string;
        };
      };
    };
    teaser: string;
    categories: {
      title: string;
    }[];
  };
}

export interface Author {
  id: string;
  slug: string;
  title: string;
  metadata: {
    image?: {
      imgix_url?: string;
    };
  };
}

内容模板允许您拖放元场以构建模型或也修改现有模型。

集成宇宙SDK

要开始使用宇宙数据,您需要安装Cosmic SDK并初始化桶客户端。

npm i @cosmicjs/sdk

然后,如果您没有使用我们提供的项目代码,请在lib文件夹中创建一个名为cosmic.ts的文件,然后打电话以创建Bucket Client。

// lib/cosmic.ts
import { createBucketClient } from '@cosmicjs/sdk';

const cosmic = createBucketClient({
  // @ts-ignore
  bucketSlug: process.env.NEXT_PUBLIC_COSMIC_BUCKET_SLUG ?? '',
  // @ts-ignore
  readKey: process.env.NEXT_PUBLIC_COSMIC_READ_KEY ?? '',
});
export default cosmic;

现在,您可以在需要从桶中获取宇宙数据的任何地方导入此信息。如果需要,您可以在开发时用硬编码键替换环境变量。从您的项目> bucket> api访问

设置环境变量

  • 另外,如果您使用Vercel(推荐)托管您的项目,则可以将BUCKET_SLUGREAD_KEY添加到您的项目中,并使用Vercel CLI链接到Vercel。

获取博客文章

要获取博客文章,我们可以对Cosmic SDK使用一个简单的调用,然后恢复我们需要的内容。

// lib/cosmic.ts

export async function getAllPosts(): Promise<Post[]> {
  try {
    // Get all posts
    const data: any = await Promise.resolve(
      cosmic.objects
        .find({
          type: 'posts',
        })
        .props('id,type,slug,title,metadata,created_at')
        .depth(1)
    );
    const posts: Post[] = await data.objects;
    return Promise.resolve(posts);
  } catch (error) {
    console.log('Oof', error);
  }
  return Promise.resolve([]);
}

在这里,我们使用承诺从lib/types文件声明我们的Post类型的数组,并使用Special objects.find()方法获取与我们的帖子类型相匹配的内容。然后,我们要求提供特定的道具,以便以后渲染UI时需要。我们对depth(1)有参考,这是因为我们只需要一层元数据。如果我们有嵌套的对象关系,我们需要声明更深的深度。

在返回语句之前,请尝试添加console.log(posts),以确保您从宇宙中获得回复。这样,您可以确定您的环境变量正常工作,并且会看到数据结构。您可以检查开发人员工具抽屉和Node.js选项卡,以查看它是否与您所处的东西匹配。

专业提示:这是一个方便的地方,可以从也从基本的API请求中获得。请注意,我们在实施中增加了类型的安全性。

降价或内容格式

在这个项目中,我们使用丰富的文本来显示我们的帖子内容,因此我们使用宇宙丰富的文本编辑器Metafield。这意味着我们确实将HTML设置在UI中。宇宙丰富的文本编辑器提供了方便的快捷方式,以使内容创建变得轻而易举。

强烈建议使用XSS消毒剂(如DOMPurify)对HTML进行消毒并预防XSS attacks。对于突出具有服务器端渲染的Next.js项目,Isomorphic DOMPurify特别有价值。它在服务器和客户端都提供无缝的消毒过程,确保在不存在本机服务器端DOM之类的环境中保持一致的HTML消毒。

如果您不是危险地设置html,则有包含此功能的软件包,以提供其他方法来呈现丰富的文本。

您也可以选择将其转换为Markdown,并使用我们的Markdown Metafield,如果您愿意,请注意,您需要安装一个Markdown软件包才能做到这一点。文章Building React Components from headless CMS markdown是一本很棒的阅读,介绍了React Markdown之类的软件包如何从无头CMS中解析Markdown,并解释了如何在Next.js应用程序中渲染Markdown。

设计博客文章概述

因此,现在我们将我们的Post数据退回,我们需要在页面上显示它。让我们在主page.tsx文件中执行此操作,因为我们希望默认显示帖子列表。这毕竟是一个博客。

首先,我们需要一张卡来显示我们的帖子。这也依赖于某些子组件,但首先让S支架。

创建一个新的PostCard.tsx文件,然后从导出一张明信片组件开始,该明信片组件希望接收post类型的Post

// compoonents/PostCard.tsx

export default function PostCard({ post }: { post: Post }) {
  return (
   // The rest of our code will go here
  )
};

我们知道我们的代码期望获得以下关键部分:图像,slug和标题。所以让我把它们放进去。

// components/PostCard.tsx

export default function PostCard({ post }: { post: Post }) {
  return (
    {post.metadata.hero?.imgix_url && (
      <Link href={`/posts/${post.slug}`}>
        <Image width={2800} height={400} className='mb-5 h-[400px] w-full rounded-xl bg-no-repeat object-cover object-center transition-transform duration-200 ease-out hover:scale-[1.02]'
        src={`${post.metadata.hero?.imgix_url}?w=1400&auto=format`}
        priority alt={post.title}
        placeholder='blur'
        blurDataURL={`${post.metadata.hero?.imgix_url}?auto=format,compress&q=1&blur=500&w=2`} />
     </Link>
    )}
  )
};

在这里,我们要防止没有图像URL。尽管在我们的Metafield中添加英雄不是可选的,但是如果网络太慢,或者imgix服务器没有响应,则可能会返回。这可以防止这种情况。

您也可能会注意到这一行:

// components/PostCard.tsx

src={`${post.metadata.hero?.imgix_url}?w=1400&auto=format`}

这使用iMgix优化的URL参数为我们提供适合我们需求的图像和格式的图像。

现在添加标题。

// components/PostCard.tsx

export default function PostCard({ post }: { post: Post }) {
  return (
    {post.metadata.hero?.imgix_url && (
      <Link href={`/posts/${post.slug}`}>
        <Image width={2800} height={400} className='mb-5 h-[400px] w-full rounded-xl bg-no-repeat object-cover object-center transition-transform duration-200 ease-out hover:scale-[1.02]'
          src={`${post.metadata.hero?.imgix_url}?w=1400&auto=format`}
          priority alt={post.title}
          placeholder='blur'
          blurDataURL={`${post.metadata.hero?.imgix_url}?auto=format,compress&q=1&blur=500&w=2`} />
        </Link>
    )}
    <h2 className='pb-3 text-xl font-semibold tracking-tight text-zinc-800 dark:text-zinc-200'>
      <Link href={`/posts/${post.slug}`}>{post.title}</Link>
    </h2>
  )
};

太好了,接下来我们要包括作者的化身及其归因。我们需要根据我们渲染的特定post获取这些数据。因此,让我们创建可以将post传递到正确结果的组件。

我们的作者化身很好,简单...

// components/AuthorAvatar.tsx

import Image from 'next/image';
import Link from 'next/link';
import { Post } from '../lib/types';

export default function AuthorAvatar({ post }: { post: Post }): JSX.Element {
  return (
      <Link href={`/author/${post.metadata.author?.slug}`}>
          <Image className='h-8 w-8 rounded-full'
          src={`${post.metadata.author?.metadata.image?.imgix_url}?w=100&auto=format`}
          width={32}
          height={32}
          alt={post.title}
          />
    </Link>
  );
}

,我们的归因也是如此...

// components/AuthorAttribution.tsx

import { Post } from '../lib/types';
import helpers from '../helpers';

export default function AuthorAttribution({ post }: { post: Post }): JSX.Element {
  return (
    <div className='flex space-x-1'>
      <span>by</span>
      <a href={`/author/${post.metadata.author?.slug}`} className='font-medium text-green-600 dark:text-green-200'>
        {post.metadata.author?.title}
      </a>
      <span>on {helpers.stringToFriendlyDate(post.metadata.published_date)}</span>
    </div>
  );
}

因此,现在在我们的PostCard.tsx组件中,我们可以导入这些并将其传递到post中,以及使卡渲染数据所需的其余代码。

// components/PostCard.tsx

import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import ArrowRight from './icons/ArrowRight';
import Tag from './Tag';
import { Post } from '../lib/types';
import AuthorAttribution from './AuthorAttribution';
import AuthorAvatar from './AuthorAvatar';

export default function PostCard({ post }: { post: Post }) {
    return (
    {post.metadata.hero?.imgix_url && (
        <Link href={`/posts/${post.slug}`}>
          <Image width={2800} height={400} className='mb-5 h-[400px] w-full rounded-xl bg-no-repeat object-cover object-center transition-transform duration-200 ease-out hover:scale-[1.02]'
          src={`${post.metadata.hero?.imgix_url}?w=1400&auto=format`}
          priority alt={post.title} placeholder='blur'
          blurDataURL={`${post.metadata.hero?.imgix_url}?auto=format,compress&q=1&blur=500&w=2`} />
        </Link>
    )}
    <h2 className='pb-3 text-xl font-semibold tracking-tight text-zinc-800 dark:text-zinc-200'>
      <Link href={`/posts/${post.slug}`}>{post.title}</Link>
    </h2>
    <div className='flex flex-col justify-between space-y-4 md:flex-row md:space-y-0'>
        <div className='flex items-center space-x-2 text-zinc-500 dark:text-zinc-400 md:space-y-0'>
          <AuthorAvatar post={post} />
          <AuthorAttribution post={post} />
        </div>
        <div className='flex select-none justify-start space-x-2 md:hidden md:justify-end'>
        {post.metadata.categories && post.metadata.categories.map((category) =>             <Tag key={category.title}>{category.title}</Tag>)}</div>
      </div>
      <div className='py-6 text-zinc-500 dark:text-zinc-300' dangerouslySetInnerHTML={{ __html: post.metadata.teaser ?? '' }} />
      <div className='flex items-center justify-between font-medium text-green-600 dark:text-green-200'>
        <Link href={`/posts/${post.slug}`}>
          <div className='flex items-center space-x-2'>
            <span>Read more</span>
            <ArrowRight className='h-4 w-4 text-inherit' />
          </div>
        </Link>
        <div className='hidden select-none justify-end space-x-2 md:flex '>{post.metadata.categories && post.metadata.categories.map((category) => <Tag key={category.title}>{category.title}</Tag>)}</div>
      </div>
    </div>
    )
};

显示博客概述

现在,我们有了我们的卡,我们将其渲染到我们的数据中,我们需要将其放入我们的主page.tsx中。多亏了React Server Components的新应用程序路由器结构,我们不需要利用getServerSideProps()等特殊功能来获取我们的数据。相反,我们可以简单地等待我们的getAllPosts()函数并将数据返回到视图。

// app/page.tsx

import React from 'react';
import PostCard from '../components/PostCard';
import { getAllPosts } from '../lib/cosmic';

export default async function Page(): Promise<JSX.Element> {
  const posts = await getAllPosts();

  return (
    // The rest of our code will go here
  )
}

因此,要返回我们的帖子列表(如果您使用了模板,您已经有5个示例帖子,否则您需要添加一些帖子),我们只需要映射我们返回的数据和将其传递给我们的PostCard组件进行渲染。

// app/page.tsx

import React from 'react';
import PostCard from '../components/PostCard';
import { getAllPosts } from '../lib/cosmic';

export default async function Page(): Promise<JSX.Element> {
 const posts = await getAllPosts();

  return (
   <main className="mx-auto mt-4 w-full max-w-3xl flex-col space-y-16 px-4 lg:px-0">
     {!posts && "You must add at least one Post to your Bucket"}
     {posts &&
       posts.map((post) => {
         return (
           <div key={post.id}>
             <PostCard post={post} />
           </div>
         );
       })}
   </main>
 );
}

生成单个博客文章页面

太好了,我们现在有一个很好的博客文章清单……但是,当我们单击任何内容时,我们都可以看到其中的任何一个。

Image of Blog Page

我会涵盖渲染单个博客文章所需的整个代码(您可以在sample code中找到,但是我将涵盖我们需要考虑的一些重要应用程序路由器元素。

首先是generateMetadata()函数。这使我们能够根据所查看的给定博客文章创建dynamic metadata。这对于良好的SEO以及通过社交平台共享时也很重要。您可以对此进行疯狂,如果需要,生成具有自定义标题和其他数据的dynamic OG images

在我们的情况下,我们将其简单简单。

// app/posts/[slug]/page.tsx

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost({ params });
  return {
    title: `${post.title} | Simple Next 13 Blog`,
  };
}

在这里,我们只需将当前博客的标题返回到我们的元数据中的标题属性。这意味着“浏览器”选项卡/窗口中的标题以及在社交媒体上共享时将与我们当前的博客文章相匹配。

我们拥有的另一个有用的元素,它利用了宇宙SDK的力量,是显示您查看当前帖子的建议帖子的能力。

在UI中,这看起来像这样(建议使用简化的UI,在结构上与典型明信片非常相似)。

// app/posts/[slug]/page.tsx

<div className='flex flex-col space-x-0 space-y-4 md:flex-row md:space-x-4 md:space-y-0'>
   {suggestedPosts.slice(0, 2).map((post) => {
       return <SuggestedPostCard key={post.id} post={post} />;
   })}
</div>

我们阻止当前发表的帖子的方式是将额外的属性传递给我们的find()方法。这只是代码的相关提取部分。

// lib/cosmic.ts

const data: any = await Promise.resolve(
  cosmic.objects
   .find({
     type: 'posts',
     slug: {
       $ne: params?.slug,
     },
   })
   .props(['id', 'type', 'slug', 'title', 'metadata', 'created_at'])
   .sort('random')
   .depth(1)
);

请注意,我们说我们想要slug不等于params?.slug的任何地方。为此,我们像So export async function getRelatedPosts({ params }: { params: { slug: string } }): Promise<Post[]>一样传递到函数的参数。这意味着,当我们在页面中引用它并传递页面params时,该功能知道避免了当前的sl。

做到这一点的获取,看起来很像:

// app/posts/[slug]/page.tsx

const suggestedPosts = await getRelatedPosts({ params });

部署

现在,如果您选择准确地遵循,那么在您实际部署此项目之前,可能会有一些丢失的作品。看看sample code,看看您可能需要什么,并进行相应的调整以使其启动并运行。

我们在Vercel上举办了示例,因此,如前所述,如果您使用它,很容易设置。否则,您可以将其推到其他选择的平台,例如Netlify

结论

通过遵循本教程,您将拥有一个由Next.js 13 App Directory供电的现代,动态和高效的博客模板。与开发人员社区互动,分享您的经验,并始终寻找改进和创新的方法。您的博客不仅是一个平台;这是一个不断发展的实体,反映了您的声音和专业知识。