使用 Strapi + PostgreSQL + Astro 快速构建博客
本文将介绍我博客搭建的过程,使用 PostgreSQL作为数据库,Strapi 快速构建 Api , Astro作为前端支撑。即使你不熟悉这些技术,通过本文也可以照着做出一个博客。通过本文你可以了解如下知识点:
- 如何较为快速的搭建出一个带后台编辑的博客系统
- 掌握
docker最基础的操作(不包含docker安装教程。当然,可以跳过这部分)。 - 了解
PostgreSQL(开源数据库)的部署、建表、建用户、权限(可跳过)。 - 了解
Navicat(图形化关系型数据库操作工具)基本使用(可跳过)。 - 了解如何快速通过
Astro(性能极好的前端框架)搭建博客。 - 掌握使用
Strapi(Handless CMS,类似于word press,不过我没用过word press,不太确定) 基础使用。
本文代码已上传到 GitHub github.com/huiboxes/bl…。
后端
安装并配置 PostgreSQL
这一步是可以省略的,因为 Strapi 默认用的 SQLite,对于小体量的博客肯定是够的。不过需要你本机安装有 Python 2.x 的环境。如果你时间充裕,还是建议用 PostgreSQL 或者 MySQL。
安装
拉取镜像:
docker pull postgres
创建容器:
docker run --name blog-postgres-db -e TZ=PRC -e POSTGRES_USER=root -e POSTGRES_DB=database -e POSTGRES_PASSWORD='GUsM$u6jX!ngk' -p 3996:5432 -v pgdata:/mydata/postgresql/data -d postgres
说明下这段命令的含义:
run,创建并运行一个容器(粗暴理解成是一个专门跑那个应用的独立服务器就行)。--name blog-postgres-db,容器名称设置为blog-postgres-db。-e TZ=PRC,时区设置为中国(默认为UTC,可能会存在时差)。-e POSTGRES_USER=root,将默认用户的用户名改为root(默认用户名为postgres)。-e POSTGRES_DB=database,DB模式为数据库模式。-e POSTGRES_PASSWORD='GUsM$u6jX!ngk',密码设置为GUsM$u6jX!ngk。-p 3996:5432,容器内端口是5432, 映射到本机的3996(后面直接访问本机的3996端口即可连接PostgreSQL服务)。-v pgdata:/mydata/postgresql/data,数据存储到本机的pgdata:/mydata/postgresql/data目录。-d,后台运行。
配置
此部分参考了阮一峰老师的 《PostgreSQL新手入门》。但是我是通过navicat 测试能够连接后直接就用 Navicat 敲命令了。
首先打开 Navicat,如下图操作:

接着会弹出一个窗口,如下填写,没有通过服务器部署的就填写本机 localhost。

连接成功后新建查询,依次之下下面的命令:
-- 创建 用户名为 "blog_dba" 的用户,设置密码为 "GUsM$u6jX!ngk"
CREATE USER blog_dba WITH PASSWORD 'GUsM$u6jX!ngk';
-- 创建 名为 "blog_cms" 的数据库,并指定所有者为 "blog_dba"。
CREATE DATABASE blog_cms OWNER blog_dba;
-- 将 "blog_cms" 数据库的所有权限都赋予 "blog_dba",否则 "blog_dba" 只能登录控制台,没有任何数据库操作权限
GRANT ALL PRIVILEGES ON DATABASE blog_cms to blog_dba;
下面是在 Navicat 操作的方法。

Strapi 构建
初始化
# 创建项目 , 项目名为blog_postgre,随便你取什么名字
npx create-strapi-app@4.3.2 blog_postgre
写这篇文章时 Strapi 最新版本是 4.3.6。但是最新版本初始化时存在问题(error: Unknown dialect undefined),网上看到出现同样问题的人是降级到 4.3.2 解决的。尽管不确定,但是我认为这可能就是版本问题导致。接下来就会让你选择,如下图中配置即可:

