From 8925cab953a2ed620e5a1c61af0acd9f1ca49b51 Mon Sep 17 00:00:00 2001 From: wutongyuonce <147830929+wutongyuonce@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:02:31 +0800 Subject: [PATCH] feat: add hosted Oneko pixel cat behind FEATURES.oneko Mount oneko.dhrv.pw once from BaseLayout with View Transitions guard, footer hide toggle, nav avoid zone, and docs for toggle/credits. --- CHANGELOG.md | 4 + ...o \345\203\217\347\264\240\347\214\253.md" | 98 +++++++++++++++++++ ...7\202\271\350\241\214\344\270\272 SPEC.md" | 2 + ...71\347\233\256\350\247\243\346\236\220.md" | 4 +- src/components/base/Footer.astro | 36 +++++++ src/components/nav/NavBar.astro | 2 +- src/config.ts | 1 + src/layouts/BaseLayout.astro | 25 ++++- src/types.ts | 20 ++++ 9 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 "docs/Oneko \345\203\217\347\264\240\347\214\253.md" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f036db4..ba87a24e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ## 未发布 +### 新增 + +- 全站可选托管 Oneko 像素猫(`FEATURES.oneko`,默认 classic):跟随光标、睡觉、气泡、记住位置、季节帽子,减少动态时隐藏;页脚可隐藏小猫,顶栏为避让区。不自托管、不开声音与激光。说明见 `docs/Oneko 像素猫.md`。 + ## 1.3.0 — 2026-10-15 ### 新增 diff --git "a/docs/Oneko \345\203\217\347\264\240\347\214\253.md" "b/docs/Oneko \345\203\217\347\264\240\347\214\253.md" new file mode 100644 index 00000000..fd0762f4 --- /dev/null +++ "b/docs/Oneko \345\203\217\347\264\240\347\214\253.md" @@ -0,0 +1,98 @@ +# Oneko 像素猫 + +全站可选的跟鼠标像素猫,使用托管脚本 [oneko.dhrv.pw](https://oneko.dhrv.pw/),不自托管引擎或皮肤资源。 + +## SPEC + 设计 + +### 背景 + +想在站点 chrome 上加一只轻量、可关掉的像素猫,作为装饰交互,而不是内容功能。托管脚本约 33 KB gzip,用 `data-*` 配置,适合 Astro + ClientRouter,无需把 React/shadcn 组件拷进仓库。 + +### 规则(已确认) + +- **启用方式**:`FEATURES.oneko: FeatureConfig`,默认 `[true, { skin: 'classic' }]`。 +- **ON**:跟随光标、睡觉、气泡、记住位置、季节帽子、`reducedMotion="hide"`、访客隐藏开关、导航避让区。 +- **OFF**:喵叫、激光笔、抓取、零食、跟随焦点、悬停栖息、打字时打盹。 +- **挂载**:`BaseLayout` 在首次进入站点时注入一次托管 `type="module"` 脚本;猫挂在 `document.body`,不占版心。 +- **隐藏**:页脚 `data-oneko-toggle` 按钮,文案「隐藏小猫」;引擎负责跨标签页记忆与 `aria-pressed` 同步。 + +### 非目标 + +- 不自托管 `oneko.js` 或皮肤表。 +- 不开启声音(`meow`)或激光笔。 +- 不做 rehype 给每个 `pre` 自动打避让区(过重);作者可自行加 `data-oneko-zone="avoid"`。 +- 不合并无关 WIP 分支,不自动合并本 PR。 + +### 权威表 + +| 事实 | 权威 | 不要 | +| :--- | :--- | :--- | +| 开/关与皮肤 | `src/config.ts` `FEATURES.oneko` | 在布局里写死开关 | +| 类型与配置形状 | `src/types.ts` `OnekoConfig` / `Features.oneko` | 另造 `boolean` 顶层开关 | +| 脚本挂载与 VT 防重 | `src/layouts/BaseLayout.astro` | 在各页面再插一份 script | +| 访客隐藏按钮 | `src/components/base/Footer.astro` 的 `data-oneko-toggle` | 自写 localStorage 逻辑 | +| 导航避让 | `NavBar.astro` `data-oneko-zone="avoid"` | 用 JS 每帧量矩形 | +| 动画引擎与皮肤 | CDN `https://oneko.dhrv.pw/oneko.js` | 拷贝进 `public/` | + +## 如何开关 + +在 `src/config.ts`: + +```ts +// 开(默认) +oneko: [true, { skin: 'classic' }], + +// 关 +oneko: false, +// 或 +oneko: [false, { skin: 'classic' }], +``` + +可选 `skin` 见类型注释;省略时按 `classic`。完整皮肤列表与署名见 [oneko 文档 · Skins](https://oneko.dhrv.pw/docs#skins-pick-a-coat) 与皮肤 credits。 + +## 启用时的行为 + +| 能力 | 设置 | +| --- | --- | +| 皮肤 | `data-skin` ← config | +| 跟随光标 | `data-follow-cursor` | +| 睡觉 | `data-sleep-enabled` | +| 气泡 | `data-bubble-enabled` | +| 记住位置 | `data-persist-position` | +| 季节帽子 | `data-seasonal` | +| 减少动态 | `data-reduced-motion="hide"` | +| 喵叫/激光 | `data-meow="false"`、`data-laser-pointer="false"` | +| 隐藏开关 | 页脚按钮 `data-oneko-toggle` | +| 避让 | 顶栏 `data-oneko-zone="avoid"`(开关关闭时属性仍在,无害) | + +## View Transitions + +站点使用 Astro `ClientRouter`。布局里的脚本在站内跳转时可能再次出现在新文档里。做法: + +1. `BaseLayout` 用 `is:inline` + `define:vars` 输出一小段引导脚本。 +2. 引导脚本用 `window.__yublogOnekoBooted` 只在首次真正注入 CDN module。 +3. 猫实例挂在 `document.body`,换页不销毁、不重建。 + +不要对 CDN script 加 `data-astro-rerun`,也不要在 `astro:page-load` 里再次 `createOneko`。 + +## 改动文件 + +| 文件 | 作用 | +| --- | --- | +| `src/types.ts` | `OnekoConfig`、`Features.oneko` | +| `src/config.ts` | 默认开启 classic | +| `src/layouts/BaseLayout.astro` | 条件挂载 + VT 防重 | +| `src/components/base/Footer.astro` | 隐藏按钮 | +| `src/components/nav/NavBar.astro` | 导航避让 | +| `docs/Oneko 像素猫.md` | 本说明 | +| `docs/项目解析.md` / `docs/站点行为 SPEC.md` / `CHANGELOG.md` | 索引与未发布条目 | + +## 访客隐藏 + +页脚「隐藏小猫」按钮带 `data-oneko-toggle`。托管脚本会同步 `aria-pressed`,并在 `localStorage`(`oneko:hidden`)记住选择,跨标签页生效。无需本站自写状态机。 + +## 署名与许可 + +- 引擎代码 MIT,托管于 [oneko.dhrv.pw](https://oneko.dhrv.pw/)。 +- 皮肤为像素画作品,再分发时请保留 [皮肤 credits](https://oneko.dhrv.pw/docs#sources-and-credits)/安装包内 `docs/oneko-skins.md` 的署名说明。 +- 本站仅通过 CDN 引用,不复制皮肤文件进仓库。 diff --git "a/docs/\347\253\231\347\202\271\350\241\214\344\270\272 SPEC.md" "b/docs/\347\253\231\347\202\271\350\241\214\344\270\272 SPEC.md" index 0dd18adb..1e459450 100644 --- "a/docs/\347\253\231\347\202\271\350\241\214\344\270\272 SPEC.md" +++ "b/docs/\347\253\231\347\202\271\350\241\214\344\270\272 SPEC.md" @@ -183,6 +183,7 @@ RSS endpoint 只渲染集合 `Content`,经静态 HTML 转换输出 `content:en | 图片与媒体 | 封面/正文图片由 Astro 生成变体,分享图 1200px;`public/` 放站点资源和独立 HTML 演示,不手写内容图片构建 URL;本地视频/音频由 `astro-media-sync` 镜像到 `public/_media/`,正文用相对路径引用;HTML 演示目录和 iframe 用法见[正文 HTML 与媒体 SPEC](./正文%20HTML%20与媒体%20SPEC.md#本地-html-交互演示) | | 页脚 | 宋体显示版权、Astro 和“萌ICP备20269668号”。作者名指向站点首页,Astro 指向 `https://astro.build`,备案指向 `https://icp.gov.moe/?keyword=20269668` 并在新标签页打开,带 `noopener noreferrer`。链接悬停或键盘焦点变为强调色 | | 访问统计 | `Head.astro` 加载一份 Umami Cloud 脚本;页面不显示计数、徽章或页脚文案 | +| Oneko 像素猫 | 由 `FEATURES.oneko` 控制;默认托管 classic 皮肤,跟随光标/睡觉/气泡/记位置/季节帽,减少动态时隐藏;页脚提供「隐藏小猫」;顶栏为避让区。不自托管、不开声音与激光。详见 [Oneko 像素猫](./Oneko%20像素猫.md) | | 主题与焦点 | 颜色源于 `--c-bg`、`--c-text`、`--accent`;点缀规则与独立偏好见 S11,变量由 `accent-palette.js` 统一生成;站点声明的浏览器原生颜色方案与当前博客主题一致,不对自定义颜色自动暗化;首访跟随系统,有效保存值在刷新后恢复;系统模式发生变化时同步切换。非法保存值回退系统,存储不可用不妨碍当次显示和切换,但不保证跨导航/刷新保存。切换按钮、颜色方案和 canvas 背景同步;减少动效时保留静态背景,仍响应缩放和主题变化,不启动动画循环。提供深浅主题及可见键盘反馈,不依靠颜色表达类别 | 装饰背景在主体内部的独立层级中,位于主体底色之上、页面内容之下;即使外部样式把主体底色改为不透明,也不因此遮挡背景。背景仍相对视口固定,不拦截内容交互。外部暗化工具可以覆盖站点声明的配色,不保证其覆盖后的颜色仍与主题按钮一致。 @@ -219,6 +220,7 @@ RSS endpoint 只渲染集合 `Content`,经静态 HTML 转换输出 `content:en | 归档分类预设、持久分配、容量与失败 | `utils/blog-category-colors.js`;数据在 `data/blog-category-colors.json` | `ArchiveView` 传入实际展示的分类集合,CSS 只选择主题 | `blog-category-colors.test.mjs`、归档产物检查 | | 社交身份 | `config.ts` `AUTHOR_LINKS` | 首页读取配置,关于正文独立维护联系方式 | 构建产物检查核对身份 | | 访问统计 | `Head.astro` 的 Umami 脚本 | 全站经 `BaseLayout` 加载;不在页脚或正文渲染 | 构建产物头部含该脚本 | +| Oneko 开关与皮肤 | `config.ts` `FEATURES.oneko` | `BaseLayout` 首次注入托管脚本;`Footer` 提供 `data-oneko-toggle`;`NavBar` 标避让 | 人工:开关配置后猫出现/消失;换页不出现第二只 | | 评论仓库与分类 | `config.ts` 的 `GISCUS` | `GiscusComments` 读取公开节点 ID,无服务端密钥 | 构建接线检查 | | 评论关联键、加载、主题与组件资源 | `GiscusComments.astro`;OAuth/评论数据由官方 `giscus` 组件与 Giscus 服务管理 | `RenderPost`、`FriendsView`、`InterestsView` 只声明挂载与文案;主题复用 `observeTheme` | 构建范围检查与 `verify-giscus.mjs` | | RSS HTML/XML与失败 | `rss-content.js`/`rss-feed.js`;endpoint编排 | `render(Content)` → 转换 → 序列化 | RSS owner-level 与产物检查 | diff --git "a/docs/\351\241\271\347\233\256\350\247\243\346\236\220.md" "b/docs/\351\241\271\347\233\256\350\247\243\346\236\220.md" index d782891b..d0c00fbe 100644 --- "a/docs/\351\241\271\347\233\256\350\247\243\346\236\220.md" +++ "b/docs/\351\241\271\347\233\256\350\247\243\346\236\220.md" @@ -65,6 +65,7 @@ Astro 7 静态站。主路径是**读文章**:首页名片、写作热力与 | 顶栏导航、顶栏 GitHub | `src/config.ts` `UI.internalNavs` / `UI.socialLinks` | 在 `NavBar` 里加死链接 | | 个人社交展示 | 首页 `config.ts` 的 `AUTHOR_LINKS`;关于页正文仅维护作者希望展示的联系方式 | 写入顶栏 `UI.socialLinks`,或在首页组件另写账号 | | TOC / 搜索开关 | `src/config.ts` `FEATURES` | 在 TOC 组件里另做一套开关语义 | +| Oneko 像素猫开关与皮肤 | `src/config.ts` `FEATURES.oneko`;挂载见 `BaseLayout`,隐藏按钮见 `Footer`,说明见 [Oneko 像素猫](./Oneko%20像素猫.md) | 自托管脚本、在页面重复注入,或自写隐藏状态 | | 文章字段合不合法 | `src/content/schema.ts` `postSchema` | 在页面里重新约定字段 | | 已发布文章列表、排序、按年分组 | `src/utils/data.ts` | 在视图里 `getCollection` 再 filter 一遍 | | 原值分类的动态入口、URL 的分类 hash/兼容 query、精确筛选、标签、页码 | `src/utils/blog-browser.js` | 写固定分类允许表、合并别名或在 `ListView` 里手写 `URLSearchParams` | @@ -162,7 +163,7 @@ KaTeX 样式与字体来自本地 `katex` 包,由 Vite 打包到 `/_astro/`; ### 4.5 布局 seam -- `BaseLayout`:文档头、背景、悬浮导航、主槽、页脚。文档头由 `Head.astro` 输出,其中一份 Umami Cloud 脚本只负责访问统计,不渲染界面。脚本带 `is:inline` 和 `defer`;不要加 `data-astro-rerun`,也不要在 `astro:page-load` 再调用 `umami.track()`,避免换页重复计数。 +- `BaseLayout`:文档头、背景、悬浮导航、主槽、页脚。文档头由 `Head.astro` 输出,其中一份 Umami Cloud 脚本只负责访问统计,不渲染界面。脚本带 `is:inline` 和 `defer`;不要加 `data-astro-rerun`,也不要在 `astro:page-load` 再调用 `umami.track()`,避免换页重复计数。可选 Oneko 由 `FEATURES.oneko` 控制:布局内联引导脚本仅首次注入托管 `oneko.js`(`window.__yublogOnekoBooted`),换页不重建;详见 [Oneko 像素猫](./Oneko%20像素猫.md)。 - `BlogIndexLayout`:导航页面的居中单栏容器。 - `StandardLayout`:文章标题、正文与普通内容排版。 @@ -438,6 +439,7 @@ Pagefind **按需加载**:面板打开时调 `window.ensurePagefind()`(打 | 新增/修改拾趣分区 | `src/content/interests/.md`(或 `.mdx`),填写 `title`/`description`/`order`,重新构建 | 改导航数组、手写路由或在组件里复制文案 | | 为拾趣分类补图标 | `src/config.ts` 的 `INTEREST_ICONS`;不配置即纯文字菜单 | 给无图标分类补占位或手工复制 safelist | | 改顶栏链接或顶栏 GitHub | `src/config.ts` | `NavBar.astro` | +| 开关或换装 Oneko 像素猫 | `src/config.ts` `FEATURES.oneko`;说明 [Oneko 像素猫](./Oneko%20像素猫.md) | 拷贝 CDN 进 `public/`,或在多处重复挂载 | | 改个人社交名字或链接 | `src/config.ts` 的 `AUTHOR_LINKS`;若关于正文也展示同一联系方式,再同步该字段 | 改顶栏 `UI.socialLinks` | | 改分页大小 | `blog-browser.js`、`ListView.astro` 首屏 CSS、测试 | JS 常量与 `nth-child(n + 9)` 的镜像 | | 改写作热力的日期、年份或年度字数口径 | `src/utils/blog-heatmap.js` | 在 `WritingHeatmap.astro` 里重算日期或字数 | diff --git a/src/components/base/Footer.astro b/src/components/base/Footer.astro index 832a90bf..dda537c9 100644 --- a/src/components/base/Footer.astro +++ b/src/components/base/Footer.astro @@ -1,7 +1,10 @@ --- +import { FEATURES } from '~/config' import { withBasePath } from '~/utils/path' const currentYear = new Date().getFullYear() +const oneko = FEATURES.oneko +const onekoEnabled = Array.isArray(oneko) && oneko[0] ---
@@ -29,6 +32,14 @@ const currentYear = new Date().getFullYear() > 萌ICP备20269668号 + {onekoEnabled && ( + <> + + + + )}
@@ -55,6 +66,31 @@ const currentYear = new Date().getFullYear() color: var(--accent); } + .footer-oneko-toggle { + appearance: none; + margin: 0; + padding: 0; + border: 0; + border-bottom: 1px solid color-mix(in srgb, var(--fg) 35%, transparent); + background: transparent; + color: color-mix(in srgb, var(--fg) 72%, transparent); + font: inherit; + cursor: pointer; + transition: + border-color 0.3s ease-in-out, + color 0.3s ease-in-out; + } + + .footer-oneko-toggle:hover, + .footer-oneko-toggle:focus-visible { + border-bottom-color: var(--accent); + color: var(--accent); + } + + .footer-oneko-toggle[aria-pressed='true'] { + color: color-mix(in srgb, var(--fg) 55%, transparent); + } + .footer-mark { flex: none; width: 0.38rem; diff --git a/src/components/nav/NavBar.astro b/src/components/nav/NavBar.astro index 004accce..3eea1a5b 100644 --- a/src/components/nav/NavBar.astro +++ b/src/components/nav/NavBar.astro @@ -12,7 +12,7 @@ const { internalNavs, socialLinks, navBarLayout } = UI const home = withBasePath('/') --- -