Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## 未发布

### 新增

- 全站可选托管 Oneko 像素猫(`FEATURES.oneko`,默认 classic):跟随光标、睡觉、气泡、记住位置、季节帽子,减少动态时隐藏;页脚可隐藏小猫,顶栏为避让区。不自托管、不开声音与激光。说明见 `docs/Oneko 像素猫.md`。

## 1.3.0 — 2026-10-15

### 新增
Expand Down
98 changes: 98 additions & 0 deletions docs/Oneko 像素猫.md
Original file line number Diff line number Diff line change
@@ -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<OnekoConfig>`,默认 `[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 引用,不复制皮肤文件进仓库。
2 changes: 2 additions & 0 deletions docs/站点行为 SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 背景同步;减少动效时保留静态背景,仍响应缩放和主题变化,不启动动画循环。提供深浅主题及可见键盘反馈,不依靠颜色表达类别 |

装饰背景在主体内部的独立层级中,位于主体底色之上、页面内容之下;即使外部样式把主体底色改为不透明,也不因此遮挡背景。背景仍相对视口固定,不拦截内容交互。外部暗化工具可以覆盖站点声明的配色,不保证其覆盖后的颜色仍与主题按钮一致。
Expand Down Expand Up @@ -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 与产物检查 |
Expand Down
4 changes: 3 additions & 1 deletion docs/项目解析.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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`:文章标题、正文与普通内容排版。

Expand Down Expand Up @@ -438,6 +439,7 @@ Pagefind **按需加载**:面板打开时调 `window.ensurePagefind()`(打
| 新增/修改拾趣分区 | `src/content/interests/<id>.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` 里重算日期或字数 |
Expand Down
36 changes: 36 additions & 0 deletions src/components/base/Footer.astro
Original file line number Diff line number Diff line change
@@ -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]
---

<footer class="slide-enter animate-delay-1600! px-7 mt-auto mb-8">
Expand Down Expand Up @@ -29,6 +32,14 @@ const currentYear = new Date().getFullYear()
>
萌ICP备20269668号
</a>
{onekoEnabled && (
<>
<span class="footer-mark lt-sm:hidden" aria-hidden="true"></span>
<button type="button" class="footer-oneko-toggle" data-oneko-toggle>
隐藏小猫
</button>
</>
)}
</div>
</div>
</footer>
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/components/nav/NavBar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { internalNavs, socialLinks, navBarLayout } = UI
const home = withBasePath('/')
---

<header class="nav-bar" data-site-header>
<header class="nav-bar" data-site-header data-oneko-zone="avoid">
<nav aria-label="主导航" class="nav-bar__inner">
{(['left', 'right'] as const).map((side) => (
<div class={`nav-bar__group nav-bar__group--${side}`}>
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export const FEATURES: Features = {
maxItemsPerPage: [true, 3],
},
],
oneko: [true, { skin: 'classic' }],
}

/** Dropdown icons also feed UnoCSS's dynamic-class safelist. */
Expand Down
25 changes: 24 additions & 1 deletion src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,15 @@ if (!hideBackLink) footerClasses.push('prose')

let style: Record<string, string> = {}

const { slideEnterAnim } = FEATURES
const { slideEnterAnim, oneko } = FEATURES
const enableSlideEnterAnim = Array.isArray(slideEnterAnim) && slideEnterAnim[0]
if (enableSlideEnterAnim)
style['--enter-step'] = `${slideEnterAnim[1].enterStep}ms`

const onekoConfig = Array.isArray(oneko) && oneko[0] ? oneko[1] : undefined
const onekoEnabled = Boolean(onekoConfig)
const onekoSkin = onekoConfig?.skin ?? 'classic'

const {
externalLink: { newTab, cursorType },
} = UI
Expand Down Expand Up @@ -201,5 +205,24 @@ if (hasCustomCursor) style['--external-link-cursor'] = cursorType
setProgressState(false)
})
</script>
{onekoEnabled && <script is:inline define:vars={{ onekoSkin }}>
;(function () {
if (window.__yublogOnekoBooted) return
window.__yublogOnekoBooted = true
const s = document.createElement('script')
s.type = 'module'
s.src = 'https://oneko.dhrv.pw/oneko.js'
s.dataset.skin = onekoSkin
s.dataset.meow = 'false'
s.dataset.laserPointer = 'false'
s.dataset.seasonal = ''
s.dataset.persistPosition = ''
s.dataset.bubbleEnabled = ''
s.dataset.followCursor = ''
s.dataset.sleepEnabled = ''
s.dataset.reducedMotion = 'hide'
document.body.appendChild(s)
})()
</script>}
</body>
</html>
20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,18 @@ interface SearchConfig {
maxItemsPerPage: FeatureConfig<number>
}

export interface OnekoConfig {
/**
* Hosted oneko skin name. Defaults to `classic` when omitted.
*
* Allowed values (oneko.dhrv.pw): classic, black, gray, calico, tora,
* catppuccin, ghost, silver, spirit, valentine, maia, vaporwave, ginger,
* sage, siamese, strawberry-milk, blue-frost, lavender, tuxedo, peach,
* honey, mocha, mint, midnight-blue.
*/
skin?: string
}

export interface Features {
/**
* Whether to enable slide-in animation on each page.
Expand All @@ -558,4 +570,12 @@ export interface Features {
* For Pagefind’s built-in configuration, directly modify `src/components/widgets/SearchSwitch.astro`.
*/
search: FeatureConfig<SearchConfig>

/**
* Whether to enable the hosted Oneko pixel cat (oneko.dhrv.pw).
*
* Mounts once from BaseLayout; Footer exposes a visitor hide toggle.
* See `docs/Oneko 像素猫.md`.
*/
oneko: FeatureConfig<OnekoConfig>
}
Loading