公共标签:站点信息与页面通用
站点名称、品牌字标、备案号、参数、地址、包含、时间、静态资源、页面片段、CSRF 令牌 —— 这些写在哪个页面都一样。
适用范围:所有页面(页头、页脚、列表页、详情页、单页……都能用)。
速查表
{naicha:site name="…"}name(站点设置里的键,默认 title);取一条站点设置。常用键:title slogan keywords description icp copyright contact_email domain{naicha:brand}—;品牌字标,输出两段(.brand-a / .brand-b)。在最后一个「小写→大写」的驼峰边界断开(NaichaCMS → Naicha+CMS);中文站名没有这种边界就整段当第一段,不会拆坏{naicha:icp}—;备案号,输出成指向工信部的链接。没填时输出空串 —— 配合 .footer-bottom span:empty{display:none} 不会在页脚留空隙。海外站不需要备案,所以它必须是「空就不显示」{naicha:company name="…"}name;公司信息,整站只此一份。name 取 name(公司名)address phone mobile fax postcode email qq weixin contact license other。没填输出空串;他那套拼写(companyname blcense)也认{naicha:param name="…"}name、default;取参数:先看控制器传的 param,再看 URL 上的同名查询参数,都没有就用 default。{naicha:param name="cid"} 是最常用的一个(当前栏目 id){naicha:url }type = list|detail|page|tag|admin,配 cid/id/alias/name/path;或直接给 path;生成站内地址。会自动带上当前语言的前缀 —— 所以英文站里的 /list/x 会变成 /en/list/x,不要自己拼前缀{naicha:include file="…"}file;套用另一个模板文件(相对当前主题)。文件名只取 basename,写路径穿越无效{naicha:now}format(默认 Y-m-d);当前时间(服务器时区){naicha:asset file="…"}file;静态资源地址,自动带 ?v= 版本号(取自文件修改时间)。改了文件 URL 就变,浏览器必然重新拉取{naicha:block name="…"}name;页面片段(后台「站点素材 → 页面片段」里维护的那段文字)。取不到时输出空串,删掉片段不会把站点弄崩{naicha:token}—;CSRF 令牌,放进表单的隐藏域。页面里有它就不会被整页缓存(缓存下来发给别人会导致提交失败)每个标签的用法演示
下面每一段都是可以直接粘进模板的最小片段。方括号里写的是「这段放在哪」。
{naicha:site name="…"}
<!-- [页头] 站点名 + 描述,回落到站点设置 -->
<title>{naicha:site name="title"}</title>
<meta name="description" content="{naicha:site name="description"}">
<!-- [页脚] 联系方式(这三个键在「站点设置 → 站点信息」里填)-->
<a href="mailto:{naicha:site name="contact_email"}">{naicha:site name="contact_email"}</a>
<span>{naicha:site name="contact_wechat"}</span>
<!-- [任意] 统计代码:站点设置里存的是一整段 HTML,必须带 html="1" -->
{naicha:site name="stat_code" html="1"}
别用它输出正文。它只读「站点设置」里那一张表,取不到就是空串 ——
想让「某个键没填时整块不显示」,外面套一层 {naicha:if var="…" notempty="1"}。
{naicha:brand}
<!-- [页头] 品牌字标:输出两段,后一段可以换个颜色 -->
<a class="logo" href="/">{naicha:brand}</a>
<!-- 配套 CSS:NaichaCMS 会被拆成 Naicha + CMS -->
<style>
.logo .brand-a{color:#1e293b}
.logo .brand-b{color:#0078d4} /* CMS 用品牌色 */
</style>
拆分规则是在最后一个「小写→大写」的驼峰边界断开; 中文站名(如「奶茶科技」)没有这种边界,就整段当第一段、第二段为空 —— 不会把站名拆坏。
{naicha:icp}
<!-- [页脚] 备案号。没填时输出空串,配合下面这条 CSS 不会留空隙 -->
<div class="footer-bottom">
{naicha:site name="copyright"}
{naicha:icp}
</div>
<style>.footer-bottom span:empty{display:none}</style>
它自己会包成指向 beian.miit.gov.cn 的链接。
海外站不需要备案,所以它必须是「空就不显示」,而不是在模板里判断有没有填。
{naicha:company name="…"}
<!-- [页脚 / 关于我们] 公司信息。填一次,全站通用 -->
<ul class="contact">
<li>公司:{naicha:company name="name"}</li>
<li>地址:{naicha:company name="address"}</li>
<li>电话:{naicha:company name="phone"} <!-- 手机用 mobile、传真用 fax --></li>
<li>邮箱:{naicha:company name="email"}</li>
</ul>
<!-- [联系方式页] 联系人与营业执照号 -->
<p>联系人:{naicha:company name="contact"}</p>
<p>营业执照号:{naicha:company name="license"}</p>
完整 12 个名字:name(公司名)address phone
mobile fax postcode email qq
weixin contact(联系人)license(营业执照号)other。
在后台「站点设置 → 站点信息 → 公司信息」里填。
没填的那一项输出空串,和 {naicha:icp} 一样 ——
配合 :empty{display:none} 就不会在页脚留下一行空的「地址:」。
他那边的写法也认:name="companyname" 等于 name="name",
name="blcense" 等于 name="license" —— 从老模板抄过来的不用改。
{naicha:upload}
它只输出上传接口的地址(/upload.php)。界面长什么样是你的事,
下面这段可以整段抄进模板:
<!-- [任意] 一个上传控件 -->
<input type="file" id="f">
<button type="button" id="go">上传</button>
<p id="out"></p>
<script>
document.getElementById('go').onclick = async () => {
const f = document.getElementById('f').files[0];
if (!f) { return; }
const fd = new FormData();
fd.append('file', f);
fd.append('_token', '{naicha:token}'); // ⚠️ 必须带,缺了会被拒(安全校验)
const r = await fetch('{naicha:upload}', { method: 'POST', body: fd });
const d = await r.json();
document.getElementById('out').textContent = d.ok ? ('地址:' + d.url) : ('失败:' + d.error);
};
</script>
返回的 JSON:成功是
{"ok":true,"url":"/uploads/202610/ab12….png","name":"头像.png","size":1234},
失败是 {"ok":false,"error":"…"}。直接把 url 存起来即可。
- 访客:只能传图片(jpg / jpeg / png / gif / webp),且文件必须真的是图片 (改扩展名的脚本会被认出并拒绝)。
- 已登录会员:可以传文档 —— PDF / Office / 压缩包 / txt / csv / psd 等。
- 谁都传不了:php / html / svg / js 这类能被浏览器或服务器执行的文件。 它们落在网站目录里就是一个后门或一段注入脚本,两张白名单里都不收。
- 限制了频率:访客每个 IP 每小时 20 次、会员每人每小时 120 次,超了回「上传太频繁了」。
upload_max_filesize /
post_max_size)。超了会明确告诉你上限是多少,而不是含糊地说「上传失败」。
表单里的「文件」字段也是同一套规矩:加了 {naicha:form}
的表单如果带「文件」类型的字段,传 PDF/Word 现在能存下来了(早先它会回一句
「文件不是有效图片」—— 那是只按图片校验的历史问题,已经修掉)。
{naicha:sendemail}
同样是只输出地址(/sendmail.php)。它做两件事:
发一个 6 位邮箱验证码,以及校验这个码。
<!-- [任意] 邮箱 + 验证码 -->
<input id="mail" type="email" placeholder="你的邮箱">
<button type="button" id="snd">获取验证码</button>
<input id="code" maxlength="6" placeholder="6 位验证码">
<button type="button" id="chk">校验</button>
<script>
const TOK = '{naicha:token}', EP = '{naicha:sendemail}';
const post = (o) => fetch(EP, {
method: 'POST',
body: new URLSearchParams({ _token: TOK, ...o })
}).then(r => r.json());
document.getElementById('snd').onclick = async () =>
alertJSON(await post({ act: 'send', email: document.getElementById('mail').value }));
document.getElementById('chk').onclick = async () =>
alertJSON(await post({ act: 'check', email: document.getElementById('mail').value,
code: document.getElementById('code').value }));
function alertJSON(d) { alert(d.ok ? '通过:' + (d.msg || '') : ('失败:' + d.error)); }
</script>
返回的 JSON:成功 {"ok":true,"msg":"验证码已发出,请查收","ttl":600};
失败 {"ok":false,"error":"…"}。校验通过是 {"ok":true,"msg":"验证通过"}。
- 验证码 6 位数字、10 分钟有效、最多试 5 次、用过即失效。
- 限流:同一个邮箱 60 秒内只能发一次、每小时 5 次;同一个 IP 每小时 10 次。 限流在发信之前拦 —— 否则被人刷就等于帮人发垃圾邮件。
- 验证码在库里存的是哈希,不是明文。
- 发信失败会如实回失败,不会假装成功。
- ⚠️ 发信要先用后台「邮件设置」配好 SMTP,没配会回「本站还没配置发信邮箱」。
act=check —— {naicha:form} 的提交链路不接受外部校验结果,
这一点别指望它。另外那个
scene 参数(默认 verify)是给同一个邮箱的
不同用途分开记的(注册 / 找回 / 你自己的表单各用一个),互不干扰。
{naicha:param name="…"}
<!-- [列表] 当前页码:交给列表标签与分页条 -->
{naicha:list cid="{naicha:param name='cid'}" num="10" page="{naicha:param name='page'}"}
…
{/naicha:list}
<!-- [搜索页] 回填关键词;不存在时用默认值 -->
<input type="search" name="kw" value="{naicha:param name="kw"}" placeholder="搜点什么">
<input type="hidden" name="from" value="{naicha:param name="from" default="nav"}">
取值顺序:控制器传的 param → URL 查询参数 → default。
{naicha:url …}
<!-- [任意] 栏目地址(别名优先,别名空则用 id)-->
<a href="{naicha:url type="list" cid="3"}">解决方案</a>
<!-- [任意] 内容详情地址 -->
<a href="{naicha:url type="detail" id="12" alias="hello-world"}">读全文</a>
<!-- [任意] 单页 / 标签聚合 / 后台 -->
<a href="{naicha:url type="page" alias="about"}">关于我们</a>
<a href="{naicha:url type="tag" name="模板"}">#模板</a>
<a href="{naicha:url type="admin" path="contents"}">内容管理</a>
<!-- [任意] 只想拼一个站内相对地址 -->
<a href="{naicha:url path="/list/news"}">新闻</a>
⚠️ 它会自动带上当前语言的前缀:英文站里 /list/news 会变成 /en/list/news。
所以不要自己拼 /en/,两处都加就会变成 /en/en/…。
{naicha:include file="…"}
<!-- [任意] 套用同主题下的另一个文件 -->
{naicha:include file="header.html"}
…页面内容…
{naicha:include file="footer.html"}
<!-- [详情] 把「骨架」拆成片段,九套版式共用同一份 -->
{naicha:include file="detail_head.html"} <!-- 标题区 -->
{naicha:include file="detail_action.html"} <!-- 购买/下载/门槛 -->
{naicha:include file="detail_foot.html"} <!-- 上下篇 + 相关推荐 -->
文件名只取 basename,所以写 ../../config/config.php 也只会去主题目录里找
config.php —— 路径穿越无效。子主题里没有这个文件时会自动回落到父主题。
{naicha:now}
<!-- [页脚] 年份:用 U 拿时间戳,配合别的标签算版权年份 -->
<span>© {naicha:now format="Y"} {naicha:site name="title"}</span>
<!-- [任意] 完整时间 / 只到天 -->
<time>{naicha:now format="Y-m-d H:i"}</time>
<time>{naicha:now}</time> <!-- 默认 Y-m-d -->
时间格式就是 PHP 的 date() 那一套。
时区取站点设置;没设就是服务器时区。
{naicha:asset file="…"}
<!-- [页头] 样式与脚本,自动带 ?v= 版本号 -->
<link rel="stylesheet" href="{naicha:asset file="static/css/site.css"}">
<script src="{naicha:asset file="static/js/site.js"}" defer></script>
<!-- [任意] 图片同理(图标、logo 这类静态图)-->
<img src="{naicha:asset file="static/img/arrow.svg"}" alt="">
版本号取自文件修改时间(filemtime 的后 7 位)——
所以改了文件 URL 就会变,不用手工维护版本号。
⚠️ 代价要知道:重新解压/覆盖部署会把 mtime 刷成新值,于是所有访客的缓存集体失效、重下一遍。
这不是 bug,但如果你在意那一次流量,就在部署时保留原 mtime(tar -m / unzip -X)。
{naicha:block name="…"}
<!-- [首页] 一段可以在后台改的文案(后台「内容与外观 → 页面片段」)-->
<section class="hero">
<h1>{naicha:block name="home_hero_title"}</h1>
<p>{naicha:block name="home_hero_sub"}</p>
</section>
<!-- [任意] 片段里存 HTML 时带 html="1" -->
<div class="notice">{naicha:block name="site_notice" html="1"}</div>
<!-- [任意] 取不到片段就整块不显示(删掉片段不会把站点弄崩)-->
{naicha:if var="site_notice" notempty="1"}
<div class="notice">{naicha:block name="site_notice" html="1"}</div>
{/naicha:if}
片段的意义是改文案不动 HTML 结构: 站长想改首页标题,不该需要去编辑模板文件。
{naicha:token}
<!-- [任意表单] 每个 POST 表单里都要有,否则会被 CSRF 拦下 -->
<form method="post">
<input type="hidden" name="_token" value="{naicha:token}">
<textarea name="content"></textarea>
<button type="submit">提交</button>
</form>
⚠️ 页面里出现它,整页缓存就会自动跳过这一页(缓存下来发给别人会让提交失败)。 所以别把它放进「人人相同、且希望被缓存」的页面里 —— 那类页面本来也不该有表单。
