Next.js TailwindCSS RemoteMDX 博客网站

后端程序员初学前端,使用Next.js TailwindCSS RemoteMDX构建的博客网站

详见:https://github.com/huiru-wang/blog

Next.js TailwindCSS RemoteMDX 博客

一个后端程序员尝试学习前端,制作的基于

Next.js
TailwindCSS
next-remote-mdx
的微像素风博客,博客文章是读取本地的md文件。

使用本地文件的方式,主要因为更习惯使用本地的Obsidian写博客文章、开发笔记,单独使用一个仓库。部署时只需要将对应的文章目录迁移到项目的指定读取目录即可;

预览博客:robinverse.me

简介

  • Markdown/MDX 支持:支持 
    .md
     和 
    .mdx
     文件格式。
  • 代码高亮:使用 
    rehype-prism-plus
     插件实现代码块高亮显示,支持单行代码高亮;
  • Markdown目录:为标题标签(如 
    h1
    h2
    )添加 ID,支持目录跳转。
  • Markdown Mermaid支持:支持渲染
    mermaid
    图表、跟随主题切换;
  • 多级目录内容支持:markdown文件从本地读取,默认加载
    examples
    下的文件,支持多级文件夹结构,自动组装slug,访问对应的blog时解析找到对应文件。
  • 响应式布局:响应式布局,支持移动端访问。

dark

white

markdownwhite

安装与配置

git clone https://github.com/huiru-wang/blog.git
cd blog
pnpm install

文件位置:

BLOG_DIR=blogs  # 博客文件存放目录,默认为 "blogs"

将md、mdx文件放在blogs目录下即可访问;需配置好frontmatter,否则读取自动跳过

export type Frontmatter = {
    title: string;
    category: string;
    tags: string[];
    keywords?: string;
    publishedAt?: string;
    description?: string;
}

启动

启动:

pnpm dev

访问 http://localhost:3000 查看博客平台。

文件结构

blog/ ├── blogs/ │ └── 博客文件.md/mdx ├── public/ │ └── ...静态资源 ├── src/ │ ├── app/ │ │ |── blogs/ │ │ └── projects/ │ │ └── layout.tsx │ │ └── page.tsx │ │ │ ├── components/ │ │ └── ...React、Markdown 组件 │ │ │ ├── lib/ │ │ └── md.ts # MDX 解析逻辑 │ │ │ ├── styles/ │ │ └── ...样式文件 │ │ │ ├── providers/ │ │ └── ThemeProvider.tsx │ │ ├── .env # 环境变量配置 ├── package.json # 项目依赖 └── README.md # 项目说明

文章内容格式

文章开头维护好对应的元数据信息

frontmatter
即可读取和加载;

--- title: Next.js TailwindCSS RemoteMDX 博客网站 category: project tags: - project publishedAt: 2024-06-15 description: 后端程序员初学前端,使用Next.js TailwindCSS RemoteMDX构建的博客网站 --- # 正文

地图旅行日记

在高德地图基础上,通过marker展示旅行日记;

项目简单部署

Nginx配置文件和SSL支持

user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
}

http {
        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        access_log /var/log/nginx/access.log;

        gzip on;

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;

        server {
                listen 80;
                listen [::]:80;
                server_name 106.15.48.213;

                return 301 https://www.robinverse.me$request_uri;
        }

        server {
                set $root /root/blog;
                listen 80;
                listen [::]:80;
                server_name robinverse.me www.robinverse.me;
                return 301 https://$host$request_uri;
        }

        server {
                listen 443 ssl;
                server_name robinverse.me www.robinverse.me;

                ssl_certificate /etc/nginx/ssl/robinverse.me.pem;
                ssl_certificate_key /etc/nginx/ssl/robinverse.me.key;

                location / {
                    proxy_pass http://127.0.0.1:3000;
                    proxy_http_version 1.1;
                    proxy_set_header Upgrade $http_upgrade;
                    proxy_set_header Connection 'upgrade';
                    proxy_set_header Host $host;
                    proxy_cache_bypass $http_upgrade;
                }
            }
    }    

部署脚本

个人博客在部署时,主要分为2块:

  • 博客项目代码;
  • 博客文章;(单独仓库,平时使用 obsidian 写文章) 部署的时候,分别拉取2个仓库的内容,在脚本中将文章迁移到项目的对应目录,然后启动项目即可;
#!/bin/bash
TARGET_DIR="/root"  # 工作目录
REPO_BLOG_URL="git@github.com:huiru-wang/blog.git"
REPO_BLOG_NAME="blog"
REPO_DEV_NOTE_URL="git@github.com:huiru-wang/dev-notes.git"
REPO_DEV_NOTE_NAME="dev-notes"

SOURCE_DEV_NOTE_DIR="${TARGET_DIR}/${REPO_DEV_NOTE_NAME}"
DEV_NOTE_DIR="${TARGET_DIR}/${REPO_BLOG_NAME}/dev-notes"

SOURCE_PROJECT_DIR="${TARGET_DIR}/${REPO_DEV_NOTE_NAME}/Projects"
PROJECT_DIR="${TARGET_DIR}/${REPO_BLOG_NAME}/blogs"

