From d215b55bc34b355a314a2a2e48bde71f697a7cae Mon Sep 17 00:00:00 2001 From: bisnsh Date: Sat, 7 Feb 2026 21:39:31 +0800 Subject: [PATCH] update 20260207 --- _config.butterfly.yml | 2 +- .../2026/2026.2/add-typesense-search.md | 720 ++++++++++++++++++ source/_posts/2026/2026.2/install-gitea.md | 110 +++ source/_posts/2026/2026.2/remove-to-gitea.md | 76 ++ 4 files changed, 907 insertions(+), 1 deletion(-) create mode 100644 source/_posts/2026/2026.2/add-typesense-search.md create mode 100644 source/_posts/2026/2026.2/install-gitea.md create mode 100644 source/_posts/2026/2026.2/remove-to-gitea.md 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 = + '
' + + '
' + + '
搜索服务加载失败
' + + '
' + + '

依赖库未能正确加载,请检查以下配置:

' + + '
    ' + + '
  1. 确认已在 _config.butterfly.yml 中正确引入依赖
  2. ' + + '
  3. 检查 JS 文件加载顺序(先 instantsearch.js,再 adapter)
  4. ' + + '
  5. 尝试更换 CDN 或使用本地文件
  6. ' + + '
  7. 打开浏览器控制台查看详细错误信息
  8. ' + + '
' + + '
' + + '
' + + '' + + '
' + + '
'; + } + + // ============================================================================ + // 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