前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >nextjs从零到一开发博客(万字长文)配合strapi

nextjs从零到一开发博客(万字长文)配合strapi

作者头像
用户6256742
发布2024-06-20 14:04:13
2720
发布2024-06-20 14:04:13
举报
文章被收录于专栏:网络日志

开发背景:

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

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

框架选择

SEO友好的前端框架-NextJS

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

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

家喻户晓的CSS框架-Tailwind CSS

包管理工具-pnpm

保姆级开发步骤

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

代码语言:javascript
复制
mkdir blog-project
# 创建目录 /Users/luke/Desktop/course/blog-project
cd blog-project
pnpm init

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

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

代码语言:javascript
复制
packages:
- "apps/*"

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
代码语言:javascript
复制
cd web
pnpm run dev
# 打开链接http://127.0.0.1:3000/,这个时候就可以打开我们启动的页面了
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

添加CMS管理后台

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

代码语言:javascript
复制
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.很简单,自己在安装一下这个包就行了

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

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

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

代码语言:javascript
复制
pnpm install @_sh/strapi-plugin-ckeditor -S --filter=cms
# 然后重新启动一下

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

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

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

添加api访问

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

我们现在去打开公共访问

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

整合项目

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

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

代码语言:javascript
复制
{
        "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文件修改一下

代码语言:javascript
复制
{
        "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目录里执行命令

代码语言:javascript
复制
pnpx shadcn-ui@latest init
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

代码语言:javascript
复制
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> ) }

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

代码语言:javascript
复制
@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%;
        }
}
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

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

代补匆页面

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

代码语言:javascript
复制
# 切换到web目录,首先添加next-theme包
pnpm install next-themes -S
# 然后添加shadcn/ui 的 dropdown-menu
pnpm dlx shadcn-ui@latest add dropdown-menu
代码语言:javascript
复制
// 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>
);
};
代码语言:javascript
复制
// 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 };

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 };
代码语言:javascript
复制
'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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

代码语言:javascript
复制
// 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>;
} 
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>;
} 
代码语言:javascript
复制
// 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已经集成了路由模式,创建文件就能创建页面

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

代码语言:javascript
复制
// 安装dayjs
    pnpm install dayjs -S

创建overlayLink组件

代码语言:javascript
复制
# 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组件

代码语言:javascript
复制
'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>

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




);
}; 
);
}; 

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

代码语言:javascript
复制
APIBASEURL=http://127.0.0.1:1337
IMAGEDOMAIN=http://127.0.0.1:1337

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

代码语言:javascript
复制
/** @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; 
module.exports = nextConfig; 
代码语言:javascript
复制
declare namespace NodeJS {
    export interface ProcessOptions {
        browser: boolean;
    }

    export interface Global {
        PORT: any;
    }

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

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

nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

代码语言:javascript
复制
// 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>
代码语言:javascript
复制
&lt;div className="ml-4 flex-1 py-4 flex flex-col justify-between"&gt;
&lt;div className="flex flex-col gap-2 "&gt;
&lt;OverlayLink className="flex-shrink-0" href={`/posts/${props.id}`}&gt;
&lt;div className="line-clamp-2 group-hover:text-primary text-2xl font-medium"&gt;
    {props.attributes.title}
    &lt;/div&gt;
    &lt;/OverlayLink&gt;
    &lt;div className="line-clamp-2 text-[#999] group-hover:text-[#999/90]"&gt;
    {props.attributes.desc}
    &lt;/div&gt;
    &lt;/div&gt;
    &lt;div className="flex justify-between pr-4 items-center  text-[#999] text-xs"&gt;
    &lt;div&gt;{dayjs(props.attributes.createdAt).fromNow()}&lt;/div&gt;
{props.attributes?.tags?.data &amp;&amp;
props.attributes?.tags?.data.length ? (
    &lt;div className="flex gap-4"&gt;
        {props.attributes.tags.data.map((tagItem) =&gt; {
                    return (
                &lt;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}
            &gt;
            &lt;Link href={`/posts/tag/${tagItem.id}`}&gt;
        {tagItem.attributes.name}
        &lt;/Link&gt;
        &lt;/div&gt;
);
})}
    &lt;/div&gt;
) : null}
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/LinkBox&gt;

); };

代码语言:javascript
复制
// 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;
代码语言:javascript
复制
// 博客详情页
// 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;
代码语言:javascript
复制
// 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;
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi
nextjs从零到一开发博客(万字长文)配合strapi

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

项目链接

blog-project

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开发背景:
  • 框架选择
  • 保姆级开发步骤
    • 创建项目文件夹,创建workspace环境
      • 在apps的目录执行命令创建NextJS的web项目
        • 添加CMS管理后台
        • 添加api访问
          • 整合项目
            • 博客默认布局以及暗黑模式开发
              • 项目链接
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档