diff --git a/_config.butterfly.yml b/_config.butterfly.yml
index dcc3397..3afc605 100644
--- a/_config.butterfly.yml
+++ b/_config.butterfly.yml
@@ -591,7 +591,7 @@ math:
search:
use: local_search
placeholder:
- path: search.json
+ path: search.xml
field: posts
content: true
diff --git a/source/_posts/2026/2026.2/add-typesense-search.md b/source/_posts/2026/2026.2/add-typesense-search.md
new file mode 100644
index 0000000..8527a95
--- /dev/null
+++ b/source/_posts/2026/2026.2/add-typesense-search.md
@@ -0,0 +1,720 @@
+---
+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:
+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
+ -
+ -
+```
+
+为了方便,我直接修改了```\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 =
+ '
' +
+ '
' +
+ '
搜索服务加载失败
' +
+ '
' +
+ '
依赖库未能正确加载,请检查以下配置:
' +
+ '
' +
+ '- 确认已在
_config.butterfly.yml 中正确引入依赖 ' +
+ '- 检查 JS 文件加载顺序(先 instantsearch.js,再 adapter)
' +
+ '- 尝试更换 CDN 或使用本地文件
' +
+ '- 打开浏览器控制台查看详细错误信息
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
';
+ }
+
+ // ============================================================================
+ // 1. 动态插入 HTML 结构
+ // ============================================================================
+ const searchHTML = `
+
+
+
+ `;
+
+ 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(' - ');
+ console.error(' - ');
+ console.error(' - ');
+ 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 '找到 ' + data.nbHits + ' 条结果 (' + data.processingTimeMS + 'ms)';
+ }
+ }
+ }),
+ instantsearch.widgets.hits({
+ container: '#hits',
+ templates: {
+ empty: function(results) {
+ return '' +
+ '
' +
+ '
找不到与 "' + results.query + '" 相关的内容
' +
+ '
试试其他关键词吧 (´·ω·`)
' +
+ '
';
+ },
+ 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 '' +
+ '' + titleHighlight + '
' +
+ '' + contentHighlight + '
' +
+ '';
+ }
+ }
+ }),
+ 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 %}
\ No newline at end of file
diff --git a/source/_posts/2026/2026.2/install-gitea.md b/source/_posts/2026/2026.2/install-gitea.md
new file mode 100644
index 0000000..8b6a99f
--- /dev/null
+++ b/source/_posts/2026/2026.2/install-gitea.md
@@ -0,0 +1,110 @@
+---
+title: 安装gitea
+cover: https://pic.biss.click/image/961bc881-cb0a-4ab7-ace5-9990e71c30a0.webp
+categories: 技术
+tags: gitea
+abbrlink: 34725d47
+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
+```
+
+在后台工作流运行器可以看见就没问题了。
\ No newline at end of file
diff --git a/source/_posts/2026/2026.2/remove-to-gitea.md b/source/_posts/2026/2026.2/remove-to-gitea.md
new file mode 100644
index 0000000..f813203
--- /dev/null
+++ b/source/_posts/2026/2026.2/remove-to-gitea.md
@@ -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,可以自己改改。
\ No newline at end of file