(从零到一)快速搭建博客网站:使用Next.js、Strapi和Shadcn-UI构建CMS管理后台和SEO友好的前端界面

757
2024-05-31 14:29
6 个月前

开发背景:

最近在群里看到有人说如何快速开发一个博客网站,那我们先拆解一下开发需求。

  1. 博客的管理就是需要个CMS的管理后台。
  2. 展示就是需要一个对SEO友好的界面。

框架选择

SEO友好的前端框架-NextJS

CMS管理后台-Strapi(Open source Node.js Headless CMS)

最近很火的UI集合-shadcn-ui

家喻户晓的CSS框架-Tailwind CSS

包管理工具-pnpm

保姆级开发步骤

创建项目文件夹,创建workspace环境

mkdir blog-project
# 创建目录 /Users/luke/Desktop/course/blog-project
cd blog-project
pnpm init

打开自己习惯用的IDE,执行命令 code . 或者 webstorm .

创建创建文件夹apps和文件pnpm-workspace.yaml

packages:
- "apps/*"

在apps的目录执行命令创建NextJS的web项目

cd web
pnpm run dev
# 打开链接http://127.0.0.1:3000/,这个时候就可以打开我们启动的页面了

添加CMS管理后台

切换到apps的目录执行安装strapi命令,演示作用我就没展示mysql的链接了,大家有兴趣我可以再下一个文章去写一下,或者去strapi官网看一下如何使用别的数据。

pnpx create-strapi-app@latest cms --quickstart --ts

进入apps/cms的目录,拷贝一下src/admin/app.example.tsx文件为app.tsx,然后再配置那里把中文简体的配置打开注释,你的文件就像下面一样

把cms的develop命令改成dev,然后启动看一下,pnpm run dev

这个时候我们会看到启动报错,遇到困难别怕,看一下报错提示,File '@strapi/typescript-utils/tsconfigs/server' not found.很简单,自己在安装一下这个包就行了

你可以在项目根目录执行pnpm install --filter也可以在cms目录直接执行pnpm install.

我是在根目录执行 pnpm install @strapi/typescript-utils --filter -D

重新启动后你还是会发现一个报错,因为typescript的报错。没有找到node模块

还需要安装一下pnpm install @types/node --filter -D

这个时候重新启动一下,我们就会成功进到一个注册的超级管理员的页面,我们根据提示填写自己的账号密码就可以了。

登录成功后我们会进到这里的一个管理后台,至此我们的工作已经完成一半了。

简单对cms的后端面板写一下解释,大家可以看这个图,后面会有别的机会给大家讲解的

点击市场找到CKeditor5插件,我们现在来安装一下,执行以下命令

pnpm install @_sh/strapi-plugin-ckeditor -S --filter=cms
# 然后重新启动一下

进去后我们点击Content-type Builder这个左侧导航,然后点击COLLECTION TYPES下面那个创建,我们来创建文章实体

创建以下基本字段title,desc,cover,content。由于strapi可以用草稿发布模式,我们文章就使用这个模式,你点击创建实体的时候会有让你选择的,默认是选择上的。新增完之后会重启服务,帮我们创建好实体

我们继续创建标签实体,定义这个实体跟我们的文章是多对多关系,下面我们先创建标签实体,这个我们不需要用发布模式,然后只需要一个短文本的name字段。

后面我们去创建内容,然后把这两个实体关联,我们可以随便创建一点内容,点击内容管理器

 

创建完之后我们需要做下一步,把它们关联起来,关系如何

文章可以有多个标签,标签也属于多个文章,我们得出个关系,就是多对多。

好了我们去添加关系,这个时候添加完之后还是会重启服务。我们点击Contenty-type builder 去给article添加一个新的字段。也就是引用字段,添加完之后去article添加一下标签

这个时候我们最简单的博客管理后端已经做好了。我们现在需要把这个服务变成api接口访问就行。这对于strapi来说也是超级简单的。下面我们来设置api访问。

添加api访问

strapi是一个集成api访问和后台管理的headLess CMS开源框架。我们只需要配置一下实体的权限就可以实现api访问控制,默认情况下strapi的接口入口是/api开始,我们刚才创建了article的实体(要加复数),那么我们可以访问http://127.0.0.1:1337/api/arciels,第一次访问的时候会返回403,这个时候是 因为我们没打开我们的公共访问。

