编程导航博客话题讨论

博客

94 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

个人博客

花了两三天把博客搭建好了,欢迎大家进来参观访问,我会不定时的把我的学习笔记和面经都分享到这上面,感谢大家捧场! 个人博客网址:https://blog.cognipix.xyz 智能协同云图库上线网址:http://picture.cognipix.xyz

刚要开始自己搭建博客系统,结果发现一个开源的博客系统基于springboot+vue的,风格太对我胃口了,那就拿这个开源的来改一改 https://gitee.com/quequnlong/shiyi-blog

一个记录C++知识的学习网站

一个记录C++知识的学习网站! 网站地址:https://stibel.icu/ GitHub:https://github.com/klc407073648/cpp-notes 学习交流: https://stibel.icu/md/about/me/about-me.html <img src="https://pic.code-nav.cn/post_picture/1623823057367928834/hiTQxResUTvcHKwa.webp" alt="image.png" width="100%" />

CSDN博客

各位鱼友,本人最近正在撰写csdn博客, 网址:https://blog.csdn.net/weixin_74047328?type=blog ID:weixin_74047328,博客名:小孟Java攻城狮。 旨在学习过程中更好理解知识点,诚邀各位大佬一起学习进步,为我的文章批评指正,通过编程导航加我的鱼友可以给我发消息我们互关一起学习! <img src="https://pic.code-nav.cn/post_picture/1798559154020491265/tIbfxY7eues6B1lb.webp" alt="image.png" width="100%" />

# day03 ## 今日学习内容 - 背单词 - 草稿箱文章发布解决 - 文章更新部分逻辑处理 - 数据排序,按照是否发布 - 其他问题尚未解决 > 本来以为今天晚上时间够,结果下午上完课,老师通知晚上期中考试,没时间了就,明天下午还有两节实验课,希望明天能够把问题解决 ## 明日额外任务 - 四六级试卷 & 整理 - 不增加新任务,完成遗留任务 - 看 JavaScript 书复习知识

get 请求在 Apifox 上面可以跑通,但是前端调用后端接收不到参数?

## 个人博客文章接口接入出现的问题 本来想着今天把博客的文章接口全部接入进去,结果出师不利,上来就是一个错误。 看一下错误: ### 前端报错 <img src="https://pic.code-nav.cn/post_picture/1764136935309303810/hKfpK7HEPWq2JBiX.webp" alt="file_1733305578434_232.png" width="100%" /> ### 后端报错 <img src="https://pic.code-nav.cn/post_picture/1764136935309303810/2B6KSfSgGFjT5Nmu.png" alt="image.png" width="100%" /> 记得当时写接口的时候跑通了啊。。。 然后就不服了,就去`apifox`跑了一下,哎呦,还是通的 <img src="https://pic.code-nav.cn/post_picture/1764136935309303810/DjIaivDnPeWukKV2.webp" alt="image.png" width="100%" /> 我寻思着参数我传的是数字啊,为啥出现`skip`报错,我就去后端打印了一下参数,结果`undefind`,好家伙直接没传过来。 然后测试了获取文章详情的接口,没问题。头大。。。 然后看到这样一篇文章,https://juejin.cn/post/7283367128195055651 最后无奈,只能把`get` 请求为了`post`请求,数据也返回了。 做下记录,希望大家写接口的时候注意一下这个问题。

【Artalk】一文教会你部署整合博客评论功能