SOURCE_TRAVEL_LOGS_DIR="${TARGET_DIR}/${REPO_DEV_NOTE_NAME}/Travel"
TRAVEL_LOGS_DIR="${TARGET_DIR}/${REPO_BLOG_NAME}/travel-logs"

SOURCE_IMAGES_DIR="${TARGET_DIR}/${REPO_DEV_NOTE_NAME}/images"
DEV_IMAGES_DIR="${TARGET_DIR}/${REPO_BLOG_NAME}/public/images"  # 图片目录

# 1. 切换到工作目录
if [ ! -d "$TARGET_DIR" ]; then
    mkdir -p "$TARGET_DIR"
fi
cd "$TARGET_DIR" || { echo "无法切换到目录 $TARGET_DIR"; exit 1; }

# 2. 拉取项目代码
echo "================= Update Blog ================="
rm -rf "$REPO_BLOG_NAME"
git clone "$REPO_BLOG_URL"

# 3. 拉取文件
echo "================= Update Dev Note ================="
rm -rf "$REPO_DEV_NOTE_NAME"
git clone "$REPO_DEV_NOTE_URL"

# 4. 将文章移动到项目的指定目录
echo "================= Copy Dev Note ================="
if [ -d "$DEV_NOTE_DIR" ]; then
    rm -rf "$DEV_NOTE_DIR"
fi
if [ -d "$PROJECT_DIR" ]; then
    rm -rf "$PROJECT_DIR"
fi
if [ -d "$DEV_IMAGES_DIR" ]; then
    rm -rf "$DEV_IMAGES_DIR"
fi
if [ -d "$TRAVEL_LOGS_DIR" ]; then
    rm -rf "$TRAVEL_LOGS_DIR"