我们现在去打开公共访问

再次看一下我们的接口请求

到这里strapi搭建的cms管理后台已经接近完成,我们可以整合前端项目去做我们的前端展示了。

整合项目

现在我们两个项目都是用typescript,我们需要使用paths去定义一下项目的引用别名,以备之后会用上

我们在项目的根目录创建tsconfig.json

{
        "compilerOptions": {
            "baseUrl": "./",
            "experimentalDecorators": true,
            "emitDecoratorMetadata": true,
            "incremental": true,
            "skipLibCheck": true,
            "strictNullChecks": true,
            "noImplicitAny": true,
            "strictBindCallApply": true,
            "forceConsistentCasingInFileNames": true,
            "noFallthroughCasesInSwitch": true,
            "paths":{
                "web/*":["./apps/web/*"],
                "cms/*":["./apps/cms/*"]
            }
        }
    }
    

然后我们可以把nextjs的项目的tsconfig.json文件修改一下

{
        "extends": "../../tsconfig.json",
        "compilerOptions": {
            "target": "es5",
            "lib": ["dom", "dom.iterable", "esnext"],
            "allowJs": true,
            "strict": true,
            "noEmit": true,
            "esModuleInterop": true,
            "module": "esnext",
            "moduleResolution": "bundler",
            "resolveJsonModule": true,
            "isolatedModules": true,
            "jsx": "preserve",
            "incremental": true,
            "plugins": [
                {
                    "name": "next"
                }
            ]
        },
        "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
        "exclude": ["node_modules"]
    }
    

现在我们内容已经有了,我们可以去做前端网站的开发了,上面我们说到我们在nextjs中需要引入shadcn/ui,这个是最近势头很猛的一个组件集合。它不称自己为组件库,而是叫集合。全部的代码开源,也可以直接拷贝进去进行使用。现在我们就去我们的next14那里去集成一下这个ui。

首先我们去到web目录里执行命令

pnpx shadcn-ui@latest init

按上图所示先选择类型,然后我们引入一下button组件试一下,我们使用pmpm dlx 命令可以在web的项目目录下载button组件到web/src/components

然后修改一下app/page.tsx文件,我们直接使用button组件看看

import {Button} from "web/src/components/ui/button";

export default function Home() {
    return (
        <main className="flex min-h-screen flex-col items-center justify-between p-24">
            <Button>测试一波</Button>
            </main>
    )
}

看到这个页面证明我们的shadcn/ui组件已经引入成功,现在我们尝试一下修改主题,打开官网,点击theme这个选项卡。这里我就不去操作了,直接贴一个主题吧,把下面的代码拷贝到app/globals.css文件上

@layer base {
:root {
        --background: 0 0% 100%;
        --foreground: 240 10% 3.9%;
        --card: 0 0% 100%;
        --card-foreground: 240 10% 3.9%;
        --popover: 0 0% 100%;
        --popover-foreground: 240 10% 3.9%;
        --primary: 346.8 77.2% 49.8%;
        --primary-foreground: 355.7 100% 97.3%;
        --secondary: 240 4.8% 95.9%;
        --secondary-foreground: 240 5.9% 10%;
        --muted: 240 4.8% 95.9%;
        --muted-foreground: 240 3.8% 46.1%;
        --accent: 240 4.8% 95.9%;
        --accent-foreground: 240 5.9% 10%;
        --destructive: 0 84.2% 60.2%;
        --destructive-foreground: 0 0% 98%;
        --border: 240 5.9% 90%;
        --input: 240 5.9% 90%;
        --ring: 346.8 77.2% 49.8%;
        --radius: 0.3rem;
    }

.dark {
        --background: 20 14.3% 4.1%;
        --foreground: 0 0% 95%;
        --card: 24 9.8% 10%;
        --card-foreground: 0 0% 95%;
        --popover: 0 0% 9%;
        --popover-foreground: 0 0% 95%;
        --primary: 346.8 77.2% 49.8%;
        --primary-foreground: 355.7 100% 97.3%;
        --secondary: 240 3.7% 15.9%;
        --secondary-foreground: 0 0% 98%;
        --muted: 0 0% 15%;
        --muted-foreground: 240 5% 64.9%;
        --accent: 12 6.5% 15.1%;
        --accent-foreground: 0 0% 98%;
        --destructive: 0 62.8% 30.6%;
        --destructive-foreground: 0 85.7% 97.3%;
        --border: 240 3.7% 15.9%;
        --input: 240 3.7% 15.9%;
        --ring: 346.8 77.2% 49.8%;
    }
}