# 前言 > 注意:在看本篇文章操作之前需要先准备一台**服务器**哦! 在写博客之前我也写过一些文章,觉得笔记和博客的区别就在于后者可以有更多的和读者互动的功能,评论就是中间很重要一个互动渠道。 本文采用 **Hexo + Butterfly + Artalk + 一台服务器**实现评论功能接入博客。该方案使用的 Artalk 和 Disqus、Gitalk 等其他评论框架不同的地方在于,Artalk 采用私有部署,在国内的服务器响应会更快。效果如下: <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/l5KfGLDtFsWB9mYi.webp" alt="image-20241118182842873" width="100%" /> # Artalk 部署 ## 介绍 Artalk 是一款简单易用但功能丰富的评论系统,你可以开箱即用地部署并置入任何博客、网站、Web 应用。[👋 Hello Friend | Artalk](https://artalk.js.org/zh/guide/intro.html) 在我看来 Artalk 是一款轻量小巧但又不缺乏功能的系统,包括但不限于:社交登录、邮件通知、多元推送、图片上传、评论审核、markdown等等(*以上来自官方文档介绍*) ## Docker 部署 Artalk 提供多种部署方式[📦 程序部署 | Artalk](https://artalk.js.org/zh/guide/deploy.html),我觉得其中最简单就是 Docker 了,一个命令直接解决。 首先你需要在服务器上安装一个 Docker(这我就不详细介绍了,没有得小伙伴可以去网上搜搜)。 然后新建一个文件夹用于存放 Artalk 文件(`/root/artalk` ),然后执行下面的命令,只需要修改中文提示的地方: ```bash cd artalk docker run -d \ --name artalk \ -p 服务器端口:23366 \ -v $(pwd)/data:/data \ -e "TZ=Asia/Shanghai" \ -e "ATK_LOCALE=zh-CN" \ -e "ATK_SITE_DEFAULT=站点名" \ -e "ATK_SITE_URL=站点URL" \ --network artalk \ artalk/artalk-go ``` > 服务器端口:需要开放该端口的防火墙,是服务器的本机实际端口 > > 站点名:Artalk 中可以部署多个站点(可以区分不同项目),你只需要为当前的这个项目写一个站点名即可。 > > 站点URL:连接到站点的 URL,如果有域名就写域名,没有域名就写服务器IP+端口,比如:http://12345:上面的服务器端口 或 https://hello-life.com(后面会教大家获取https证书)。 接着输入命令创建管理员账户: ```bash docker exec -it artalk artalk admin ``` 浏览器输入 `http://站点URL` 进入 Artalk 后台登录界面。 <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/8x51z4IDlKa8UX6T.webp" alt="image-20241118184907846" width="100%" /> 至此,你就完成了 Artalk 的后端搭建,很简单吧!Artalk 会在前面指定的文件夹生成几个文件,其中 artalk.yml 用于保存配置,我们可以直接在管理页面进行修改,不用修改文件。 <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/ACRdBHCAqQKltXD9.png" alt="image-20241118185036875" width="209px" /> ## 配置数据库 在使用前需要配置好数据库(存放评论、用户等信息),我使用的是 MySQL,可以参考如下: <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/wlPI4MN7Im1QP9Y4.webp" alt="image-20241118233843584" width="100%" /> 注意:如果 MySQL 也是使用的 Docker 部署,只需要将其和 Artalk 加入同一个 Docker 网络,配置中直接写容器名即可。 ## 建议 如果你前端和服务器**都采用 http**,没有做 https 认证,可以直接跳到 Hexo 整合部分,进行配置。 # SSL 证书配置 ## 介绍 Artalk 后端默认采用 http,如果前端是 https 的话,在请求 Artalk 时就会出现混合内容出错(*现代浏览器对 HTTPS 页面加载 HTTP 资源(如图片、脚本、CSS 等)视为不安全行为*),比如报错如下: ```text Mixed Content: The page at 'https://example.com' was loaded over HTTPS, but requested an insecure resource 'http://example.com/script.js'. This request has been blocked; the content must be served over HTTPS. ``` 我们就需要为 Artalk 配置一个有效的 SSL 证书,帮助他变成 HTTPS。 在这之前你可能需要 **一个域名**。网上有很多方法获取免费域名,这里我就用我在腾讯云买的一个域名做演示。 ## Let's Encrypt 申请免费证书 **Let’s Encrypt** 是一个免费、自动化、开放的证书颁发机构。简单的说,借助 Let's Encrypt 颁发的证书可以为我们的网站免费启用 HTTPS(SSL/TLS) 。 Let’s Encrypt 的证书有效期为 **90 天**,需要定期续期。 ## 安装 Certbot Certbot 是一个命令行工具,用于自动化整个 SSL 证书的管理流程。 ### Linux 首先查看自己的服务器是哪种系统。 Ubuntu/Debian: ```bash apt update apt install certbot ``` CentOS/RHEL: ```bash yum install epel-release yum install certbot ``` ### 其他系统 参考 Certbot 官网:[Certbot](https://certbot.eff.org/) <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/Ib52YujkOCKQyyuu.webp" alt="image-20241118210006804" width="100%" /> ## 单域名 SSL 证书 如果你只需要为单个域名(如 `example.com` 或 `www.example.com`)申请证书,可以使用下面命令 ```bash certbot --nginx -d example.com -d www.example.com ``` 这需要服务器首先安装好 Nginx,Certbot 获取到证书后会自动修改 Nginx 的配置将证书装在 nginx 上。 你也可以选择使用手动申请证书的方式: ```bash sudo certbot certonly --manual -d example.com ``` Certbot 会提示使用 HTTP-01 验证,要求你在站点根目录创建一个特定的文件,用于验证域名的所有权。示例步骤: - 提示以下内容 ```bash Create a file containing the following content: ABC12345... And make it available on your web server at: http://example.com/.well-known/acme-challenge/XYZ12345... ``` - 在你的站点目录(根据 Nginx 域名配置的指定文件夹,如 `root /usr/share/nginx/html/域名`)下创建 `.well-known/acme-challenge` 目录。 - 完成验证之后, Certbot 就会生成证书 - 证书文件:`/etc/letsencrypt/live/example.com/fullchain.pem` - 私钥文件:`/etc/letsencrypt/live/example.com/privkey.pem` ## 泛域名 SSL 证书 泛域名证书可以为同一主域名下的所有子域名提供 HTTPS 支持。例如,`*.example.com` 可以覆盖 `blog.example.com`、`api.example.com` 等子域名。 Let’s Encrypt 要求通过 **DNS-01** 验证来申请泛域名证书,运行下面的命令: ```bash sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com ``` 运行命令后,Certbot 会生成类似下面的信息: ```yaml Please deploy a DNS TXT record under the name: _acme-challenge.example.com with the following value: AbCdEfGhIjKlMnOpQrStUvWxYz1234567890 ``` 这时候前往你的**域名 DNS 管理页面**,添加 TXT 记录,如下: <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/YwXvr7YGFLzMZ60a.png" alt="image-20241118235308700" width="100%" /> 注意主机记录名就是 `_acme-challenge` 不需要加后面的域名,Certbot 可能会连续要求两次添加,就像图片里一样,每次添加完按 `Enter` 即可。最后等待几十秒,确保 DNS 传播开来,就会颁发证书到下列位置,这是可以删除 DNS 解析记录了。 - **证书文件**:`/etc/letsencrypt/live/example.com/fullchain.pem` - **私钥文件**:`/etc/letsencrypt/live/example.com/privkey.pem` ## Artalk SSL 配置 Artalk 管理页面提供 SSL 直接配置,如果你不需要采用 Nginx 代理,可以直接在这里配置。 <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/s08MGVMG4wjsjx8h.png" alt="image-20241118223041622" width="100%" /> 由于我们前面使用的是 Docker 部署的 Artalk,同样需要将外部的证书文件映射到容器内部,可以参考 **第4.2节-Docker 容器重新挂载数据卷**。 # Nginx 配置 如果你只有一个服务器,并且需要转发请求到多个项目上,可以使用 Nginx 进行反向代理转发到 Artalk。 ## 配置文件 注意下面配置文件中的域名要和 Artalk 部署时的 **站点URL** 相同。 ```shell # Artalk 评论 # 配置 HTTP 重定向到 HTTPS server { listen 80; server_name 域名; return 301 https://$host$request_uri; } # 配置 HTTPS 服务 server { listen 443 ssl; server_name 域名; ssl_certificate /etc/nginx/certificates/fullchain.pem; ssl_certificate_key /etc/nginx/certificates/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://Artalk容器名:23366/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` 关于已有 Nginx 容器如何加入证书数据卷的问题看下一节。 ## Docker 容器重新挂载数据卷 如果你是使用的 Docker 部署的 Nginx,由于配置文件中需要引用到证书文件,但是证书文件又在容器外面,这时候就需要重新挂载数据卷了。 Docker 不支持已有容器修改数据卷,我们需要重新开启一个容器。不用担心,容器删除,原先挂在的数据卷本地还会存在,你只需要停止、删除原先 Nginx 容器,重新挂载启动一个即可: ```bash docker stop nginx docker rm nginx docker run -d \ --name nginx \ -p 80:80 -p 443:443 \ -v /etc/letsencrypt/live/example.com:/etc/letsencrypt/live/example.com:ro \ -v /etc/letsencrypt/archive/example.com:/etc/letsencrypt/archive/example.com:ro \ -v /path/to/nginx/conf:/etc/nginx/conf.d \ nginx ``` 如果不放心的话,你可以提前备份一个当前容器的配置: ```bash docker commit <容器名称或容器ID> nginx-backup ``` 此命令会创建一个名为 `nginx-backup` 的镜像,以备恢复。 ## 跨域问题踩坑 假设博客的网址为 `https://www.example.com`,在请求 Artalk `https://artalk.example.com` 时,会出现跨域问题如下: <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/OHf1Dp5apbgLwy0e.png" alt="image-20241118230131919" width="100%" /> 这是因为在 `https://www.example.com` 页面请求一个不同源的资源引起的。跨域问题指的是当一个网页发起的请求与当前网页所在的源(协议、域名和端口)不同而被浏览器阻止的现象。这种限制是由浏览器的**同源策略(Same-Origin Policy,SOP)**引起的,它是一种重要的安全机制,旨在防止恶意网站窃取或篡改用户的敏感数据。 在这里我们可以采用配置配置 CORS 的方法,通常会在 Nginx 中配置,但是 Artalk 后端提供设置可信域名,在文档中介绍其是通过控制 `Access-Control-Allow-Origin` 实现的,也就是会影响跨域。 <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/hiojqEsoLBcLsnhm.webp" alt="image-20241118232540636" width="100%" /> 经实践检验,如果在 **Nginx 配置跨域,Artalk 不设置**,在请求一些类似于上传图片,发送评论的 api 时会出现重复 CORS 的情况,所以 Nginx 还是采用上面的配置,**只在 Artalk 设置跨域!!** <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/CcLw0k4fY8ty4qs0.png" alt="image-20241118232723042" width="100%" /> # 置入博客 Artalk 官方对置入博客提供文档(如果没有用到下面提到的主题,直接看文档即可):[置入博客 | Artalk](https://artalk.js.org/zh/develop/import-blog.html)。 这里采用 Hexo + ButterFly 主题 + Artalk 实现评论功能。首先打开 ButterFly 主题配置文件进行如下配置(如:`_config.butterfly.yml`)。 <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/GsYt99DXSBQ7BZ6S.png" alt="image-20241118233529132" width="100%" /> 在同文件中启用评论,并修改使用对象为 Artalk。 <img src="https://pic.code-nav.cn/post_picture/1641343494503059458/34qpmUSuUVOxFUGI.webp" alt="image-20241118233617519" width="100%" /> 至此评论功能就引入成功了! # 踩坑和解决方案 - 跨域问题? - 参考4.3节-跨域问题踩坑。 - 修改 Artalk 数据库配置失败,导致容器崩溃? - 需要先创建好数据库,Artalk 只能检测到已有数据库自动创建表,不能自动创建数据库。 - 评论显示出来了,但是提交评论或者名字为中文导致提交失败? - 这是因为 Artalk 在自动生成表时,虽然配置里写了utf8mb4,但是表结构却不是,不支持中文,需要手动修改 MySQL 表结构。 *最后!如果觉得文章对你有帮助的话,不妨来我的博客 [Hello-Life](https://www.guershi.cn) 来看看吧~*

