暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Next.js + Github Issue 构建博客解决方案

giscafer 2022-04-02
632

8年前,上大学的时候,第一次自己动手搭建自己的博客,也是 markdown,但用的是 Hexo,生成 html 部署到 gh-pages 。几年前,就停用了Hexo,一直用 GitHub issues 来写博客,不是正式的博客网站,逼格不够,SEO 不友好等问题都有,一直想弄一下,但是懒得弄,觉得这种东西按理是自己生成就好了。我写了文章,就自动帮我同步到个人网站或者是语雀、公众号等。

就在昨天,我看到 Github 上一个外国coder,用 Next.js + Notion(相当于国内语雀)来实现自己的博客,从而得到了灵感。于是,我就想 Next.js + Github Issues 自动化博客展示。

技术栈:Next.js/Typescript
& 部署在 Vercel
。博客数据来自 github issues 列表

博客原理:通过 ci 监听 issues 变更,自动更新 mdx 文件到项目 data/blog/*.mdx
文件夹中,Vercel 自动化构建更新。

(一)根据issue创建博文数据

通过 Github Action 检测到 issue 的 opened
状态,会自动触发工作流

github action 的 ci 配置代码如下:

name: Sync Post

# Controls when the workflow will run
on:
  # schedule:
  #   - cron: "30 1 * * *"
  issues:
    types:
      - opened
      - closed
      - labeled
  workflow_dispatch:
env:
  GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
  Publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v2

      - name: Git config 🔧
        run: |
          git config --global user.name "xxx"
          git config --global user.email "xxx@outlook.com"

      - name: Display runtime info 
        run: |
          echo '当前目录:'
          pwd

      - name: Install 🔧
        run: yarn install

      - name: Update blog files ⛏️
        run: |
          yarn sync-post  # 主要脚本
          git add .
          git commit -m 'chore(ci): blog sync'
          git push

复制

CI 执行 了 yarn sync-post
脚本,脚本主要是通过 Github Api 去获取指定项目 的 issue 列表,如 giscafer/blog
,然后生成 mdx 文件到 next.js 项目工程的 data/blog
目录

/* eslint-disable */
const GitHub = require('github-api')
const fs = require('fs-extra')
const path = require('path')
const pinyin = require('pinyin')
const _ = require('lodash')

const gh = new GitHub({
  token: process.env.GH_TOKEN,
})

const blogOutputPath = '../../data/blog'

// get blog list
const issueInstance = gh.getIssues('giscafer''blog')

