> ## Content Index
> Fetch the complete content index at: https://blog.luoplanet.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 把个人网站建成一颗星球：LuoPlanet 上线与 Ghost 域名迁移实录
- URL: https://blog.luoplanet.com/luoplanet-workstation-ghost-migration-2026-10-07/
- Published: 2026-10-07T12:58:46.000Z
- Updated: 2026-10-07T12:58:46.000Z
- Description: 从可旋转的微缩星球到真正上线的个人网站：记录 Vite、Three.js 的部署流程、Ghost 子域名迁移、旧邮件链接兼容，以及邮件配置和回滚中的实际经验。
- Author: luodengsheng
- Tags: 建站记录, Web3D, Ghost

2026 年 10 月 7 日，LuoPlanet 有了一个新的首页：一颗可以转动的微缩星球，星球上有工作站、太阳能板、天线和树木。访问 [luoplanet.com](https://luoplanet.com/?ref=blog.luoplanet.com)，可以先认识我，再进入博客阅读文章。

这次改造的起点很简单：我想给 Web3D、全栈开发和持续探索新技术的兴趣，找一个更具体的表达方式。LuoPlanet 这个名字本来就带着“星球”的意味，微缩星球工作站便成了首页的主题。

但服务器上已经运行着 Ghost。新首页上线时，文章、管理后台、邮件和过去发出去的链接，都需要继续找到各自的位置。这篇文章记录的，就是从空代码仓库到第一版上线，以及把 Ghost 从主域名迁到博客子域名的过程。

## 先让第一颗星球落地

第一版使用 Vite、TypeScript 和 Three.js，场景中的星球与建筑由程序几何生成。页面支持拖动、方向键旋转、自动旋转和视角复位，文字介绍与博客入口使用普通 HTML。

我把第一版的范围定得比较小：先把首页和部署流程跑通，再逐步增加作品和实验。现在的“Web3D 实验室”仍标注为筹备中，博客通过链接进入，尚未接入 Ghost Content API 的文章列表。

除了 3D 画面，这次还处理了几个基础体验：手机布局、减少动态效果偏好、WebGL 不可用时的静态图片，以及 JavaScript 被禁用时仍可阅读的文字和链接。原网站的备案信息也保留在了新页脚。

## 给工作站、博客和后台各留一个地址

最后确定的域名分工是：

- **luoplanet.com**：个人工作站，展示 3D 场景和个人信息。
- **blog.luoplanet.com**：Ghost 博客，负责文章阅读、会员和订阅。
- **ghost.luoplanet.com/ghost/**：继续使用原有的 Ghost 管理后台。

它们仍在同一台服务器上，由 Nginx 按访问域名处理请求：工作站读取静态文件，博客和后台转发给现有 Ghost 容器。Ghost 的数据库、内容卷和 OSS 存储适配器继续沿用。

```
访客 → Cloudflare → Nginx
                    ├─ luoplanet.com        → 工作站静态文件
                    ├─ blog.luoplanet.com   → Ghost 博客
                    └─ ghost.luoplanet.com  → Ghost 后台
                                              ↓
                                         原有数据与存储
```

这样，更新星球场景和发布文章就各有一条流程。前端更新走代码仓库和发布脚本；写文章仍然在 Ghost 后台完成。

## 从空仓库到可重复发布

工作站使用独立的 `luoplanet-workstation` 仓库，管理页面、3D 场景、公开素材、构建配置和部署脚本。服务器凭据、Ghost 数据库和内容备份留在各自的运行环境里。

Vite 构建后的产物是 `dist/`，由 Nginx 提供访问。Three.js 在访客浏览器中渲染，因此这版首页无需额外运行一个常驻 Node 服务。`npm run preview` 只用于本地检查构建结果，这也符合 [Vite 的静态部署方式](https://vite.dev/guide/static-deploy.html?ref=blog.luoplanet.com)。

发布过程被整理成脚本：安装依赖、检查 TypeScript、构建、打包，通过阿里云 Workbench CLI 上传，核对 SHA-256，然后切换版本。服务器上的目录结构大致是：

```
luoplanet-workstation/
├── releases/
│   ├── 版本 A/
│   └── 版本 B/
├── current → releases/版本 B/
└── shared/assets/
```

`current` 指向正在提供访问的版本。新文件完整上传后再切换链接，发布检查失败时恢复之前的版本。带内容哈希的资源保存在共享目录中，让已经打开的旧页面仍能加载对应文件，也让回滚后需要的资源继续可用。

仓库还配置了 GitHub Actions，用于构建检查和保存产物。**当前实际生产发布由本地脚本调用 Workbench 完成，推送 GitHub 并不会自动发布生产站点。**

## Ghost 搬到子域名，改动集中在入口与公开地址

开始服务器操作前，我已经在 Cloudflare 配置好了 `blog` 子域名。随后核对证书，确认现有 Cloudflare Origin 证书覆盖根域名和 `*.luoplanet.com`，可以继续用于 Cloudflare 到源站的 HTTPS 连接。

修改前，先备份 Ghost Compose 配置、Nginx 配置、数据库、内容卷和存储适配器，并检查备份压缩文件的完整性。接着，在 Ghost 的 Compose 环境变量里调整公开地址：

```
url: https://blog.luoplanet.com
admin__url: https://ghost.luoplanet.com
```

这里需要改变的是 `url`。独立后台原本就在 `ghost.luoplanet.com`，因此 `admin__url` 保留。Ghost 官方把网站 URL、后台 URL 和邮件配置分别管理；使用 Docker 时，可以通过对应环境变量覆盖这些设置。参见 [Ghost 配置文档](https://docs.ghost.org/config/?ref=blog.luoplanet.com)。

然后让 Nginx 为博客子域名转发请求、为主域名提供新首页，检查配置语法，重建 Ghost 容器并等待其就绪，再 reload Nginx、检查各个入口。迁移过程中没有重装 Ghost，也没有批量重写文章数据。

## 容易漏掉的，是已经发出去的链接

主域名换成工作站以后，旧的文章地址依然可能存在于收藏夹、搜索结果或邮件里。服务器需要将这些路径转到博客，并保留原来的查询参数。

这次兼容的范围包括旧文章、图片、RSS、会员入口、退订和追踪路径。例如：

```
luoplanet.com/coming-soon/
  → blog.luoplanet.com/coming-soon/

luoplanet.com/members/?token=…&action=signin
  → blog.luoplanet.com/members/?token=…&action=signin
```

工作站当前使用页面内锚点导航。Nginx 为首页和静态资源安排明确的路径，其余旧博客路径转到 `blog`，避免旧文章请求误入一个与它无关的首页。

还有一种链接，仅靠 Nginx 处理不到：`https://luoplanet.com/#/portal/account`。URL 中 `#` 后面的 fragment 不会发送给服务器，旧邮件里的订阅管理入口就可能落到新的星球首页。

因此，新首页在加载应用前先判断 Ghost 的 Portal 链接，保留查询参数和 hash 后转到博客。核心逻辑如下：

```
if (/^#\/portal(?:[\/=?]|$)/.test(location.hash)) {
  location.replace(
    'https://blog.luoplanet.com/' + location.search + location.hash
  );
}
```

普通的 `#about` 等工作站锚点仍留在首页。域名迁移到这里，才开始照顾到访客从不同地方抵达网站的情况。

## 网站域名改了，Mailgun 也要跟着改吗？

这是这次迁移中我特别关心的一点。核对实际配置后，答案是：**这次可以保留现有邮件发送配置。**

网站公开地址和邮件发送域名承担不同的职责。我的配置分为两部分：

- **事务邮件**：登录、注册等邮件通过阿里云 SMTP 发送，现有发件地址为 `no-reply@send.luoplanet.com`。
- **Newsletter**：通过 Mailgun 发送，Mailgun 中配置的发送域名为 `mg.luoplanet.com`。

博客迁到 `blog.luoplanet.com`，不要求把上述发送域名一起改成 `blog`。现有 SMTP 配置、Mailgun API Key，以及 SPF、DKIM 等邮件 DNS 记录都可以继续使用。

还核对了一个细节：当前 Newsletter 没有单独填写发件地址，已安装的 Ghost 版本会使用有效的 `mail.from`。因此，在这套配置下，网站 URL 改变后默认发件地址仍然是原来的地址。

检查结果是：Mailgun 域名处于 active 状态，域名级 webhook 列表为空；切换前后的 SMTP 连接与认证都通过验证。这次没有实际发送测试邮件或群发，所以这些检查不能替代收件箱投递验收。

## 第一次切换后的检查失败了，回滚发挥了作用

这次上线并非一次操作就全部通过。第一次切换后，紧接着执行的博客源站健康检查没有通过，脚本自动恢复了原来的 Ghost 和 Nginx 配置。

后续检查确认原博客已恢复。随后，我在脚本中加入了新路由的就绪等待：Nginx 收到 reload 命令后，给新配置生效和工作进程切换留出有限的检查窗口。第二次切换通过了验证。

这件事提醒我，发布流程需要检查服务最终呈现的结果。除了命令退出码，还要看页面是否可访问、是否返回预期内容、博客的 canonical 是否已经指向新地址。失败时也需要有明确的恢复路径。

## 上线之后，检查页面与它周围的连接

最终检查覆盖了以下内容：

- 主域名展示新工作站，博客子域名展示原有文章，独立后台可以打开。
- 桌面和手机上的 3D 场景正常加载，方向键、自动旋转与复位可用，页面没有资源 404 或 JavaScript 错误。
- WebGL 不可用、上下文丢失或 JavaScript 被禁用时，基础内容与链接仍然可用。
- 旧文章、RSS，以及带查询参数的会员、退订和追踪入口按配置跳转；历史 Portal hash 链接也能抵达博客。
- SMTP 认证与 Mailgun 域名状态正常；原有 Ghost 自动更新脚本仍检查独立后台地址，不受首页更换影响。

其中，会员与退订检查针对入口和参数保留，没有实际执行用户账户操作。备份文件也做了完整性检查，但尚未进行一次完整的数据恢复演练。这些是后续维护时需要继续补足的验证。

## 工作站已经开门，接下来慢慢建造

现在，第一版星球已经可以访问，博客也有了独立的阅读空间。下一步，我想在星球上增加真实的 Web3D 实验和项目案例，再通过 Ghost Content API 将最新文章接入“技术观测台”。

随着内容增加，也需要继续观察模型体积、移动端体验和加载成本。对这个个人网站来说，场景会逐渐丰富，而写作和发布仍应该保持顺手。

欢迎来 [LuoPlanet](https://luoplanet.com/?ref=blog.luoplanet.com) 转一转这颗小星球，也欢迎在 [博客](https://blog.luoplanet.com/) 里继续阅读我的实践记录。