From d19c4da51de78f2bb468f8103598b9024705d08c Mon Sep 17 00:00:00 2001 From: rbetree Date: Fri, 16 Jan 2026 22:51:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(icons):=20faviconV2=20=E5=8A=A0=E5=85=A5=20?= =?UTF-8?q?drop=5F404=5Ficon=3Dtrue=20=E5=8F=82=E6=95=B0=E9=81=BF=E5=85=8D?= =?UTF-8?q?404=E5=8D=A0=E4=BD=8D=E5=9B=BE=E4=BB=A5=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - helpers:faviconV2Url / faviconFallbackUrl 统一追加 drop_404_icon=true - runtime:新增站点的 faviconV2 com/cn URL 同步追加该参数 - docs:更新模板与 helper 文档示例 - test:新增用例防止参数回归 --- src/helpers/README.md | 103 +++++++++++-------- src/helpers/utils.js | 6 +- src/runtime/menav/addElement.js | 5 +- templates/README.md | 110 +++++++++++++-------- test/favicon-v2-drop-404-icon.node-test.js | 32 ++++++ 5 files changed, 168 insertions(+), 88 deletions(-) create mode 100644 test/favicon-v2-drop-404-icon.node-test.js diff --git a/src/helpers/README.md b/src/helpers/README.md index 94237ef..92bdd2d 100644 --- a/src/helpers/README.md +++ b/src/helpers/README.md @@ -56,10 +56,13 @@ MeNav 的助手函数分为四类: ```handlebars {{#ifHttpUrl url}} - {{!-- 只有 http/https 才尝试加载 favicon --}} - {{name}} favicon + {{! 只有 http/https 才尝试加载 favicon }} + {{name}} favicon {{else}} - + {{/ifHttpUrl}} ``` @@ -79,8 +82,13 @@ MeNav 的助手函数分为四类: 对字符串进行 URL 组件编码(同名于浏览器 API,用作模板内联助手),适用于将动态 URL 参数安全拼接到查询串: ```handlebars -{{!-- 构造第三方 Favicon API 的 url 参数 --}} -favicon +{{! 构造第三方 Favicon API 的 url 参数 }} +favicon ``` ### 核心函数 @@ -101,7 +109,7 @@ MeNav 的助手函数分为四类: 用于生成内容的助手函数: ```handlebars -{{formatDate created "YYYY-MM-DD"}} +{{formatDate created 'YYYY-MM-DD'}} {{limit description 100}} {{json data}} ``` @@ -111,14 +119,14 @@ MeNav 的助手函数分为四类: 用于控制结构的助手函数: ```handlebars -{{#ifEquals type "article"}} - 文章 +{{#ifEquals type 'article'}} + 文章 {{else}} - 页面 + 页面 {{/ifEquals}} {{#each (range 1 5)}} - {{this}} + {{this}} {{/each}} ``` @@ -128,7 +136,7 @@ MeNav 的助手函数分为四类: ```handlebars {{#each (slice items 0 5)}} -
  • {{toUpperCase name}}
  • +
  • {{toUpperCase name}}
  • {{/each}} ``` @@ -141,12 +149,16 @@ MeNav 的助手函数分为四类: 格式化日期: ```handlebars -{{formatDate date "YYYY-MM-DD"}} {{!-- 2023-05-15 --}} -{{formatDate date "YYYY年MM月DD日"}} {{!-- 2023年05月15日 --}} -{{formatDate date "YYYY-MM-DD HH:mm:ss"}} {{!-- 2023-05-15 14:30:00 --}} +{{formatDate date 'YYYY-MM-DD'}} +{{! 2023-05-15 }} +{{formatDate date 'YYYY年MM月DD日'}} +{{! 2023年05月15日 }} +{{formatDate date 'YYYY-MM-DD HH:mm:ss'}} +{{! 2023-05-15 14:30:00 }} ``` 支持的格式: + - `YYYY`: 四位年份 - `MM`: 两位月份 - `DD`: 两位日期 @@ -159,7 +171,7 @@ MeNav 的助手函数分为四类: 限制文本长度,超出部分显示省略号: ```handlebars -{{limit "这是一段很长的文本内容" 5}} {{!-- 这是一段... --}} +{{limit '这是一段很长的文本内容' 5}} {{! 这是一段... }} ``` #### toLowerCase / toUpperCase @@ -167,8 +179,10 @@ MeNav 的助手函数分为四类: 转换文本大小写: ```handlebars -{{toLowerCase "Hello"}} {{!-- hello --}} -{{toUpperCase "world"}} {{!-- WORLD --}} +{{toLowerCase 'Hello'}} +{{! hello }} +{{toUpperCase 'world'}} +{{! WORLD }} ``` #### json @@ -194,10 +208,10 @@ MeNav 的助手函数分为四类: 比较两个值是否相等/不相等: ```handlebars -{{#ifEquals status "active"}} - 当前状态:活跃 +{{#ifEquals status 'active'}} + 当前状态:活跃 {{else}} - 当前状态:非活跃 + 当前状态:非活跃 {{/ifEquals}} ``` @@ -206,14 +220,17 @@ MeNav 的助手函数分为四类: 通用条件比较: ```handlebars -{{#ifCond count ">" 0}} - 有 {{count}} 个项目 +{{#ifCond count '>' 0}} + 有 + {{count}} + 个项目 {{else}} - 没有项目 + 没有项目 {{/ifCond}} ``` 支持的运算符: + - `==`, `===`, `!=`, `!==` - `<`, `<=`, `>`, `>=` - `&&`, `||` @@ -224,13 +241,13 @@ MeNav 的助手函数分为四类: ```handlebars {{#isEmpty items}} -

    暂无数据

    +

    暂无数据

    {{else}} - + {{/isEmpty}} ``` @@ -240,15 +257,15 @@ MeNav 的助手函数分为四类: ```handlebars {{#and isPremium isActive}} - 高级活跃用户 + 高级活跃用户 {{/and}} {{#or isPremium isAdmin}} - 有访问权限 + 有访问权限 {{/or}} {{#not isDisabled}} - 此功能可用 + 此功能可用 {{/not}} ``` @@ -260,7 +277,7 @@ MeNav 的助手函数分为四类: ```handlebars {{#each (slice array 0 3)}} -
  • {{this}}
  • +
  • {{this}}
  • {{/each}} ``` @@ -270,7 +287,7 @@ MeNav 的助手函数分为四类: ```handlebars {{#each (concat array1 array2)}} -
  • {{this}}
  • +
  • {{this}}
  • {{/each}} ``` @@ -287,8 +304,10 @@ MeNav 的助手函数分为四类: 获取数组的第一个/最后一个元素: ```handlebars -第一项: {{first items}} -最后一项: {{last items}} +第一项: +{{first items}} +最后一项: +{{last items}} ``` #### range @@ -297,7 +316,7 @@ MeNav 的助手函数分为四类: ```handlebars {{#each (range 1 5)}} - {{this}} + {{this}} {{/each}} ``` @@ -306,7 +325,7 @@ MeNav 的助手函数分为四类: 从对象中选择指定的属性: ```handlebars -{{json (pick user "name" "email")}} +{{json (pick user 'name' 'email')}} ``` #### keys @@ -315,7 +334,7 @@ MeNav 的助手函数分为四类: ```handlebars {{#each (keys object)}} -
  • {{this}}
  • +
  • {{this}}
  • {{/each}} ``` @@ -382,7 +401,7 @@ module.exports = { toLowerCase, toUpperCase, json, - formatNumber // 添加新函数 + formatNumber, // 添加新函数 }; ``` @@ -398,7 +417,7 @@ const newHelpers = require('./new-helpers'); function registerAllHelpers(handlebars) { // 现有注册代码... - + // 注册新的助手函数 Object.entries(newHelpers).forEach(([name, helper]) => { handlebars.registerHelper(name, helper); @@ -425,4 +444,4 @@ function registerAllHelpers(handlebars) { 4. **避免副作用** - 助手函数应为纯函数,不修改传入的数据 -5. **保持简单** - 每个助手函数应只完成一个明确的任务 +5. **保持简单** - 每个助手函数应只完成一个明确的任务 diff --git a/src/helpers/utils.js b/src/helpers/utils.js index 1189d3c..991d273 100644 --- a/src/helpers/utils.js +++ b/src/helpers/utils.js @@ -209,7 +209,8 @@ function faviconV2Url(url, options) { try { const encodedUrl = encodeURIComponent(String(url)); - return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32`; + // drop_404_icon=true:缺失 favicon 时返回空 404,避免“小地球”占位图并可靠触发 回退 + return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32&drop_404_icon=true`; } catch (e) { return ''; } @@ -230,7 +231,8 @@ function faviconFallbackUrl(url, options) { try { const encodedUrl = encodeURIComponent(String(url)); - return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32`; + // drop_404_icon=true:缺失 favicon 时返回空 404,避免“小地球”占位图并可靠触发 回退 + return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32&drop_404_icon=true`; } catch (e) { return ''; } diff --git a/src/runtime/menav/addElement.js b/src/runtime/menav/addElement.js index 9000228..769f29c 100644 --- a/src/runtime/menav/addElement.js +++ b/src/runtime/menav/addElement.js @@ -240,12 +240,13 @@ module.exports = function addElement(type, parentId, data) { if (urlToUse) { // 根据 icons.region 配置决定优先使用哪个域名 const urls = []; + // drop_404_icon=true:缺失 favicon 时返回空 404,避免占位图导致 onerror 不触发,从而可靠走回退逻辑 const comUrl = `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent( urlToUse - )}&size=32`; + )}&size=32&drop_404_icon=true`; const cnUrl = `https://t3.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent( urlToUse - )}&size=32`; + )}&size=32&drop_404_icon=true`; if (iconsRegion === 'cn') { urls.push(cnUrl, comUrl); } else { diff --git a/templates/README.md b/templates/README.md index a6b72e4..1f376d7 100644 --- a/templates/README.md +++ b/templates/README.md @@ -53,9 +53,11 @@ templates/ **位置**: `templates/layouts/` **主要布局**: + - `default.hbs` - 默认布局,定义整个页面框架 **示例**: + ```handlebars @@ -70,7 +72,7 @@ templates/ - +
    {{#each pages}} @@ -91,6 +93,7 @@ templates/ **位置**: `templates/pages/` **主要页面**: + - `page.hbs` - 通用页面模板(默认/回退模板;普通页面常用) - `bookmarks.hbs` - 书签页 - `projects.hbs` - 项目页 @@ -102,6 +105,7 @@ templates/ > “首页/默认打开页”由 `site.yml -> navigation` 的**第一项**决定;首页可使用任意页面模板,具体取决于该页面配置(`pages/.yml` 的 `template` 字段与回退规则)。 **示例** (`page.hbs`): + ```handlebars
    {{> page-header}} @@ -120,6 +124,7 @@ templates/ > 说明:生成器启动时会自动扫描 `templates/components/` 下的所有 `.hbs` 并注册为 Handlebars partial(partial 名称=文件名去掉 `.hbs`)。因此新增组件后无需手动“注册步骤”,可直接通过 `{{> component-name}}` 引用。 **主要组件**: + - `page-header.hbs` - 统一页面标题区(首页/非首页/书签更新时间/项目热力图) - `navigation.hbs` - 导航菜单 - `site-card.hbs` - 站点卡片 @@ -128,6 +133,7 @@ templates/ - `social-links.hbs` - 社交链接 **示例** (`site-card.hbs`,精简展示关键结构): + ```handlebars {{#if url}} subcategories -> groups -> sites` 的结构;更深一层的 `subgroups` 由 `group.hbs` 负责渲染。 **功能特性**: + - 支持 2~4 层嵌套(`categories -> subcategories -> groups -> subgroups -> sites`,其中 `subgroups` 可选) - 自动计算标题层级(h2/h3/h4/h5) - 根据层级自动应用对应的 CSS 类(如 `category-level-2`、`group-level-4`) @@ -163,6 +171,7 @@ templates/ **递归渲染原理**: 通过在模板内部调用自身实现递归渲染: + ```handlebars {{#each subcategories}} {{> category level=2}} @@ -170,12 +179,14 @@ templates/ ``` **level参数的作用**: + - 用于跟踪当前嵌套层级 - 控制标题标签的层级(h{{add level 1}}) - 应用对应的CSS类(category-level-{{level}}) - 传递给子组件以保持层级一致性 **使用示例**: + ```handlebars {{> category category}} @@ -189,6 +200,7 @@ templates/ `group.hbs` 是用于在分类内组织站点的组件,同样支持层级参数。 **功能特性**: + - 支持在分类内创建站点分组 - 支持子分组(`subgroups`,用于第 4 层结构) - 自动应用层级样式 @@ -196,6 +208,7 @@ templates/ - 与category.hbs保持一致的层级系统 **使用示例**: + ```handlebars {{#each groups}} @@ -213,27 +226,28 @@ templates/ ```yaml # 配置示例 categories: - - name: "技术" - icon: "fas fa-code" + - name: '技术' + icon: 'fas fa-code' subcategories: - - name: "前端开发" - icon: "fas fa-laptop-code" + - name: '前端开发' + icon: 'fas fa-laptop-code' groups: - - name: "框架" - icon: "fas fa-cubes" + - name: '框架' + icon: 'fas fa-cubes' subgroups: - - name: "React生态" - icon: "fab fa-react" + - name: 'React生态' + icon: 'fab fa-react' sites: - - name: "React" - url: "https://reactjs.org" - icon: "fab fa-react" - - name: "Next.js" - url: "https://nextjs.org" - icon: "fas fa-triangle" + - name: 'React' + url: 'https://reactjs.org' + icon: 'fab fa-react' + - name: 'Next.js' + url: 'https://nextjs.org' + icon: 'fas fa-triangle' ``` 对应的模板渲染: + ```handlebars
    @@ -287,10 +301,12 @@ categories: - **层级4 (level=4)**: 子分组,使用h5标题(用于 4 层结构) 每个层级都有对应的CSS类: + - `category-level-1`, `category-level-2` - `group-level-3`, `group-level-4` 这种设计确保了: + 1. 语义化的HTML结构 2. 一致的视觉层级 3. 可预测的嵌套深度(当前导入脚本与样式保证到 level=4) @@ -301,6 +317,7 @@ categories: 当启用 `icons.mode: favicon`(默认)时,站点卡片会优先显示站点 favicon;当 URL 非 http/https、加载失败或网络受限,则自动回退到 Font Awesome 图标。相关助手:`ifHttpUrl`(条件)与 `encodeURIComponent`(工具)。 站点级覆盖(可选,写在每个 `sites[]` 节点上): + - `faviconUrl`:为单站点指定图标链接(优先级最高,失败回退到手动图标;本地路径建议以 `assets/` 开头,构建会复制到 `dist/` 同路径) - `forceIconMode: favicon | manual`:强制该站点使用指定模式(不设置则跟随全局 `icons.mode`) - 优先级:`faviconUrl` > `forceIconMode` > 全局 `icons.mode` @@ -324,7 +341,7 @@ categories: {{name}} favicon component-name}}` 引用 主要数据对象: + - `site` - 网站配置信息 - `navigationData` - 导航菜单数据 - `categories` - 分类和站点数据 @@ -366,6 +385,7 @@ MeNav 模板系统的数据流如下: - `social` - 社交链接数据 常见派生字段(由生成器注入,供模板差异化使用): + - `homePageId`:首页页面 ID(始终等于 `navigation` 第一项的 `id`) - `pageId`:当前页面 ID(用于 `.page-template-{{pageId}}` 等) - `pageMeta.updatedAt/updatedAtSource`:仅 bookmarks 模板页用于“update: YYYY-MM-DD | from: ...”展示 @@ -377,26 +397,29 @@ MeNav 模板系统的数据流如下: ## 模板使用示例 ### 布局模板使用 + 布局模板通常只有一个 `default.hbs`,会自动被系统使用。 ### 页面模板使用 + 页面模板对应导航中的各个页面,有两种使用方式: 1. **自动匹配**:系统会尝试使用与页面ID同名的模板(例如:页面ID为 `projects` 时会使用 `projects.hbs`) 2. **显式指定**:在页面配置中使用 `template` 字段指定要使用的模板 #### 模板指定示例 + 在 `config/user/pages/项目.yml` 中: ```yaml -title: "我的项目" -subtitle: "这里展示我的所有项目" -template: "projects" # 使用 projects.hbs 模板而不是使用页面ID命名的模板 +title: '我的项目' +subtitle: '这里展示我的所有项目' +template: 'projects' # 使用 projects.hbs 模板而不是使用页面ID命名的模板 categories: - - name: "网站项目" - icon: "fas fa-globe" + - name: '网站项目' + icon: 'fas fa-globe' sites: - - name: "个人博客" + - name: '个人博客' # ... 其他字段 ``` @@ -417,9 +440,9 @@ categories: ```handlebars {{#if profile.title}} -

    {{profile.title}}

    +

    {{profile.title}}

    {{else}} -

    欢迎使用

    +

    欢迎使用

    {{/if}} ``` @@ -467,22 +490,23 @@ categories: 3. 页面内容可引用现有组件或创建新组件 示例: + ```handlebars -
    -

    关于我

    -

    {{about.description}}

    - - {{#if about.skills}} -
    -

    技能

    -
      - {{#each about.skills}} -
    • {{this}}
    • - {{/each}} -
    +
    +

    关于我

    +

    {{about.description}}

    + + {{#if about.skills}} +
    +

    技能

    +
      + {{#each about.skills}} +
    • {{this}}
    • + {{/each}} +
    - {{/if}} + {{/if}}
    ``` @@ -492,17 +516,19 @@ categories: 2. 在页面或其他组件中引用 示例: + ```handlebars -
    -

    {{name}}

    -
    -
    -
    +
    +

    {{name}}

    +
    +
    +
    ``` 使用新组件: + ```handlebars {{#each skills}} {{> skill-card}} diff --git a/test/favicon-v2-drop-404-icon.node-test.js b/test/favicon-v2-drop-404-icon.node-test.js new file mode 100644 index 0000000..c280cdf --- /dev/null +++ b/test/favicon-v2-drop-404-icon.node-test.js @@ -0,0 +1,32 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const { faviconV2Url, faviconFallbackUrl } = require('../src/helpers/utils'); + +test('faviconV2:应追加 drop_404_icon=true 以避免返回占位图', () => { + const optionsCom = { data: { root: { icons: { region: 'com' } } } }; + const optionsCn = { data: { root: { icons: { region: 'cn' } } } }; + + const url = 'https://example.com'; + + const com = faviconV2Url(url, optionsCom); + const cn = faviconV2Url(url, optionsCn); + const fallbackCom = faviconFallbackUrl(url, optionsCom); + const fallbackCn = faviconFallbackUrl(url, optionsCn); + + for (const out of [com, cn, fallbackCom, fallbackCn]) { + assert.ok(out.includes('drop_404_icon=true'), '生成的 URL 应包含 drop_404_icon=true'); + } +}); + +test('运行时新增站点:faviconV2 URL 也应包含 drop_404_icon=true', () => { + const repoRoot = path.resolve(__dirname, '..'); + const runtimePath = path.join(repoRoot, 'src', 'runtime', 'menav', 'addElement.js'); + const content = fs.readFileSync(runtimePath, 'utf8'); + assert.ok( + content.includes('drop_404_icon=true'), + 'src/runtime/menav/addElement.js 应追加 drop_404_icon=true' + ); +});