/** * 模板辅助类 - 集中所有共用逻辑 (★ 完整修复版 v2.0) * 位置: core/TemplateHelper.php * * 主要修复: * 1. kw2() 修复 标签嵌套字符串语法错误 * 2. k w() 增加 包裹首次出现的品牌名 * 3. 移除 fallback 中的双重 esc() 调用 * 4. kwBudget 提升到 8 (适合首页多 section 场景) * 5. 内容池来源是受信内容, 替换品牌时不重复 esc */ class TemplateHelper { private array $site; private $assets; private $tmpl; private $pageContent; // 关键词分布管理 (反 keyword stuffing) private int $kwBudget = 8; // ★ 提升预算 8 次 private int $kwUsed = 0; private int $kwLastUsedAt = 0; private int $kwCallCount = 0; private string $brandName; private string $brandShort; private string $primaryKeyword; // ★ 拉丁品牌池 (从 keywords 字段自动解析) private array $latinKeywords = []; private string $latinPrimary = ''; private int $latinIndex = 0; public function __construct(array $site, $assets = null, $tmpl = null, $pageContent = null) { $this->site = $site; $this->assets = $assets; $this->tmpl = $tmpl; $this->pageContent = $pageContent; $this->brandName = $site['name'] ?? ''; $this->brandShort = $site['name_short'] ?? mb_substr($this->brandName, 0, 2); // 解析关键词列表 - 自动分离中文与拉丁文 $keywords = array_filter(array_map('trim', explode(',', $site['keywords'] ?? ''))); // 中文关键词池 (含汉字的) $chineseKw = array_values(array_filter($keywords, fn($k) => preg_match('/[\x{4e00}-\x{9fff}]/u', $k) )); // 拉丁文关键词池 (纯英文/拼音, 用作国际品牌名) $latinKw = array_values(array_filter($keywords, fn($k) => !preg_match('/[\x{4e00}-\x{9fff}]/u', $k) && preg_match('/[a-zA-Z]/', $k) )); $this->primaryKeyword = $chineseKw[0] ?? $this->brandName; $this->latinKeywords = $latinKw; $this->latinPrimary = $latinKw[0] ?? ''; $this->latinIndex = 0; } // ============================================================ // 关键词密度管理 (反 thin content / keyword stuffing) // ============================================================ /** * ★ 智能关键词注入 (单语版本) — 已修复 包裹 * - 控制品牌词出现频率 (默认 8 次) * - 距离感知: 相邻 3 句内不重复 * - 首次出现的品牌名自动包 增加 SEO 权重 * - 超额时自动降级为短称, 不加 */ public function kw(string $text): string { $this->kwCallCount++; $hasBrand = mb_strpos($text, $this->brandName) !== false || mb_strpos($text, '{site_name}') !== false; // 没有品牌词 → 直接处理 placeholder 后 escape if (!$hasBrand) { $text = str_replace('{site_name}', $this->brandName, $text); return $this->esc($text); } // 预算耗尽 或 距离过近 → 降级为短称, 不加 $isFirstCall = ($this->kwLastUsedAt === 0); $tooClose = !$isFirstCall && ($this->kwCallCount - $this->kwLastUsedAt) < 3; if ($this->kwUsed >= $this->kwBudget || $tooClose) { $text = str_replace( ['{site_name}', $this->brandName], $this->brandShort, $text ); $text = preg_replace('/\s{2,}/', ' ', trim($text)); return $this->esc($text); } // 正常情况: 消耗预算, 第一次出现的品牌名包 $this->kwUsed++; $this->kwLastUsedAt = $this->kwCallCount; // 先标准化占位符 $text = str_replace('{site_name}', $this->brandName, $text); // 整段 escape 保证安全 $safeText = $this->esc($text); return $safeText; } /** * ★ 双语关键词输出 (中+拉丁混合版本) — 完全重写 * * 占位符: * {site_name} → 中文品牌 * {site_name_en} → 拉丁品牌 (轮询使用 latinKeywords 池) * {keyword} → 主关键词 * {year}/{domain} * * @param string $text 文本模板 * @param bool $strong 是否用 包裹 brand (默认 true) */ public function kw2(string $text, bool $strong = false): string { $this->kwCallCount++; $hasBrand = str_contains($text, '{site_name}') || str_contains($text, '{site_name_en}'); // 预算耗尽 或 距离过近 → 降级处理 $isFirstCall = ($this->kwLastUsedAt === 0); $tooClose = !$isFirstCall && ($this->kwCallCount - $this->kwLastUsedAt) < 3; if ($hasBrand && ($this->kwUsed >= $this->kwBudget || $tooClose)) { $text = str_replace('{site_name}', $this->brandShort, $text); $text = str_replace('{site_name_en}', '', $text); // 清理空括号 "( )" "()" $text = preg_replace('/[\((]\s*[\))]/u', '', $text); $text = preg_replace('/\s{2,}/', ' ', trim($text)); // 替换其他 placeholder 后 escape $text = strtr($text, [ '{keyword}' => $this->primaryKeyword, '{year}' => date('Y'), '{domain}' => $this->site['domain'] ?? '', ]); return $this->esc($text); } // 消耗预算 if ($hasBrand) { $this->kwUsed++; $this->kwLastUsedAt = $this->kwCallCount; } // 轮询取 Latin 关键词 $latinName = ''; if (!empty($this->latinKeywords) && str_contains($text, '{site_name_en}')) { $latinName = $this->latinKeywords[$this->latinIndex % count($this->latinKeywords)]; $this->latinIndex++; } // 没有 Latin 关键词 → 移除占位符 + 清理空括号 if ($latinName === '') { $text = str_replace('{site_name_en}', '', $text); $text = preg_replace('/[\((]\s*[\))]/u', '', $text); } // ★ 关键修复: 先 escape 整段, 再注入预先生成的 HTML // 步骤 1: 把 placeholder 替换为唯一标记 (避免 escape 后无法匹配) $markerBrand = "\x01BRAND_MARKER\x01"; $markerBrandEn = "\x01BRAND_EN_MARKER\x01"; $text = strtr($text, [ '{site_name}' => $markerBrand, '{site_name_en}' => $markerBrandEn, '{keyword}' => $this->primaryKeyword, '{year}' => date('Y'), '{domain}' => $this->site['domain'] ?? '', ]); // 步骤 2: escape 整段 $safeText = $this->esc($text); // 步骤 3: 把标记替换为 ... 或纯文本 $brandSafe = $this->esc($this->brandName); $latinSafe = $this->esc($latinName); // ★ v4.0: 永远不在 kw2() 中包 // Section.php 的 smartInjectBrandStrong() 会在需要时自动添加 $brandHtml = $brandSafe; $brandEnHtml = $latinSafe; $safeText = str_replace($markerBrand, $brandHtml, $safeText); $safeText = str_replace($markerBrandEn, $brandEnHtml, $safeText); return $safeText; } /** * 获取主 Latin 品牌 (用于 alt/title 等属性) */ public function brandEn(): string { return $this->esc($this->latinPrimary); } /** * 轮询取 Latin 关键词 (多次调用获得不同变体) */ public function latinKw(): string { if (empty($this->latinKeywords)) return ''; $kw = $this->latinKeywords[$this->latinIndex % count($this->latinKeywords)]; $this->latinIndex++; return $this->esc($kw); } /** * 强制使用品牌名 (H1/Title 等关键位置, 不消耗预算) */ public function brand(): string { $this->kwLastUsedAt = ++$this->kwCallCount; return $this->esc($this->brandName); } public function primaryKw(): string { return $this->esc($this->primaryKeyword); } public function brandShort(): string { return $this->esc($this->brandShort); } // ============================================================ // 图标渲染 (统一 FA + Emoji + Image) // ============================================================ public function icon(string $icon, array $opts = []): string { if (empty($icon)) return ''; $size = $opts['size'] ?? '2x'; $color = $opts['color'] ?? 'primary'; $extra = $opts['class'] ?? ''; // FontAwesome if (preg_match('/^(fa[srlb]?\s|fa-)/', $icon)) { return sprintf( '', $this->esc($icon), $this->esc($size), $this->esc($color), $this->esc($extra) ); } // 图片路径 if (preg_match('/\.(webp|png|jpg|jpeg|svg)$/i', $icon)) { return sprintf( '', $this->esc($icon), $this->esc($extra) ); } // Emoji 或文字 return '' . $this->esc($icon) . ''; } // ============================================================ // 图片资源 // ============================================================ public function image(string $slot, ?string $custom = null): string { if (!$this->assets) return ''; return $this->assets->image($slot, $custom); } public function images(string $prefix, int $count): array { if (!$this->assets) return []; return $this->assets->images($prefix, $count); } public function placeholder(): string { return $this->assets ? $this->assets->placeholder() : ''; } // ============================================================ // 文本渲染 (PageContent 代理 + 自动转义) // ============================================================ public function text(string $value, string $fallback = ''): string { return $this->esc($value !== '' ? $value : $fallback); } public function hero(string $key, string $fallback = ''): string { if (!$this->pageContent) return $this->esc($fallback); return $this->esc($this->pageContent->hero($key) ?: $fallback); } public function home(string $key, string $fallback = ''): string { if (!$this->pageContent) return $this->esc($fallback); return $this->esc($this->pageContent->homeText($key) ?: $fallback); } public function about(string $key, string $fallback = '') { if (!$this->pageContent) return $this->esc($fallback); $val = $this->pageContent->aboutText($key); if (is_array($val)) return $val; return $this->esc($val ?: $fallback); } public function app(string $key, string $fallback = ''): string { if (!$this->pageContent) return $this->esc($fallback); return $this->esc($this->pageContent->appText($key) ?: $fallback); } public function contact(string $key, string $fallback = ''): string { if (!$this->pageContent) return $this->esc($fallback); return $this->esc($this->pageContent->contactText($key) ?: $fallback); } public function lsi(string $pool, int $count = 3, string $glue = '、'): string { if (!$this->pageContent) return ''; return $this->esc($this->pageContent->lsi($pool, $count, $glue)); } // ============================================================ // 数据池访问 // ============================================================ public function pool(string $name, int $limit = 0): array { if (!$this->pageContent) return []; $data = $this->pageContent->$name ?? []; return $limit > 0 ? array_slice($data, 0, $limit) : $data; } // ============================================================ // 模板 / Block / Section 加载 // ============================================================ public function block(string $name, array $vars = []): void { if ($this->tmpl) $this->tmpl->block($name, $vars); } public function partial(string $name, array $vars = []): void { $this->block($name, $vars); } public function section(string $name, $composer = null): void { if ($composer) { echo $composer->renderOnly([$name]); } } // ============================================================ // SEO 辅助 // ============================================================ public function metaDesc(string $text, int $maxLen = 155): string { $text = strip_tags($text); $text = preg_replace('/\s+/u', ' ', trim($text)); if (mb_strlen($text) > $maxLen) { $text = mb_substr($text, 0, $maxLen - 1) . '…'; } return $this->esc($text); } public function readTime(string $content): int { $words = mb_strlen(strip_tags($content)); return max(1, (int)ceil($words / 400)); } // ============================================================ // 工具 // ============================================================ public function esc(?string $str): string { if ($str === null) return ''; return htmlspecialchars($str, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } public function isPage(string $type): bool { $uri = trim(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH), '/'); return match($type) { 'home' => $uri === '' || $uri === 'index.php', 'about' => $uri === 'about', 'contact' => $uri === 'contact', 'app' => in_array($uri, ['app', 'download']), 'single' => str_ends_with($uri, '.html'), default => false, }; } public function site(string $key, $default = '') { return $this->site[$key] ?? $default; } public function resetKeywordBudget(int $budget = 8): void { $this->kwBudget = $budget; $this->kwUsed = 0; $this->kwLastUsedAt = 0; $this->kwCallCount = 0; $this->latinIndex = 0; } }
Fatal error: Uncaught Error: Class "TemplateHelper" not found in /www/wwwroot/admin.com/core/Section.php:257 Stack trace: #0 /www/wwwroot/admin.com/core/Router.php(213): Section::getSharedHelper() #1 /www/wwwroot/admin.com/core/Router.php(1380): Router->buildContext() #2 /www/wwwroot/admin.com/core/Router.php(328): Router->routeSearch() #3 /www/wwwroot/admin.com/public/index.php(90) : eval()'d code(387): Router->dispatch() #4 /www/wwwroot/admin.com/public/index.php(90): eval() #5 {main} thrown in /www/wwwroot/admin.com/core/Section.php on line 257