Typecho 默认使用 admin 作为后台目录。出于部署习惯,有些站点会在上线后修改这个目录名。如果插件后台页面在本地显示正常,上线后却丢失顶部导航、页面宽度和表单样式,而插件自己的布局仍然存在,问题往往不在 CSS 内容本身,而在插件加载后台公共模板的方式。

本文记录一次完整的定位和修复过程,并给出一种同时兼容默认后台目录与自定义后台目录的写法。

image.png

问题现象

同一个插件页面在两个环境中出现了明显差异:

  • 本地环境使用默认的 admin 目录,后台顶部导航、居中容器、按钮和输入框样式都正常。
  • 线上环境修改了后台目录名,页面只剩插件自身的双栏、边框和标签页样式。
  • 线上页面的字体更大,链接恢复为浏览器默认的蓝色下划线,内容也不再居中限宽。
  • 页面功能主体仍能输出,没有直接显示致命错误。

这些特征说明插件自己的 admin.css 已经加载,但 Typecho 后台的 normalize.cssgrid.cssstyle.css 没有进入页面。同时,后台菜单模板也没有正常输出。

从资源路径缩小范围

Typecho 后台公共样式由后台的 header.php 输出,资源地址通过 $options->adminStaticUrl() 生成。插件自己的静态资源通常通过 $options->pluginUrl 生成。

这两类资源使用不同的入口:

后台公共资源:/<后台目录>/css/style.css
插件静态资源:/usr/plugins/<插件名>/assets/admin.css

因此,出现“插件样式还在、后台外壳消失”的情况时,应优先检查下面两项:

  1. 页面是否成功包含了后台的 header.phpmenu.php
  2. 浏览器开发者工具中,后台公共 CSS 的地址和响应状态是否正确。

根本原因:插件写死了 admin 目录

问题插件使用了下面这种模板加载方式:

include __TYPECHO_ROOT_DIR__ . '/admin/header.php';
include __TYPECHO_ROOT_DIR__ . '/admin/menu.php';

页尾的 copyright.phpcommon-js.phpfooter.php 也使用了相同的硬编码路径。

本地后台目录就是 admin,所以问题不会暴露。线上将后台目录改名后,PHP 仍然尝试读取 <站点根目录>/admin/header.php,文件自然不存在。

这里使用的是 include。文件包含失败时 PHP 会产生警告,但脚本通常还会继续执行。生产环境关闭错误显示后,管理员看不到警告,只会看到插件主体被输出,而后台页头、菜单、公共样式和脚本全部缺失。这正是页面看起来“只坏了一半”的原因。

正确读取 Typecho 后台目录

Typecho 使用 __TYPECHO_ADMIN_DIR__ 表示后台目录。插件不应该假设它一定是 /admin/,而应根据该常量构造后台模板的文件系统路径。

可以为插件增加一个专用的路径函数:

function plugin_admin_file(string $file): string
{
    $adminDir = defined('__TYPECHO_ADMIN_DIR__')
        ? __TYPECHO_ADMIN_DIR__
        : '/admin/';

    return rtrim(__TYPECHO_ROOT_DIR__, '/\\') . DIRECTORY_SEPARATOR
        . trim($adminDir, '/\\') . DIRECTORY_SEPARATOR
        . ltrim($file, '/\\');
}

然后统一通过这个函数加载后台模板:

include plugin_admin_file('header.php');
include plugin_admin_file('menu.php');

// 插件页面主体

include plugin_admin_file('copyright.php');
include plugin_admin_file('common-js.php');
include plugin_admin_file('footer.php');

这段代码有几个考虑:

  • 优先读取 Typecho 配置的后台目录。
  • 常量不存在时回退到默认的 /admin/,兼容常规安装。
  • 使用 trim() 处理配置值两端可能存在的斜杠。
  • 使用 DIRECTORY_SEPARATOR 兼容 Linux 和 Windows 文件系统。
  • 每个插件使用带自身前缀的函数名,避免多个插件在同一请求中发生全局函数重名。

本次修复分别为 FriendLinksZeyuAiVisitAnalytics 定义了独立的后台路径函数,并替换了三个插件中共 15 处后台模板硬编码引用。

配置也必须与真实目录一致

代码支持自定义目录后,线上 config.inc.php 仍需正确配置。例如实际后台文件夹名为 custom-admin,则应设置:

define('__TYPECHO_ADMIN_DIR__', '/custom-admin/');

配置值、服务器上的真实目录名以及访问后台时使用的 URL 必须一致。否则 Typecho 生成的后台链接和静态资源地址仍可能出错。

不建议通过 $_SERVER['SCRIPT_FILENAME'] 猜测后台目录。插件面板由 extending.php 转发加载,不同 Web 服务器、反向代理和部署结构下,请求路径与文件路径不一定具有稳定关系。使用 Typecho 提供的配置常量更可靠。

修复后的检查方法

先扫描插件代码,确认没有残留的硬编码后台模板路径:

rg -n -F "/admin/" usr/plugins/FriendLinks usr/plugins/ZeyuAi usr/plugins/VisitAnalytics

正常情况下,只应看到路径函数中的默认回退值,不应再看到类似 include __TYPECHO_ROOT_DIR__ . '/admin/...' 的语句。

然后对修改过的 PHP 文件执行语法检查:

php -n -l usr/plugins/FriendLinks/panels/manage.php
php -n -l usr/plugins/ZeyuAi/panels/common.php
php -n -l usr/plugins/VisitAnalytics/panels/common.php

部署到线上后,再完成以下验证:

  1. 分别打开三个插件的后台页面,确认顶部导航和页面基础样式恢复。
  2. 在浏览器开发者工具的 Network 面板中确认后台公共 CSS 返回 200,且 URL 使用当前后台目录。
  3. 检查按钮、表格和表单交互,确认后台公共 JavaScript 已成功加载。
  4. 清理浏览器、CDN 和 PHP OPcache 缓存,避免旧页面或旧脚本影响判断。
  5. 同时在默认 admin 目录环境和自定义目录环境中回归测试。

小结

这类问题表面上是“线上和本地 CSS 不一致”,实际根因是插件把 Typecho 后台目录当成了固定路径。插件自身样式通过 pluginUrl 正常加载,进一步掩盖了后台公共模板包含失败的问题。

对于 Typecho 插件开发,后台目录、插件目录和站点地址都应通过框架提供的常量或 Options 属性取得,避免在代码中写死部署路径。修改后台目录可以减少默认入口暴露,但不能替代强密码、多因素认证、访问控制和及时更新等真正的安全措施。