[{"content":"CI-CD-Demo完整实践笔记 项目背景与最终目标 这次 CI/CD 实验没有直接从复杂项目开始，而是先做了一个很小的静态网页，用它把 Git、GitHub、GitHub Actions、SSH、Docker、Docker Compose 和 Linux VPS 串起来。\n这样做的好处是，每一层出了问题都比较容易定位。网页本身没有复杂业务逻辑，部署失败时基本可以判断是 Git、Actions、SSH、Docker、Compose 或服务器环境的问题，而不是应用代码本身。\n本次项目的本地目录是：\n1 E:\\ci-cd-demo 最终项目结构：\n1 2 3 4 5 6 7 8 9 10 11 ci-cd-demo/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── ssh-test.yml ├── frontend/ │ ├── index.html │ ├── style.css │ └── script.js ├── Dockerfile └── docker-compose.yml 其中：\nfrontend/ 保存网页代码。 Dockerfile 负责说明 Docker 镜像如何构建。 docker-compose.yml 负责说明容器如何运行。 .github/workflows/ci.yml 负责 CI。 .github/workflows/ssh-test.yml 后来改成了 CD 工作流，负责自动连接 VPS 并部署。 整个流程最终形成：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 本地代码 │ │ git push ▼ GitHub │ │ push 触发 ▼ GitHub Actions │ ├── CI：检查文件 ├── CI：构建 Docker 镜像 │ └── CD：SSH 登录 VPS │ ▼ git pull origin main │ ▼ docker compose up -d --build │ ▼ Docker 容器 │ ▼ VPS:80 │ ▼ 网页 这里需要区分 CI 和 CD。\nCI（Continuous Integration，持续集成）主要解决的是“代码提交以后有没有问题”。本次实验中，CI 会检查项目文件是否存在，并尝试构建 Docker 镜像。\nCD（Continuous Deployment，持续部署）解决的是“代码确认没有明显问题以后，怎么自动更新到服务器”。本次实验中，CD 通过 GitHub Actions 使用 SSH 连接 VPS，然后在服务器上拉取最新代码并重新构建、启动 Docker Compose。\n因此，单独执行 git push 只是把代码提交到了 GitHub；真正形成 CI/CD，是因为 GitHub Actions 接收到 push 事件以后继续自动执行检查和部署。\n本地项目与 Docker 部署 先准备一个最小可运行项目 最开始没有直接做后端服务，而是使用 Nginx 提供静态网页。\nfrontend/index.html 最初是一个简单页面：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;CI/CD Demo\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;style.css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;CI/CD Demo\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;这是我的第一个自动部署项目。\u0026lt;/p\u0026gt; \u0026lt;button onclick=\u0026#34;sayHello()\u0026#34;\u0026gt;测试按钮\u0026lt;/button\u0026gt; \u0026lt;p id=\u0026#34;message\u0026#34;\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script src=\u0026#34;script.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; style.css 和 script.js 分别负责页面样式和按钮交互。\n最初的网页比较简单，后面为了让部署后的页面更像一个正式的 Demo，又把前端改成了一个深色科技风的 CI/CD 展示页面，增加了自动部署状态、Pipeline、Docker 等内容。\n这里有一个值得注意的地方：网页怎么改并不影响 CI/CD 的基本流程。只要代码进入 Git 仓库，后面的自动化过程都可以保持不变。\n编写 Dockerfile 项目根目录下创建 Dockerfile：\n1 2 3 4 5 6 7 8 9 FROM nginx:alpine WORKDIR /usr/share/nginx/html COPY frontend/ . EXPOSE 80 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] 这个 Dockerfile 虽然很短，但已经包含了一个完整 Docker 镜像的基本构建过程。\n1 FROM nginx:alpine 表示以 nginx:alpine 作为基础镜像。Alpine 版本比较小，适合这种简单实验。\n1 WORKDIR /usr/share/nginx/html 设置工作目录。Nginx 默认会从这个目录提供静态文件。\n1 COPY frontend/ . 把项目中的 frontend/ 目录复制到 Nginx 的网页目录。\n1 EXPOSE 80 说明容器中的应用使用 80 端口。它更多是一个镜像层面的声明，并不会自动让宿主机开放 80 端口。\n1 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] 让 Nginx 在前台运行。Docker 容器需要有一个持续运行的前台进程，否则容器会退出。\n本地第一次构建和运行 先进入项目目录：\n1 cd E:\\ci-cd-demo 构建镜像：\n1 docker build -t ci-cd-demo:v1 . 这里：\ndocker build 表示构建镜像。 -t ci-cd-demo:v1 给镜像设置名称和标签。 . 表示使用当前目录作为 Docker build context。 构建成功以后，可以查看镜像：\n1 docker images 然后使用 docker run 启动：\n1 docker run -d --name ci-cd-demo -p 8080:80 ci-cd-demo:v1 这里的：\n1 8080:80 含义是：\n1 2 3 Windows 主机 8080 ↓ Docker 容器 80 访问：\n1 http://localhost:8080 可以看到网页。\n这一步主要是为了先证明 Dockerfile 本身能够正常构建和运行。\n从 docker run 过渡到 Docker Compose 手动 docker run 可以运行容器，但参数比较容易越来越多。后面改成使用 Compose 管理。\ndocker-compose.yml：\n1 2 3 4 5 6 7 8 9 services: web: build: context: . dockerfile: Dockerfile container_name: ci-cd-demo ports: - \u0026#34;8080:80\u0026#34; restart: unless-stopped 这里没有使用 version: 字段，因为新版 Docker Compose 已经不需要这个字段。\n主要配置：\n1 2 3 build: context: . dockerfile: Dockerfile 表示 Compose 不直接拉一个现成应用镜像，而是根据当前目录中的 Dockerfile 构建。\n1 container_name: ci-cd-demo 指定容器名称，方便后面排查。\n1 2 ports: - \u0026#34;8080:80\u0026#34; 把宿主机 8080 映射到容器 80。\n1 restart: unless-stopped 让 Docker 在容器异常退出或 Docker 服务重启后自动尝试恢复容器，除非人为停止。\n使用 Compose 启动：\n1 docker compose up -d --build 其中：\nup：创建并启动服务。 -d：后台运行。 --build：启动前重新构建镜像。 查看运行状态：\n1 docker compose ps 到这里，本地的 Docker + Compose 部分已经验证完成。\nGit 与 GitHub 仓库 Docker 能正常运行以后，再把项目纳入 Git 管理。\n进入项目：\n1 cd E:\\ci-cd-demo 初始化 Git：\n1 git init 查看状态：\n1 git status 第一次提交：\n1 2 git add . git commit -m \u0026#34;Initial CI/CD demo\u0026#34; 然后把主分支统一命名为 main：\n1 git branch -M main 接下来在 GitHub 创建仓库，然后添加远程仓库。\n本次仓库使用 SSH 地址：\n1 git@github.com:soeos/ci-cd-demo.git 添加：\n1 git remote add origin git@github.com:soeos/ci-cd-demo.git 第一次推送：\n1 git push -u origin main 成功以后，本地代码就进入 GitHub。\n这一阶段的关键认识是：Git 和 GitHub 是两个不同的概念。\nGit 负责本地版本管理、提交、分支等操作；GitHub 是远程 Git 仓库，同时还提供 GitHub Actions 等自动化能力。\n后面 CI/CD 能自动运行，是因为代码被 push 到 GitHub 以后，GitHub Actions 可以监听这个事件。\nCI：使用 GitHub Actions 自动检查项目 项目进入 GitHub 后，开始配置 CI。\n在：\n1 .github/workflows/ci.yml 中写入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 name: CI on: push: branches: - main pull_request: branches: - main jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Check project files run: | test -f Dockerfile test -f docker-compose.yml test -f frontend/index.html test -f frontend/style.css test -f frontend/script.js - name: Build Docker image run: docker build -t ci-cd-demo:test . 这份配置可以拆开理解。\n1 2 3 4 on: push: branches: - main 表示 main 分支发生 push 时触发。\n1 2 3 pull_request: branches: - main 表示针对 main 分支的 Pull Request 也可以触发 CI。\n1 runs-on: ubuntu-latest 表示这个 Job 运行在 GitHub 提供的 Ubuntu Runner 上。\n第一步：\n1 uses: actions/checkout@v4 把当前 GitHub 仓库的代码下载到 Runner。\n第二步：\n1 2 3 test -f Dockerfile test -f docker-compose.yml ... 检查关键文件是否存在。\n这不是复杂的自动化测试，但对于当前 Demo 很合适，因为首先需要确认项目结构没有被破坏。\n第三步：\n1 docker build -t ci-cd-demo:test . 尝试构建 Docker 镜像。\n这一点很重要，因为 CI 不只是检查 Git 文件是否存在，还提前验证 Dockerfile 能不能构建。\n整个 CI 的意义可以理解为：\n1 2 3 4 5 6 7 8 9 10 11 开发者 push ↓ GitHub Actions ↓ 拉取代码 ↓ 检查项目文件 ↓ 构建 Docker 镜像 ↓ 成功 / 失败 如果 CI 失败，就应该先解决问题，而不是直接认为代码可以部署。\nSSH：让 GitHub Actions 和 VPS 能互相完成工作 真正做 CD 之前，需要解决两个不同方向的 SSH 问题。\n这次特意使用了两套 SSH Key，因为它们解决的是完全不同的事情。\nGitHub Actions → VPS GitHub Actions 需要主动登录 VPS，所以在 VPS 上生成了一套专门给 GitHub Actions 使用的密钥：\n1 2 /root/.ssh/github_actions /root/.ssh/github_actions.pub 公钥加入：\n1 /root/.ssh/authorized_keys 私钥则保存到 GitHub 仓库的 Actions Secrets 中。\n配置了：\n1 2 3 VPS_HOST VPS_USER VPS_SSH_KEY 其中：\nVPS_HOST：VPS 公网 IP。 VPS_USER：登录用户，本次使用 root。 VPS_SSH_KEY：GitHub Actions 登录 VPS 使用的私钥。 之后使用 appleboy/ssh-action@v1 测试。\n测试成功时日志中出现了：\n1 2 3 Drone SSH version 1.8.2 Docker Compose version v5.5.0 ✅ Successfully executed commands to all hosts. 这一步非常重要，因为它证明：\n1 2 3 GitHub Actions ↓ SSH VPS 这条链路已经打通。\nVPS → GitHub 但是 CD 还有另外一个方向。\nGitHub Actions 登录 VPS 后，VPS 需要执行：\n1 git pull origin main 因此 VPS 自己也需要能够访问 GitHub。\n一开始直接：\n1 git clone git@github.com:soeos/ci-cd-demo.git 出现：\n1 Permission denied (publickey) 这不是 GitHub Actions 的 Key 有问题，而是因为 VPS → GitHub 这一方向没有认证。\n于是又在 VPS 上生成了一套独立的密钥：\n1 2 /root/.ssh/github /root/.ssh/github.pub 把 /root/.ssh/github.pub 添加到 GitHub 账户的 SSH and GPG keys 中。\n然后测试：\n1 ssh -i /root/.ssh/github -o IdentitiesOnly=yes -T git@github.com 成功返回：\n1 Hi soeos! You\u0026#39;ve successfully authenticated, but GitHub does not provide shell access. 这句话的含义不是失败。\nGitHub 本身不提供普通 SSH Shell，但已经确认 GitHub 成功识别了这把 SSH Key。\n为了以后不需要每次手动指定：\n1 -i /root/.ssh/github 又配置了：\n1 /root/.ssh/config 内容：\n1 2 3 4 5 Host github.com HostName github.com User git IdentityFile /root/.ssh/github IdentitiesOnly yes 然后：\n1 chmod 600 /root/.ssh/config 这样 VPS 以后执行：\n1 git pull origin main 就可以自动使用这把 Key。\n在 VPS 上准备项目目录 最终把项目放在：\n1 /opt/ci-cd-demo 也就是说，服务器上的工作目录大致是：\n1 2 3 4 5 /opt/ci-cd-demo ├── .github/ ├── frontend/ ├── Dockerfile └── docker-compose.yml 这里的逻辑是：\n1 2 3 4 5 GitHub ↑ │ git pull │ VPS /opt/ci-cd-demo GitHub Actions 本身并不需要把整个项目文件通过 SSH 传过去。\n它只需要：\nSSH 登录 VPS。 进入项目目录。 git pull 获取最新代码。 使用最新代码重新构建并启动容器。 CD：把部署真正自动化 前面的 CI 和 SSH 都验证以后，开始配置 CD。\n原本用于 SSH 测试的：\n1 .github/workflows/ssh-test.yml 后来直接改成真正的 CD 工作流：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 name: CD on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - name: Deploy to VPS uses: appleboy/ssh-action@v1 with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} script: | cd /opt/ci-cd-demo echo \u0026#34;Pull latest code...\u0026#34; git pull origin main echo \u0026#34;Build and restart...\u0026#34; docker compose up -d --build echo \u0026#34;Deployment completed!\u0026#34; echo \u0026#34;Container status:\u0026#34; docker compose ps 这里的执行过程非常清楚。\n第一步：push 触发 1 2 3 4 on: push: branches: - main 只要代码 push 到 main，CD 就会开始。\n第二步：GitHub Actions 登录 VPS 1 uses: appleboy/ssh-action@v1 使用前面配置好的：\n1 2 3 VPS_HOST VPS_USER VPS_SSH_KEY 建立 SSH 连接。\n第三步：进入服务器项目目录 1 cd /opt/ci-cd-demo 第四步：拉取最新代码 1 git pull origin main 这一条命令把 GitHub 上刚刚 push 的代码同步到服务器。\n实际测试时出现过一次：\n1 2 Updating 4f490d7..fa136ad Fast-forward 说明 VPS 成功从 GitHub 拉到了新的提交。\n第五步：重新构建并启动 1 docker compose up -d --build 因为网页代码发生变化，所以需要重新构建镜像。\n完整过程：\n1 2 3 4 5 6 7 8 9 10 11 GitHub 新代码 ↓ git pull ↓ Dockerfile ↓ 重新 build ↓ 生成新镜像 ↓ 重新创建 / 启动容器 第六步：查看容器 1 docker compose ps 用于确认服务是否正常运行。\n第一次真正部署时遇到的问题：8080 端口冲突 第一次 CD 部署时，GitHub Actions、SSH、Git Pull、Docker Build 都成功了，但是容器启动失败：\n1 Bind for 0.0.0.0:8080 failed: port is already allocated 这说明问题已经不是 GitHub、SSH 或 Dockerfile，而是 VPS 上已经有其他程序占用了 8080。\n当时没有直接执行停止命令，而是先要求检查：\n1 docker ps --format \u0026#34;table {{.Names}}\\t{{.Ports}}\u0026#34; 以及：\n1 ss -lntp | grep \u0026#39;:8080\u0026#39; 原因是 VPS 上还有其他正在运行的服务，不能为了部署 Demo 就随便停止某个容器。\n最终决定把 Demo 的宿主机端口改成 80。\nCompose 改成：\n1 2 3 4 5 6 7 8 9 services: web: build: context: . dockerfile: Dockerfile container_name: ci-cd-demo ports: - \u0026#34;80:80\u0026#34; restart: unless-stopped 这里仍然是：\n1 宿主机 80 → 容器 80 修改后再次：\n1 2 3 git add docker-compose.yml git commit -m \u0026#34;Change web port to 80\u0026#34; git push GitHub Actions 再次触发 CD，最终部署成功。\n这个问题很值得记录下来，因为实际工作中部署失败并不一定是代码问题。端口被占用、磁盘不足、权限错误、Docker 网络问题等，都可能导致部署失败。\n一个容易忽略的问题：部署失败时 Workflow 可能仍显示成功 第一次部署时还发现了一个脚本层面的隐患。\n虽然：\n1 docker compose up -d --build 已经报错，但是后面的：\n1 2 echo \u0026#34;Deployment completed!\u0026#34; docker compose ps 仍然继续执行。\n因此 GitHub Actions 最终可能把整个 SSH Step 当成成功。\n这说明当前 Workflow 还不够严谨。\n后续应该在脚本开头增加：\n1 set -e 例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 script: | set -e cd /opt/ci-cd-demo echo \u0026#34;Pull latest code...\u0026#34; git pull origin main echo \u0026#34;Build and restart...\u0026#34; docker compose up -d --build echo \u0026#34;Deployment completed!\u0026#34; echo \u0026#34;Container status:\u0026#34; docker compose ps 这样只要其中一个命令返回非 0 状态，脚本就会停止，GitHub Actions 才能正确显示部署失败。\n这也是这次实验中比较重要的一点：自动化不是把几个命令串起来就结束了，还需要保证失败能够被正确传递。\n最终验证自动部署 CD 成功以后，最重要的不是只看 GitHub Actions 绿色，而是验证完整闭环。\n下一次修改网页内容，例如修改：\n1 \u0026lt;h1\u0026gt;CI/CD Demo\u0026lt;/h1\u0026gt; 为：\n1 \u0026lt;h1\u0026gt;My CI/CD Project\u0026lt;/h1\u0026gt; 然后本地：\n1 2 3 git add . git commit -m \u0026#34;Update homepage\u0026#34; git push 这时候不再登录 VPS 手动执行：\n1 git pull 也不再手动执行：\n1 docker compose up -d --build 而是直接等待：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 git push ↓ GitHub ↓ GitHub Actions ↓ CI ↓ CD ↓ SSH ↓ git pull ↓ Docker Compose ↓ 网页更新 然后访问服务器的 80 端口，确认页面已经发生变化。\n到这里，整个实验才真正完成了“自动部署”的验证。\n最终流程、问题记录与后续改进 这次实验最终形成的基础 CI/CD 架构可以概括为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 ┌──────────────────┐ │ Windows 本地开发 │ │ │ │ 修改 HTML/CSS/JS │ └────────┬─────────┘ │ │ git push ▼ ┌──────────────────┐ │ GitHub │ │ main 分支 │ └────────┬─────────┘ │ │ push trigger ▼ ┌────────────────────────────┐ │ GitHub Actions │ │ │ │ CI： │ │ 1. checkout │ │ 2. 检查项目文件 │ │ 3. docker build │ │ │ │ CD： │ │ 4. SSH 登录 VPS │ └────────┬───────────────────┘ │ │ SSH ▼ ┌────────────────────────────┐ │ Ubuntu VPS │ │ │ │ /opt/ci-cd-demo │ │ │ │ │ git pull │ │ ↓ │ │ docker compose │ │ ↓ │ │ Docker Container │ │ ↓ │ │ nginx:80 │ └────────────────────────────┘ 整个实验过程中实际遇到的问题主要集中在以下几个方面。\nDocker 与服务器环境问题 Dockerfile 本身可以构建，但服务器上的 Docker 环境和本地并不完全一样。\n尤其是服务器拉取：\n1 nginx:alpine 速度比较慢，所以 Docker Build 在 VPS 上花费的时间明显比本地长。\n这说明“本地能跑”并不代表“服务器一定能快速部署”。实际项目还需要考虑镜像缓存、镜像仓库、网络环境等问题。\nSSH 是两个方向的问题 这次最容易混淆的地方之一就是两套 SSH Key。\n实际关系是：\n1 2 github_actions GitHub Actions → VPS 以及：\n1 2 github VPS → GitHub 两者不能混为一谈。\n前者解决：\n1 Actions 怎么登录服务器？ 后者解决：\n1 服务器怎么从 GitHub 拉代码？ 这两个方向都打通以后，CD 才能顺利执行。\nGitHub Actions Secret 的作用 VPS 地址、登录用户和私钥没有直接写到公开的 Workflow 中，而是使用：\n1 2 3 ${{ secrets.VPS_HOST }} ${{ secrets.VPS_USER }} ${{ secrets.VPS_SSH_KEY }} 这样可以避免把 SSH 私钥直接写进 Git 仓库。\n特别是 SSH 私钥、Token、密码等内容，不能提交到 Git。\n如果密钥意外暴露，应当及时更换，而不是继续使用已经泄露的凭证。\n端口冲突的排查思路 第一次部署失败时：\n1 2 Bind for 0.0.0.0:8080 failed: port is already allocated 正确的思路不是直接执行：\n1 kill 或者随便：\n1 docker stop 而是先确认是谁占用了端口：\n1 docker ps --format \u0026#34;table {{.Names}}\\t{{.Ports}}\u0026#34; 1 ss -lntp | grep \u0026#39;:8080\u0026#39; 确认之后，再决定是释放端口，还是修改项目端口。\n对于一台已经运行多个服务的 VPS，这一点尤其重要。\n当前方案为什么适合学习，但还不算最完善 现在的 CD 是：\n1 2 3 4 5 6 7 8 9 GitHub ↓ SSH ↓ VPS git pull ↓ VPS docker build ↓ VPS docker compose up 它的优点是简单，能够把 CI/CD 的核心流程完整跑通。\n但它也有一个比较明显的问题：每次部署都要在 VPS 上重新构建镜像，而且 VPS 还需要访问 Docker Hub 拉取基础镜像。\n更进一步的做法可以变成：\n1 2 3 4 5 6 7 8 9 10 11 GitHub ↓ GitHub Actions ↓ 构建 Docker 镜像 ↓ 推送到 GHCR 等镜像仓库 ↓ VPS docker pull ↓ docker compose up 这样 VPS 主要负责运行容器，不负责完整的镜像构建。\n不过对于第一次学习 CI/CD 来说，目前这种：\n1 2 3 git pull + docker compose up -d --build 反而更容易理解每一步到底发生了什么，因此适合作为第一版。\n这次实验真正需要掌握的并不是某一条命令，而是整个部署思路：\n1 2 代码管理 → 自动检查 → 自动连接服务器 → 拉取代码 → 构建镜像 → 启动容器 → 验证服务 以后换成 FastAPI、Node.js、Java、前后端分离项目，甚至更复杂的服务，只是 Dockerfile、Compose 和测试命令发生变化，整个 CI/CD 的基本思想仍然一样。\n","date":"2026-09-17T23:40:25+08:00","permalink":"/posts/test-archetype/","title":"CI-CD-Demo完整实践笔记"},{"content":"这是我的第一篇文章。\n这里以后会记录我的学习、开发和折腾过程。\n← 这里是Stack主题下的写法\n","date":"2026-09-17T21:59:47+08:00","permalink":"/posts/my-first-post/","title":"我的第一篇文章"}]