我们这个时候看到页面的按钮变成红色,则意味着主题已经应用上了,下面我们可以开发页面了

博客默认布局以及暗黑模式开发

代补匆页面

我们先把真个页面布局弄出来。然后第一步我们完成头部导航。我们第一步需要安装next-theme,然后创建layout组件,创建header组件,创建暗黑模式切换组件(modeToogle),切换组件我们使用点击切换,所以需要使用到了shadcn/ui 里面的dropdown-menu组件。

# 切换到web目录,首先添加next-theme包
pnpm install next-themes -S
# 然后添加shadcn/ui 的 dropdown-menu
pnpm dlx shadcn-ui@latest add dropdown-menu
// web/src/components/layout/default.tsx
import React, { CSSProperties, PropsWithChildren } from 'react';
import { clsx } from 'clsx';
import { Header } from 'web/src/components/ui/header';
export interface IDefaultLayoutProps extends PropsWithChildren {
    className?: string;
    headerPosition?: CSSProperties['position'];
}
export const DefaultLayout: React.FC<IDefaultLayoutProps> = ({
    headerPosition = 'relative',
    className,
    children,
}) => {
    return (
        <div
            className={clsx(
            'full flex flex-col min-h-screen relative',
            className,
            'default-layout',
    )}
>
    <div style={{ position: headerPosition }} className="z-10 w-full">
        <Header></Header>
        </div>
        <main className="flex flex-col flex-1 container z-20  min-h-0 min-w-0 basis-auto w-full h-full">
        {children}
        </main>
        <footer className="flex-shrink-0 z-10 ">
    <div className="container flex justify-center items-center py-4 text-base">
        footer
        </div>
        </footer>
        </div>
);
};
// web/src/components/ui/header.tsx
    'use client';
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from 'web/src/lib/utils';
import Link from 'next/link';
import { ModeToggle } from 'web/src/components/ui/modeToggle';

const headerVariants = cva('flex w-full', {
    variants: {
        variant: {
            default: 'text-2xl',
        },
    },
    defaultVariants: {
        variant: 'default',
    },
});
export interface HeaderProps
    extends React.BaseHTMLAttributes<HTMLDivElement>,
        VariantProps<typeof headerVariants> {}

const Header: React.FC<HeaderProps> = ({ className, variant, ...props }) => {
    return (
        <div className={cn(headerVariants({ variant, className }))} {...props}>
    <div className="container mx-auto h-14 flex items-center justify-between">
        <div>
            My First <span className="text-primary dark:text-pink-300">BLOG</span>
        </div>
        <div className="flex text-base gap-6 items-center">
    <Link className="text-primary hover:text-primary/90" href="/">
        首页
        </Link>
        <Link className="text-primary hover:text-primary/90" href="/posts">
        博客
        </Link>
        <ModeToggle />
        </div>
        </div>
        </div>
);
};
export { Header, headerVariants };
'use client';

import * as React from 'react';
import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
import { useTheme } from 'next-themes';

import { Button } from 'web/components/ui/button';
import {
    DropdownMenu,
    DropdownMenuContent,
    DropdownMenuItem,
    DropdownMenuTrigger,
} from 'web/components/ui/dropdown-menu';

export function ModeToggle() {
    const { setTheme } = useTheme();

    return (
        <DropdownMenu>
            <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
    <SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
    <MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
    <span className="sr-only">Toggle theme</span>
    </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end">
    <DropdownMenuItem onClick={() => setTheme('light')}>
    Light
    </DropdownMenuItem>
    <DropdownMenuItem onClick={() => setTheme('dark')}>
    Dark
    </DropdownMenuItem>
    <DropdownMenuItem onClick={() => setTheme('system')}>
    System
    </DropdownMenuItem>
    </DropdownMenuContent>
    </DropdownMenu>
);
}