# 成功后打开文件夹并运行项目
cd blog_postgre
# 运行项目,我第一件事是在package.json的script选项中将 develop 改成了 dev,为了方便
yarn develop
此时,你可能会报下列错误:

出现上图中的错误,可以选择将项目目录中 config/database.ts 的 ssl: env.bool('DATABASE_SSL', true) 这一行的 true 改为 false(当然,这会造成安全隐患,不过这很方便)。
重新启动项目后将是下面的界面:

创建 Collection。
你可以把它当作是一种数据库中数据表的映射,跟着下面的流程走,你的思路将组件清晰。在 Content-Type Build中创建 Collection。

创建 Category ,存放分类
只需要在 Display name 中如下输入:

添加
点击 Continue 后,在新的页面选择 Text,表明创建一个文本型的字段。这里叫做 name ,表示分类名称。


点击 Finish 后就创建成功了,点击 Add another field 将添加分类表的其它字段。下图是在新弹出的界面中选择 UID 添加一个 slug 字段,每个分类都有一个独一无二的 slug。

最后点击 Finish 后点击 Save 保存刚刚的操作。
。
创建 Post ,存放文章
首先创建关系(前面创建 Post 这个 Collection 的步骤没有放上来):
选择 Relation 类型,输入 categories ,因为一个文章可以有多个类型,每个类型可以对应多个文章(多对多),所以是如下的创建方式:

其它字段根据下图中的类型和字段名自行创建(后面会解释每个字段的含义):

编辑文章
根据下图步骤操作:

一个精美的文档编辑页面呈现在眼前,编辑完内容后点击 Save 保存内容,然后 Publish 将文章发布出去:

复制几份,方便后面调前端页面:

下图为我创建的数据,注意要是 Published 状态,并且 slug 字段的值一定要修改。

开放获取 Post 数据的权限
首先如下图进入对应界面:

找到 Post 后如下图设置,主要要点击 Save 保存。

为了保证 Category 的数据也能展示,将它也如 Post 般设置。
接口使用方法
使用接口测试工具或者直接使用浏览器,访问 http://localhost:1337/api/posts ,如下图所示(我装了浏览器插件所以会数据更好看清楚):

