IA 迁移与别名策略

旧版文档是扁平目录(/docs/*.html),新版按 IA 分层(/docs/guides/**、/docs/platform-chat-api/**)。为了不让外部收藏、搜索引擎与旧链接 404,生成器维护一份唯一别名清单:既产出跳转页,也产出 redirects.json 供静态服务器返回真正的 301。本页的别名表由生成器代码直接渲染,不是手写清单。

策略​

  • canonical 唯一:每个主题只有一个规范地址(新 IA);旧地址只作为别名存在,不再生成内容页。
  • 两类别名:legacy = 历史真实存在的 /docs/*.html;compat = 为常用页面提供的稳定短链(如 /docs/push),不是历史路径。
  • 生成期校验:别名目标必须是本次真实生成的页面;不允许链式跳转;别名生成物不得与页面路径冲突,否则构建直接失败。
  • 双通道兼容:静态页面内是 <meta http-equiv="refresh"> + <link rel="canonical"> + location.replace();服务器读取 redirects.json 后返回 HTTP 301。
  • 不删旧地址:禁用别名等于制造 404;如需下线,先确认没有外部引用,再改为指向新页面的说明页。
生成物作用消费者
docs-site/*.html + **/index.html跳转页(meta refresh + canonical)静态托管 / 浏览器
docs-site/redirects.json机器可读别名表(from/to/status/kind)静态服务器、反向代理、CI 链接检查
generated-manifest.json#compatPaths本次构建的全部别名 URL回归/验收脚本
本页别名表人工可读的迁移说明文档读者

静态服务器如何实现真正的 301​

生成器只写文件,不控制 HTTP 状态码。生产环境请让服务器读取 redirects.json;下面两个片段足以落地:

nginx
# 由 CI 把 redirects.json 转成 map(jq -r '.redirects[] | "\(.from) \(.to);"')
map $uri $im_alias_target {
  include /etc/nginx/im-aliases.map;
}

server {
  listen 443 ssl;
  location /docs/ {
    if ($im_alias_target) { return 301 $im_alias_target; }
    try_files $uri $uri/index.html $uri.html =404;
  }
}
JavaScript
// 最小静态服务器(Node):先查别名,再回落到文件
import { readFileSync, existsSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
import { createServer } from 'node:http';

const root = 'docs-site';
const { redirects } = JSON.parse(readFileSync(join(root, 'redirects.json'), 'utf8'));
const alias = new Map(redirects.map((entry) => [entry.from, entry.to]));

createServer((req, res) => {
  const path = decodeURIComponent(new URL(req.url, 'http://x').pathname);
  if (alias.has(path)) {
    res.writeHead(301, { location: alias.get(path) });
    res.end();
    return;
  }
  const rel = path.replace(/^\/docs\/?/, '');
  const candidates = [join(root, rel), join(root, rel, 'index.html'), extname(rel) ? null : join(root, rel + '.html')].filter(Boolean);
  const file = candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
  if (!file) { res.writeHead(404).end('not found'); return; }
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
  res.end(readFileSync(file));
}).listen(8080);

别名表(63 条)​

旧版扁平地址(legacy,54 条)​

旧地址新地址(canonical)状态
/docs/quickstart.html/docs/guides/realtime-chat/client-setup301
/docs/concepts.html/docs/guides301
/docs/message-flow.html/docs/guides/realtime-chat/intro-chat/message-flow301
/docs/guides-basic.html/docs/guides/realtime-chat/intro-chat/im-feature-basic301
/docs/guides-advanced.html/docs/guides/realtime-chat/intro-chat/im-feature-advanced301
/docs/server-integration.html/docs/guides/rest-apis/im-server-integration301
/docs/glossary.html/docs/guides/glossary/imglossary301
/docs/faq.html/docs/faq301
/docs/changelog.html/docs/changelog301
/docs/migration.html/docs/guides/realtime-chat/migration301
/docs/console-guide.html/docs/console-guide301
/docs/demo-guide.html/docs/demo-guide301
/docs/samples.html/docs/samples301
/docs/api.html/docs/platform-chat-api301
/docs/api-reference.html/docs/platform-chat-api301
/docs/api-user.html/docs/platform-chat-api/user301
/docs/api-message.html/docs/platform-chat-api/message301
/docs/api-group.html/docs/platform-chat-api/group301
/docs/api-chatroom.html/docs/platform-chat-api/chatroom301
/docs/api-supergroup.html/docs/platform-chat-api/supergroup301
/docs/api-conversation.html/docs/platform-chat-api/conversation301
/docs/api-friend.html/docs/platform-chat-api/friend301
/docs/api-blacklist.html/docs/platform-chat-api/blacklist301
/docs/api-presence.html/docs/platform-chat-api/presence301
/docs/api-sensitive-word.html/docs/platform-chat-api/sensitive-word301
/docs/api-callback.html/docs/platform-chat-api/callback301
/docs/api-service.html/docs/platform-chat-api/service301
/docs/api-stats.html/docs/platform-chat-api/stats301
/docs/api-agent.html/docs/platform-chat-api/agent301
/docs/api-bot.html/docs/platform-chat-api/bot301
/docs/api-push.html/docs/platform-chat-api/push301
/docs/errors.html/docs/platform-chat-api/code301
/docs/callbacks.html/docs/platform-chat-api/callback301
/docs/protocol.html/docs/protocol301
/docs/sdk-web.html/docs/web-imlib301
/docs/sdk-web-imlib.html/docs/web-imlib301
/docs/sdk-web-imkit.html/docs/web-imkit301
/docs/sdk-flutter.html/docs/flutter-imlib301
/docs/sdk-flutter-imlib.html/docs/flutter-imlib301
/docs/sdk-flutter-imkit.html/docs/flutter-imkit301
/docs/sdk-native.html/docs/native-imlib301
/docs/sdk-native-imlib.html/docs/native-imlib301
/docs/sdk-native-imkit.html/docs/native-imkit301
/docs/sdk-android.html/docs/android-imlib301
/docs/sdk-android-imkit.html/docs/android-imkit301
/docs/sdk-electron.html/docs/electron-imlib301
/docs/sdk-macos.html/docs/macos301
/docs/sdk-macos-imlib.html/docs/macos-imlib301
/docs/sdk-macos-imkit.html/docs/macos-imkit301
/docs/sdk-ios.html/docs/ios-imlib301
/docs/sdk-ios-imkit.html/docs/ios-imkit301
/docs/sdk-ios-objc.html/docs/ios-imlib/objc301
/docs/sdk-harmony.html/docs/harmony-imlib301
/docs/demo.html/docs/demo-guide301

稳定短链(compat,9 条)​

短链新地址(canonical)用途状态
/docs/quickstart/docs/guides/realtime-chat/client-setup客户端集成总览的短链301
/docs/macos-desktop/docs/macosmacOS 桌面端文档分区短链301
/docs/ios-objc/docs/ios-imlib/objciOS Objective-C 接入短链301
/docs/push/docs/guides/push推送通道与投递301
/docs/translation/docs/guides/translation翻译能力301
/docs/user-profile/docs/guides/user-profile-hosting用户信息托管301
/docs/server-sdk/docs/guides/server-sdk/nodeApp Server SDK(Node)301
/docs/limits/docs/platform-chat-api/limitsAPI 语义与限制301
/docs/ia-migration/docs/guides/ia-migration本策略页自身的短链301

如何新增一条别名​

  1. 只改一个地方

    在文档平台的别名清单中登记(历史路径 / 稳定短链两类);不要手写跳转 HTML。

  2. 重新生成

    重新发布文档中心;生成器会校验目标页面存在、非链式、无路径冲突。

  3. 跑检查

    发布前执行零漂移校验、链接可达检查与编码检查。

  4. 验证 301
    curl -I http://127.0.0.1:8080/docs/quickstart.html。注意:im-server 内置静态托管返回的是 200 跳转页,生产 301 需按上文配置服务器。
内置演示服务器的状态码

内置 im-server 的 /docs 静态托管会直接返回跳转页 HTML(HTTP 200 + meta refresh),不是 301。这不影响浏览器跳转,但搜索引擎与缓存层应以生产服务器的 301 为准。

校验与回归​

  • redirects.json 每次构建重写,内容确定性(按 from 排序),可被 --check 逐字节校验。
  • 别名页面不进入搜索索引(只索引 canonical 页面),避免搜索结果重复。
  • compatPaths 在生成清单与 API 快照中可查,便于回归脚本断言“旧地址仍然存在”。