在src/app/layout.tsx上添加我们的layout

点击主题切换的时候我们会发现没有作用,这个时候我们要往layout.tsx添加一个theme-provier

// web/src/components/provider/theme-provider.tsx
    'use client';

import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { type ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
    return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
// web/src/web/layuot.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { DefaultLayout } from 'web/src/components/layout/default';
import { ThemeProvider } from 'web/src/components/provider/theme-provider';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
    title: 'Create Next App',
    description: 'Generated by create next app',
};

export default function RootLayout({
                                       children,
                                   }: {
    children: React.ReactNode;
}) {
    return (
        <html suppressHydrationWarning>
        <body>
            <ThemeProvider
                attribute="class"
    defaultTheme="system"
    enableSystem
    disableTransitionOnChange
    >
    <DefaultLayout>{children}</DefaultLayout>
    </ThemeProvider>
    </body>
    </html>
);
}

 现在我们再点击一下切换dark主题就可以切换成暗黑模式了,至此ui组件以及主题切换也完成了。下面我们可以开始完成主要页面的开发了,因为nextjs已经集成了路由模式,创建文件就能创建页面

下面我们先看一下我们的首页设置成什么样子,然后分析一下页面需要什么组件

从图中可以看出,我们需要新增一个article-long-item.tsx组件,并且安装dayjs来显示发布时间到现在的时间。那我们下面看一下详细代码。

// 安装dayjs
    pnpm install dayjs -S

创建overlayLink组件

# overlayLink组件,因为MDN规范里,不推荐使用<a><a><a/><a/>模式,nextjs直接就会报错了,我们需要添加一个overlayLink组件,让这个卡片全局可以点击跳转到文章详情
import Link from 'next/link';
import { LinkProps } from 'next/dist/client/link';
import * as React from 'react';
import { cn } from 'web/src/lib/utils';

type OverlayLinkProps = LinkProps & {
    className?: string;
    children: React.ReactNode;
};
export const OverlayLink: React.FC<OverlayLinkProps> = ({
    children,
    className,
    ...rest
}) => {
    return (
        <Link {...rest} className={cn('plumjs-linkbox__overlay', className)}>
    {children}
    </Link>
);
};

export const LinkBox: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
    children,
    className,
    ...rest
}) => {
    return (
        <div {...rest} className={cn('plumjs-linkbox', className)}>
    {children}
    </div>
);
};

在components/ui创建article-long-item.tsx组件

'use client';
import { LinkBox, OverlayLink } from 'web/src/components/ui/overlayLink';
import { AspectRatio } from 'web/src/components/ui/aspect-ratio';
import Image from 'next/image';
import { addImageDomain } from 'web/src/lib/utils';
import Link from 'next/link';
import React from 'react';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
import relativeTime from 'dayjs/plugin/relativeTime';
import { IArticleDatum } from 'web/types';
dayjs.extend(relativeTime);
export const ArticleLongItem: React.FC<IArticleDatum> = (props) => {
    return (
        <LinkBox
            key={props.id}
    className="group shadow dark:shadow-primary text-md"
    >
    <div className="flex justify-items-stretch p-0 cursor-pointer">
    <div className="flex-shrink-0 basis-[300px] 2xl:basis-[400px]">
    <AspectRatio ratio={16 / 9} className="overflow-hidden ">
    <Image
        className="object-cover w-full h-full transition ease-in-out duration-500  group-hover:scale-[1.2]"
    width={props.attributes.cover.data.attributes.width}
    height={props.attributes.cover.data.attributes.height}
    src={addImageDomain(props.attributes.cover.data.attributes.url)}
    alt={props.attributes.cover.data.attributes.name}
    />
    </AspectRatio>
    </div>

    <div className="ml-4 flex-1 py-4 flex flex-col justify-between">
    <div className="flex flex-col gap-2 ">
    <OverlayLink href={`/posts/${props.id}`}>
    <div className="line-clamp-2 group-hover:text-primary text-2xl font-medium">
        {props.attributes.title}
        </div>
        </OverlayLink>
        <div className="line-clamp-2 text-[#999]">
        {props.attributes.desc}
        </div>
        </div>
        <div className="flex justify-between pr-4 items-center text-[#999] text-xs">
        <div>{dayjs(props.attributes.createdAt).fromNow()}</div>
    {props.attributes?.tags?.data &&
    props.attributes?.tags?.data.length ? (
            <div className="flex gap-4">
                {props.attributes.tags.data.map((tagItem: any) => {
                    return (
                <div
        className="px-2 shadow py-1 bg-primary rounded-2xl text-secondary transition hover:bg-primary/90 hover:text-secondary/50"
        key={tagItem.id}
        >
        <Link href={`/posts/tag/${tagItem.id}`}>
        {tagItem.attributes.name}
        </Link>
        </div>
    );
    })}
        </div>
    ) : null}
    </div>
    </div>
    </div>
    </LinkBox>
);
};

