简介面向帝国CMS站点管理员与SEO从业者的免费插件合集围绕内容采集、原创化处理与站内优化展开适合希望以较低成本完善网站SEO流程、减少重复手工操作的建站人员。合集涵盖全自动采集、伪原创发布、自动内链、关键词插入和自动配图等能力可分别用于内容获取、降低重复度、串联站内页面、控制关键词布局以及提升图文体验形成从内容生产到页面优化的完整链路。压缩包约312.51MB文件总数显示为0类型明细暂无数据从描述可知其中包含插件运行所需的依赖组件与SEO工具执行程序便于在本地环境中部署和调用相关功能。已有275人浏览学习可作为帝国CMS插件配置与SEO功能整合的参考。读者可据此了解各插件模块的分工、依赖环境组成及工具调用关系快速搭建采集、伪原创、内链与配图等功能的测试环境并在此基础上按站点需求筛选组合节省逐项寻找插件与排查运行依赖的时间。1. 花三个晚上手改 800 条 TDK 之后我开始找一套能一次装完的 SEO 插件一个站刚迁到帝国CMS栏目 30 多个历史文章 800 多条后台一打开就傻眼keyboard全是空的smalltext全是复制正文前 200 字内容页的 title 用的是[!--pagetitle--]也就是全站共用一个站点名。手工改到第 60 条的时候我放弃了开始翻这套免费帝国CMS插件合集——它把伪静态、TDK 批量生成、结构化数据、内链替换、图片 alt 补齐、站点地图、URL 主动推送、静态化刷新塞进一个扩展目录后台挂一个菜单进去。这套东西适合谁手里已经有帝国CMS站点、有数据库权限、能改模板的从业者。不适合谁指望上传一个 zip 就自动收录的。免费帝国CMS插件不等于零成本成本全在适配你的表结构和模板命名上这篇就按我自己装一遍的顺序拆。2. 插件到底落在哪帝国CMS 的模板层、插件目录与计划任务三层拆解拿到一个插件压缩包先别急着覆盖文件。我一般先把它解压出来的目录结构对着站点目录走一遍看清楚它往哪几个地方伸手。帝国CMS的扩展能力说实话不弱但入口分散插件作者的做法也各不相同不了解落点出问题就只能靠猜。2.1 三层插入点分别管什么第一层是模板层。帝国CMS的模板不是纯 HTML是被编译成 PHP 再执行的模板里能写灵动标签[e:loop{...}]、万能标签[ecmsinfo]...[/ecmsinfo]也能在必要处嵌 PHP。这一层决定的是「页面上看得到什么」title、description、canonical、JSON-LD、面包屑、图片 alt全部在模板里出结果。绝大多数所谓 SEO 插件核心工作量其实是在这一层做模板改造插件目录只是提供了一个统一的取值函数。第二层是扩展目录。帝国CMS的插件一般放在e/extend/下面一个插件一个子目录通过后台的扩展菜单或者模板里的require引入。这一层负责计算逻辑怎么从title拼出一个不重复的页面标题、怎么在正文里做关键词替换、怎么遍历数据表生成 sitemap。它不直接面向用户改错了前台也不报错这是最容易被忽略的地方。第三层是计划任务。帝国CMS后台有计划任务功能能定时执行一段 PHP。SEO 相关的定时活儿就三类刷新静态页、生成 sitemap、把待推送的 URL 批量提交出去。这一层是「没人看着也得跑」的部分也是最容易悄悄失败的部分——任务失败了后台不会弹窗你只会发现半个月后收录没动静。三层的关系要先想明白模板层出结果扩展目录出数据计划任务保时效。很多人装完插件发现「没效果」八成是只装了第二层模板没改或者只改了模板定时任务没挂。2.2 和 SEO 直接相关的几张表与字段改模板之前先看清楚数据从哪来。帝国CMS的表前缀默认是phome_装站时可能改过先在配置文件里确认一遍。下面这几张表是我每次都要过一眼的。表名关键字段对 SEO 的实际影响phome_ecms_newsid、classid、title、keyboard、smalltext、titlepic、newstime、onclick内容页 title、description、关键词、列表页摘要的唯一来源phome_ecms_news_data_1id、newstext正文所在表内链替换、图片 alt 补齐都在这里动手phome_enewsclassclassid、classname、bname、filename、classpath、islist、tbname栏目 URL、面包屑层级、伪静态路径都由它决定phome_enewstagstagid、tagname、numTAG 聚合页是否值得留、有没有内容可聚合有两个细节必须提前确认不然脚本会跑出乱七八糟的结果。一个是副表分表。newstext不在主表里而在_data_1、_data_2这样的副表里具体分了几张、某条数据落在哪张取决于建站时的设置。写内链替换脚本时图省事直接UPDATE phome_ecms_news_data_1会漏掉一部分文章。常见做法是先SELECT id FROM phome_ecms_news拿到主键再按id去对应副表更新或者干脆用帝国CMS自带的取文接口逐条读。另一个是smalltext。这个字段在后台叫「简介」很多站从别的 CMS 迁移过来时被整段塞了正文开头长度不受控直接输出到meta description里会被截断成半句话。要么在入库时截断要么在模板输出时截断二选一别两处都做。-- 先摸清楚数据底子再决定改哪里 -- 1) 有多少条文章压根没有关键词 SELECT COUNT(*) AS no_keyboard FROM phome_ecms_news WHERE keyboard OR keyboard IS NULL; -- 2) 简介长度超过 120 字的有多少条超出部分前端会被截断 SELECT COUNT(*) AS long_smalltext FROM phome_ecms_news WHERE CHAR_LENGTH(smalltext) 120; -- 3) 同一栏目下标题重复的情况重复标题是收录内耗的重灾区 SELECT classid, title, COUNT(*) AS c FROM phome_ecms_news GROUP BY classid, title HAVING c 1 ORDER BY c DESC LIMIT 20; -- 4) 确认副表到底有几张别只更新 _data_1 SHOW TABLES LIKE phome_ecms_news_data_%;这几条 SQL 的意义在于「先量化再动手」。第一条告诉你需要补关键词的量级第二条决定 description 是走截断还是走重写第三条查出来的重复标题要先人工合并因为后面批量生成标题的逻辑对重复标题无能为力——两条一模一样的标题怎么拼装还是撞车。第四条是很多人栽跟头的地方副表数量不对改完发现只有一部分文章生效。参数上没什么可调的把phome_换成你自己的前缀就行。CHAR_LENGTH和LENGTH的区别要注意中文在 UTF-8 下LENGTH是字节数一个字算三个判断中文长度必须用CHAR_LENGTH。2.3 装之前先做三件事备份、影子站、关缓存插件要动模板文件和数据库直接在生产站上试翻车了恢复起来很难看。我现在的固定流程是三件事。第一件全量备份数据库和站点目录都要而且备份文件要落到另一台机器上。只mysqldump不备份模板等于只买了一半的保险因为插件改模板改得比改库狠。#!/bin/bash # 上线前备份库 站点目录落到独立备份盘 set -e SITE_DIR/www/wwwroot/example.com DB_NAMEexample_db DB_USERdbuser DB_PASSdbpass STAMP$(date %Y%m%d_%H%M) BAK_DIR/data/backup/${STAMP} mkdir -p ${BAK_DIR} # 1) 数据库单事务导出避免 InnoDB 表出现半截数据 mysqldump -u${DB_USER} -p${DB_PASS} \ --single-transaction --quick --default-character-setutf8mb4 \ ${DB_NAME} | gzip ${BAK_DIR}/db.sql.gz # 2) 站点目录模板和插件都在里面排除缓存和上传附件省空间 tar -czf ${BAK_DIR}/site.tar.gz \ --exclude${SITE_DIR}/d/file \ --exclude${SITE_DIR}/e/data/cache \ -C $(dirname ${SITE_DIR}) $(basename ${SITE_DIR}) # 3) 记录当前模板文件的校验值改完能一眼看出动了哪些 find ${SITE_DIR}/e/data/html -type f -name *.html -exec md5sum {} \ ${BAK_DIR}/template.md5 echo backup done: ${BAK_DIR}脚本里几个选择说明一下。--single-transaction是给 InnoDB 用的导出期间不锁表线上站不至于卡住如果站点还有 MyISAM 表加上它意义不大但也不会有害。--quick让 mysqldump 逐行取数据大表不会一次性把内存吃满。排除d/file附件目录和e/data/cache缓存目录是为了让备份包小一点附件另做增量同步更合适。最后那句md5sum是我自己的习惯插件覆盖模板之后拿这份清单md5sum -c跑一遍改了哪些文件清清楚楚回滚时不用凭记忆。第二件搭影子站。把生产库导一份到测试库站点目录复制一份绑个临时域名或者改 hosts。伪静态规则、模板改动、计划任务全部先在影子站上跑通。这一步花半小时能省掉后面半天的救火。第三件关缓存。帝国CMS有数据缓存和模板缓存插件改完模板不生效十有八九是缓存没清。后台的数据更新里可以清也可以直接删e/data/cache下的缓存文件。提示影子站一定要用独立的数据库名和独立的缓存目录两站共用一个库调试改坏了生产数据一起没。3. 伪静态与 URL 结构规则错一条后面全站白改URL 是后面所有工作的地基。TDK 写得再好URL 里带一串问号参数、同一个内容有四个地址能打开权重全被自己稀释掉了。这一步做完再谈模板顺序不能反。3.1 URL 分层设计栏目、内容、TAG 各留什么位置帝国CMS的动态地址形式是/e/action/ListInfo/?classid1和/e/action/ShowInfo/?classid1id2直接暴露出来既难看也不好记。伪静态的目标是把它们映射成有层级感的静态形式。我一般用这套结构页面类型目标 URL 形式说明栏目列表首页/news/一级栏目用短目录别带list之类的前缀栏目列表分页/news/list_2.html分页从 2 开始第一页不出现页码内容页/news/202405/1234.html月份目录 文章 ID避免纯 ID 被遍历TAG 聚合页/tag/关键词.html只给num大于阈值的 TAG 开页专题页/zhuanti/名称/独立目录和栏目分开内容页用「月份 ID」而不是「拼音标题」理由很实在帝国CMS的标题改起来太随意编辑改一次标题拼音 URL 就失效一条得配 301配漏了就是死链。用 ID 稳定标题随便改。至于 ID 不连续暴露发布量这个在意的话就在生成时加个偏移量但要在伪静态规则里同步别只改一边。TAG 页要克制。帝国CMS的 TAG 是自动积累的跑一段时间能攒出几千个只有一篇文章的 TAG 页这些页面内容单薄、互相重复收进去只会拖后腿。常见做法是在phome_enewstags上加个判断num小于 3 的不生成页面、不加进 sitemap或者干脆robots里挡掉/tag/整个目录。3.2 nginx 与 Apache 两套重写规则帝国CMS后台有一套「伪静态页面设置」里面填的是「目标 URL 形式」真正生效的还得是 Web 服务器上的重写规则。两边必须严格对应差一个参数顺序就全 404。nginx 下的规则# 帝国CMS伪静态放到 server { } 块内location / 之前 # 栏目列表分页/news/list_2.html - 动态列表接口 rewrite ^/([a-z0-9_-])/list_([0-9])\.html$ /e/action/ListInfo/?classid$1page$2 last; # 栏目首页/news/ - 列表第一页 rewrite ^/([a-z0-9_-])/$ /e/action/ListInfo/?classid$1 last; # 内容页/news/202405/1234.html - 动态内容接口 rewrite ^/([a-z0-9_-])/([0-9]{6})/([0-9])\.html$ /e/action/ShowInfo/?classid$1id$3 last; # TAG 聚合页 rewrite ^/tag/(.)\.html$ /e/tags/?tagname$1 last; # 真实存在的静态文件和目录优先别被上面的规则抢走 location ~* \.(gif|jpg|jpeg|png|webp|css|js|woff2)$ { expires 30d; access_log off; }Apache 下等价的一套写进.htaccessIfModule mod_rewrite.c RewriteEngine On RewriteBase / # 已经存在的文件和目录直接返回避免重写死循环 RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^ - [L] # 列表分页 RewriteRule ^([a-z0-9_-])/list_([0-9])\.html$ e/action/ListInfo/?classid$1page$2 [L,QSA] # 栏目首页 RewriteRule ^([a-z0-9_-])/$ e/action/ListInfo/?classid$1 [L,QSA] # 内容页 RewriteRule ^([a-z0-9_-])/([0-9]{6})/([0-9])\.html$ e/action/ShowInfo/?classid$1id$3 [L,QSA] /IfModule几个容易出问题的地方。classid在 URL 里用的是栏目 ID 还是栏目别名取决于后台「栏目管理」里怎么设的。上面这套规则假设 URL 段就是classid数字如果你用的是bname别名规则里得先查一次库把别名换成 ID性能和复杂度都上去了我一般不用。last和break的区别last会重新走一遍 location 匹配break不会。帝国CMS的动态入口在e/action/下用last让请求重新进 PHP 处理break在这里会得到 404。Apache 的QSA标志不能省它把原始查询串保留下来翻页参数page靠它传。少了它翻页永远停在第一页这种问题在日志里看不出来只有真去点分页才发现。规则写完先nginx -t或者apachectl configtest校验语法再 reload。直接 reload 一个语法错误的配置站点当场躺下。3.3 模板里的链接同步与整站状态码验证重写规则只是让「旧地址能访问新形式」页面里输出的链接还得跟着改。帝国CMS的模板里栏目链接一般来自[!---classurl--]之类的标签或者面包屑标签[!--newsnav--]这些是跟着后台设置走的规则改完通常自动就对。真正要手动查的是硬编码的链接——列表模板里写死的/e/action/ListInfo/?classid5、导航里手写的/about/这些不会自己变。同步完之后必须验证而且是全量验证不是点几个链接看看。#!/bin/bash # 从 sitemap 里取出全部 URL批量看状态码 SITEMAPhttps://example.com/sitemap.xml OUT/tmp/status.tsv # 抽 URL逐个请求只取状态码 grep -oP (?loc)[^] ${SITEMAP} \ | while read -r url; do code$(curl -s -o /dev/null -w %{http_code} -A Mozilla/5.0 --max-time 10 ${url}) printf %s\t%s\n ${code} ${url} done ${OUT} # 汇总非 200 的全部列出来 awk -F\t $1 ! 200 ${OUT} | sort | head -50 echo --- awk -F\t {print $1} ${OUT} | sort | uniq -c | sort -rn这段脚本的价值在于「一次看全」。手工点 20 个链接发现都正常不代表第 300 个正常——分页到第 47 页、某个特殊栏目、某篇带附件的文章问题往往藏在这些边角。--max-time 10是防止某个地址卡住把整个循环拖死-A带一个正常 UA有些站会对空 UA 直接返回 403测出来全是假的非 200。结果里如果出现 301说明还有旧的动态地址没换干净找出来改掉出现 404先看是规则没覆盖还是数据本身不存在出现 500多半是重写把请求打到了一个不存在的 PHP 入口回头核对e/action/的路径。注意验证要在静态化关闭的状态下先做一遍。静态页会掩盖伪静态的问题等静态页生成好了再测测的是静态文件不是规则。4. 模板层批量改造TDK、JSON-LD、内链与站点地图一次覆盖地基铺完开始改页面本身。这一章是整套插件里最花时间的部分也是效果最直接的部分。原则只有一条能在模板里一次算出来的绝不留到手工。4.1 标题拼装别让 800 条页面共用一个 title帝国CMS里和标题相关的变量有好几个混淆是常态。[!--pagetitle--]是页面标题内容页默认取站点名[!--title--]才是文章标题[!--classname--]是当前栏目名。把内容模板的title写成[!--pagetitle--]结果就是 800 个页面全站同名这在收录里是硬伤。我用的拼装规则是三层文章标题 差异化后缀 站点名后缀按栏目轮换。!-- 内容页模板头部三层拼装后缀按栏目 ID 取模轮换 -- title[!--title--] - [e:loop{select classname from phome_enewsclass where classid.$GLOBALS[navclassid].,1,0,0,,}]?$bqr[classname]?[/e:loop] - 站点名/title meta namekeywords content[!--keyboard--] meta namedescription content[!--smalltext--]这段写法里[!--title--]保证唯一性栏目名保证相关性站点名放最后。真正的差异化其实靠的是文章标题本身所以前面那条「查重复标题」的 SQL 必须先跑完重复的合并掉否则拼出来还是撞。列表页的标题要单独处理分页必须带页码否则第 2 页和第 1 页标题一样。!-- 列表模板首页与分页用不同标题避免分页重复 -- title [e:loop{select 1,1,0,0,,}] ?php $page (int)($_GET[page] ?? 1); echo $page 1 ? 栏目名 - 第{$page}页 - 站点名 : 栏目名 - 站点名; ? [/e:loop] /title参数说明页码从查询串里取做整型转换防注入首页不加页码是因为首页要作为栏目主入口加个「第1页」既难看又和分页地址重复。判断放在模板里做比在插件里做省事因为分页参数本来就只在列表页存在。如果栏目有几十个硬编码栏目名不现实把「栏目名」换成[!--classname--]或者从phome_enewsclass里按classid查一次。帝国CMS的模板编译成 PHP 之后可以正常跑查询但要注意别在循环里查库——列表页一次查一次就够循环内查库是拖慢整站最快的方式。4.2 description 生成与长度截断smalltext的问题前面提过长度不受控。我在插件里加了一个统一出口模板只调这个出口不在模板里写截断逻辑这样以后改规则只改一处。?php /** * 生成 description优先用简介空则截正文统一长度 * 放在 e/extend/seo_tools/helper.php模板里 require 后调用 */ function seo_description(string $smalltext, string $newstext, int $limit 110): string { // 1) 去掉 HTML 标签和多余空白编辑器带出来的 p 不能进 meta $text strip_tags($smalltext ! ? $smalltext : $newstext); $text preg_replace(/[\s\x{3000}]/u, , $text); $text trim($text); // 2) 空内容兜底别输出空的 description if ($text ) { return 这里填栏目或站点的一句话说明; } // 3) 按字符截断不是按字节中文一个字算一个 if (mb_strlen($text, UTF-8) $limit) { $text mb_substr($text, 0, $limit, UTF-8); // 4) 不要切在半句话上回退到最后一个标点 $pos max( (int)mb_strrpos($text, , UTF-8), (int)mb_strrpos($text, 。, UTF-8), (int)mb_strrpos($text, 、, UTF-8) ); if ($pos $limit * 0.6) { $text mb_substr($text, 0, $pos, UTF-8); } $text . …; } // 5) 引号要转义否则 meta 标签会被截断 return htmlspecialchars($text, ENT_QUOTES, UTF-8); }逐段说。第一步strip_tags是必须的编辑器里粘进来的内容带pimg直接塞进content里会把标签属性撑破。第二步的空格处理里\x{3000}是全角空格中文站里特别常见只处理半角空格是不够的。第三步用mb_substr而不是substr这是最容易翻车的地方substr按字节切一个中文字三个字节切在第 110 个字节上会切出半个字输出成乱码。第四步回退到标点是为了不让 description 结尾是半句话$limit * 0.6这个阈值是防止回退太狠——如果最后一个逗号出现在第 10 个字回退过去就只剩一句话了那不如直接硬截断。第五步的htmlspecialchars经常被漏掉。文章简介里出现英文双引号输出到meta content...里就把属性闭合了后面的内容跑到标签外面页面结构直接坏掉。模板里调用就一行meta namedescription content?seo_description([!--smalltext--], , 110)?长度我给 110 个字符这是留了余量的做法——不同搜索引擎展示宽度不一样中文按 1 个字算110 字差不多是两行到三行的量。想更保守就压到 80。4.3 JSON-LD 与 canonical 插在哪一行结构化数据这块帝国CMS没有原生支持只能自己在模板里拼。我用的是 JSON-LD 形式放在head里比微数据好维护。!-- 内容页 head 内Article BreadcrumbList 两段结构化数据 -- script typeapplication/ldjson { context: https://schema.org, type: Article, headline: [!--title--], datePublished: [!--newstime--], dateModified: [!--lastdotime--], image: [!--titlepic--], author: { type: Organization, name: 站点名 }, publisher: { type: Organization, name: 站点名, logo: { type: ImageObject, url: https://example.com/logo.png } }, mainEntityOfPage: { type: WebPage, id: 当前页规范地址 } } /script参数上有几个必须对上的地方。datePublished和dateModified要用 ISO 8601 格式帝国CMS的[!--newstime--]默认输出的是Y-m-d H:i:s中间带空格严格校验会报错需要替换成T或者在插件里统一格式化好再输出。image必须是绝对地址相对路径会被判定无效。author用组织而不是个人是因为大部分站的内容是团队产出硬写一个人名反而不实。canonical 的位置更讲究它必须出现在head里而且要指向「这一篇的唯一正确地址」。!-- 内容页canonical 指向不带任何查询参数的规范地址 -- link relcanonical hrefhttps://example.com/[!--classpath--]/[!--id--].html !-- 列表页分页第 2 页以后 canonical 指向自己不要全指第一页 -- link relcanonical hrefhttps://example.com/news/list_[!--page--].html列表页分页的 canonical 有个常见误用把第 2 页到第 20 页全部 canonical 到第 1 页。理由是「避免重复内容」结果是把后面 19 页的内容全部否定了收录自然上不去。正确的做法是分页各自 canonical 到自己。内容页的 canonical 要特别注意带参数的访问。帝国CMS内容页可能通过?classid1id2这种动态地址访问也可能是伪静态地址还可能有?fromxxx之类的来源参数。canonical 的作用就是告诉搜索引擎「这些都算同一个页面认这一个」。所以 canonical 里绝对不能出现查询串。提示JSON-LD 里的地址和 canonical 里的地址必须一致两处不一致时搜索引擎通常以 canonical 为准结构化数据就被浪费了。4.4 内链自动替换和图片 alt 补齐内链这块插件通常提供一个关键词到 URL 的映射表然后在保存文章或生成静态页时做正文替换。这里有个必须接受的前提自动内链一定会出错所以只做「第一次出现」每篇最多替换 2 到 3 个词。?php /** * 正文内链替换每个关键词只替换首次出现且跳过已有链接 * $map 形如 [关键词 /news/123.html, ...] */ function seo_inner_link(string $html, array $map, int $maxLinks 3): string { $done 0; foreach ($map as $word $url) { if ($done $maxLinks) { break; } // 1) 先把已有的 a 整段挖掉避免链接套链接 $parts preg_split(/(a\b[^]*.*?\/a)/is, $html, -1, PREG_SPLIT_DELIM_CAPTURE); foreach ($parts as $i $seg) { // 2) 偶数下标是普通文本奇数下标是 a 段只在文本段里替换 if ($i % 2 1) { continue; } // 3) 跳过 script / style / 标签属性内部 if (preg_match(/(script|style)\b/i, $seg)) { continue; } $pos mb_strpos($seg, $word, 0, UTF-8); if ($pos false) { continue; } $link a href . htmlspecialchars($url, ENT_QUOTES, UTF-8) . . htmlspecialchars($word, ENT_QUOTES, UTF-8) . /a; $parts[$i] mb_substr($seg, 0, $pos, UTF-8) . $link . mb_substr($seg, $pos mb_strlen($word, UTF-8), null, UTF-8); $done; break; } $html implode(, $parts); } return $html; }逻辑说明preg_split带PREG_SPLIT_DELIM_CAPTURE把字符串切成「文本段 链接段 文本段」交替的数组偶数下标一定是文本奇数下标一定是已有的a。只在偶数下标里做替换就天然避开了链接套链接。这是最省事也最稳的做法比写复杂的负向断言正则可维护得多。$maxLinks限制每篇 3 条是因为内链密度过高会被当成作弊而且读起来也难受。关键词按长度从长到短排一遍再传进来能避免「数据分析」被「数据」先吃掉。图片 alt 补齐简单一些在内容保存时统一处理?php // 给正文中缺 alt 的图片补上「文章标题 序号」 function seo_fill_img_alt(string $html, string $title): string { $i 0; return preg_replace_callback( /img\b([^]*)/i, function ($m) use ($title, $i) { $attr $m[1]; $i; // 已有非空 alt 的不动尊重编辑的手工设置 if (preg_match(/\balt\s*\s*[\][^\][\]/i, $attr)) { return $m[0]; } $alt htmlspecialchars($title . 配图 . $i, ENT_QUOTES, UTF-8); // 去掉可能存在的空 alt再补新的 $attr preg_replace(/\balt\s*\s*[\][\]/i, , $attr); return img . $attr . alt . $alt . ; }, $html ); }已有的 alt 不能覆盖这点很重要。编辑手工写的 alt 通常比「标题 配图1」有意义得多插件无脑覆盖等于把有用的信息洗掉了。$i用引用传进去保证同一篇文章里的序号递增不会出现五个「配图1」。4.5 sitemap 与主动推送把生成端和提交端分开站点地图不要用爬虫去抓整站那样一定会把登录页、搜索页、分页参数无穷的地址全收进去。正确做法是直接从数据表出。?php /** * 生成 sitemap.xml只从数据表出 URL白名单控制 * 放到 e/extend/seo_tools/sitemap.php由计划任务调用 */ require(../class/connect.php); require(../class/db_sql.php); $link db_connect(); $empire new mysqlquery(); $host https://example.com; $limit 30000; // 单文件上限别超过 5 万 $out []; // 1) 栏目页只要开启列表且没被隐藏的栏目 $rows $empire-query(SELECT classid, classpath FROM phome_enewsclass WHERE islist 1 AND ishide 0 ORDER BY classid); while ($r $empire-fetch($rows)) { $out[] [loc $host . / . trim($r[classpath], /) . /, pri 0.8]; } // 2) 内容页只取已审核的按更新时间倒序超量的截断 $rows $empire-query(SELECT id, classpath, newstime FROM phome_ecms_news WHERE checked 1 ORDER BY newstime DESC LIMIT {$limit}); while ($r $empire-fetch($rows)) { $out[] [ loc $host . / . trim($r[classpath], /) . / . $r[id] . .html, pri 0.6, mod date(c, (int)$r[newstime]), // ISO 8601lastmod 必须用这个格式 ]; } // 3) 输出注意 XML 转义 header(Content-Type: application/xml; charsetutf-8); echo ?xml version1.0 encodingUTF-8? . \n; echo urlset xmlnshttp://www.sitemaps.org/schemas/sitemap/0.9 . \n; foreach ($out as $u) { echo url\n; echo loc . htmlspecialchars($u[loc], ENT_XML1) . /loc\n; if (!empty($u[mod])) { echo lastmod . $u[mod] . /lastmod\n; } echo priority . $u[pri] . /priority\n; echo /url\n; } echo /urlset; $empire-close();几个关键参数。$limit设 30000 是为了留余量站点地图协议单个文件上限是 5 万条、未压缩不超过 50MB压到 3 万条比较安全超了就拆成索引文件。checked 1这个条件不能漏帝国CMS里未审核的信息在后台是草稿状态前台访问不到写进 sitemap 就是一堆 404。lastmod用date(c)输出 ISO 8601格式错了搜索引擎会直接忽略这个字段。主动推送和生成 sitemap 是两件事要拆开做。生成端负责「有哪些 URL」提交端负责「告诉搜索引擎」。中间加一张队列表好处是失败了能重试不会因为一次接口超时把当天的推送全丢了。-- 推送队列记录每条 URL 的提交状态失败可重试 CREATE TABLE IF NOT EXISTS phome_seo_push ( id int(10) unsigned NOT NULL AUTO_INCREMENT, url varchar(255) NOT NULL COMMENT 待提交的绝对地址, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待提交 1成功 2失败, retry tinyint(3) NOT NULL DEFAULT 0 COMMENT 重试次数, addtime int(10) unsigned NOT NULL DEFAULT 0, lasttime int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_url (url), KEY idx_status (status,retry) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表设计里uk_url唯一索引是关键同一篇文章改三次标题不会推三遍。idx_status是给「捞待提交任务」用的WHERE status 0 AND retry 3走这个索引队列几万条也不慢。推送脚本按批取一次 50 条左右失败的回写status 2并把retry加一重试 3 次还失败的标成人工处理不要无限重试——接口持续报错的时候无限重试只会把队列越堵越死。接口地址和密钥从配置里读别写死在脚本里。# 计划任务里这样挂先生成再推送最后刷新静态页 # 每天凌晨低峰跑避免和编辑发布撞在一起 10 3 * * * /usr/bin/php /www/wwwroot/example.com/e/extend/seo_tools/sitemap.php /dev/null 21 20 3 * * * /usr/bin/php /www/wwwroot/example.com/e/extend/seo_tools/push.php /var/log/seo_push.log 21 30 3 * * * /usr/bin/php /www/wwwroot/example.com/e/extend/seo_tools/refresh.php /var/log/seo_refresh.log 21三个任务错开 10 分钟是给前一个跑完留时间。sitemap 那条输出丢到/dev/null因为它是直接回显 XML 的脚本跑计划任务时输出没意义后两条重定向到日志文件出问题时能翻。refresh.php负责刷新静态页顺序是先内容页、再列表页、最后首页反过来的话首页会链到还没刷新的旧列表。5. 五个翻车现场从 404 洪水到 sitemap 里全是登录页这套东西我前后装过好几个站翻来覆去踩的就是那几个坑。按「现象 → 原因 → 解决」记下来装的时候照着对一遍。5.1 列表页正常内容页全部 404现象伪静态改完栏目页/news/能打开翻页也正常唯独内容页/news/202405/1234.html全是 404。原因后台「伪静态页面设置」里的内容页格式和 nginx 规则对不上。最常见的是后台配的是/news/2024/1234.html四位年份nginx 规则里写的是[0-9]{6}六位年月正则压根不匹配其次是参数顺序反了规则里classid在前id在后而后台生成的是id在前。解决先把后台的伪静态格式贴出来和 nginx 规则一个字一个字对用在线正则工具把实际 URL 丢进去测匹配。确认之后nginx -t再 reload。还有一招更快的定位方式直接访问动态地址/e/action/ShowInfo/?classid1id1234如果能打开说明数据没问题纯粹是重写没匹配上。5.2 sitemap 里混进了登录页和搜索页现象sitemap.xml 打开一看里面有/e/member/login/、/e/search/?keyxxx、还有一堆带?page和from的参数地址。原因生成脚本不是从数据表出的而是拿爬虫抓了一遍整站或者直接扫了目录。功能页、搜索结果页、带参数的重复地址全被当成正常页面收进去了。解决把生成逻辑改成只从phome_ecms_news和phome_enewsclass出 URL功能目录一律不进。已经生成错的重新跑一遍覆盖掉同时去站长平台把明显无效的地址提交删除。另外 robots.txt 里把/e/整个目录挡掉双保险。5.3 批量内链之后正文排版乱了现象改完内链部分文章正文的链接套着链接点进去跳到别的页面还有一些段落的加粗、斜体失效了。原因替换用的是简单的字符串替换或者str_replace没有排除已有的a标签也没排除img alt...属性里的文字。关键词刚好出现在图片 alt 里替换进去就把标签属性撑破了。解决换成 4.4 里那段按a分段处理的方式只在纯文本段替换。已经改坏的文章从备份的newstext恢复或者写个脚本把a嵌套的层剥掉——嵌套的a在浏览器里会被自动拆开恢复起来不算难但量大就很痛苦。所以替换脚本上生产之前先拿 20 篇不同类型的文章跑一遍人工看。5.4 新文章发布了前台还是旧的现象编辑发了一篇新文章后台能看到前台列表和首页都没有手动去「数据更新」里刷新一下才出来。原因计划任务本文还有配套的精品资源点击获取