likes
comments
collection
share

使用 Strapi + PostgreSQL + Astro 快速构建博客

作者站长头像
站长
· 阅读数 90

  本文将介绍我博客搭建的过程,使用 PostgreSQL作为数据库,Strapi 快速构建 ApiAstro作为前端支撑。即使你不熟悉这些技术,通过本文也可以照着做出一个博客。通过本文你可以了解如下知识点:

  • 如何较为快速的搭建出一个带后台编辑的博客系统
  • 掌握 docker 最基础的操作(不包含 docker 安装教程。当然,可以跳过这部分)。
  • 了解 PostgreSQL (开源数据库)的部署、建表、建用户、权限(可跳过)。
  • 了解 Navicat (图形化关系型数据库操作工具)基本使用(可跳过)。
  • 了解如何快速通过 Astro(性能极好的前端框架)搭建博客。
  • 掌握使用 StrapiHandless 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=databaseDB 模式为数据库模式。
  • -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,如下图操作:

使用 Strapi + PostgreSQL + Astro 快速构建博客

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

使用 Strapi + PostgreSQL + Astro 快速构建博客

  连接成功后新建查询,依次之下下面的命令:

-- 创建 用户名为 "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 + PostgreSQL + Astro 快速构建博客

Strapi 构建

初始化

# 创建项目 , 项目名为blog_postgre,随便你取什么名字
npx create-strapi-app@4.3.2 blog_postgre

  写这篇文章时 Strapi 最新版本是 4.3.6。但是最新版本初始化时存在问题(error: Unknown dialect undefined),网上看到出现同样问题的人是降级到 4.3.2 解决的。尽管不确定,但是我认为这可能就是版本问题导致。接下来就会让你选择,如下图中配置即可:

使用 Strapi + PostgreSQL + Astro 快速构建博客

# 成功后打开文件夹并运行项目
cd blog_postgre
# 运行项目,我第一件事是在package.json的script选项中将 develop 改成了 dev,为了方便
yarn develop

  此时,你可能会报下列错误:

使用 Strapi + PostgreSQL + Astro 快速构建博客

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

  重新启动项目后将是下面的界面:

使用 Strapi + PostgreSQL + Astro 快速构建博客

创建 Collection

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

使用 Strapi + PostgreSQL + Astro 快速构建博客

创建 Category ,存放分类

  只需要在 Display name 中如下输入:

使用 Strapi + PostgreSQL + Astro 快速构建博客

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

使用 Strapi + PostgreSQL + Astro 快速构建博客

使用 Strapi + PostgreSQL + Astro 快速构建博客

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

使用 Strapi + PostgreSQL + Astro 快速构建博客

  最后点击 Finish 后点击 Save 保存刚刚的操作。 使用 Strapi + PostgreSQL + Astro 快速构建博客

创建 Post ,存放文章

  首先创建关系(前面创建 Post 这个 Collection 的步骤没有放上来): 使用 Strapi + PostgreSQL + Astro 快速构建博客   选择 Relation 类型,输入 categories ,因为一个文章可以有多个类型,每个类型可以对应多个文章(多对多),所以是如下的创建方式:

使用 Strapi + PostgreSQL + Astro 快速构建博客

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

使用 Strapi + PostgreSQL + Astro 快速构建博客

编辑文章

  根据下图步骤操作:

使用 Strapi + PostgreSQL + Astro 快速构建博客

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

使用 Strapi + PostgreSQL + Astro 快速构建博客

  复制几份,方便后面调前端页面: 使用 Strapi + PostgreSQL + Astro 快速构建博客

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

使用 Strapi + PostgreSQL + Astro 快速构建博客

开放获取 Post 数据的权限

  首先如下图进入对应界面:

使用 Strapi + PostgreSQL + Astro 快速构建博客

  找到 Post 后如下图设置,主要要点击 Save 保存。 使用 Strapi + PostgreSQL + Astro 快速构建博客

  为了保证 Category 的数据也能展示,将它也如 Post 般设置。

接口使用方法

  使用接口测试工具或者直接使用浏览器,访问 http://localhost:1337/api/posts ,如下图所示(我装了浏览器插件所以会数据更好看清楚):

使用 Strapi + PostgreSQL + Astro 快速构建博客

  就是如此方便,一个获取文章的接口就好了,后面直接渲染到前端就行。走到这一步时,回头看看现在所拥有的吧!此时,已完成如下功能:

  • 账号管理、权限设置功能。
  • 一个存储文章的表(创建 Collection 并保存后将自动生成,不信你看看数据库中的表)。
  • 一个存储文章分类的表。
  • 拥有文章以及文章分类的增删改查的接口(详细的请点我查看,建议先看完本文后再看)。

  但是如果留心观察,你会发现存在两个现象:

  1. 创建 Post 这个 Collection 时,不是创建过一个 Image 类型的 Cover (用来存放文章的封面图)字段吗?它为什么没有在这儿显示?
  2. 创建 Post 时,创建了一个 categoriesRelation 表示与文章关联的 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 部署

  部署流程如下:

  1. 代码上传到服务器。
  2. 安装依赖。
  3. 安装pm2。
  4. 在项目根目录创建 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天前给出的解决方案。
  1. pm2 start server.js

前端搭建

克隆项目

  接下来的前端制作将基于一个开源的模板来做,你如果拥有 Svelte 基础,后面就可以自己改更多想改的地方,不会也没关系,跟着本文走也能搭建一个博客。同样的,如果你更喜欢用VueReact 或者 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>

  此时,博客列表的内容就是动态的了。

使用 Strapi + PostgreSQL + Astro 快速构建博客

点击文章查看文章详细内容

  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 所对应的文章内容了,也就是说,在博客列表页点击标题后会跳转到对应的内容。

使用 Strapi + PostgreSQL + Astro 快速构建博客

  但是仔细观察可以看到,这不对啊,我 markdown 标签怎么也直接出来了,这可不行。于是有了下面解析 markdown 的步骤:

  1. 安装依赖:

yarn add @astropub/md

  1. 引入依赖:
// src/pages/blog/[slug].astro
---

import BaseLayout from '../../layouts/BaseLayout.astro';
import { markdown } from '@astropub/md'

......
---

  1. 修改渲染方式:
// src/pages/blog/[slug].astro

- <article class="content">
-       {
-         content
-       }
- </article>


+ <article class="content">
+       { async () => await markdown(content) }
+ </article>

  好了,markdown 已经渲染到页面,看看效果吧:

使用 Strapi + PostgreSQL + Astro 快速构建博客

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