现在我们开始对接strapi的接口,我们开始完成首页,我们的接口域名跟图片域名都是可以配置的,因此我们创建一个.env文件,重启服务

APIBASEURL=http://127.0.0.1:1337
IMAGEDOMAIN=http://127.0.0.1:1337

修改next.config.js文件,创建global.d.ts

/** @type {import('next').NextConfig} */
const nextConfig = {
        env: {
            IMAGEDOMAIN: process.env.IMAGEDOMAIN,
            APIBASEURL: process.env.APIBASEURL,
        },
        eslint: {
            ignoreDuringBuilds: true,
        },
        images: {
            remotePatterns: [
                {
                    protocol: 'http',
                    hostname: '127.0.0.1',
                    port: '1337',
                },
            ],
        },
    };

module.exports = nextConfig;
declare namespace NodeJS {
    export interface ProcessOptions {
        browser: boolean;
    }

    export interface Global {
        PORT: any;
    }

    export interface ProcessEnv {
        APIBASEURL: string;
        IMAGEDOMAIN: string;
    }
}

然后修改page.tsx文件,我们的首页就制作完成

接下来我们继续创建博客列表页和博客详情页,博客列表

// web/src/components/ui/article-short-item.tsx
import { LinkBox, OverlayLink } from 'web/src/components/ui/overlayLink';
import { AspectRatio } from 'web/src/components/ui/aspect-ratio';
import Image from 'next/image';
import { addImageDomain } from 'web/src/lib/utils';
import Link from 'next/link';
import React from 'react';
import { IArticleDatum } from 'web/types';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export const ArticleShortItem: React.FC<IArticleDatum> = (props) => {
    return (
        <LinkBox
            key={props.id}
    className="group shadow dark:shadow-primary rounded-2xl overflow-hidden text-md"
    >
    <div className="flex justify-items-stretch p-0 cursor-pointer flex-col">
    <AspectRatio ratio={16 / 9} className="overflow-hidden rounded-2xl">
    <Image
        className="object-cover w-full h-full transition ease-in-out duration-500  group-hover:scale-[1.2] "
    width={props.attributes.cover.data.attributes.width}
    height={props.attributes.cover.data.attributes.height}
    src={addImageDomain(props.attributes.cover.data.attributes.url)}
    alt={props.attributes.cover.data.attributes.name}
    />
    </AspectRatio>

    <div className="ml-4 flex-1 py-4 flex flex-col justify-between">
    <div className="flex flex-col gap-2 ">
    <OverlayLink className="flex-shrink-0" href={`/posts/${props.id}`}>
    <div className="line-clamp-2 group-hover:text-primary text-2xl font-medium">
        {props.attributes.title}
        </div>
        </OverlayLink>
        <div className="line-clamp-2 text-[#999] group-hover:text-[#999/90]">
        {props.attributes.desc}
        </div>
        </div>
        <div className="flex justify-between pr-4 items-center  text-[#999] text-xs">
        <div>{dayjs(props.attributes.createdAt).fromNow()}</div>
    {props.attributes?.tags?.data &&
    props.attributes?.tags?.data.length ? (
        <div className="flex gap-4">
            {props.attributes.tags.data.map((tagItem) => {
                        return (
                    <div
                className="px-2 shadow py-1 bg-primary rounded-2xl text-secondary transition hover:bg-primary/90 hover:text-secondary/50"
                key={tagItem.id}
                >
                <Link href={`/posts/tag/${tagItem.id}`}>
            {tagItem.attributes.name}
            </Link>
            </div>
    );
    })}
        </div>
    ) : null}
    </div>
    </div>
    </div>
    </LinkBox>
);
};
// web/src/app/posts/page.tsx
import { addApiDomain } from 'web/src/lib/utils';
import { ArticleShortItem } from 'web/src/components/ui/article-short-item';
import { Metadata } from 'next';
import { IArticleData } from 'web/types';
export const metadata: Metadata = {
    title: '博客列表',
    description: '博客列表',
};
const getData: () => Promise<IArticleData> = async () => {
    const result = await fetch(
        addApiDomain('/api/articles?populate=*&sort[0]=publishedAt:desc'),
    );
    return await result.json();
};
const Home = async () => {
    const result = await getData();

    return (
        <>
            <div className="h-20 flex justify-center items-center">
        <div className="px-24 text-[40px] bg-gradient-to-r from-primary to-secondary text-transparent bg-clip-text">
            My First BLOG
    </div>
    </div>
    <div className="mt-8 grid grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-3">
        {result.data.map((item) => {
                return <ArticleShortItem key={item.id} {...item} />;
})}
    </div>
    </>
);
};
export default Home;
// 博客详情页
// web/src/app/posts/[id]/page.tsx
import { IArticleItemData } from 'web/types';
import { Metadata } from 'next';
import { addApiDomain } from 'web/src/lib/utils';
import { redirect } from 'next/navigation';