分享一些常用的开源博客社区网站

> 一般查找技术之类的问题搜索,使用中文搜索引擎(百度),最常见的问题就会搜索到这些社区博客网站之类的。下面就分享个我经常使用的博客社区。 >不得不说,谷歌搜索确实更略胜一筹。一般搜技术类问题(程序之类的)一般是搜到 Stack Overflow或Github 上的issue。 > 以下社区都支持 Markdown 格式编写。一般我最常用的是在CSDN写博文(相较于其他平台的编写较为方便些) 1. **CSDN**:毫无疑问在百度搜索中出现次数最多的社区之一。一般技术类问题的解决方法都在这上面看得到,与百度深度的绑定。(有中文版的Stack Overflow之称呼!),最后推荐下我博客的主页,觉得不错的小伙伴可以关注下!!! [https://xdr630.blog.csdn.net/](https://xdr630.blog.csdn.net/) 2. **博客园**:同样和CSDN有着不相上下的开源社区,用户还可以自定义博客的css、js样式之类的装饰,让人瞬间高大上的感觉(前提是你前端学的好)。 [https://www.cnblogs.com/xdr630/](https://www.cnblogs.com/xdr630/) 3. **简书**:属于后起之秀,近年来也吸引了不少用户。文章排版简洁明了,给人一种非常舒服的感觉,最重要的发布的文章还可以打包好一键下载,也是一个不错的平台之一。 [https://www.jianshu.com/u/f186fb1bcb90](https://www.jianshu.com/u/f186fb1bcb90) 4. **开源中国**:想必大家早就耳闻目染了,gitee就是开源中国旗下的产品,有着中国版的github之称的gitee(中文又叫码云),几乎和github功能差不多,大家可以把自己写的项目什么的托管到这上面。开源中国的博客社区确实还不错,包涵技术类、资讯类、工具类的信息。 [https://my.oschina.net/xdr630](https://my.oschina.net/xdr630) 5. **51CTO**:这个技术型的博客社区也好早就成立了,和CSDN差不多类似,博客社区啥的和在线学院,可以去上面在线学习些免费的教程,也是最近,我通过搬家功能,把CSDN的文章全部搬到那边去了,对CSDN这边没影响,就是把自己的文章的曝光率提高下。 [https://blog.51cto.com/xdr630](https://blog.51cto.com/xdr630) 6. **思否(segmentfault)**:这个社区才是名副其实的中国版Stack Overflow(开玩笑),问答功能确实做的不错,也有在线课程啥的,但感觉人气没有前面几位“火”,希望以后发展的更好! [https://segmentfault.com/u/xdr630](https://segmentfault.com/u/xdr630) 7. **掘金**:近年来比较火的社区之一,现在已经被字节收购了,社区还在不断成长中,最近上线的创作中心也还不错,社区排版也还清爽,虽然没有前面几位历史悠久,但需要的是茁壮成长。 [https://juejin.cn/user/3491704662668878](https://juejin.cn/user/3491704662668878) > 以下社区是云厂商下的云社区平台,也可以发表些对厂商产品的提问或使用教程啥的。 - **华为云开发者社区**:[https://bbs.huaweicloud.com/community/usersnew/id_1565860397808771](https://bbs.huaweicloud.com/community/usersnew/id_1565860397808771) - **腾讯云开发者社区**:[https://cloud.tencent.com/developer/column/90924](https://cloud.tencent.com/developer/column/90924) - **阿里云开发者社区**:[https://developer.aliyun.com/profile/expert/bjq2ogst3ulqs](https://developer.aliyun.com/profile/expert/bjq2ogst3ulqs)

使用next.js搭建一个免费的博客网站

# 使用next.js搭建一个免费的博客网站 你好!这是我用Next.js搭建的第一个项目[blogs💡](https://blogs-sable-zeta.vercel.app),访问🪜[原作者视频地址](https://www.youtube.com/watch?v=y7JCnfbETPs) <img src="https://pic.code-nav.cn/post_picture/1623235832938774529/3igyyH2hxQwvGzDt.webp" alt="Pasted image 20241012150557.png" width="100%" /> >完成项目的过程中,我掌握了 `react`,`next.js`,`typeScript`,`tailwindcss`,[`prisma(数据库)`](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices) [`kinde(登录授权)`](https://kinde.com),[`vercel(部署)`](https://vercel.com/)的部分知识🍻 >搭建部署过程我梳理并总结成了文档,希望你能喜欢 ~ ## 一. 搭建过程 ### 1. 创建并初始化项目 ##### 1.1 创建一个`my-blogs`的文件夹,在`my-blogs`中创建next.js应用: ``` npx create-next-app@14.2.3 . ``` 或最新版本。 ``` npx create-next-app@latest . ``` 选择的一些配置。 ``` √ Would you like to use TypeScript? ... Yes √ Would you like to use ESLint? ... Yes √ Would you like to use Tailwind CSS? ... Yes √ Would you like to use `src/` directory? ... Yes √ Would you like to use App Router? (recommended) ... Yes √ Would you like to customize the default import alias (@/*)? ... No ``` ##### 1.2 初始化部分文件: ```tsx // src\app\page.tsx export default function Page() {   return (     <div>       Page     </div>   ) } ``` ```css /* src\app\globals.css */ @tailwind base; @tailwind components; @tailwind utilities; ``` ### 2. 组件化,布局,响应式容器,首页,底部,头部实现 > 对next.js项目结构不熟悉可以查看文档 [Project Structure](https://nextjs.org/docs/getting-started/project-structure)。 > 在编写tailwindcss时推荐插件:Tailwind CSS IntelliSense,Tailwind Snippets。 ##### 2.1 组件化: 将响应式容器,头部,底部独立成组件,创建目录。 ``` src\components\container.tsx src\components\header.tsx src\components\footer.tsx ``` ##### 2.2 布局: ```tsx // src\app\layout.tsx import type { Metadata } from "next"; import { Inter } from "next/font/google"; import Header from "../components/header"; import Footer from "../components/footer"; import Container from "../components/container"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); //网页字体 export const metadata: Metadata = { //静态元数据   title: "My Posts",   description: "Generated by create next app", }; export default function RootLayout({   children, }: Readonly<{   children: React.ReactNode; }>) {   return (     <html lang="en">       <body className={`${inter.className} bg-zinc-100 text-zinc-900 `}>         <Container>           <Header/>           {children}           <Footer/>         </Container>       </body>     </html>   ); } ``` ##### 2.3 响应式容器: ```tsx // src\component\container.tsx export default function Container({ children }: Readonly<{ children: React.ReactNode }>) {     return (         <div className="max-w-[1100px] mx-auto bg-white flex flex-col min-h-screen border-l border-r">             {children}         </div>     ) } ``` ##### 2.4 首页: ```tsx // src\app\page.tsx export default function Page() {   return (     <main className="text-center pt-32 px-5">       <h1 className="text-4xl md:text-5xl font-bold mb-5">欢迎来到我的博客!</h1>       <p className="max-w-[750px] mx-auto leading-8">         Lorem ipsum dolor sit amet consectetur         adipisicing elit. Ducimus,a         natus? Dolores veritatis perferendis         non doloribus numquam praesentium         aliquid tempora qui similique,         exercitationem distinctio labore culpa         nam natus consequuntur rem!       </p>     </main>   ) } ``` ##### 2.5 底部: ```tsx // src\component\footer.tsx export default function Footer() {     return (         /* 当前Footer是在一个垂直方向的Flexbox容器中,mt-auto (margin-top:auto),可以将footer推到容器的底部。 */         <div className="mt-auto text-center text-zinc-400 py-5 px-7 border-t">             <small>&copy; 2024. All Rights Reserved.</small>         </div>     ) } ``` ##### 2.6 头部: >Image标签中src放的地址可能会出现==next-image-unconfigured-host==提示,原因在next.js官方文档可以找到[`next/image` Un-configured Host](https://nextjs.org/docs/messages/next-image-unconfigured-host),解决方法是在next.config.mjs中增加Image配置。 ```js // next.config.mjs /** @type {import('next').NextConfig} */ const nextConfig = {     images: {         remotePatterns: [             {                 protocol: 'https',                 hostname: 'bytegrad.com'             }         ]     } }; export default nextConfig; ``` ```tsx // src\component\header.tsx 'use client' //当前页面是客户端组件 import Image from "next/image" //next.js提供的image组件 import Link from "next/link"; //next.js提供的路由组件 import { usePathname } from "next/navigation"; //通过usePathname获取当前页面路由地址 const navLinks = [     {         href: '/',         label: 'Home'     },     {         href: "/posts",         label: "Posts"     },     {         href: '/create-post',         label: "Create post"     } ] export default function Header() {     const pathname = usePathname();     return (         <header className="flex justify-between items-center py-4  px-7 border-b">             <Link href='/'>                 <Image                     src="https://bytegrad.com/course-assets/youtube/example-logo.png"                     alt='logo'                     className="w-[35px] h-[35px]"                     width='35'                     height='35'                 />             </Link>             <nav>                 <ul className="flex gap-x-5 text-[14px]">                     {                         navLinks.map((link) => (                             <li key={link.href}>                                 <Link                                     className={`font-bold ${pathname === link.href ? 'text-zinc-900' : "text-zinc-400"                                         }`}                                     href={link.href}                                 >                                     {link.label}                                 </Link>                             </li>                         ))                     }                 </ul>             </nav>         </header>     ) } ``` ### 3. Posts页面,通过动态路由进入post详情,Create Post页面 ##### 3.1 创建posts页面(文档 [Pages](https://nextjs.org/docs/app/building-your-application/routing/pages)) 点击标题进入详情页面时,通过标题对应的id进入到子路由页面 ``/posts/${post.id}``,每次点击标题的id都是不固定的,所以接下来需要用到动态路由。 ```tsx // src\app\posts\page.tsx import Link from "next/link"; export default async function Posts() {     const response = await fetch('https:dummyjson.com/posts?limit=10');     const { posts } = await response.json()     return (         <main className="text-center pt-16 px-5">             <h1 className="text-4xl md:text-5xl font-bold mb-5">all posts</h1>             <ul >                 {posts.map((post: { id: number, title: string }) => (                     <li key={post.id} className="mb-3">                         <Link href={`/posts/${post.id}`} className="text-2xl font-bold">{post.title}</Link>                     </li>                 ))}             </ul>         </main>     ) } ``` ##### 3.2 通过 [动态路由](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes)进入post详情 ```tsx // src/app/posts/[id]/page.tsx export default async function Page({ params }: { params: { id: string } }) {     const response = await fetch(`https://dummyjson.com/posts/${params.id}`);     const post = await response.json();     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) } ``` 这样,就可以动态获取到每个posts标题对应的内容了。此时会有一些小问题:1.id不存在时如何处理。2.详情页面渲染等待时卡顿。 优化:加入404页面,loading页面。 ```tsx // src/app/posts/[id]/page.tsx import { notFound } from "next/navigation"; export default async function Page({ params }: { params: { id: string } }) { await new Promise((resolve) => setTimeout(resolve, 1000));//不会立即完成。会等待1秒钟后调用resolve函数     const response = await fetch(`https://dummyjson.com/posts/${params.id}`);     const post = await response.json();     //id不存在,跳转到404页面     if (!post.id) {         notFound();     }     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) } ``` ```tsx // src/app/posts/[id]/not-found.tsx export default function NotFound() {     return (         <div>             No post found for that id.         </div>     ) } ``` ```tsx // src/app/posts/[id]/loading.tsx export default function Loading() {     return (         <div className="text-center mt-5">             Loading...         </div>     ) } ``` 最后将Posts中标题列表独立成单独的组件。 ```tsx // src/components/posts-list.tsx import Link from "next/link"; export default async function PostsList() {     const response = await fetch('https:dummyjson.com/posts?limit=10');     const { posts } = await response.json()     return (         <ul >             {posts.map((post: { id: number, title: string }) => (                 <li key={post.id} className="mb-3">                     <Link href={`/posts/${post.id}`} className="text-2xl font-bold">{post.title}</Link>                 </li>             ))}         </ul>     ) } ``` ```tsx // src/app/posts/page.tsx import PostsList from "@/components/posts-list"; import { Suspense } from "react"; export default function Posts() {     return (         <main className="text-center pt-16 px-5">             <h1 className="text-4xl md:text-5xl font-bold mb-5">all posts</h1>             {/* 当ui尚未加载时显示loading */}             <Suspense fallback="Loading...">                 <PostsList />             </Suspense>         </main>     ) } ``` ##### 3.3 Create Post页面 ```tsx // src\app\create-post\page.tsx import Form from '@/components/form'; export default async function CreatePost() {     return (         <main className="text-center pt-16">             <h1 className='text-4xl md:text-5xl font-bold mb-5'>Create post</h1>             <Form />         </main>     ) } ``` ```tsx // src\components\form.tsx import { createPost } from "@/actions/actions"; export default function Form() {     return (         <form         action={createPost}             className="flex flex-col max-w-[400px] mx-auto space-x-2 gap-2 my-10"         >             <input                 name="title"                 type='text'                 placeholder="Title for new post"                 className="border rounded px-3 h-10"                 required             />             <textarea                 name="body"                 placeholder="Body content for new post"                 className="border rounded px-3 py-2"                 rows={6}                 required             />             <button className="h-10 bg-blue-500 px-5 rounded text-white">Submit</button>         </form>     ) } ``` ### 4. 用[prisma](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices)数据库存放数据 ##### 4.1 安装prisma 安装依赖。 ``` npm i prisma -save-dev ``` 初始化数据库后会在根目录生成一个 `prisma`文件夹和 `.env`文件。 ``` npx prisma init --datasource-provider sqlite ``` 定义一个Post模型。 ```prisma // prisma\schema.prisma // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client {   provider = "prisma-client-js" } datasource db {   provider = "sqlite"   url      = env("DATABASE_URL") } //定义一个post模型 model Post {   id       Int      @id @default(autoincrement())   title    String   body     String   createAt DateTime @default(now())   updateAt DateTime @updatedAt } ``` 更新数据库,成功后prisma文件夹内会生成一个`dev.db`文件。 ``` npx prisma db push ``` 查看数据库UI,成功后进入 http://localhost:5555 ,点击post进入查看刚才创建的数据。 ``` npx prisma studio ``` ##### 4.2 使用prisma 在actions中配置请求。 ```ts // src\actions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; export async function createPost(formData:FormData) {     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       }) } ``` ```ts // src\lib\db.ts import { PrismaClient } from '@prisma/client' const prismaClientSingleton = () => { // 创建一个新的 Prisma Client 实例,以便后续与数据库交互。   return new PrismaClient() } declare const globalThis: {   prismaGlobal: ReturnType<typeof prismaClientSingleton>; } & typeof global; const prisma = globalThis.prismaGlobal ?? prismaClientSingleton() // 确保在整个应用程序中只有一个 Prisma Client 实例,避免了多个实例同时存在导致的连接问题 export default prisma if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma // 如果当前环境不是生产环境,则将 prisma 实例赋值给 globalThis.prismaGlobal ``` 此时,在create post页面中输入内容并提交后,在 http://localhost:5555 中查看数据是否已经写入进prisma。 接下来对posts-list列表页,post详情页获取数据的方法进行改造。 ```tsx // src\components\posts-list.tsx import prisma from "@/lib/db"; import Link from "next/link"; export default async function PostsList() {     await new Promise((resolve) => setTimeout(resolve, 1000));     const posts = await prisma.post.findMany();     return (         <ul>             {posts.map((post) => (                 <li key={post.id} className="mb-3">                     <Link href={`/posts/${post.id}`} >{post.title}</Link>                 </li>             ))}         </ul>     ) } ``` ```tsx // src\app\posts\[id]\page.tsx import prisma from "@/lib/db"; import { notFound } from "next/navigation"; export default async function Page({ params }: { params: { id: string } }) {     await new Promise((resolve) => setTimeout(resolve, 1000));//不会立即完成。会等待1秒钟后调用resolve函数     const post = await prisma.post.findUnique({         where: {             id: parseInt(params.id)         }     })     if (!post) {         notFound();     }     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) } ``` ##### 4.3 优化页面未及时更新 此时有一个问题需要解决:当在create post中创建数据后,切换到posts页面时,标题列表中的数据没有更新到最新创建的数据。[解决方法](https://nextjs.org/docs/app/api-reference/functions/revalidatePath):每次访问 `/posts` 路由时都刷新缓存。 ```tsx // src\sctions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; import { revalidatePath } from "next/cache"; export async function createPost(formData:FormData) {     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       })     revalidatePath("/posts")//访问该路由的时候刷新缓存 } ``` ### 5. kinde授权登录 ##### 5.1 安装kinde和配置 如果我们希望登录的用户才能访问create post页面,可以用[kinde](https://kinde.com)进行登录授权。[视频教程 1:14:28](https://www.youtube.com/watch?v=y7JCnfbETPs) ``` 1. 点击 start for free 2. 点击 add application 3. 输入项目名 选择 Back-end web 后 save 4. Quick start 选择 Next.js 后 save 5. 点击 Existing codebase 后看到 Set callback URL, Set logout URL 点击 Set 6. 下载依赖 npm i @kinde-oss/kinde-auth-nextjs 7. 继续滚动页面到 Update environment vars 复制粘贴到根部录下.env.local中,注意:未避免泄露信息,请不要将环境变量中的信息提交;检查.gitignore文件 8. 继续滚动页面到 API endpoints 创建目录 `src\app\api\auth\[kindeAuth]\route.js` 9. 选择 Authentication 选择 Social connenctions 中 登录的方式 后 save ``` ``` npm i @kinde-oss/kinde-auth-nextjs ``` ```js // src\app\api\auth\[kindeAuth]\route.js import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server"; export const GET = handleAuth(); ``` ##### 5.2 使用kinde 需要用到 [中间件](https://docs.kinde.com/developer-tools/sdks/backend/nextjs-prev-sdk/?r=search#protect-routes-using-middleware),在每次访问 `Create post` 页面时验证是否有登录信息。 ```ts // src\middleware.ts import {withAuth} from "@kinde-oss/kinde-auth-nextjs/middleware"; import {NextRequest} from 'next/server'; export default function middleware(req:NextRequest) { // 定义一个中间件函数,接收请求对象 req   return withAuth(req, {     isReturnToCurrentPage: true // 设置选项,表示在认证后返回当前页面   }); } export const config = { // 导出配置对象   matcher: ["/create-post"]// 设置匹配路径,只有当请求路径为 "/create-post" 时才会应用该中间件 }; ``` 每次`/create-post`页面发起请求时进行验证是否登录授权。 ```tsx // src\actions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; export async function createPost(formData:FormData) {     /* 每次发起请求时,都进行验证是否授权 */     const {isAuthenticated} = getKindeServerSession();     if(!(await isAuthenticated())){         redirect("/api/auth/login");     }     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       })     revalidatePath("/posts")//访问该路由的时候刷新缓存 } ``` 加入logout按钮。 ```tsx // src\app\create-post\page.tsx import { LogoutLink } from '@kinde-oss/kinde-auth-nextjs/components'; import Form from '@/components/form'; export default async function CreatePost() {     return (         <main className="text-center pt-16">             <h1 className='text-4xl md:text-5xl font-bold mb-5'>Create post</h1>             <Form />             <LogoutLink>logout</LogoutLink>         </main>     ) } ``` (可忽略)将环境变量统一放入一个文件夹内,将 `.env.local` 中内容复制粘贴到 `.env` 中,只保留 `.env` , 注意请在.gitignore中配置不要将.env文件提交到远程仓库。 ## 二. 部署过程 >将代码部署到 [vercel](https://vercel.com):先把代码提交到github仓库,然后在vercel中导入项目进行部署。(在首次部署的过程中会出现以下问题。 [视频教程1:56:10](https://www.youtube.com/watch?v=y7JCnfbETPs)) ##### prisma数据库问题 在package.json中 `"scripts"` 中加入。 ``` "postinstall":"prisma generate" ``` 进入vercel中刚才导入的项目,选择Storge,创建一个用于生产的数据库。 ``` - 选择 Postgres 后 Create - Accept --> Create - 进入新建的数据库 - 选择prisma 复制粘贴到本地 schema.prisma 中替换掉 datasource db 对应代码 - 选择.env.local 复制粘贴到本地 .env 中替换掉 DATABASE_URL - 终端将数据库模型同步到数据库 npx prisma db push - 再次将代码提交到github,此时vercel会自动部署刚才提交的代码。若没有自动部署也可以点进项目中选择Deployments 选择 Redeploy 重新部署,绿灯表示部署成功 ``` ##### 环境变量问题 在预览项目是,切换到post页面发现此时postlist数据为空,create post页面打不开,发现地址还是本地的,所以需要将URL地址替换成生产URL地址。 ``` - 选择 Project 复制 Domains 中 生产URL地址 - 选择 Settings 选择 Environment Variables - 选择 KINDE_SITE_URL 修改 value 替换成 https://你的生产地址 后 SAVE - 选择 KINDE_POST_LOGOUT_REDIRECT_URL 修改 value 替换成 https://你的生产地址 后 SAVE - 选择 KINDE_POST_LOGIN_REDIRECT_URL 修改 value 替换成 https://你的生产地址/create-post 后 SAVE ``` 还需要在kinde中配置退出登录后重定向地址。 ``` - 进入kinde中之前建好的项目内 - 选择 Details - 滚动至 Allowed callback URLs 加入地址 https://你的生产地址/auth/kinde_callback - 滚动至 Allowed logout redirect URLs 加入地址 https://你的生产地址 后 SAVE ``` 最后再部署一次,等到Status变绿后,此时你已经自己搭建并部署了你的博客🎉🎉🎉 如果我的文档对你有帮助,别忘了[Star⭐️~ ](https://github.com/MuscleProgrammerXiao/blogs) 我会更加积极的更新🍻

我搭建了一个博客,欢迎大家来围观 😝 https://pandaer.space/

下载 APP