fix: 重命名 favicon helper 避免与 sites.faviconUrl 同名冲突
将 Handlebars helper `faviconUrl(url)` 更名为 `faviconV2Url(url)`,解决自定义字段 `sites[].faviconUrl`
在模板中被误解析为 helper(无参调用)导致的渲染崩溃。
- helper:faviconUrl -> faviconV2Url
- 模板:site-card 中调用同步更新
BREAKING CHANGE:
自定义模板如使用 `{{faviconUrl url}}` 生成 faviconV2 地址,需要改为 `{{faviconV2Url url}}`。
Fixes: #32
This commit is contained in:
@@ -59,6 +59,10 @@
|
|||||||
- `sync-articles` 对齐 best-effort:同步失败不再以非 0 退出码阻断构建/部署
|
- `sync-articles` 对齐 best-effort:同步失败不再以非 0 退出码阻断构建/部署
|
||||||
- 版本号来源统一:`window.MeNav.version` 不再写死,自动读取构建注入版本(用于扩展/调试识别)
|
- 版本号来源统一:`window.MeNav.version` 不再写死,自动读取构建注入版本(用于扩展/调试识别)
|
||||||
|
|
||||||
|
**3. 模板图标 helper(Breaking)**
|
||||||
|
|
||||||
|
- 模板 helper `faviconUrl` 更名为 `faviconV2Url`,避免与站点字段 `sites[].faviconUrl` 同名冲突;如有自定义模板调用 `{{faviconUrl url}}`,需同步改为 `{{faviconV2Url url}}`
|
||||||
|
|
||||||
### 2026/01/03
|
### 2026/01/03
|
||||||
|
|
||||||
关联 Issue:[#31](https://github.com/rbetree/menav/issues/31)
|
关联 Issue:[#31](https://github.com/rbetree/menav/issues/31)
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ categories:
|
|||||||
url: https://linux.do/ # http/https URL(favicon 模式将尝试加载站点图标)
|
url: https://linux.do/ # http/https URL(favicon 模式将尝试加载站点图标)
|
||||||
icon: fab fa-linux # 手动图标:manual 模式使用;favicon 模式下作为回退
|
icon: fab fa-linux # 手动图标:manual 模式使用;favicon 模式下作为回退
|
||||||
description: 新的理想型社区 # 站点描述
|
description: 新的理想型社区 # 站点描述
|
||||||
|
- name: Menav
|
||||||
|
url: https://rbetree.github.io/menav
|
||||||
|
icon: fas fa-star
|
||||||
|
description: 个人导航站
|
||||||
|
faviconUrl: assets/menav.svg
|
||||||
- name: Google
|
- name: Google
|
||||||
url: https://www.google.com
|
url: https://www.google.com
|
||||||
icon: fab fa-google
|
icon: fab fa-google
|
||||||
|
|||||||
@@ -199,9 +199,9 @@ function add(a, b) {
|
|||||||
* @param {string} url 站点 URL
|
* @param {string} url 站点 URL
|
||||||
* @param {Object} options Handlebars options 对象
|
* @param {Object} options Handlebars options 对象
|
||||||
* @returns {string} favicon URL
|
* @returns {string} favicon URL
|
||||||
* @example {{faviconUrl url}}
|
* @example {{faviconV2Url url}}
|
||||||
*/
|
*/
|
||||||
function faviconUrl(url, options) {
|
function faviconV2Url(url, options) {
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
|
|
||||||
const region = options.data.root.icons?.region || 'com';
|
const region = options.data.root.icons?.region || 'com';
|
||||||
@@ -304,7 +304,7 @@ module.exports = {
|
|||||||
keys,
|
keys,
|
||||||
encodeURIComponent: encodeURIComponentHelper,
|
encodeURIComponent: encodeURIComponentHelper,
|
||||||
add,
|
add,
|
||||||
faviconUrl,
|
faviconV2Url,
|
||||||
faviconFallbackUrl,
|
faviconFallbackUrl,
|
||||||
safeUrl
|
safeUrl
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -305,6 +305,8 @@ categories:
|
|||||||
- `forceIconMode: favicon | manual`:强制该站点使用指定模式(不设置则跟随全局 `icons.mode`)
|
- `forceIconMode: favicon | manual`:强制该站点使用指定模式(不设置则跟随全局 `icons.mode`)
|
||||||
- 优先级:`faviconUrl` > `forceIconMode` > 全局 `icons.mode`
|
- 优先级:`faviconUrl` > `forceIconMode` > 全局 `icons.mode`
|
||||||
|
|
||||||
|
> 注意:用于根据站点 URL 生成 faviconV2 地址的模板 helper 已更名为 `faviconV2Url`,从而避免与站点字段 `faviconUrl` 同名冲突;自定义模板如需生成 faviconV2 地址,请使用 `{{faviconV2Url url}}`。如需强制读取站点字段 `faviconUrl`,也可使用 `{{lookup . "faviconUrl"}}`(推荐在复杂上下文中显式读取字段)。
|
||||||
|
|
||||||
示例(与内置组件实现保持一致):
|
示例(与内置组件实现保持一致):
|
||||||
|
|
||||||
```handlebars
|
```handlebars
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{#if url}}
|
{{#if url}}
|
||||||
<a href="{{safeUrl url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}" {{#if external}}target="_blank"
|
<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}}"
|
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}}"
|
data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}" {{#if (lookup . "faviconUrl")}}data-favicon-url="{{lookup . "faviconUrl"}}"
|
||||||
{{/if}} {{#if forceIconMode}}data-force-icon-mode="{{forceIconMode}}" {{/if}}
|
{{/if}} {{#if forceIconMode}}data-force-icon-mode="{{forceIconMode}}" {{/if}}
|
||||||
data-description="{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}"
|
data-description="{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}"
|
||||||
data-tooltip="{{#if name}}{{name}}{{else}}未命名站点{{/if}}{{#if description}} - {{description}}{{else}} - {{extractDomain url}}{{/if}}"
|
data-tooltip="{{#if name}}{{name}}{{else}}未命名站点{{/if}}{{#if description}} - {{description}}{{else}} - {{extractDomain url}}{{/if}}"
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
<div class="article-card-header">
|
<div class="article-card-header">
|
||||||
<div class="site-card-icon" aria-hidden="true">
|
<div class="site-card-icon" aria-hidden="true">
|
||||||
{{!-- 站点图标优先级:faviconUrl > forceIconMode > 全局 icons.mode --}}
|
{{!-- 站点图标优先级:faviconUrl > forceIconMode > 全局 icons.mode --}}
|
||||||
{{#if faviconUrl}}
|
{{#if (lookup . "faviconUrl")}}
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
<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"
|
<img class="favicon-icon" src="{{lookup . "faviconUrl"}}" alt="{{name}} favicon" loading="lazy"
|
||||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
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');" />
|
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>
|
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
{{#ifHttpUrl url}}
|
{{#ifHttpUrl url}}
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
<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"
|
<img class="favicon-icon" src="{{faviconV2Url url}}" alt="{{name}} favicon" loading="lazy"
|
||||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
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');" />
|
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>
|
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
{{#ifHttpUrl url}}
|
{{#ifHttpUrl url}}
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
<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"
|
<img class="favicon-icon" src="{{faviconV2Url url}}" alt="{{name}} favicon" loading="lazy"
|
||||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
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');" />
|
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>
|
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||||
@@ -117,10 +117,10 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<div class="site-card-icon" aria-hidden="true">
|
<div class="site-card-icon" aria-hidden="true">
|
||||||
{{!-- 站点图标优先级:faviconUrl > forceIconMode > 全局 icons.mode --}}
|
{{!-- 站点图标优先级:faviconUrl > forceIconMode > 全局 icons.mode --}}
|
||||||
{{#if faviconUrl}}
|
{{#if (lookup . "faviconUrl")}}
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
<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"
|
<img class="favicon-icon" src="{{lookup . "faviconUrl"}}" alt="{{name}} favicon" loading="lazy"
|
||||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
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');" />
|
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>
|
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
{{#ifHttpUrl url}}
|
{{#ifHttpUrl url}}
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
<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"
|
<img class="favicon-icon" src="{{faviconV2Url url}}" alt="{{name}} favicon" loading="lazy"
|
||||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
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');" />
|
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>
|
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
{{#ifHttpUrl url}}
|
{{#ifHttpUrl url}}
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
|
<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"
|
<img class="favicon-icon" src="{{faviconV2Url url}}" alt="{{name}} favicon" loading="lazy"
|
||||||
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
|
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');" />
|
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>
|
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
|
||||||
|
|||||||
76
test/favicon-url-field-render.node-test.js
Normal file
76
test/favicon-url-field-render.node-test.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const { loadHandlebarsTemplates, renderTemplate } = require('../src/generator.js');
|
||||||
|
|
||||||
|
function withRepoCwd(callback) {
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
const repoRoot = path.join(__dirname, '..');
|
||||||
|
try {
|
||||||
|
process.chdir(repoRoot);
|
||||||
|
callback();
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBookmarksWithSite(site) {
|
||||||
|
loadHandlebarsTemplates();
|
||||||
|
return renderTemplate(
|
||||||
|
'bookmarks',
|
||||||
|
{
|
||||||
|
pageId: 'bookmarks',
|
||||||
|
homePageId: 'bookmarks',
|
||||||
|
title: '书签',
|
||||||
|
subtitle: '测试',
|
||||||
|
siteCardStyle: '',
|
||||||
|
icons: { mode: 'favicon', region: 'com' },
|
||||||
|
categories: [
|
||||||
|
{
|
||||||
|
name: '分类',
|
||||||
|
icon: 'fas fa-folder',
|
||||||
|
sites: [site]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('站点配置包含 faviconUrl(本地 assets 路径)时,渲染 bookmarks 不应崩溃', () => {
|
||||||
|
withRepoCwd(() => {
|
||||||
|
const html = renderBookmarksWithSite({
|
||||||
|
name: '内部系统',
|
||||||
|
url: 'https://intranet.example/',
|
||||||
|
faviconUrl: 'assets/menav.svg',
|
||||||
|
icon: 'fas fa-link',
|
||||||
|
external: true
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.match(html, /data-favicon-url="assets\/menav\.svg"/);
|
||||||
|
assert.match(html, /src="assets\/menav\.svg"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('站点配置包含 faviconUrl(在线 ico)时,渲染 bookmarks 不应崩溃', () => {
|
||||||
|
withRepoCwd(() => {
|
||||||
|
const html = renderBookmarksWithSite({
|
||||||
|
name: 'WebCull',
|
||||||
|
url: 'https://example.com/',
|
||||||
|
faviconUrl: 'https://content.webcull.com/images/websites/icons/470/695/b788b0.ico',
|
||||||
|
icon: 'fas fa-link',
|
||||||
|
external: true
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
/data-favicon-url="https:\/\/content\.webcull\.com\/images\/websites\/icons\/470\/695\/b788b0\.ico"/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
/src="https:\/\/content\.webcull\.com\/images\/websites\/icons\/470\/695\/b788b0\.ico"/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user