export interface IPageParams {
    params: {
        id: string;
    };
}
export const generateMetadata = async ({ params }: IPageParams) => {
    const resp = await getData(params?.id || '');
    const {
        data: { attributes },
    } = resp;
    const metadata: Metadata = {
        title: attributes.title,
        description: attributes.desc,
    };
    return metadata;
};
const getData: (id: string) => Promise<IArticleItemData> = async (
    id: string,
) => {
    const result = await fetch(addApiDomain(`/api/articles/${id}?populate=*`));
    if (result.status !== 200) {
        return redirect('/404');
    }

    return await result.json();
};
const PostPage = async ({ params }: IPageParams) => {
    let result = await getData(params.id);
    return (
        <>
            <div className="h-20 flex justify-center items-center">
        <div className="px-24 text-[40px] bg-gradient-to-r from-primary to-secondary text-transparent bg-clip-text">
            {result.data.attributes.title}
            </div>
            </div>
            <div
    className="w-full max-w-full prose lg:prose-lg h-full prose-img:rounded-xl prose-img:w-full prose-img:object-cover"
    dangerouslySetInnerHTML={{ __html: result.data.attributes.content }}
></div>
    </>
);
};
export default PostPage;
// web/src/app/posts/tag/[id]/page.tsx
// 页面是此标签下的所有文章

import { Metadata } from 'next';
import { addApiDomain } from 'web/src/lib/utils';
import { ArticleShortItem } from 'web/src/components/ui/article-short-item';
import { ITagArticleItemData } from 'web/types';
export interface IPageParams {
    params: {
        id: string;
    };
}

export const generateMetadata = async ({ params }: IPageParams) => {
    const resp = await getData(params?.id || '');
    const {
        data: { attributes },
    } = resp;
    const metadata: Metadata = {
        title: attributes.name,
        description: '标签列表',
    };
    return metadata;
};
const getData: (id: string) => Promise<ITagArticleItemData> = async (id) => {
    const result = await fetch(
        addApiDomain(
            `/api/tags/${id}?populate[articles][populate][0]=cover&populate[articles][populate][1]=tags`,
        ),
    );
    return await result.json();
};
const TagPostPage = async ({ params }: IPageParams) => {
    let result = await getData(params.id);
    return (
        <>
            <div className="h-20 flex justify-center items-center">
        <div className="px-24 text-[40px] bg-gradient-to-r from-primary to-secondary text-transparent bg-clip-text">
            {result.data.attributes.name}
            </div>
            </div>
            <div className="mt-8 grid grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-3">
        {result.data.attributes.articles.data.map((item) => {
                return <ArticleShortItem key={item.id} {...item} />;
})}
    </div>
    </>
);
};
export default TagPostPage;

至此我们的博客网站已经开发完成,可以愉快的添加文章了

项目链接

 

blog-project