fi
mkdir -p "$DEV_NOTE_DIR"
mkdir -p "$PROJECT_DIR"
mkdir -p "$DEV_IMAGES_DIR"
mkdir -p "$TRAVEL_LOGS_DIR"
mv ${REPO_DEV_NOTE_NAME}/meta.json ${REPO_BLOG_NAME}/
mv ${SOURCE_TRAVEL_LOGS_DIR}/* ${TRAVEL_LOGS_DIR}/
mv ${SOURCE_PROJECT_DIR}/* ${PROJECT_DIR}/
mv ${SOURCE_IMAGES_DIR}/* ${DEV_IMAGES_DIR}/
mv ${SOURCE_DEV_NOTE_DIR}/* ${DEV_NOTE_DIR}/
rm -rf SOURCE_DEV_NOTE_DIR

# 5. 构建启动项目
echo "================= Build Blog App ================="
cd "${TARGET_DIR}/${REPO_BLOG_NAME}"
pnpm install
pnpm build

# 6. 重启 pm2 中的应用程序
if pm2 list | grep -q "blog"; then
    # 应用程序已经在 pm2 中,重启它
    echo "===================== Restarting application ====================="
    pm2 restart blog || { echo "Failed to restart pm2 application. Exiting."; exit 1; }
else
    # 应用程序不在 pm2 中,启动它
    echo "===================== Starting application ====================="
    pm2 start pnpm --name 'blog' -- start || { echo "Failed to start pm2 application. Exiting."; exit 1; }
fi

Github-Webhook自动化更新

1. 内容更新脚本

脚本拉取最新的文章仓库,将文章移动到项目的对应目录,重启项目即可;

#!/bin/bash

TARGET_DIR="/root"  # 工作目录
REPO_BLOG_NAME="blog"
REPO_DEV_NOTE_URL="git@github.com:huiru-wang/dev-notes.git"
REPO_DEV_NOTE_NAME="dev-notes"

SOURCE_DEV_NOTE_DIR="${TARGET_DIR}/${REPO_DEV_NOTE_NAME}"
DEV_NOTE_DIR="${TARGET_DIR}/${REPO_BLOG_NAME}/dev-notes"

SOURCE_PROJECT_DIR="${TARGET_DIR}/${REPO_DEV_NOTE_NAME}/Projects"
PROJECT_DIR="${TARGET_DIR}/${REPO_BLOG_NAME}/blogs"

SOURCE_IMAGES_DIR="${TARGET_DIR}/${REPO_DEV_NOTE_NAME}/images"
DEV_IMAGES_DIR="${TARGET_DIR}/${REPO_BLOG_NAME}/public/images"  # 图片目录

# 1. 切换到工作目录
if [ ! -d "$TARGET_DIR" ]; then
    mkdir -p "$TARGET_DIR"
fi
cd "$TARGET_DIR" || { echo "无法切换到目录 $TARGET_DIR"; exit 1; }

# 2. 拉取文件
echo "================= Update Dev Note ================="
rm -rf "$REPO_DEV_NOTE_NAME"
git clone "$REPO_DEV_NOTE_URL"

# 3. 将文章移动到项目的指定目录
echo "================= Copy Dev Note ================="
if [ -d "$DEV_NOTE_DIR" ]; then
    rm -rf "$DEV_NOTE_DIR"
fi
if [ -d "$PROJECT_DIR" ]; then
    rm -rf "$PROJECT_DIR"
fi
if [ -d "$DEV_IMAGES_DIR" ]; then
    rm -rf "$DEV_IMAGES_DIR"
fi
mkdir -p "$DEV_NOTE_DIR"
mkdir -p "$PROJECT_DIR"
mkdir -p "$DEV_IMAGES_DIR"
mv ${SOURCE_PROJECT_DIR}/* ${PROJECT_DIR}/
mv ${SOURCE_IMAGES_DIR}/* ${DEV_IMAGES_DIR}/
mv ${SOURCE_DEV_NOTE_DIR}/* ${DEV_NOTE_DIR}/
rm -rf "${DEV_NOTE_DIR}/.git"
rm "${DEV_NOTE_DIR}/README.md"

# 4. 重启 pm2 中的应用程序
echo "================= Restart Blog App ================="
cd "${TARGET_DIR}/${REPO_BLOG_NAME}"

if pm2 list | grep -q "blog"; then
    # 应用程序已经在 pm2 中,重启它
    echo "===================== Restarting application ====================="
    pm2 stop blog
    pm2 start blog || { echo "Failed to restart pm2 application. Exiting."; exit 1; }
else
    # 应用程序不在 pm2 中,启动它
    echo "===================== Starting application ====================="
    pm2 start pnpm --name 'blog' -- start || { echo "Failed to start pm2 application. Exiting."; exit 1; }
fi

2. Github Webhook服务

在服务器启动一个Webhook服务;服务对外暴露一个接口,来提供给github调用;接口内执行对应的脚本即可;

2.1. 创建webhook暴露到公网

创建一个简易的node服务,提供一个

post
接口,来实现webhook:

mkdir github-webhook
cd github-webhook
pnpm init
pnpm add express child_process

添加启动脚本:

{
  "name": "blog-webhook",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "child_process": "^1.0.2",
    "crypto": "^1.0.1",
    "dotenv": "^16.4.7",
    "express": "^4.21.2"
  }
}

创建

server.js

const express = require('express');
const { exec } = require('child_process');

const app = express();
const port = process.env.PORT || 5000;

const HOOK_ID = '536763309';
const SCRIPT_PATH = '/root/blog-website/update_blog.sh';

// Webhook路由
app.post('/webhook', (req, res) => {
    const hookId = req.headers['x-github-hook-id'];
    const event = req.headers['x-github-event'];
    if (!hookId || hookId !== HOOK_ID) {
        console.log("HookId:", hookId);
        return res.status(400).send("Not Support Service");
    }
    if (!event) {
        console.error("Empty Event");
        return res.status(400).send("Empty Event");
    }
    switch (event) {
        case 'ping':
            return res.status(200).send(`pong`);
        case 'push':
            execShell();
            console.info("Start Exec Shell");
            return res.status(200).send("Success");
        default:
            console.error("Ignore Not Support Event");
            return res.status(200).send(`Not Support ${event} event`);
    }
});


function execShell() {
    // 执行部署脚本
    exec(`bash ${SCRIPT_PATH}`, (error, stdout, stderr) => {
        if (error) {
            console.error(`Blog Update Fail: ${error}`);
            return res.status(500).send(`Blog Update Fail: ${error.message}\n${stderr}`);
        }
        console.log(`Blog Update Success: ${stdout}`);
    });
}

// 错误处理
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Internel Error, dont do that again');
});

app.listen(port, () => {
    console.log(`Github Webhook serve on http://localhost:${port}`);
});

运行实例:

pm2 start pnpm --name 'github-webhook' -- start

修改Nginx配置,接口暴露到公网:

	server {
			listen 443 ssl;
			server_name robinverse.me www.robinverse.me;
	
			ssl_certificate /etc/nginx/ssl/robinverse.me.pem;
			ssl_certificate_key /etc/nginx/ssl/robinverse.me.key;

			# webhook路由
			location /webhook {
				proxy_pass http://127.0.0.1:5000;
				proxy_http_version 1.1;
				proxy_set_header Upgrade $http_upgrade;
				proxy_set_header Connection 'upgrade';
				proxy_set_header Host $host;
				proxy_cache_bypass $http_upgrade;
			}
	
			location / {
				proxy_pass http://127.0.0.1:3000;
				proxy_http_version 1.1;
				proxy_set_header Upgrade $http_upgrade;
				proxy_set_header Connection 'upgrade';
				proxy_set_header Host $host;
				proxy_cache_bypass $http_upgrade;
			}
		}

2.2 对应的内容仓库设置webhook

位置:仓库 -> Settings -> webhooks

  • Payload URL:填上自己服务器的webhook;
  • Content type:我这里选
    application/json
  • Secret:自行创建密钥;(服务器内自行添加验证逻辑)

Enable SSL verification:Optional;

Just the 

push
 event.:我这里只选push;

✅Active

创建即可,创建后会自动执行一次

ping
的webhook。后续push操作,自动执行。