iHomepage CMS 官方技术文档。一套开放的网站内容管理底座:自由的内容模型、原生多语言、SEO 友好、模板化渲染,并支持 AI Agent 接入。涵盖快速开始、核心概念、设计思想、模板、路由、静态化、后台与 API、配置参考与上线清单。
iHomepage CMS 是一套开放的网站内容管理底座。它只提供最基础、最稳定的机制——一张内容表、两种关系、一套模板规则——其余全部交给你定义,因此比同类 CMS 拥有更高的自由度。多语言、SEO 友好、模板化是它与生俱来的能力,而不是插件;统一、可推断的接口也让 AI Agent 可以直接接入,替你建站和维护内容。
本文面向两类读者:建设和运营网站的团队,以及替团队完成建站工作的 AI Agent。读完你将知道:如何从零搭起一个站点、内容应该怎样组织、模板怎么写、有哪些开箱即用的能力,以及使用这套 CMS 必须遵守的规范。
iHomepage CMS 是一套典型的网站内容管理系统,也是一个开放的底座:
你不需要部署服务器、设计数据库或编写后台,只需要定义内容、编写模板、录入内容。
| 特性 | 说明 |
|---|---|
| 高自由度 | 内容类型、内容之间的关系、页面长什么样全部由你定义;新增一种内容不需要改数据库,也不需要写插件 |
| 原生多语言 | 语言前缀、译文分组、hreflang、语言切换全部内置,配置一项即可启用 |
| SEO 友好 | 规范地址、canonical、路径式分页、文章目录锚点、静态化,默认就对搜索引擎友好 |
| 模板化 | 每个页面都由 Twig 模板渲染,模板可以放在文件里,也可以在后台在线编辑 |
| AI / Agent 接入 | 内置 MCP 服务,Agent 登录授权后用自然语言管理网站;内容模型可推断,模板与内容都是纯文本 |
| 开箱即用 | 用户、表单、评论、点赞收藏、邮件订阅、在线支付、内容管理后台均已内置 |
以 WordPress 这类传统 CMS 为参照:
| 传统 CMS(如 WordPress) | iHomepage CMS | |
|---|---|---|
| 新增内容类型 | 注册自定义文章类型,通常要写代码或安装插件 | 起一个新的 taxonomy 值,配一个同名模板 |
| 内容之间的关系 | 分类之外的关联通常依赖插件或自定义字段 | 内置多对多关联,关系含义由两端类型决定 |
| 自定义字段 | 常借助字段类插件 | meta_json,模板里直接读取 |
| 多语言 | 需要安装多语言插件 | 原生支持 |
| SEO | 常借助 SEO 插件补全 | 默认内置 |
| 页面外观 | 在主题框架内定制 | 模板完全由你编写,没有主题框架约束 |
| 性能 | 常借助缓存插件 | 内置静态化,页面以静态文件直接返回 |
| AI 接入 | 视插件而定 | 内置 MCP 服务,OAuth 授权即可接入,文档同时面向 Agent 编写 |
| 部分 | 内容 |
|---|---|
| 快速开始 | 从零搭起一个可访问的站点 |
| 核心概念 / 设计思想 | 理解这套 CMS 为什么这样设计 |
| 内容模型 / 模板 / 路由 | 日常开发最常用的三块 |
| 多语言 / SEO / 静态化 / 用户与支付 / AI 接入 | 内置特性 |
| 后台 / API / 配置参考 | 查阅用的参考资料 |
| 安全 / 职责划分与规范 / 扩展 | 使用这套 CMS 必须遵守的规则 |
| 上线清单 / 常见问题 / 术语表 | 收尾与排查 |
不是每个网站都需要 CMS。先判断你的站点属于哪一种:
| 形态 | 适合 | 是否需要 CMS 内容库 |
|---|---|---|
| 纯静态站 | 几个固定页面,很少更新 | 不需要 |
| 静态页面 + 独立应用 | 在线工具、计算器、SaaS 前台 | 不需要 |
| CMS 内容站 | 文章、产品、案例、文档等持续增加、需要管理的内容 | 需要 |
| 混合站 | 首页和工具自己做,博客、产品、文档交给 CMS | 需要 |
混合站是最常见的形态:你自己写的页面和应用永远优先,CMS 负责承接内容类页面。
每个网站有一个属于自己的站点目录:
你的站点/
├── public/ 公开文件:静态页面、CSS、JS、图片、字体、前端应用的构建产物
├── templates/ (可选)Twig 文件模板
└── db/
└── cms.db (可选)站点的 CMS 内容库:内容、模板、配置、用户、表单、订单
public/ 里的文件由服务器直接返回,访问地址与文件路径一一对应。templates/ 目录,也可以存进内容库(在后台编辑),二选一即可。db/ 下只有 cms.db 会被 CMS 当作内容库;你自己的业务数据库起别的名字,CMS 不会读取。在后台「站点设置」中填写(或写入配置表):
| 配置 | 示例 | 说明 |
|---|---|---|
site_name | Acme | 站点名,会拼进页面标题 |
domain | www.acme.com | 规范域名 |
scheme | https | 协议 |
home_title / home_description | 首页的标题和描述 |
完整配置项见「配置参考」。
创建一个页头片段 header.html:
<!doctype html>
<html lang="{{ SITE.html_lang|default('en') }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ base.title }}</title>
<meta name="description" content="{{ base.description|e }}">
<link rel="canonical" href="{{ base.url }}">
<link rel="stylesheet" href="/css/site.css">
</head>
<body>
首页模板 home.html:
{{ include('header.html') }}
<main>
<h1>{{ SITE.site_name }}</h1>
{% set latest = list_post({taxonomy: 'article'}, 6) %}
{% for item in latest.list %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
</main>
{{ include('footer.html') }}
文章模板 article.html:
{{ include('header.html') }}
<article>
<h1>{{ the.title }}</h1>
<div class="content">{{ the.body|raw }}</div>
</article>
{{ include('footer.html') }}
在后台新建一条内容:
| 字段 | 值 |
|---|---|
| 标题 | Hello World |
| 类型(taxonomy) | article |
| 地址(url) | /articles/hello-world/ |
| 状态 | 已发布 |
访问 https://你的域名/articles/hello-world/,CMS 会用 article.html 渲染它。首页的列表里也会出现这一篇。
给登录入口加一个 class 即可:
<a href="#" class="user-panel-login-title">Login</a>
CMS 会在每个页面自动引入 /init.js,它发现页面上有用户面板的元素时就加载面板,不需要再手写脚本标签。/init.js 同时负责访问统计,统计结果显示在后台仪表盘。两项都可以用配置项调整:
| 配置项 | 取值 |
|---|---|
cms_js_panel | auto(默认,页面上有面板元素才加载)、on(每个页面都加载)、off(不加载) |
cms_js_stats | 默认开启;设为 0 关闭访问统计 |
访客点击后弹出登录框;站点管理员登录后可以切换到内容管理后台。每个站点的第一个管理员由平台开通。
到这里,一个可以持续运营的内容站就搭好了。接下来建议按顺序读「核心概念」和「内容模型」。
访客请求 https://你的域名/某个地址
↓
站点 public/ 里有这个文件吗?
├─ 有:直接返回(静态文件,最快)
↓ 没有
交给 CMS
→ 是平台接口(如 /user/api/login)?交给对应接口
→ 站点有内容库吗?没有:返回 404
→ 这个地址设置了跳转?跳到目标地址
→ 按地址查找内容或模板,渲染页面返回
三个要点:
public/ 的文件一定先被命中,CMS 不会覆盖它。在 iHomepage CMS 里,文章、产品、栏目、标签、案例、下载、门店、历史版本……全部是“内容”,存在同一张内容表 post 里,用 taxonomy(类型)区分。
| 关系 | 基数 | 字段 | 例子 |
|---|---|---|---|
| 隶属(层级) | 一对一 | parent_id | 文章属于某个栏目;子栏目属于父栏目 |
| 关联 | 多对多 | 关联表 post_post | 文章有多个标签;产品在多个门店有售 |
CMS 对每一条内容做完全相同的事:把内容本身、它的父级、它的关联、挂在它下面的列表都准备好,交给模板。这条内容渲染成详情页还是列表页,由模板决定,CMS 不做判断。
每条内容有一个规范地址(url)。访客、搜索引擎、静态化、站内链接都围绕这个地址工作。
理解下面几条,你就能推断出绝大多数问题的答案。
新增一种内容类型、一种关系,都不需要修改数据库结构:新类型只是一个新的 taxonomy 值,新关系只是两种类型之间的一条关联。所以结构始终稳定,底座升级时你的网站直接受益。
平台只提供机制:一张内容表、两种关系、一套模板查找规则、一条渲染路径。“什么算栏目”“这条关联代表什么”“页面长什么样”全部由网站定义。平台不会也不应该认识你的业务。
网站自己的文件、页面和应用始终优先于 CMS。CMS 只处理网站没有覆盖的地址。
底座只沉淀通用的能力。进入底座的每一项能力都必须具备通用性,并且默认不改变任何现有网站的行为。平台不提供只对某个网站生效的开关、例外或特殊分支。
平台升级默认不改变现有页面的输出。旧写法被新写法取代时,旧写法继续可用并标注为“已废弃”,不强制网站立即修改。
富文本在保存时清洗、凭据不回显、匿名上传默认不公开、管理权限每次请求实时校验——安全措施做在平台里,网站不需要各自实现。
taxonomy 的取值由网站自由定义,平台不预设任何值。常见约定:
article / post 文章 news 新闻 blog 博客
product 产品 service 服务 project 项目
case 案例 document 文档 download 下载
dir 栏目 tag 标签 version 历史版本
新增一种内容类型 = 起一个新的 taxonomy 名 + 写一个同名模板。 不需要其它任何操作。
parent_id 指向这条内容的上一级,只表达一对一的隶属:
模板中用 parent_of(the) 取父级。
post_post(post_id, child_post_id, weight)
post_id 是主体,child_post_id 是从属。 例如“栏目 → 文章”“文章 → 标签”。方向写反,同样的数据含义就反了。(dir, article) 文章在这个栏目下 (article, tag) 文章的标签
(product, service) 产品可选的增值服务 (store, product) 门店在售的产品
(guide, download) 指南的配套下载 (course, teacher) 课程的讲师
weight 是这条内容在这组关系中的排序位次。同一篇文章可以在不同栏目里排不同的位置。模板中用 related(the, 'tag') 取“它关联了谁”,用 list_related(id, …) 取“谁挂在它下面”。
一对一用
parent_id,多对多用关联。 不要把同一种关系同时写在两处。
dir 栏目 + article 文章(归入栏目)+ case 案例(关联 industry 行业)product 挂在 dir 下,打若干 tag,关联可选 service,在多个 store 有售guide 归入分类,关联 download 附件,用 version 记录历史版本thread 属于 topic,关联 tag| 字段 | 说明 |
|---|---|
title | 标题 |
description | 摘要,同时作为页面描述 |
keywords | 关键词 |
body | 正文 HTML |
url | 规范地址,建议总是显式填写 |
taxonomy | 内容类型 |
template | 指定模板;为空时使用 taxonomy 同名模板 |
status | 2 为已发布;其它值前台不可见 |
parent_id | 父级内容 |
pic_url | 封面图 |
author | 作者 |
publish_time / update_time | 发布与更新时间 |
price | 售价,单位是分(填 5999 表示 $59.99)。购物车、下单、订单全链路统一按分 |
weight | 全局排序权重 |
is_top / is_headline / is_featured / is_home | 置顶、头条、推荐、首页展示标记 |
click_count / like_count / fav_count | 浏览、点赞、收藏计数 |
lang / slug | 多语言:语言码与译文分组键 |
meta_json | 扩展字段(JSON) |
标准字段覆盖不到的数据——产品规格、FAQ 列表、价格区间、外部链接、按钮文案——一律存进 meta_json:
{"specs": {"weight": "1.2kg", "size": "30×20cm"}, "faq": [{"q": "包邮吗?", "a": "满 99 包邮"}]}
模板中直接读取,无需解析:
{{ the.meta_json.specs.weight }}
{% for item in the.meta_json.faq %}<dt>{{ item.q }}</dt><dd>{{ item.a }}</dd>{% endfor %}
所有名称以 _json 结尾的字段都会被自动解析。空值解析为空数组,格式错误的 JSON 保留为原字符串,页面不会因此报错。
多图:后台内容编辑器里的「多图」(常用作内容页轮播图)存在 meta_json.gallery,是图片地址的数组,其它键不受影响:
{% for src in the.meta_json.gallery %}<img src="{{ src }}" alt="">{% endfor %}
不要为了一个新字段去修改数据库结构。 如果某个字段所有网站都会用到,请按「扩展与需求反馈」提出。
| status | 含义 |
|---|---|
2 | 已发布:可访问、出现在列表中、会被静态化 |
0 等其它值 | 草稿或下线:前台返回 404,已生成的静态页自动移除 |
-1 | 已删除(可恢复,关联关系保留) |
模板使用 Twig 语法。
按以下顺序查找,找到即停止:
templates/ 目录下的 {名称}templates/ 目录下的 {名称}.html{名称} 或 {名称}.html 的模板同一个模板不要两处都放:文件模板会遮住后台里的同名模板,你在后台怎么改都不会生效。后台模板的引擎必须是 twig。
| 页面 | 使用的模板 |
|---|---|
首页 / | home |
搜索页 /search | search |
| 内容页 | 内容的 template 字段 → 否则 taxonomy 同名模板 → 否则 post |
| 找不到页面 | 404(可选;没有时输出纯文本 404) |
方式一:include 片段。 每个页面模板自己引入页头页尾:
{{ include('header.html') }}
……页面内容……
{{ include('footer.html') }}
方式二:base_layout。 创建 base_layout.html,用字面占位符 {{BODY}} 表示页面模板插入的位置:
<!doctype html>
<html lang="{{ SITE.html_lang }}">
<head><title>{{ base.title }}</title></head>
<body>
{{ include('header.html') }}
{{BODY}}
{{ include('footer.html') }}
</body>
</html>
存在 base_layout.html 时,页面模板自动套进去;页面模板本身已经是完整 HTML 文档时则不套。
| 变量 | 内容 |
|---|---|
SITE | 站点配置:site_name、domain、scheme、logo、menu、URL_ENDING、home_title、home_description;多语言站点另有 lang、lang_prefix、html_lang、languages |
base | 当前页的 title、description、keywords、url(canonical 完整地址,分页页已带页码) |
the | 当前内容,*_json 字段已解析 |
the.term | 父级内容 |
the.taglist | 关联的全部内容 |
the._toc / the._toc_html | 正文二级标题生成的目录(数组 / 现成 HTML),正文中已插入对应锚点 |
the.comments | 已审核的评论 |
LIST | 挂在当前内容下的内容,已分页:{list, total} |
TERMS | 地址查询参数中的筛选项 |
GET | 当前请求的查询参数 |
PAGE | 当前页码;路径式分页 /blog/page/2/ 不在查询参数里,只能从这里取 |
| 函数 | 作用 |
|---|---|
include(name, vars) | 引入模板片段;第二个参数是传给它的额外变量 |
list_post(where, limit, page, order) | 按条件查询内容,返回 {list, total} |
list_related(ids, limit, page, order, where) | 挂在指定内容下面的内容;多个 id 取交集,分组写法见下 |
related(item, taxonomy) | 这条内容关联了哪些内容,可按类型过滤 |
parent_of(item) | 父级内容 |
translations(item) | 这条内容的各语言版本 |
make_paginate(total, page, size) | 生成分页 HTML;page 不传就用当前页码 |
get_options(name) | 读取一项站点配置 |
示例:
{# 栏目页:列出栏目下的文章并分页 #}
{% for item in LIST.list %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
{{ make_paginate(LIST.total) }}
{# 首页:6 个推荐案例,按权重排序 #}
{% set cases = list_post({taxonomy: 'case', is_featured: 1}, 6, 1, 'weight') %}
{# 详情页:标签与面包屑 #}
{% for tag in related(the, 'tag') %}<a href="{{ tag.url }}">{{ tag.title }}</a>{% endfor %}
{% set parent = parent_of(the) %}
{% if parent %}<a href="{{ parent.url }}">{{ parent.title }}</a>{% endif %}
{# 多重筛选:同时属于栏目 3 和标签 7 的内容 #}
{% set items = list_related([3, 7], 12, 1) %}
{# 分组筛选:属于领域 101 或 102,并且属于片区 205 的中文内容 #}
{% set items = list_related([[101, 102], [205]], 12, 1, [], {taxonomy: 'firm', lang: 'cn'}) %}
list_related 的 ids 有两种写法:扁平的 [3, 7] 表示同时挂在每一个下面;分组的[[101, 102], [205]] 表示组内「或」、组间「且」,两种可以混用。空组会被忽略。第五个参数 where 是附加条件,写法与 list_post 相同。
list_post、list_related 的第四个参数用于排序,支持四种写法:
{{ list_related(8, 10, 1, 'weight') }} {# 单字段,默认降序 #}
{{ list_related(8, 10, 1, ['weight', 'publish_time']) }} {# 多字段 #}
{{ list_post({taxonomy: 'product'}, 12, 1, {'price': 'ASC'}) }} {# 指定方向 #}
{{ list_related(8, 10, 1, [{'weight': 'DESC'}, {'title': 'ASC'}]) }} {# 有序列表 #}
list_post 为发布时间倒序;list_related 为创建顺序倒序。需要确定顺序时请显式指定。taxonomy、slug 或 url 定位。id 失效时页面不会报错,只会悄悄显示空列表。{{ the.body|raw }};访客输入、查询参数等不可信数据不要加 |raw,必要时用 |e。/css/site.css,不要写 css/site.css,否则多级地址下资源会 404。base,不要在页头写死标题和描述。/ 开头的模板(如 /app.css、/robots.txt)按原文输出,并会被静态化。站点地图不需要自己写,CMS 会自动生成(见「SEO」)。templates/ 目录中只有 css、js、json、xml、txt、ico 文件能被直接访问,布局片段不会暴露在网上。SITE.site_name、SITE.logo、SITE.menu 和 get_options('contact_info'),不要写死在模板里。这样在后台或通过 Agent 修改设置时,全站一次生效。| 请求 | 结果 |
|---|---|
| 在后台「地址跳转」中启用了规则的地址 | 跳转到目标地址(优先于下面所有规则,平台接口除外) |
/ | 首页,home 模板 |
/index.html | 301 跳转到 / |
/search?keyword=关键词 | 搜索已发布内容的标题和摘要,search 模板 |
/uploads/… | 附件 |
/.well-known/… | 域名验证与协议发现文件,在后台「.well-known」维护 |
/user/api/…、/user/pay/…、/cms/api/…、/mcp | 平台接口 |
/init.js | 页面入口脚本,CMS 自动注入每个页面 |
与名称以 / 开头的模板相同的地址 | 输出该模板 |
既不以 / 结尾也不以 .html 结尾的地址 | 301 补上结尾的 / |
与某条内容的 url 相同 | 渲染该内容 |
/{taxonomy}/{id} 等非规范地址 | 301 跳转到该内容的规范地址 |
/{列表地址}/page/{n}/ | 列表第 n 页 |
| 其它 | 404 |
/mcp、/.well-known/、/init.js、/js/analysis2.js以及上表中平台接口的地址由平台保留,不要给内容或模板使用。
url 字段;其它能找到它的地址一律 301 到规范地址。/blog/my-post/ 或文件式 /blog/my-post.html。配置项 URL_ENDING 决定未填写 url 时自动生成的地址结尾。/ 结尾(如 /blog/),才能使用可静态化的路径式分页。.html(如 /design/index.html/26.html)。内容改版、地址调整后,旧地址不要直接 404,在后台「地址跳转」里把它跳到新地址。
| 字段 | 说明 |
|---|---|
| 来源地址 | 站内路径,如 /old/page.html;按路径精确匹配,不含域名和查询参数,同一地址只能有一条规则 |
| 目标地址 | 站内路径(/new/page/)或完整的 http(s):// 地址 |
| 状态码 | 301(默认)、302、307、308 |
| 状态 | 启用 / 停用;停用的规则不生效 |
| 备注 | 自由填写,如改版原因 |
后台同时显示每条规则的命中次数和最后命中时间,方便判断旧地址是否还有流量。
/ 和平台接口地址不能设置跳转。/old?utm_source=x 会跳到 /new?utm_source=x;目标地址自己带了查询参数时不再追加。public/ 里的同名文件不会被删除,后台会提示冲突——它不删掉,跳转就不会生效。/{taxonomy}/{id})CMS 已经自动 301 到规范地址,不需要另建规则。/blog/page/2/;查询式 ?page=2 同样可用。/blog/page/1/ 会 301 到 /blog/;超出范围的页码返回 404,不会生成空列表页。page_size 决定(默认 15)。make_paginate 不传第三个参数时自动与之一致。make_paginate 的第二个参数(当前页)也可以不传。写成 GET.page|default(1) 在路径式分页地址上会永远算成第 1 页。?dir=3&tag=7)时,分页链接自动使用 ?page=N。多语言是可选特性。只有配置了 languages 的站点启用,单语言站点不受任何影响。
{"default": "en", "available": {"en": "English", "cn": "中文", "es": "Español"}, "hreflang": {"cn": "zh-CN"}}
也可以简写为 ["en", "cn", "es"],第一个为默认语言。语言码只能包含小写字母、数字和 -。
同一篇内容在各语言下地址完全相同,只差一个语言前缀。默认语言不带前缀。
/guides/getting-started/ English(默认语言)
/cn/guides/getting-started/ 中文
/es/guides/getting-started/ Español
这是多语言能正常工作的前提:语言切换和 hreflang 都直接按这条规则推导,不需要查询。
lang:这条内容的语言码slug:译文分组键。同类型、同 slug、不同 lang 的内容互为译文渲染一条内容时,按它自己的 lang 决定页面语言,而不是按地址前缀。<html lang>、hreflang、语言切换器和 SITE.lang_prefix 都由它推导,所以 lang 填错,这些会整页一起错。
启用了多语言的站点,凡是有 url、会渲染成页面的内容,必须满足:
lang 必填,且必须是 available 里的语言码。 默认语言也要写明(如 en),不要留空。留空的内容虽然按默认语言渲染,但 list_post({lang: SITE.lang}) 这类按语言过滤的列表会漏掉它。lang 与地址前缀一致。 非默认语言的地址以 /{lang}/ 开头,默认语言的地址不带前缀。lang 为空或写成默认语言、地址却在 /cn/ 下,页面会被当成默认语言渲染,语言切换和 hreflang 会拼出 /cn/cn/... 这样的错误地址。taxonomy 相同、slug 相同,去掉语言前缀后地址相同。 slug 在同一 taxonomy、同一 lang 内唯一。没有 url、只作为筛选项或数据使用的内容不受第 2 条约束,但建议同样填写 lang,便于按语言取用。
单语言站点(未配置 languages)的 lang、slug 保持为空即可。
以后给单语言站点启用多语言时,先把已有内容的 lang 补成默认语言码,再上线其他语言。
<html lang="{{ SITE.html_lang }}">
{# hreflang #}
{% for t in translations(the) %}
<link rel="alternate" hreflang="{{ t.hreflang }}" href="{{ t.url }}">
{% endfor %}
{# 语言切换器 #}
{% for l in SITE.languages %}
{% if l.current %}<strong>{{ l.label }}</strong>{% else %}<a href="{{ l.prefix }}/">{{ l.label }}</a>{% endif %}
{% endfor %}
{# 按当前语言列内容 #}
{{ list_post({taxonomy: 'guide', lang: SITE.lang}, 10) }}
{# 站内链接带语言前缀 #}
<a href="{{ SITE.lang_prefix }}/about/">About</a>
单语言站点中 SITE.lang 和 SITE.lang_prefix 为空、translations() 返回空数组,同一份模板可以同时用于单语言和多语言站点。
iHomepage CMS 把搜索引擎友好作为默认行为,而不是需要额外安装的插件。
| 能力 | 说明 |
|---|---|
| 标题、描述、关键词 | 每个页面自动生成 base.title / base.description / base.keywords;内容页标题为「标题 | 站点名」 |
| Canonical | base.url 是完整规范地址,分页页自动带页码,避免第 2 页起被视为重复页 |
| 唯一地址 | 非规范地址 301 到规范地址;缺少结尾 / 的地址 301 补全;/index.html 301 到 / |
| 路径式分页 | /blog/page/2/ 可被抓取、可静态化 |
| 无空页面 | 超出范围的分页返回 404,不产生无限的空列表页 |
| 多语言 | hreflang、<html lang>、语言前缀约定 |
| 文章目录 | 正文二级标题自动生成目录与锚点(the._toc_html) |
| 静态化 | 页面预先生成为静态文件,响应快 |
| 站点地图 | 自动生成 /sitemap.xml,每天刷新,见下文 |
| 404 记录 | 找不到的地址会被记录,可在后台查看并修复 |
| 附件 | 图片等附件拥有稳定的 /uploads/ 地址 |
base.title、base.description、base.url,不要写死。title、description,并显式填写简洁、稳定的 url。<h1>(通常由模板输出标题)。alt。/robots.txt 的模板提供 robots,并在其中写上 Sitemap: https://你的域名/sitemap.xml。CMS 按内容生成站点地图文件,不需要安装插件或手工维护:
/sitemap.xml 是索引,指向 /sitemap_1.xml、/sitemap_2.xml……每个分片最多 50000 个地址。<lastmod>,取内容的更新时间。/sitemap.xml 会得到 404。/sitemap.xml 与 /sitemap_{数字}.xml:这两类文件名归 CMS。站点脚本、模板或上传写进去的同名文件都会被 CMS 直接覆盖,同名模板也不会生效。/sitemap_brands.xml),并自己提交给搜索引擎(在 robots.txt 里加一行 Sitemap:,或在站长平台提交)。CMS 不管这些文件,也不会把它们挂进 /sitemap.xml。CMS 会把页面预先生成为静态文件放进站点的 public/,访问时由服务器直接返回,不经过数据库和模板渲染。
static_publish 设为 yes(未设置即关闭)。site_blocked 为 yes)时不会生成。/ 开头的资源模板(CSS、JS、robots.txt 等)不在范围内:带查询参数的地址(搜索、多重筛选组合),它们始终动态生成。
以下能力由平台提供,网站直接调用即可,不需要也不应该自行实现一套。
the.comments/user/api/form_submit 接收任意字段的表单(联系我们、询盘、报名……):
contact_info.email)发送通知邮件form_receipt)。回执优先用本站的 sendmail_smtp 发送,没配置就用平台邮箱jpg、jpeg、png、gif、webp、pdf,不支持 svg不接在线支付也可以下单,适合「先下单、后人工确认」的站点:
/user/api/order_submit,传入商品明细(标题、单价、数量,可带内容 id)与联系人、收货信息pending)状态保存,可在后台「订单」中查看和改状态form_receipt)订单明细是下单当时的商品快照(标题、单价、数量):商品事后改名、改价、下架都不影响历史订单。明细里的内容 id 只作为备查线索,用来统计某个商品卖了多少,可以没有。传了内容 id 时,单价以服务端 post.price 为准;与前端传来的价格不一致时按服务端价格计算,并把差异记在订单备注里。
配置项 order_auto_pay 设为 yes 表示下单后直接进入支付流程(在线支付分支尚未开放);缺省或 no 时就是上面这套「等待处理」的流程,不会跳转支付。
基于 Stripe Checkout(金额同样以分为单位):
/user/pay/order_create,传入商品(内容 id 与数量)price 计算,不接受前端传价/user/pay/stripe_webhook 后订单变为已支付,并写入实际金额、税费和收货信息需要在配置项 stripe_config 中填写密钥,并在 Stripe 后台注册 Webhook 地址 https://你的域名/user/pay/stripe_webhook。税费由 Stripe Tax 按收货地址计算,需在 Stripe 后台开通。
iHomepage CMS 在设计上就考虑了 AI Agent 的使用:结构简单、行为确定、接口统一,Agent 不需要猜。
code 一致;列表接口共用同一套分页、排序和筛选参数。| 任务 | 做法 |
|---|---|
| 修改网站设置(电话、Logo、菜单) | update_settings;改之前先用 find_text 确认这段文字是否还写死在模板里 |
| 新增栏目与页面 | create_category_with_items 一步建好栏目和其中的条目 |
| 撰写、修改与翻译内容 | create_content、update_content、translate_content(地址与语言按多语言约定自动生成) |
| 修改模板 | update_template,保存前自动检查 Twig 语法 |
| 维护 SEO | 通过内容工具维护标题、描述、地址与译文 |
| 读取公开内容 | /user/api/post_list、/user/api/post_get,无需登录 |
站点内置 MCP 服务(Model Context Protocol,Claude、ChatGPT、Cursor 等主流 Agent 都支持的接入标准),不需要复制任何密钥:
https://你的域名/mcp| 权限 | 允许 Agent 做什么 |
|---|---|
| 读取(必需) | 查看内容、模板和网站设置 |
| 管理内容 | 新建、修改、删除内容与栏目,创建译文 |
| 修改设置 | 网站名称、Logo、联系方式、菜单 |
| 修改模板 | 编辑数据库模板,保存前检查语法 |
| 重新生成页面 | 按地址重新生成静态页 |
Agent 的修改立即生效,效果与管理员在后台保存完全相同:同样的清洗、关联同步和静态化。每次调用都会重新校验授权人的管理员身份;在后台「Agent 接入」里可以随时查看和撤销授权,撤销立即生效。短信、邮件、支付等凭据类配置对 Agent 始终不可见。
权限:只有本站管理员可以进入。管理员身份每次请求实时校验,撤销立即生效;在 A 站登录的管理员只能管理 A 站。第一个管理员由平台开通,之后管理员可以在后台授予或收回其他用户的管理员身份。
| 模块 | 能力 |
|---|---|
| 仪表盘 | 近 30 天访问量、访客数、新增用户、新增内容、爬虫访问 |
| 内容 | 新建、编辑、删除(可恢复)、设置层级与关联及其排序 |
| 模板 | 在线编辑 Twig 模板 |
| 站点设置 | 基础信息、SEO、联系方式、各类配置 |
| 附件 | 上传、编辑信息、删除 |
| 评论 | 审核、编辑 |
| 表单 | 查看、处理、删除 |
| 订单 | 查看、更新状态 |
| 用户 | 新建、编辑、停用、授予管理员 |
| 404 日志 | 找不到的地址及来源 |
| 页面更新 | 按地址重新生成静态页 |
| 数据结构 | 检查并升级本站内容库结构 |
| Agent 接入 | 接入地址、生成授权元数据、查看与撤销已授权的应用 |
| .well-known | 管理域名验证与协议发现文件(JSON 或纯文本) |
| 地址跳转 | 旧地址跳转到新地址:新建、编辑、启停、删除,查看命中次数 |
https://你的域名/user/api/login{"code": 200, "data": { }}
{"code": 401, "error": "login_required", "message": "Login Required"}
HTTP 状态码与 code 一致。失败时:
error 是稳定的错误码,程序据此判断出了什么错、怎样提示用户(比如按网站语言翻译);message 是默认的英文说明,可以直接展示,但措辞可能调整,不要拿它做判断。每个失败响应都带 error。没有专门错误码的情况按状态码给通用码:bad_request(400)、unauthorized(401)、forbidden(403)、not_found(404)、not_allowed(405)、payload_too_large(413)、unsupported_media_type(415)、too_many_requests(429)、server_error(500 等)、service_unavailable(503)。
用户接口 /user/api/* 的错误码:
| 分类 | 错误码 |
|---|---|
| 登录状态 | login_required 未登录、session_expired 登录已过期、too_many_requests 请求太频繁 |
| 账号 | invalid_credentials 账号或密码错误、account_disabled 账号不可用、account_create_failed 创建失败、user_not_found、email_taken、mobile_taken、userid_taken、old_password_incorrect、password_too_short |
| 验证码 | verification_code_required 缺验证码、sms_code_invalid、email_code_invalid、invalid_email、unsupported_area_code 不支持的手机区号 |
| 发送通道 | sms_not_configured、sms_send_failed、email_not_configured、email_send_failed |
| Google 登录 | google_not_configured、google_unavailable、google_credential_invalid、google_email_missing、google_email_unverified |
| 内容与互动 | post_not_found、comment_empty |
| 表单、订单与支付 | form_unavailable、order_unavailable、order_not_found、order_create_failed、invalid_order_items、product_unavailable、payment_not_configured、payment_failed |
| 上传 | upload_failed、file_too_large、file_type_not_allowed |
| 参数 | missing_parameter 缺少参数、invalid_parameter 参数不合法(具体哪一项看 message) |
/user/api/*| 接口 | 登录 | 说明 |
|---|---|---|
session | 否 | 当前登录状态与用户信息 |
join | 否 | 注册 |
login / logout | 否 | 登录 / 退出 |
login_google | 否 | Google 登录 |
get_profile / edit_profile | 是 | 读取 / 修改资料 |
edit_passwd | 是 | 修改密码 |
password_reset | 否 | 用邮箱验证码重置密码 |
send_email_code / send_sms_code | 否 | 发送验证码 |
update_email / update_mobile | 是 | 换绑邮箱 / 手机 |
comments_submit | 是 | 提交评论 |
like / unlike / favorite / unfavorite | 是 | 点赞、收藏 |
like_status | 否 | 批量查询点赞收藏状态与计数 |
subscribe | 否 | 邮件订阅 |
form_submit | 否 | 提交表单 |
order_submit | 是 | 下单(不经在线支付,订单状态为待处理) |
put_file | 否 | 上传文件 |
post_list / post_get | 否 | 已发布内容的列表与详情 |
orders_list / orders_get | 是 | 我的订单 |
内容列表示例:
GET /user/api/post_list?where[taxonomy]=product&keyword=chair&page=1&page_size=20
post_list 参数:
| 参数 | 说明 |
|---|---|
where | 按 taxonomy、parent_id、lang、slug 和推荐标记精确过滤,其它字段忽略 |
keyword | 匹配标题和描述 |
related | 可选,按关联筛选,写法同模板 list_related 的分组写法:[[101, 102], [205]] 表示挂在 101 或 102 下面,并且挂在 205 下面。空组忽略;最多 10 组、共 100 个 id |
order | 可选,写法同模板函数的排序参数;显式指定时末尾自动补 id 倒序 |
page / page_size | 分页,page_size 最大 100 |
where、order、related 三种传法等价:JSON 请求体;查询串方括号写法(where[taxonomy]=firm);查询串里的 JSON 字符串(where={"taxonomy":"firm"})。JSON 写错时返回 400,不会忽略条件后返回全部内容。
传 related 时,默认顺序与模板 list_related 相同,列表页用它做勾选筛选、Ajax 刷新,结果与页面首屏同序:
POST /user/api/post_list
{"where": {"taxonomy": "firm", "lang": "cn"}, "related": [[101, 102], [205]], "page": 1, "page_size": 15}
{"code": 200, "data": {"list": [], "total": 0, "page": 1, "page_size": 20}}
/user/pay/*| 接口 | 鉴权 | 说明 |
|---|---|---|
order_create | 登录 | 创建订单并返回 Stripe 收银台地址 checkout_url |
stripe_webhook | Stripe 签名 | Stripe 回调 |
/cms/api/*需要本站管理员身份,供内容管理后台使用。涵盖内容、模板、配置、附件、评论、表单、订单、用户、地址跳转、统计与静态页更新。列表类接口共用分页、排序和筛选参数:
{"_page": 1, "_page_size": 20, "_order": {"id": "DESC"}, "_filter": {"status": 2, "title[~]": "关键词"}}
_page_size 最大 200!、>、<、>=、<=、~(包含)、!~、<>(区间)站点配置在后台「站点设置」中维护。JSON 类配置填写 JSON 文本。
| 配置项 | 说明 | 默认 |
|---|---|---|
site_name | 站点名称 | |
domain | 规范域名 | 当前访问域名 |
scheme | 协议 | https |
logo | Logo 地址 | |
menu | 菜单数据,供模板使用 | |
home_title / home_description | 首页标题与描述 | |
URL_ENDING | 自动生成地址的结尾,/ 或 .html | / |
page_size | 列表每页条数 | 15 |
site_blocked | yes 时屏蔽全站:所有地址只输出 site_message,相当于关站/维护模式 | no |
site_message | 屏蔽时显示的公告;只在 site_blocked 为 yes 时生效,留空则显示一句通用的暂不可访问提示 |
| 配置项 | 说明 | 默认 |
|---|---|---|
languages | 多语言配置(JSON),见「多语言」 | 未启用 |
static_publish | 设为 yes 开启静态化 | 关闭 |
join_need_check | 设为 1 时新注册用户需审核 | 直接可用 |
| 配置项 | 格式 | 说明 |
|---|---|---|
contact_info | {"email": "...", "name": "...", "mobile": "...", "address": "..."} | 联系方式;email 接收表单通知 |
sendmail_smtp | {"host","port","secure","username","password","email","name"} | 本站发信账号:验证码、表单回执 |
form_receipt | {"enabled": true, "content": "..."} | 表单提交后给提交人发送回执 |
sms_config | {"apiUrl","apiKey","tplId"} | 短信验证码。不填就用平台共享的短信通道,只有自带通道时才需要填 |
user_agreement_url | 站内路径 | 注册框里「用户协议」的链接,默认 /user-agreement/ |
privacy_policy_url | 站内路径 | 注册框里「隐私政策」的链接,默认 /privacy-policy/ |
panel_hide_register | yes / no | yes 时登录框里不显示「去注册」。给「注册前必须先走完站点自己的流程」的网站用——注册界面只由页面上的注册按钮唤起,访客不能从登录框绕过那一步。缺省允许 |
| 配置项 | 格式 | 说明 |
|---|---|---|
order_auto_pay | yes / no | yes 表示下单后直接进入支付流程;缺省或 no 时订单等待人工处理,只发通知邮件 |
stripe_config | {"secret_key","webhook_secret","success_url","cancel_url","tax_countries"} | Stripe 支付;跳转地址可含 {ORDER_NO} 占位符 |
social_login | JSON | 社交登录配置 |
凭据类配置(SMTP、短信、Stripe、社交登录)保存后不会在后台明文回显。
平台已内置以下保护,网站需要配合遵守对应的要求。
| 平台保护 | 你需要做的 |
|---|---|
正文保存时按白名单清洗:script、style、iframe、form 等被移除,事件属性被去掉 | 交互和样式写在模板里,不要放进正文。正文用 class 挂模板里的样式(排版类标签都保留 class);折叠内容用原生 <details>/<summary> |
| 模板不自动转义 | 不可信数据不要使用 |raw |
| 凭据只存本站配置,不回显 | 不要把密钥写进模板、前端代码或公开文件 |
| 管理权限每次请求实时校验,站点之间天然隔离 | 及时收回不再需要的管理员 |
| 上传使用扩展名白名单,拒绝 SVG,匿名上传默认不公开 | 不要另建上传接口 |
| 支付价格由服务端计算,Webhook 校验签名 | 不要在前端计算或传递价格 |
| 登录、注册、验证码、表单接口限流 | 不要另建登录或表单接口 |
| 删除为软删除,关联保留 | 需要恢复时改回状态即可 |
| 静态化只写入安全的文件类型和路径 | 不要手工修改生成的静态页 |
CMS 底座和基于它建设的网站各自持续迭代。清晰的边界让底座可以放心升级,也让网站可以放心开发。
| 事项 | 负责方 |
|---|---|
| 内容数据库的结构(表、字段、索引) | 平台 |
| 页面渲染、路由规则、模板函数 | 平台 |
| 平台接口(用户、支付、管理) | 平台 |
| 静态化机制及其生成的文件 | 平台 |
| 内容库里的数据:内容、模板、配置、用户、表单、订单 | 网站 |
站点 public/ 中网站自己放置的文件 | 网站 |
站点 templates/ 目录 | 网站 |
| 网站自己的数据库、脚本和独立应用 | 网站 |
一句话:内容库的结构归平台,内容库的数据归网站。
可以自由做:
taxonomy + parent_id + 关联 + meta_json)public/ 中放置自己的页面、资源和前端应用不要做:
|rawparent_id 和关联绝大多数需求不需要平台改动:
| 需求 | 做法 |
|---|---|
| 增加字段 | meta_json |
| 增加内容类型 | 新 taxonomy + 同名模板 |
| 增加关系类型 | 新类型组合 + 关联 |
| 某个页面特殊排版 | 在内容的 template 字段指定专用模板 |
| 复杂列表 | 组合 list_post / list_related 的条件和排序 |
| 独立功能 | 在站点 public/ 中实现独立应用 |
平台不会为单个网站调整。一项需求要进入平台,需要同时满足:
请说明:
平台不会为某一个网站增加专属开关。
页面
base<html lang> 正确;多语言站点 hreflang 完整/ 结尾,分页正常,超范围页码 404内容
url 显式填写且唯一meta_json功能与安全
|raw静态化站点中,模板修改后只有首页自动更新,其它页面需要全站重建。如果仍未生效,检查 templates/ 目录中是否有同名文件模板遮住了后台模板。
依次检查:内容状态是否为已发布;关联是否存在且方向正确(主体在前);模板里是否写死了已失效的 id;排序字段是否写错。
你访问的不是它的规范地址。这是为了避免重复收录的正常行为。
使用关联的 weight,模板中 list_related(栏目id, 10, 1, 'weight')。
使用 meta_json。如果所有网站都需要,请按「扩展与需求反馈」提出。
检查模板名称是否与内容的 template 或 taxonomy 一致(.html 后缀可以省略);后台模板的引擎是否为 twig。
{{ 或 {% 时报错模板中的这两组符号会被当作 Twig 语法。需要原样输出时使用 {% verbatim %}…{% endverbatim %}。正文内容中的这两组符号不受影响。
正常行为,不需要处理。
该语言下没有这篇内容的译文。按 URI 约定补齐译文即可。
| 术语 | 含义 |
|---|---|
| 内容(post) | CMS 中的一切数据单元:文章、产品、栏目、标签…… |
| taxonomy | 内容类型,由网站自由定义 |
| 层级(parent_id) | 一对一的隶属关系 |
| 关联(post_post) | 多对多的关系,含主体端、从属端和排序权重 |
| 主体 / 从属 | 关联的两端,如“栏目 → 文章”中栏目是主体 |
| meta_json | 内容的扩展字段 |
| 规范地址 | 内容的 url,唯一对外地址 |
| 模板 | 决定内容如何展示的 Twig 文件或后台模板 |
| base_layout | 可选的全站页面外壳 |
| 静态化 | 把页面预先生成为静态文件 |
| 全站重建 | 重新生成全站所有静态页面 |
| 站点配置(options) | 站点级设置项 |
本页本身就是用 iHomepage CMS 建成的:它是官网内容库中类型为 document 的内容,中英文两个版本共用同一个 URI(/documents/ 与 /cn/documents/),由同名模板 document.html 渲染,左侧目录来自 the._toc,并已静态化。