Files
menav/templates/components/site-card.hbs
rbetree 9929f60170 fix: 加固链接/图标安全与版本一致性;sync-articles 对齐 best-effort
- 模板与运行时统一做 URL scheme 白名单校验(不安全降级为 #),并清洗 icon class;分类标题/新增分类改用 DOM API 避免 innerHTML 注入
- sync-articles 主入口异常不再返回非 0 退出码,避免阻断 build/deploy
- window.MeNav.version 改为从 meta menav-version/配置自动读取,避免写死版本
- 文档/配置:新增 security.allowedSchemes 配置说明
2026-01-04 18:24:01 +08:00

174 lines
8.4 KiB
Handlebars
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{#if url}}
<a href="{{safeUrl url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}" {{#if external}}target="_blank"
rel="noopener" {{/if}} data-type="{{#if type}}{{type}}{{else}}site{{/if}}" data-name="{{name}}" data-url="{{url}}"
data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}" {{#if faviconUrl}}data-favicon-url="{{faviconUrl}}"
{{/if}} {{#if forceIconMode}}data-force-icon-mode="{{forceIconMode}}" {{/if}}
data-description="{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}"
data-tooltip="{{#if name}}{{name}}{{else}}未命名站点{{/if}}{{#if description}} - {{description}}{{else}} - {{extractDomain url}}{{/if}}"
{{#if publishedAt}}data-published-at="{{publishedAt}}" {{/if}} {{#if source}}data-source="{{source}}" {{/if}}>
{{!-- articles首行图标+标题;下方“时间/来源 + 简介”全宽对齐,不被图标列缩进 --}}
{{#ifEquals type "article"}}
<div class="article-card-header">
<div class="site-card-icon" aria-hidden="true">
{{!-- 站点图标优先级faviconUrl > forceIconMode > 全局 icons.mode --}}
{{#if faviconUrl}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img class="favicon-icon" src="{{faviconUrl}}" alt="{{name}} favicon" loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div>
{{else}}
{{#ifEquals forceIconMode "manual"}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} site-icon" aria-hidden="true"></i>
{{else}}
{{#ifEquals forceIconMode "favicon"}}
{{#ifHttpUrl url}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div>
{{else}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} site-icon" aria-hidden="true"></i>
{{/ifHttpUrl}}
{{else}}
{{#ifEquals @root.icons.mode "favicon"}}
{{#ifHttpUrl url}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div>
{{else}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} site-icon" aria-hidden="true"></i>
{{/ifHttpUrl}}
{{else}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} site-icon" aria-hidden="true"></i>
{{/ifEquals}}
{{/ifEquals}}
{{/ifEquals}}
{{/if}}
</div>
<div class="article-card-title">
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
</div>
</div>
<div class="article-card-body">
{{#ifCond publishedAt '||' source}}
<div class="site-card-meta">
{{#if publishedAt}}
<span class="site-card-meta-date">{{formatDate publishedAt "YYYY-MM-DD"}}</span>
{{/if}}
{{#ifCond publishedAt '&&' source}}
<span class="site-card-meta-sep">·</span>
{{/ifCond}}
{{#if source}}
<span class="site-card-meta-source">{{source}}</span>
{{/if}}
</div>
{{/ifCond}}
<p>{{#if
description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p>
</div>
{{else}}
{{!-- projects代码仓库风格卡片保留 data-* 结构,便于扩展识别与写回) --}}
{{#ifEquals style "repo"}}
<div class="repo-header">
<i class="{{#if icon}}{{icon}}{{else}}fas fa-code{{/if}} repo-icon" aria-hidden="true"></i>
<div class="repo-title">{{#if name}}{{name}}{{else}}未命名项目{{/if}}</div>
</div>
<div class="repo-desc">{{#if
description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</div>
{{#ifCond language '||' stars}}
<div class="repo-stats">
{{#if language}}
<div class="stat-item">
<span class="lang-dot"
style="background-color: {{#if languageColor}}{{languageColor}}{{else}}#909296{{/if}};"></span>
{{language}}
</div>
{{/if}}
{{#if stars}}
<div class="stat-item">
<i class="far fa-star" aria-hidden="true"></i> {{stars}}
</div>
{{/if}}
{{#if forks}}
<div class="stat-item">
<i class="fas fa-code-branch" aria-hidden="true"></i> {{forks}}
</div>
{{/if}}
{{#if issues}}
<div class="stat-item">
<i class="fas fa-exclamation-circle" aria-hidden="true"></i> {{issues}}
</div>
{{/if}}
</div>
{{/ifCond}}
{{else}}
<div class="site-card-icon" aria-hidden="true">
{{!-- 站点图标优先级faviconUrl > forceIconMode > 全局 icons.mode --}}
{{#if faviconUrl}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img class="favicon-icon" src="{{faviconUrl}}" alt="{{name}} favicon" loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div>
{{else}}
{{#ifEquals forceIconMode "manual"}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} site-icon" aria-hidden="true"></i>
{{else}}
{{#ifEquals forceIconMode "favicon"}}
{{#ifHttpUrl url}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div>
{{else}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} site-icon" aria-hidden="true"></i>
{{/ifHttpUrl}}
{{else}}
{{#ifEquals @root.icons.mode "favicon"}}
{{#ifHttpUrl url}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div>
{{else}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} site-icon" aria-hidden="true"></i>
{{/ifHttpUrl}}
{{else}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} site-icon" aria-hidden="true"></i>
{{/ifEquals}}
{{/ifEquals}}
{{/ifEquals}}
{{/if}}
</div>
<div class="site-card-content">
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
<p>{{#if
description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p>
</div>
{{/ifEquals}}
{{/ifEquals}}
</a>
{{/if}}