function generateMdx(issue{
  const { title, labels, created_at, body } = issue
  return `---
  title: ${title}
  publishedAt: ${created_at}
  summary:
  tags: ${JSON.stringify(labels.map(item => item.name))}
---

${body.replace(/<br \/>/g'\n')}
`

}

function main({
  const filePath = path.resolve(__dirname, blogOutputPath)

  issueInstance.listIssues().then(({ data }) => {
    let successCount = 0
    fs.ensureDirSync(filePath)
    fs.emptyDirSync(filePath)
    for (const item of data) {
      try {
        const content = generateMdx(item)
        const tempFileName = item.title.replace(/\//g'&').replace(/、/g'-').replace(/ - /g'-').replace(/\s/g'-')
        const result = pinyin(tempFileName, {
          style0,
        })
        const fileName = _.flatten(result).join('')
        fs.writeFileSync(`${filePath}/${fileName}.mdx`, content)
        console.log(`${filePath}/${fileName}.mdx`'success')
        successCount++
      } catch (error) {
        console.log(error)
      }
    }
    if (successCount === data.length) {
      console.log('文章全部同步成功!', data.length)
    } else {
      console.log('文章同步失败!失败数量=', data.length - successCount)
    }
  })
}

module.exports = main


复制
image.png

由于 中文的文章标题会有问题,这里我们通过 pinyin
的工具库区将汉字转为拼音作为文件名。但文章的标题还是保留的。如上图 mdx 文件的头部内容:

---
  title: 理解 Virtual DOM
  publishedAt: 2019-03-13T02:48:34Z
  tags: ["Review","React"]
---

复制

(二)Next.js 渲染mdx文件为博客

使用 contentlayer
模块工具,可以方便得将 mdx 转成可渲染的 json 文件。再配合 next-contentlayer
提供的 withContentlayer
在构建时转换 mdx 资源。

import { defineDocumentType, makeSource, ComputedFields } from 'contentlayer/source-files' // eslint-disable-line
import readingTime from 'reading-time'
import rehypePrism from 'rehype-prism-plus'
import codeTitle from 'remark-code-titles'

const imgReg = new RegExp(/https:\/\/(.*)\.(png|jpeg|gif|svg|jpg)/)

const getCoverImg = doc => {
  const { raw } = doc.body

  const match = raw.match(imgReg)
  if (match) {
    return match[0]
  }
  return '/blog/default/image.png'
}

const getSlug = doc => {
  const name = doc._raw.sourceFileName.replace(/\.mdx$/'')
  return name
}

const computedFields: ComputedFields = {
  slug: {
    type'string',
    resolve: doc => getSlug(doc),
  },
  image: {
    type'string',
    resolve: doc => getCoverImg(doc),
    // resolve: doc => `/blog/${getSlug(doc)}/image.png`,
  },
  og: {
    type'string',
    resolve: doc => `/blog/${getSlug(doc)}/og.png`,
  },
  readingTime: { type'json', resolve: doc => readingTime(doc.body.raw) },
}

export const Post = defineDocumentType(() => ({
  name: 'Post',
  filePathPattern: `**/*.mdx`,
  bodyType: 'mdx',
  fields: {
    title: { type'string', required: true },
    summary: { type'string', required: true },
    publishedAt: { type'string', required: true },
    updatedAt: { type'string', required: false },
    tags: { type'json', required: false },
  },
  computedFields,
}))

export default makeSource({
  contentDirPath: 'data/blog',
  documentTypes: [Post],
  mdx: {
    rehypePlugins: [rehypePrism],
    remarkPlugins: [codeTitle],
  },
})


复制

next.config.ts
文件为

const { withContentlayer } = require('next-contentlayer'// eslint-disable-line

module.exports = withContentlayer()({
  webpack5: true,
  images: {
    domains: [
      'user-images.githubusercontent.com',
      'files.mdnice.com',
      'cdn.nlark.com',
      'wpimg.wallstcn.com',
      'github.com',
      'giscafer.com',
      'ww1.sinaimg.cn',
    ],
    formats: ['image/avif''image/webp'],
  },
  webpack: (config, { isServer }) => {
    if (isServer) {
      require('./scripts/generate-sitemap'// eslint-disable-line
      require('./scripts/generate-rss'// eslint-disable-line
    }

    return config
  },
})


复制

接着 使用 useMDXComponent
来渲染 mdx 内容

import { useMDXComponent } from 'next-contentlayer/hooks'

// 省略其他
const Component = useMDXComponent(post.body.code);// code 即为 mdx 文本内容
// 使用组件(conponents 参数支持自定义页面)
<Component components={components} />

复制

(三)利用 Vercel 部署

Vercel 会监听 Github 参考代码变动,一旦变动,就会自动构建部署,这就是所谓的 CI/CD 这个过程。对于个人而言,Vercel 也是免费的。

在上面我们通过 github action 自动监听 issues 变化,就会触发 mdx 文件自动生成提交到 blog 仓库。代码变化之后 Vercel 自动构建部署。所以最终我们就可以看到 issue 的文章显示在博客网站上了

如下图是 Vercel 部署的 blog 项目的工作流执行情况。

当部署成功后,访问我们的博客网址,就可以看到博文了。比如 https://www.giscafer.com/blog/tuopubianjiqijishufangan

image.png

因为公开的项目,任何人都可以创建 issues,所以如果是公开的项目,这里无法控制别人提交issue。这个可以考虑简单处理:在生成 mdx 的脚本中,判断是否为本人创建的 issue,如果不是本人,就过滤掉即可。

// 只查询自己的issues,避免别人创建的也更新到博客
  issueInstance.listIssues({ creator: 'giscafer' })

复制

总结

日后会考虑对接语雀文档,持续改进,欢迎交流!

个人博客地址 :https://www.giscafer.com/

源码仓库:https://github.com/giscafer/blog


文章转载自giscafer,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论