优化文件结构

This commit is contained in:
2026-06-18 21:22:17 +08:00 Unverified
parent d98d2989cb
commit 5678b95f6f
45 changed files with 9 additions and 6 deletions
+291
View File
@@ -0,0 +1,291 @@
---
title: 添加网站左上角菜单
cover: https://pic.biss.click/image/a40baba1-b296-4b3b-a781-da45bffb1b95.webp
categories: 建站手札
series: webcustom
tags:
- 网站
abbrlink: '7e921903'
summary: >-
这篇文章详细介绍了如何在柳神的网站上添加一个自定义的左上角菜单,包括菜单的结构、样式以及响应式设计。作者提供了具体的HTML和CSS代码示例,展示了如何实现一个包含多个链接和功能的导航栏,同时确保在不同屏幕尺寸下的适配性。此外,还解释了一些关键的CSS属性和JavaScript交互效果,以实现平滑的菜单显示和隐藏。通过这些步骤,用户可以轻松地自定义和美化网站的顶部导航栏,提升用户体验。
date: 2026-02-10 07:11:14
---
看到柳神的网站有这种菜单,但是没有写魔改教程,只好自己慢慢摸索了。
{% link liushen,liushen,https://blog.liushen.fun %}
预计做好后是这种效果:
![示意图](https://pic.biss.click/image/8d374eff-8817-4337-86ed-5909ff0eefaa.webp)
首先,修改```\themes\butterfly\layout\includes\header\nav.pug```:
```pug
nav#nav
//- 左侧区域:包含指纹菜单和网站名
span#blog-info
#ls-menu-container
i.fas.fa-fingerprint
#ls-menu-panel
.ls-section
.ls-title 😀 个人网站
.ls-grid
a(href="/") #[i.fas.fa-rss] 个人博客
a(href="https://github.com/bishshi") #[i.fab.fa-github] Github
.ls-section
.ls-title 😎 常用服务
.ls-grid
a(href="https://git.biss.click/biss") #[i.fas.fa-code] 代码仓库
a(href="https://mm.biss.click") #[i.fas.fa-pen-nib] 日常yy
a(href="https://statstic.biss.click") #[i.fas.fa-users] 访客统计
a(href="https://pic.biss.click") #[i.fas.fa-image] 图床
a(href="https://chat.biss.click") #[i.fas.fa-robot] AI网站
.ls-section
.ls-title 🛸 实用工具
.ls-grid
a(href="https://cover.biss.click") #[i.fas.fa-palette] 封面设计
a.nav-site-title(href=url_for('/'))
if theme.nav.logo
img.site-icon(src=url_for(theme.nav.logo) alt='Logo')
if theme.nav.display_title
span.site-name=config.title
//- 中间区域:关键!必须在 nav 下一级,以便 JS 切换类名
if globalPageType === 'post' && theme.nav.display_post_title
a.nav-page-title(href='javascript:void(0);' onclick='btf.scrollToDest(0, 500)')
span.site-name=(page.title || config.title)
//- 右侧区域
#nav-right
if theme.menu
#menus
!= partial('includes/header/menu_item', {}, {cache: true})
if theme.search.use || true
#random-post-button
a.site-page.social-icon#random-post-link(href='javascript:void(0);' onclick='randomPost()')
i.fas.fa-solid.fa-shuffle
#search-button
a.site-page.social-icon.search-typesense-trigger
i.fas.fa-search.fa-fw
#toggle-menu
span.site-page
i.fas.fa-bars.fa-fw
```
在合适的目录下新建```nav.css```(例如```\themes\butterfly\source\css\nav.css```),这份css是磨砂玻璃的样式:
```css
#nav-right{
flex:1 1 auto;
justify-content: flex-end;
margin-left: auto;
display: flex;
flex-wrap:nowrap;
}
/* 导航栏居中 */
#sidebar #sidebar-menus .menus_items .menus_item {
margin: 10px 0;
}
#sidebar #sidebar-menus .menus_items a.site-page {
padding-left: 0;
}
#sidebar #sidebar-menus .menus_items .site-page {
position: relative;
display: block;
padding: 6px 30px 6px 22px;
color: var(--font-color);
font-size: 1.15em;
border: var(--style-border-always);
background: var(--icat-card-bg);
font-size: 14px;
border-radius: 12px;
}
#sidebar #sidebar-menus .menus_items .site-page i:first-child {
text-align: left;
padding-left: 10px;
}
#nav #menus {
display: flex;
justify-content: center;
width: 100%;
position: absolute;
left: 0;
margin: 0;
transform: translateZ(0);
}
#nav #blog-info {
flex-wrap: nowrap;
display: flex;
align-items: center;
z-index: 102;
max-width: fit-content;
}
@media screen and (max-width: 900px) {
#nav {
padding: 0 15px;
}
#nav-group {
padding: 0 0.2rem;
}
#rightside {
right: -42px;
}
}
/* IPAD菜单栏调整 */
/* 1. 容器溢出穿透 */
#nav, #blog-info, #nav-group {
overflow: visible !important;
}
#ls-menu-container {
position: relative !important;
display: inline-flex !important;
align-items: center;
height: 100%;
padding: 0 15px;
cursor: pointer !important;
z-index: 2000 !important;
}
/* 2. 悬浮面板:宽大且高不透明度 */
#ls-menu-panel {
position: absolute !important;
top: 100% !important;
left: 0 !important;
margin-top: 15px !important;
width: 420px;
padding: 24px;
border-radius: 18px;
/* 高不透明度磨砂玻璃 (0.9) */
background: rgba(255, 255, 255, 0.95) !important;
backdrop-filter: blur(20px) saturate(180%) !important;
-webkit-backdrop-filter: blur(20px) saturate(180%) !important;
border: 1px solid rgba(255, 255, 255, 0.5) !important;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18) !important;
opacity: 0 !important;
visibility: hidden !important;
transform: translateY(12px) !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
z-index: 999999 !important;
cursor: default;
}
/* 3. 透明感应桥梁 */
#ls-menu-panel::before {
content: "";
position: absolute;
top: -20px;
left: 0;
right: 0;
height: 25px;
background: transparent !important;
}
/* 4. 触发效果 */
#ls-menu-container:hover #ls-menu-panel {
opacity: 1 !important;
visibility: visible !important;
transform: translateY(0) !important;
}
#ls-menu-container:hover i.fa-fingerprint {
color: #49b1f5;
transform: scale(1.1);
}
/* 5. 内部网格:保持两列 */
.ls-section { margin-bottom: 22px; }
.ls-section:last-child { margin-bottom: 0; }
.ls-title {
color: #333;
font-weight: 800;
font-size: 15px;
margin-bottom: 12px;
opacity: 0.9;
}
.ls-grid {
display: grid !important;
grid-template-columns: repeat(2, 1fr); /* 恢复为两列 */
gap: 10px;
}
.ls-grid a {
color: #444 !important;
font-size: 14px !important;
padding: 10px 12px;
border-radius: 12px;
display: flex;
align-items: center;
transition: all 0.2s ease;
background: rgba(0, 0, 0, 0.02);
}
.ls-grid a:hover {
background: #49b1f5 !important;
color: #fff !important;
transform: translateX(4px); /* 悬停时轻微右移,增加动感 */
}
.ls-grid a i {
margin-right: 12px;
width: 20px;
text-align: center;
font-size: 15px;
}
/* 6. 深色模式适配 */
[data-theme='dark'] #ls-menu-panel {
background: rgba(30, 30, 30, 0.95) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
}
[data-theme='dark'] .ls-title { color: #eee; }
[data-theme='dark'] .ls-grid a {
color: #ccc !important;
background: rgba(255, 255, 255, 0.05);
}
#nav.show-title .nav-page-title {
display: flex !important;
opacity: 1 !important;
visibility: visible !important;
position: absolute !important;
left: 50% !important;
transform: translateX(-50%) !important;
white-space: nowrap !important;
z-index: 1000 !important;
}
#nav.show-title #menus,
#nav.show-title .nav-site-title {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
#nav .nav-page-title {
display: none;
transition: opacity 0.3s ease;
}
#nav #blog-info {
overflow: visible !important;
}
```
然后在```_config.butterfly.yml```里面引用该css
```yml
- <link rel="stylesheet" href="/css/nav.css">
```
然后重新构建应该就可以看到效果了。
+43
View File
@@ -0,0 +1,43 @@
---
title: 自建renovate-bot
categories:
- 技术
cover: https://pic.biss.click/image/fca16741-64fa-495b-aa5e-a2ef077461ef.webp
swiper_index: 3
tags:
- gitea
abbrlink: 56f57c0b
summary: >-
这篇文章介绍了如何自建Renovate Bot以自动更新依赖,并通过Docker Compose进行部署。首先,文章提供了Renovate
Bot的Docker Compose文件配置,包括使用特定平台(gitea)、设置Git
API地址、生成Token以及启用自动发现功能。接着,建议创建一个专门用于管理Renovate
Bot的账户,并为其生成登录Token。最后,通过在系统的crontab中设置定时任务,可以自动化运行更新流程。
date: 2026-02-23 10:49:17
---
最近自建了`gitea`,所以把`renovatebot`也自建一下,毕竟`renovatebot`可以自动更新依赖,但是它不对自建`git`提供服务。
这是`docker compose`文件:
```yaml
services:
renovate:
image: renovate/renovate:latest
environment:
RENOVATE_PLATFORM: 'gitea'
RENOVATE_ENDPOINT: '你的git api地址'
RENOVATE_TOKEN: 'token'
RENOVATE_AUTODISCOVER: 'true'
RENOVATE_GIT_AUTHOR: Renovate Bot <bot@biss.click>
RENOVATE_USERNAME: renovate-bot
volumes:
- ./renovate-data:/tmp/renovate
```
可以新建一个`renovate`用户来专门管理,然后登录这个账户创建一个登录`token`。然后再需要使用这个的仓库添加这个用户为协作者就可以了。
这个`docker`容器在运行后会自动退出,这是正常的。
可以在系统crontab新建一个定时任务,这样就可以自动运行更新。
```bash
cd /www/compose/renovatebot && docker compose run --rm renovate
```
{% link 官方文档,Renovatebot,https://docs.renovatebot.com/examples/self-hosting/ %}
+721
View File
@@ -0,0 +1,721 @@
---
title: 添加typesense搜索
categories:
- 技术
- 建站手札
abbrlink: f287c563
summary: >-
这篇文章介绍了如何在博客中集成Typesense搜索引擎,包括安装Typesense、添加数据集以及同步数据的详细步骤。首先,通过命令行安装Typesense并配置相关参数,如API密钥和数据目录。接着,安装必要的npm包,并在Hexo博客中配置搜索功能,指定存储搜索数据的JSON文件路径。然后,编写了一个同步脚本,用于读取XML文件、解析数据、检查或创建集合以及导入数据到Typesense。最后,在博客底部添加了InstantSearch和Typesense的JavaScript库,并根据实际情况修改了配置。
date: 2026-02-05 13:14:16
series: webcustom
tags:
---
最近在构建班级博客,用```ghost cms```,在构建搜索时发现了typesense,所以把他移植到这个博客上。
# 安装typesense
直接用```docker-compose```:
```yaml
services:
typesense:
image: typesense/typesense:30.1
restart: always
ports:
- "8108:8108"
volumes:
- ./typesense-data:/data
command: '--data-dir /data --api-key=填写key --enable-cors'
```
然后就是反向代理之类的,不过多写了。
# 添加数据集
```bash
# 先安装库
npm install hexo-generator-search
npm install typesense xml2js
```
然后在```config.yml```配置(就是把文章生成json):
```yml
search:
path: search.json
field: post
content: true
```
创建一个数据同步脚本```sync_typesense.js```:
```js
const Typesense = require('typesense');
const fs = require('fs');
const xml2js = require('xml2js');
// --- 配置区域 ---
const CONFIG = {
apiKey: '你的Admin-API-Key', // 必须是 Admin Key
host: '你的Typesense主机地址',
port: 443,
protocol: 'https',
collectionName: 'blogs'
};
const client = new Typesense.Client({
'nodes': [{ 'host': CONFIG.host, 'port': CONFIG.port, 'protocol': CONFIG.protocol }],
'apiKey': CONFIG.apiKey,
'connectionTimeoutSeconds': 5
});
async function sync() {
try {
// 1. 读取并解析 XML
const xml = fs.readFileSync('./public/search.xml', 'utf8');
const parser = new xml2js.Parser({ explicitArray: false });
const result = await parser.parseStringPromise(xml);
// 提取文章列表 (处理单篇文章和多篇文章的情况)
let entries = result.search.entry;
if (!Array.isArray(entries)) entries = [entries];
// 格式化数据以适配 Typesense
const documents = entries.map(post => ({
title: post.title,
url: post.url,
content: post.content,
categories: post.categories ? (Array.isArray(post.categories.category) ? post.categories.category : [post.categories.category]) : [],
tags: post.tags ? (Array.isArray(post.tags.tag) ? post.tags.tag : [post.tags.tag]) : [],
}));
// 2. 检查或创建 Collection (Schema)
try {
await client.collections(CONFIG.collectionName).retrieve();
} catch (err) {
const schema = {
name: CONFIG.collectionName,
fields: [
{ name: 'title', type: 'string', locale: 'zh'},
{ name: 'content', type: 'string', locale: 'zh'},
{ name: 'url', type: 'string' },
{ name: 'categories', type: 'string[]', facet: true },
{ name: 'tags', type: 'string[]', facet: true }
]
};
await client.collections().create(schema);
console.log('Collection created!');
}
// 3. 导入数据 (使用 upsert 模式:存在则更新,不存在则创建)
console.log(`Syncing ${documents.length} posts to Typesense...`);
await client.collections(CONFIG.collectionName).documents().import(documents, { action: 'upsert' });
console.log('Sync complete!');
} catch (error) {
console.error('Sync failed:', error);
}
}
sync();
```
然后运行```node sync_typesense.js```
# 创建只读key
把下面代码存储成js,node运行就行。
```js
const http = require('https');
const data = JSON.stringify({
"description": "Public search only key",
"actions": ["documents:search"],
"collections": ["blogs"]
});
const options = {
hostname: '', // 不要带 https://
port: 443,
path: '/keys',
method: 'POST',
headers: {
'X-TYPESENSE-API-KEY': '你的admin key',
'Content-Type': 'application/json',
'Content-Length': data.length
}
};
const req = http.request(options, res => {
res.on('data', d => { process.stdout.write(d); });
});
req.on('error', error => { console.error(error); });
req.write(data);
req.end();
```
# 博客添加搜索
在```config.yaml```inject bottom添加:
```yaml
- <script src="https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0"></script>
- <script src="https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>
```
为了方便,我直接修改了```\themes\butterfly\source\js\search\local_search.js```
```js
(function () {
'use strict';
// ============================================================================
// 配置区域 - 请根据实际情况修改
// ============================================================================
const CONFIG = {
apiKey: "", // ⚠️ 建议使用 Search-Only API Key
server: {
host: "host",
port: "443",
protocol: "https"
},
indexName: "blogs",
searchParams: {
query_by: "title,content",
highlight_full_fields: "title,content",
per_page: 8,
num_typos: 1,
typo_tokens_threshold: 1,
prefix: true
},
ui: {
maxRetries: 10,
retryDelay: 100,
animationDuration: 300
}
};
// ============================================================================
// 状态管理
// ============================================================================
let searchInstance = null;
let isInitialized = false;
let isSearchOpen = false;
let initRetryCount = 0;
const MAX_INIT_RETRIES = 30; // 最多重试30次 (3秒)
// ============================================================================
// 错误提示函数
// ============================================================================
function showErrorMessage() {
const hitsContainer = document.getElementById('hits');
if (!hitsContainer) return;
hitsContainer.innerHTML =
'<div class="ts-empty">' +
'<div style="color: #f44336;"><i class="fas fa-exclamation-triangle" style="font-size: 3rem;"></i></div>' +
'<div style="font-size: 1.1rem; font-weight: bold; margin: 15px 0;">搜索服务加载失败</div>' +
'<div style="font-size: 0.9rem; color: #666; line-height: 1.8;">' +
'<p>依赖库未能正确加载,请检查以下配置:</p>' +
'<ol style="text-align: left; max-width: 500px; margin: 15px auto;">' +
'<li>确认已在 <code>_config.butterfly.yml</code> 中正确引入依赖</li>' +
'<li>检查 JS 文件加载顺序(先 instantsearch.js,再 adapter</li>' +
'<li>尝试更换 CDN 或使用本地文件</li>' +
'<li>打开浏览器控制台查看详细错误信息</li>' +
'</ol>' +
'</div>' +
'<div style="margin-top: 20px;">' +
'<button onclick="location.reload()" style="padding: 10px 20px; background: #49b1f5; color: white; border: none; border-radius: 5px; cursor: pointer;">重新加载页面</button>' +
'</div>' +
'</div>';
}
// ============================================================================
// 1. 动态插入 HTML 结构
// ============================================================================
const searchHTML = `
<div id="typesense-search-mask" class="ts-mask" style="display:none;">
<div id="typesense-search-container" class="ts-container">
<div class="ts-header">
<span class="ts-title">
<i class="fas fa-search"></i> 本站搜索
</span>
<span id="close-typesense" class="ts-close" aria-label="关闭搜索">&times;</span>
</div>
<div id="searchbox"></div>
<div id="stats" class="ts-stats"></div>
<div id="hits" class="ts-hits"></div>
<div id="pagination" class="ts-pagination"></div>
<div class="ts-footer">
<small>Search powered by <strong>Typesense</strong></small>
</div>
</div>
</div>
<style>
.ts-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
opacity: 0;
transition: opacity 300ms ease;
}
.ts-mask.active { opacity: 1; }
.ts-container {
margin: 5% auto;
width: 90%;
max-width: 650px;
background: var(--search-bg, var(--card-bg, #fff));
padding: 25px;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 10001;
transform: translateY(-50px);
opacity: 0;
transition: all 300ms ease;
}
.ts-mask.active .ts-container {
transform: translateY(0);
opacity: 1;
}
.ts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 2px solid var(--text-highlight-color, #49b1f5);
padding-bottom: 10px;
}
.ts-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--text-highlight-color, #49b1f5);
}
.ts-close {
cursor: pointer;
font-size: 28px;
color: var(--font-color, #333);
line-height: 1;
transition: color 0.2s, transform 0.2s;
}
.ts-close:hover {
color: var(--text-highlight-color, #49b1f5);
transform: scale(1.1);
}
.ais-SearchBox-input {
position: relative;
z-index: 10002;
cursor: text;
padding: 12px 40px 12px 15px !important;
border-radius: 8px !important;
border: 2px solid #eee !important;
width: 100%;
outline: none;
transition: border-color 0.3s, box-shadow 0.3s;
background: var(--card-bg, #fff);
color: var(--font-color, #333);
font-size: 1rem;
}
.ais-SearchBox-input:focus {
border-color: var(--text-highlight-color, #49b1f5) !important;
box-shadow: 0 0 0 3px rgba(73, 177, 245, 0.1);
}
.ts-stats {
margin: 10px 0;
font-size: 0.85rem;
color: var(--font-color, #666);
opacity: 0.8;
}
.ts-hits {
max-height: 55vh;
overflow-y: auto;
margin-top: 15px;
padding-right: 5px;
}
.ts-hits::-webkit-scrollbar { width: 6px; }
.ts-hits::-webkit-scrollbar-track {
background: var(--card-bg, #f1f1f1);
border-radius: 10px;
}
.ts-hits::-webkit-scrollbar-thumb {
background: var(--text-highlight-color, #49b1f5);
border-radius: 10px;
}
.ts-empty {
text-align: center;
padding: 40px 20px;
color: var(--font-color, #999);
}
.ts-empty i {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.3;
}
.ts-empty code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
color: #e91e63;
}
.ts-empty ol {
padding-left: 20px;
}
.ts-empty li {
margin: 8px 0;
}
.ts-result-item {
border-radius: 8px;
transition: all 0.2s ease;
margin-bottom: 10px;
padding: 15px;
border: 1px solid transparent;
text-decoration: none;
display: block;
background: var(--card-bg, #fff);
}
.ts-result-item:hover {
background: var(--text-bg-hover, rgba(73, 177, 245, 0.05));
border-color: var(--text-highlight-color, #49b1f5);
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.ts-result-title {
font-weight: bold;
color: var(--text-highlight-color, #49b1f5);
font-size: 1.1rem;
margin-bottom: 8px;
display: block;
line-height: 1.4;
}
.ts-result-content {
font-size: 0.9rem;
color: var(--font-color, #666);
line-height: 1.6;
opacity: 0.85;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ts-result-item mark {
background: #ffeb3b;
color: #000;
padding: 2px 4px;
border-radius: 3px;
font-weight: 500;
}
.ts-pagination {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 5px;
}
.ais-Pagination-list {
display: flex;
list-style: none;
padding: 0;
margin: 0;
gap: 5px;
}
.ais-Pagination-link {
display: block;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 5px;
color: var(--font-color, #333);
text-decoration: none;
transition: all 0.2s;
background: var(--card-bg, #fff);
}
.ais-Pagination-link:hover {
background: var(--text-highlight-color, #49b1f5);
color: #fff;
}
.ais-Pagination-item--selected .ais-Pagination-link {
background: var(--text-highlight-color, #49b1f5);
color: #fff;
}
.ts-footer {
text-align: right;
margin-top: 15px;
border-top: 1px solid var(--border-color, #eee);
padding-top: 10px;
}
@media (max-width: 768px) {
.ts-container {
margin: 10px;
width: calc(100% - 20px);
padding: 20px 15px;
}
.ts-hits { max-height: 50vh; }
}
[data-theme="dark"] .ts-mask,
.dark-mode .ts-mask {
background: rgba(0, 0, 0, 0.85);
}
</style>
`;
document.body.insertAdjacentHTML('beforeend', searchHTML);
const mask = document.getElementById('typesense-search-mask');
const closeBtn = document.getElementById('close-typesense');
const container = document.getElementById('typesense-search-container');
// ============================================================================
// 搜索控制
// ============================================================================
function openSearch() {
if (isSearchOpen) return;
isSearchOpen = true;
mask.style.display = 'block';
void mask.offsetWidth;
mask.classList.add('active');
document.body.style.overflow = 'hidden';
if (!isInitialized) {
initTypesense();
}
focusSearchInput();
}
function closeSearch() {
if (!isSearchOpen) return;
isSearchOpen = false;
mask.classList.remove('active');
setTimeout(function() {
mask.style.display = 'none';
document.body.style.overflow = '';
}, CONFIG.ui.animationDuration);
}
function focusSearchInput(retryCount) {
retryCount = retryCount || 0;
const input = document.querySelector('.ais-SearchBox-input');
if (input) {
input.focus();
input.select();
} else if (retryCount < CONFIG.ui.maxRetries) {
setTimeout(function() {
focusSearchInput(retryCount + 1);
}, CONFIG.ui.retryDelay);
}
}
// ============================================================================
// 事件监听
// ============================================================================
document.addEventListener('click', function(e) {
if (e.target.closest('.search-typesense-trigger')) {
e.preventDefault();
openSearch();
}
});
closeBtn.addEventListener('click', closeSearch);
mask.addEventListener('click', function(e) {
if (e.target === mask) closeSearch();
});
container.addEventListener('click', function(e) {
e.stopPropagation();
});
window.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isSearchOpen) {
closeSearch();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
openSearch();
}
});
// ============================================================================
// Typesense 初始化(带重试限制)
// ============================================================================
function initTypesense() {
if (isInitialized || searchInstance) {
console.warn('Typesense 搜索已初始化');
return;
}
var instantsearchLoaded = typeof instantsearch !== 'undefined';
var adapterLoaded = typeof TypesenseInstantSearchAdapter !== 'undefined' ||
typeof window.TypesenseInstantSearchAdapter !== 'undefined';
console.log('📦 依赖库检查 (' + (initRetryCount + 1) + '/' + MAX_INIT_RETRIES + '):');
console.log(' instantsearch.js:', instantsearchLoaded ? '✅ 已加载' : '❌ 未加载');
console.log(' TypesenseAdapter:', adapterLoaded ? '✅ 已加载' : '❌ 未加载');
if (!instantsearchLoaded || !adapterLoaded) {
initRetryCount++;
if (initRetryCount >= MAX_INIT_RETRIES) {
console.error('');
console.error('❌ Typesense 依赖库加载失败!');
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.error('');
console.error('🔧 请检查 _config.butterfly.yml 配置:');
console.error('');
console.error('inject:');
console.error(' bottom: # ⚠️ 使用 bottom 而不是 head');
console.error(' - <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.56.0"></script>');
console.error(' - <script src="https://cdn.jsdelivr.net/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>');
console.error(' - <script src="/js/typesense-search-fixed.js"></script>');
console.error('');
console.error('💡 或在控制台手动检查:');
console.error(' typeof instantsearch');
console.error(' typeof TypesenseInstantSearchAdapter');
console.error('');
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
showErrorMessage();
return;
}
console.warn('⏳ 100ms 后重试...');
setTimeout(initTypesense, 100);
return;
}
initRetryCount = 0;
console.log('🚀 开始初始化 Typesense...');
try {
const typesenseAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: CONFIG.apiKey,
nodes: [{
host: CONFIG.server.host,
port: CONFIG.server.port,
protocol: CONFIG.server.protocol
}],
cacheSearchResultsForSeconds: 120
},
additionalSearchParameters: CONFIG.searchParams
});
searchInstance = instantsearch({
searchClient: typesenseAdapter.searchClient,
indexName: CONFIG.indexName,
routing: false
});
searchInstance.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox',
placeholder: '输入关键词寻找故事...',
autofocus: true,
showReset: true,
showSubmit: false,
showLoadingIndicator: true
}),
instantsearch.widgets.stats({
container: '#stats',
templates: {
text: function(data) {
if (!data.query) return '';
return '找到 <strong>' + data.nbHits + '</strong> 条结果 (' + data.processingTimeMS + 'ms)';
}
}
}),
instantsearch.widgets.hits({
container: '#hits',
templates: {
empty: function(results) {
return '<div class="ts-empty">' +
'<div><i class="fas fa-search"></i></div>' +
'<div>找不到与 "<strong>' + results.query + '</strong>" 相关的内容</div>' +
'<div style="margin-top: 10px;">试试其他关键词吧 (´·ω·`)</div>' +
'</div>';
},
item: function(hit) {
// 使用 _highlightResult 获取高亮文本
var titleHighlight = hit._highlightResult && hit._highlightResult.title
? hit._highlightResult.title.value
: (hit.title || '');
var contentHighlight = hit._highlightResult && hit._highlightResult.content
? hit._highlightResult.content.value
: (hit.content || '');
// 截取内容长度
if (contentHighlight.length > 200) {
contentHighlight = contentHighlight.substring(0, 200) + '...';
}
return '<a href="' + hit.url + '" class="ts-result-item">' +
'<div class="ts-result-title">' + titleHighlight + '</div>' +
'<div class="ts-result-content">' + contentHighlight + '</div>' +
'</a>';
}
}
}),
instantsearch.widgets.pagination({
container: '#pagination',
padding: 2,
showFirst: false,
showLast: false
})
]);
searchInstance.start();
isInitialized = true;
console.log('✅ Typesense 初始化成功!');
searchInstance.on('render', function() {
if (isSearchOpen) {
const input = document.querySelector('.ais-SearchBox-input');
if (input && document.activeElement !== input) {
input.focus();
}
}
});
} catch (error) {
console.error('❌ 初始化失败:', error);
showErrorMessage();
}
}
// ============================================================================
// 初始化
// ============================================================================
function init() {
console.log('🔍 Typesense 搜索已准备就绪');
console.log('💡 快捷键: Ctrl/Cmd + K');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ============================================================================
// 全局接口
// ============================================================================
window.TypesenseSearch = {
open: openSearch,
close: closeSearch,
isOpen: function() { return isSearchOpen; },
getInstance: function() { return searchInstance; }
};
})();
```
# 注意
{% note red %}
indexName: "blogs" 和 collectionName: 'posts' 要一致!!!
{% endnote %}
+21
View File
@@ -0,0 +1,21 @@
---
title: 新年快乐!
summary: >-
这篇文章是一则关于新年祝福和幸福指数提升的通知,主要内容包含以下几点:首先,强调在假期期间要确保饮食质量,避免emo情绪的影响;其次,鼓励大家在收到通知后在留言区分享新年愿望,以便后台系统收集并祝愿他们实现愿望;最后,表达了对大家新年的美好祝愿,希望他们身体健康,万事如意,所得皆所愿,所行化坦途。
tags:
- 生活
abbrlink: 5785bd01
date: 2026-02-14 06:56:18
categories:
series:
---
各位亲朋好友、合作伙伴及屏幕前的你:
值此二〇二六年蛇年来临之际,为贯彻落实“快乐至上”的核心价值观,进一步提升全体人员的幸福指数,现将有关事项通知如下:
一、各单位要切实做好“吃好喝好”保障工作,严禁在假期期间进行任何形式的emo。
二、请各有关人员在收到本通知后,务必在下方留言区留下你的新年愿望,由后台系统统一收集并祝愿其实现。
三、祝大家在新的一年里,身体健康,万事如意,所得皆所愿,所行化坦途!
+41
View File
@@ -0,0 +1,41 @@
---
title: 自建bitwarden服务
categories:
- 技术
cover: https://pic.biss.click/image/deaddd0c-e808-45f3-8354-3c44b2b65167.webp
abbrlink: 7baa41fc
summary: >-
这篇文章介绍了如何自建Bitwarden服务,以替代浏览器自带的密码管理器。Bitwarden是一款开源、端到端加密的密码管理器,支持多种操作系统和设备,具有安全存储、自动同步、一键填充、生成随机密码和检测弱密码等功能。文章详细描述了部署Bitwarden服务的步骤,包括安装Vaultwarden、配置Nginx反向代理以及设置域名和数据卷等。通过这些步骤,用户可以轻松搭建一个功能强大的密码管理服务器,提高个人信息安全。
date: 2026-02-25 11:09:23
---
Bitwarden 是一款开源、端到端加密的密码管理器,支持 Windows、macOS、Linux、Android、iOS 以及几乎所有主流浏览器。
它可以帮你:
- 安全存储密码、银行卡、笔记、密钥等敏感信息
- 全设备自动同步
- 一键自动填充账号密码
- 生成高强度随机密码
- 检测弱密码、重复密码、泄露密码
之前一直使用浏览器自带的密码管理器,跨平台不太好用,所以自建一个Bitwarden服务器。
但是我们一般用Vaultwarden,占用更小,有一些Bitwarden的商业功能。
```yaml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vw.domain.tld" #更改成自己的域名
volumes:
- ./vw-data/:/data/
ports:
- 127.0.0.1:8000:80
```
然后使用nignx进行反向代理,然后就可以尽情享用了。
{% link vaultwarden,vaultwarden,https://github.com/dani-garcia/vaultwarden %}
+111
View File
@@ -0,0 +1,111 @@
---
title: 安装gitea
cover: https://pic.biss.click/image/961bc881-cb0a-4ab7-ace5-9990e71c30a0.webp
categories: 技术
tags: gitea
abbrlink: 34725d47
swiper_index: 1
summary: >-
这篇文章详细介绍了如何在2C2G的服务器上安装Gitea,包括获取二进制文件、创建用户和工作目录、创建系统服务、创建数据库以及安装Runner等步骤。Gitea作为一个轻量级的自助Git服务,适合用于托管网站源码。文章还提供了关于如何配置反向代理和数据库的信息,以及如何使用Docker运行Gitea
Runner以实现GitHub Actions功能。通过这些步骤,用户可以在自己的服务器上成功部署Gitea,并利用其进行版本控制和自动化工作流程。
date: 2026-02-07 06:32:04
series:
---
今天想把网站的源码转移到自建git仓,所以先来安装gitea吧(gitlab过于庞大,服务器配置不够)
PS:我的服务器为2C2G
# 安装gitea
这里用二进制文件安装
## 获取二进制文件:
```bash
wget -O gitea https://dl.gitea.com/gitea/1.25.4/gitea-1.25.4-linux-amd64
chmod +x gitea
cp gitea /usr/local/bin/gitea
```
## 创建用户
这一步不是必须的,但是推荐这样,用root用户很容易出问题。
```bash
# On Ubuntu/Debian:
adduser \
--system \
--shell /bin/bash \
--gecos 'Git Version Control' \
--group \
--disabled-password \
--home /home/git \
git
# On Fedora/RHEL/CentOS:
groupadd --system git
adduser \
--system \
--shell /bin/bash \
--comment 'Git Version Control' \
--gid git \
--home-dir /home/git \
--create-home \
git
```
## 创建工作目录
```bash
mkdir -p /var/lib/gitea/{custom,data,log}
chown -R git:git /var/lib/gitea/
chmod -R 750 /var/lib/gitea/
mkdir /etc/gitea
chown root:git /etc/gitea
chmod 770 /etc/gitea
chmod 750 /etc/gitea
chmod 640 /etc/gitea/app.ini
```
## 创建系统服务
直接把github上面的挪过来就可以
{% link service文件,github,https://github.com/go-gitea/gitea/blob/release/v1.25/contrib/systemd/gitea.service %}
然后注册服务并启动
```bash
sudo systemctl enable gitea
sudo systemctl start gitea
```
## 创建数据库
可以用MySQL数据库或者PostgreSQL,创建一个数据库在web页面填写进去就行。
反向代理略过,和普通网站的反向代理配置没有什么区别。
# 安装runner
这个runner也不是必须的,是为了实现github的action功能;在2C2G服务器上我看运行的还可以,当然,只是这个hexo博客的自动构建,占用资源也少;
使用doker,这也是官方建议。以下是compose文件:
```yaml
services:
runner:
image: gitea/act_runner:latest
ports:
- 8088:8088
environment:
- CONFIG_FILE=/config.yaml
- GITEA_INSTANCE_URL=https://git.biss.click
- GITEA_RUNNER_REGISTRATION_TOKEN= #替换成自己的token
volumes:
- ./config.yaml:/config.yaml
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock # 允许 Runner 调用宿主机 Docker
```
token在管理后台 工作流-运行器-新建运行器获取
config文件需要这样生成
```bash
docker run --entrypoint="" --rm -it docker.io/gitea/act_runner:latest act_runner generate-config > config.yaml
```
在后台工作流运行器可以看见就没问题了。
+76
View File
@@ -0,0 +1,76 @@
---
title: 将博客仓库转移到gitea
cover: https://pic.biss.click/image/f9767ecf-b8de-461b-8e62-8f7444297ea6.webp
categories: 技术
tags: gitea
abbrlink: d2c8521
summary: >-
这篇文章详细介绍了如何将博客仓库从现有的平台转移到Gitea。首先,确保已经成功安装了Gitea。接着,文章重点讲解了action文件的修改,包括检查分支、缓存项目、安装Node和Hexo、安装依赖、清理文件树、生成静态文件并压缩、部署到本地仓库以及Webhook的配置。这些步骤确保了博客能够顺利迁移到Gitea,并且保持了原有的功能和结构。需要注意的是,文章中的action文件包含了一些具体的指令和变量,如ref:
master、cache-node-modules等,这些在实际操作时需要替换为具体的值。此外,文章还提到了Webhook的配置,这是一个可选但推荐的自定义事件触发器,用于在特定事件发生时自动触发部署流程。
date: 2026-02-07 12:30:39
series:
---
在上一篇文章中已经完成了gitea的安装
那么博客源码迁移倒是没问题,直接```git remote add origin```就行,但是action文件就有些变更。
这是我修改的action文件:
```yaml
name: 自动部署
on:
push:
branches:
- master
release:
types:
- published
workflow_dispatch:
env:
TZ: Asia/Shanghai
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 检查分支
uses: actions/checkout@v4
with:
ref: master
- name: 缓存项目 npm 包
id: cache-node-modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-nodeModules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('package.json') }}
restore-keys: |
${{ runner.os }}-nodeModules-
- name: 安装 Node
uses: actions/setup-node@v4
with:
node-version: "22.x"
- name: 安装 Hexo
run: |
npm install hexo-cli --global
- name: 安装依赖
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: |
npm install
- name: 清理文件树
run: |
npm run clean
- name: 生成静态文件并压缩
run: |
npm run build
- name: 部署
run: |
cd ./public
git init
git config user.name "${{ gitea.actor }}"
git config user.email "${{ gitea.actor }}@noreply.gitea.io"
git add .
git commit -m "${{ gitea.event.head_commit.message }}··[$(date +"%Z %Y-%m-%d %A %H:%M:%S")]"
git push --force --quiet "https://${{ gitea.actor }}:${{ secrets.DEPLOY_TOKEN }}@git.biss.click/biss/blog.git" master:page
- name: Deploy to Server
run: |
curl -k -X POST
```
仅供参考吧,最后面是webhook,可以自己改改。