就是如此方便,一个获取文章的接口就好了,后面直接渲染到前端就行。走到这一步时,回头看看现在所拥有的吧!此时,已完成如下功能:
- 账号管理、权限设置功能。
- 一个存储文章的表(创建
Collection并保存后将自动生成,不信你看看数据库中的表)。 - 一个存储文章分类的表。
- 拥有文章以及文章分类的增删改查的接口(详细的请点我查看,建议先看完本文后再看)。
但是如果留心观察,你会发现存在两个现象:
- 创建
Post这个Collection时,不是创建过一个Image类型的Cover(用来存放文章的封面图)字段吗?它为什么没有在这儿显示? - 创建
Post时,创建了一个categories的Relation表示与文章关联的Category中的数据,为什么也没有显示呢(编辑文章时我是选择了分类的,所以数据应该返回这个字段)?
如果你使用的 Strapi 版本是 v3.x 的,那么不会存在这个现象。浏览很久以前的 Strapi 文档都不会提及,因为它们那个版本都是直接返回全部信息的。在 v4.x 版本,默认不返回媒体文件或关系等类型数据。我在官方文档中找到了答案(但是建议先跟着文章走完流程再看) 。
当使用 /api/posts?populate=* 时,会发现所有数据都展示了。就目前的配置来说,与使用 http://localhost:1337/api/posts?populate[0]=categories&populate[1]=cover 的效果是一样的。通过观察变化,可以猜出需要什么 Imgae 或者 Relation 类型的数据就在 url 拼接 &populate[序号]=要查的字段名(请看问本文后带着疑问看上面给出的 “ 答案 ” )。
Strapi 部署
部署流程如下:
- 代码上传到服务器。
- 安装依赖。
- 安装pm2。
- 在项目根目录创建
server.js,内容如下:
// 项目根目录,与 src 同级
const strapi = require("@strapi/strapi");
strapi.compile().then((appContext) => strapi(appContext).start());
// 如果使用的 TypeScript ,在本文使用的版本 4.3.2 ,生产环境会报错。
// 必须使用本文这种写法才能正常部署。
// 2022年8月30日时在 github issues 上看到的解决方案,5天前给出的解决方案。
pm2 start server.js
前端搭建
克隆项目
接下来的前端制作将基于一个开源的模板来做,你如果拥有 Svelte 基础,后面就可以自己改更多想改的地方,不会也没关系,跟着本文走也能搭建一个博客。同样的,如果你更喜欢用Vue、React 或者 Angular ,也可以找其它基于它们的模板,Astro 都支持!首先拉取代码:
# 拉取代码
git clone git@github.com:Charca/astro-blog-template.git
# 修改 clone 下来的目录文件名,不修改也行,随便你
mv astro-blog-template/ blog_frontend
# 打开目录并安装依赖 此处可以不用 && ,&& 左右的内容分两行写。 也可以直接图形化界面用鼠标操作
cd blog_frontend && yarn # 如果没有yarn ,使用 npm install 来安装依赖
依赖安装完成后,使用 yarn dev 或者 npm run dev,项目即可运行。如果想初步了解 Astro ,可以点击此处(本站一位大佬写的,简洁明了)。
将博客的文章数据源换为从接口获取
找到 src/pages/blog/index.astro 文件,观看源码可以得知,之前文章是获取本地文件夹里的内容渲染的,这样书写以及发布文章都比较麻烦。结合 Strapi 就可以非常方便的管理文章。
获取所有文章数据
在 .astro 文件中,获取数据的操作是在顶部 --- 包裹的范围内进行的。所以获取接口数据的代码如下:
// src/pages/blog/index.astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
const title = 'Blog';
const description = 'Latest articles.';
const permalink = `${Astro.site.href}blog`;
/* 以前的代码,注释掉
* let allPosts = await Astro.glob('../../data/blog-posts/*.md');
*
* allPosts = allPosts.sort((a, b) => new Date(b.frontmatter.publishDate).valueOf() - new Date(a.frontmatter.publishDate).valueOf());
*/
// 获取数据
const fetchPosts = await fetch("http://localhost:1337/api/posts?populate[0]=categories&populate[1]=cover").then(res => res.json())
---
将获取的数据渲染到博客列表页
继续修改 src/pages/blog/index.astro 文件,将内容通过 map 渲染到页面,这里不进行任何封装,只为尽快做出效果。整个src/pages/blog/index.astro 文件内容如下:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
const title = 'Blog';
const description = 'Latest articles.';
const permalink = `${Astro.site.href}blog`;
let allPosts = await Astro.glob('../../data/blog-posts/*.md');
const fetchPosts = await fetch("http://localhost:1337/api/posts?populate[0]=categories&populate[1]=cover").then(res => res.json())
---
<BaseLayout title={title} description={description} permalink={permalink} current="blog">
<div class="container">
<h1>Blog</h1>
{fetchPosts.data.map((post, index) => {
const href = `/blog/${post.attributes.slug}`;
return (
<div>
{ index !== 0 && <hr /> }
<div class="post-item">
<div style="display: flex;">
<div class="left">
<img src={ 'http://localhost:1337' + post.attributes.cover.data.attributes.url } alt="">
</div>
<div class="right">
<h2>
<a href={href}>{post.attributes.title}</a>
</h2>
<p>{post.attributes.excerpt}</p>
<div class="post-item-footer">
<span class="post-item-date">
— {post.attributes.publishedAt}
— {post.attributes.categories.data.map(category=>category.attributes.name + " ")}
</span>
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
</BaseLayout>
<style>
h2,
.post-item-footer {
font-family: var(--font-family-sans);
font-weight: 700;
}
.post-item-date {
color: var(--text-secondary);
text-align: left;
text-transform: uppercase;
margin-right: 16px;
}
hr {
margin: 60px auto;
}
</style>
此时,博客列表的内容就是动态的了。

点击文章查看文章详细内容
src/pages/blog/index.astro 文件中,可以看到渲染 title 的地方使用的超链接,链接的地址是 /blog/${post.attributes.slug}。在页面中点击时,会抛出 404 ,毕竟这个路由(路径、地址或者说是URL)并没有与之对应的内容。现在想要的效果是,点击标题,跳转到 /blog/文章的slug ,然后新的页面中显示这个 slug 对应的文章(前面约定好了,slug 是唯一不重复的,当然也可以用文章的 id 作为路径,随便你)。这样做的目的是可以保证能够生成唯一的路径,对应唯一的文章。
如何让某个路径返回内容呢?在 src/pages 目录下,创建一个 .astro 文件,这里面渲染啥,这个路由就能看到啥,就是一个文件对应一个路由,不需要额外的配置。这么多文章,不可能每个文章都去写个 slug.astro(前面约定路由路径为 文章的slug,所以创建的文件名应该是 文章的slug.astro)。动态匹配可以做到只写一个文件,可以匹配多个路由,动态匹配的文件名为 [随便一个名字].astro。
下面开始修改 [slug].astro 的代码,实现动态匹配。首先找到 getStaticPaths 函数,观察原来的代码,return 前和 src/pages/blog/index.astro 最开始部分一样是获取所有文章。那么也直接将自己新增的通过 fetch 获取的数据替换过来。接着看向 return ,依然是如此眼熟。照葫芦画瓢改就行了,下面是修改后的 [slug].astro 全部内容:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await Astro.glob('../../data/blog-posts/*.md');
const fetchPosts = await fetch("http://localhost:1337/api/posts?populate[0]=categories&populate[1]=cover").then(res => res.json())
// 这里 return 的东西,可以在下面接收到
return fetchPosts.data.map(post => ({
params: { slug: post.attributes.slug },
props: { post: post },
}));
}
const { title, content, slug, excerpt, publishedAt } = Astro.props.post.attributes; // 这里对应 getStaticPaths 中的 return
const permalink = `${Astro.site.href}${slug}`;
---
<BaseLayout title={title} description={excerpt} permalink={permalink} current="blog">
<header>
<p>{publishedAt}</p>
<h1>{title}</h1>
<hr />
</header>
<div class="container">
<article class="content">
{
content
}
</article>
<hr />
</div>
</BaseLayout>
<style>
header {
text-align: center;
}
header h1 {
margin-bottom: 0.7em;
}
header p {
color: var(--text-secondary);
text-transform: uppercase;
font-family: var(--font-family-sans);
font-weight: 600;
}
header hr {
min-width: 100px;
width: 30%;
}
</style>
此时,你会发现通过 /blog/文章的slug 就可以访问 slug 所对应的文章内容了,也就是说,在博客列表页点击标题后会跳转到对应的内容。

但是仔细观察可以看到,这不对啊,我 markdown 标签怎么也直接出来了,这可不行。于是有了下面解析 markdown 的步骤:
- 安装依赖:
yarn add @astropub/md
- 引入依赖:
// src/pages/blog/[slug].astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { markdown } from '@astropub/md'
......
---
- 修改渲染方式:
// src/pages/blog/[slug].astro
- <article class="content">
- {
- content
- }
- </article>
+ <article class="content">
+ { async () => await markdown(content) }
+ </article>
好了,markdown 已经渲染到页面,看看效果吧:

至此,大功告成,一个博客完成。接着你可以自己修改样式,改成自己想要的样子。也可以再创建一个 Collection 存放评论,为博客加上评论功能。后面可能会继续更新,拭目以待吧。
转载自:https://juejin.cn/post/7136883209001762829