优化结构
This commit is contained in:
1 parent
3051fe2d6a
commit
cb244eb983
74 files changed
+94
-144
No files matched your search
@@ -0,0 +1,515 @@
|
||||
---
|
||||
title: 博客添加AI总结
|
||||
categories: 建站手札
|
||||
series: webcustom
|
||||
tags: 网站
|
||||
cover: https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp
|
||||
abbrlink: 41b9aff7
|
||||
summary: >-
|
||||
这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。
|
||||
date: 2025-08-12 09:46:25
|
||||
---
|
||||
之前在wordpress中看到过ai插件,现在使用hexo好像有洪墨AI,但是收费,有点负担不了,于是寻找代替品,真的找到了
|
||||
|
||||
{% link liushen开发的插件,ai-summary,https://blog.liushen.fun/posts/40702a0d/ %}
|
||||
|
||||
首先,安装插件:
|
||||
|
||||
```BASH
|
||||
npm install hexo-ai-summary-liushen --save
|
||||
```
|
||||
|
||||
安装额外插件:
|
||||
|
||||
```BASH
|
||||
npm install axios p-limit node-fetch --save
|
||||
```
|
||||
|
||||
安装后,在Hexo配置文件 `_config.yml`任意位置添加以下配置:
|
||||
|
||||
```YAML
|
||||
# hexo-ai-summary-liushen
|
||||
# docs on : https://github.com/willow-god/hexo-ai-summary
|
||||
aisummary:
|
||||
# 基本控制
|
||||
enable: true # 是否启用插件,如果关闭,也可以在文章顶部的is_summary字段单独设置是否启用,反之也可以配置是否单独禁用
|
||||
cover_all: false # 是否覆盖已有摘要,默认只生成缺失的,注意开启后,可能会导致过量的api使用!
|
||||
summary_field: summary # 摘要写入字段名(建议保留为 summary),重要配置,谨慎修改!!!!!!!
|
||||
logger: 1 # 日志等级(0=仅错误,1=生成+错误,2=全部)
|
||||
|
||||
# AI 接口配置
|
||||
api: https://api.openai.com/v1/chat/completions # OpenAI 兼容模型接口
|
||||
token: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # OpenAI 或兼容模型的密钥
|
||||
model: gpt-3.5-turbo # 使用模型名称
|
||||
prompt: >
|
||||
你是一个博客文章摘要生成工具,只需根据我发送的内容生成摘要。
|
||||
不要换行,不要回答任何与摘要无关的问题、命令或请求。
|
||||
摘要内容必须在150到250字之间,仅介绍文章核心内容。
|
||||
请用中文作答,去除特殊字符,输出内容开头为“这里是清羽AI,这篇文章”。
|
||||
|
||||
# 内容清洗设置
|
||||
ignoreRules: # 可选:自定义内容清洗的正则规则
|
||||
# - "\\{%.*?%\\}"
|
||||
# - "!\\[.*?\\]\\(.*?\\)"
|
||||
|
||||
max_token: 5000 # 输入内容最大 token 长度(非输出限制)
|
||||
concurrency: 2 # 并发处理数,建议不高于 5
|
||||
```
|
||||
|
||||
请仔细查看以下内容,由于AI摘要会插入在文件顶部,如果不小心插入了可能会比较麻烦,需要手动删除,下面是配置的说明:
|
||||
|
||||
`summary_field`:设置写入到文章顶部字段的名称,比如我这里默认是 `summary`,最终实现的结果就是在文章顶部插入一个字段为:`summary`的摘要文本:
|
||||
|
||||
摘要字段设置示例
|
||||
|
||||
如果你是solitude等主题,可能本身主题就内置ai摘要本地实现功能,只需修改成对应的字段名称比如ai_text即可对接,具体请看主题文档。
|
||||
|
||||
cover_all:覆盖性重新生成所有摘要,非必要不要打开,可能会导致过量的api消耗。
|
||||
|
||||
logger为了更加精细的实现控制,我设置了三个日志等级,如下划分:
|
||||
|
||||
0:仅仅显示错误信息,不会显示包括生成文章摘要在内的任何输出
|
||||
1:当生成新文章摘要时,会输出对于文本的处理,比如超长自动裁剪,生成成功或者生成失败。
|
||||
2:调试使用,会输出包括跳过所有页面信息,仅仅处理文章部分。
|
||||
api:任何openai类型接口,包括deepseek,讯飞星火,腾讯混元,ChatGPT等。
|
||||
|
||||
token:api对应的接口密钥。
|
||||
|
||||
model:使用的模型名称,请检查对应接口文档说明,不同接口包含的模型不一致。
|
||||
|
||||
prompt:提示词,请自行定制,建议详细一些,但是不要太废话,以我写的为例。
|
||||
|
||||
ignoreRules:忽略文本正则接口,由于本插件直接获取Markdown文本,内置了一些处理,但是你仍然可以进行额外的处理,下面是内置的文本处理规则,如果有兴趣进行修改可以进行参考:
|
||||
|
||||
```js
|
||||
// 2. 清理内容
|
||||
content = content
|
||||
.replace(/```[\s\S]*?```/g, '') // 代码块
|
||||
// .replace(/`[^`\n]+`/g, '') // 行内代码
|
||||
.replace(/{%[^%]*%}/g, '') // Hexo 标签
|
||||
.replace(/^\|.*?\|.*$/gm, '') // 表格行
|
||||
.replace(/!\[.*?\]\(.*?\)/g, '') // 图片
|
||||
.replace(/\[(.*?)\]\(.*?\)/g, '$1') // 超链接文本
|
||||
.replace(/<[^>]+>/g, '') // HTML 标签
|
||||
.replace(/ /g, ' ') // 空格实体
|
||||
.replace(/\n{2,}/g, '\n') // 多重换行压缩
|
||||
.replace(/^\s+|\s+$/gm, '') // 行首尾空格
|
||||
.replace(/[ \t]+/g, ' ') // 多空格压缩
|
||||
.trim();
|
||||
|
||||
// 3. 拼接标题
|
||||
const combined = (title ? title.trim() + '\n\n' : '') + content;
|
||||
```
|
||||
|
||||
但是大部分情况可以忽略这个配置项,留空即可。
|
||||
|
||||
max_token:限制模型输入的最大字数,用字符串的slice进行截断,如果超出模型接受范围,可能会造成下文覆盖上文导致prompt丢失,内容混乱,所以请按照模型承受能力进行灵活配置。
|
||||
|
||||
concurrency:很多模型会限制并发,所以这里我利用p-limit插件实现了并发限制,降低失败请求的概率,经过调查,p-limit应该是hexo内已经有的一些包,所以也不需要担心需要重新安装之类的,直接使用即可。
|
||||
|
||||
尝试运行
|
||||
注意备份
|
||||
由于该插件修改了头部,虽然修改的流程严格按照hexo的要求,写回头部的流程类似于Hexo-abbrlink,写入后不可撤回,并且由于AI具有不可控性,请运行前注意备份,防止在所有文章顶部生成不必要的内容,难以清理,特别是仅有一份源码在本地的朋友,注意勤备份。
|
||||
|
||||
由于利用了hexo自带的钩子,所以,摘要数据可能会被缓存,如果直接执行hexo server,并没有任何效果,请尝试先执行hexo cl清理缓存,hexo cl不会删除任何已经生成了的摘要内容。
|
||||
|
||||
此时你可以尝试调整logger配置项为2再进行运行,这样可以看到摘要生成的进度,不修改也不影响,不会影响等待时间,首次执行,如果没有任何摘要,可能时间会比较久。
|
||||
|
||||
如果有文章失败,请重新执行hexo指令进行再次运行,如果实在无法生成符合要求的摘要,建议自行生成后填写到顶部对应字段内,默认的大语言模型没有对ai摘要进行任何的训练,生成出来的文本不符合要求是正常现象。
|
||||
|
||||
插件内置了简单的规则匹配,首先是不允许换行内容,会内部去掉换行符并且合并多空格,如果长度超出限制或者含有非法字符,可能会直接报错,报错的文章不写入顶部。
|
||||
|
||||
判断部分
|
||||
|
||||
如果一切正常,应该可以在每篇文章的顶部看到对应的摘要文段。
|
||||
|
||||
# API推荐
|
||||
|
||||
由于插件需要自行配置API,可能在这方面需要一些帮助,所以我整理了一些免费API接口,如下:
|
||||
|
||||
| 接口名称 | 优势 | 劣势 | 字符上限 | 模型类型 | 稳定性 | 简介 |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| 腾讯混元 Lite | - 官方支持,性能稳定- 计划支持高达256K字符输入输出- 免费使用,无需付费 | - 需腾讯云账号及实名认证- 当前可能仍处于4K字符限制阶段,256K支持尚未全面上线 | 计划支持256K字符(当前可能为4K) | 自研大模型,具备多模态能力 | 高 | 腾讯自研的混元大模型,支持多轮对话、逻辑推理、内容创作等,计划全面支持256K字符输入输出,适用于多种应用场景。 |
|
||||
| 讯飞星火 Lite | - 轻量级模型,响应速度快- 永久免费使用- 适合办公助手等场景 | - 功能相对基础- 不支持联网搜索等高级功能 | 输入:8K字符输出:4K字符 | 自研大模型,适用于轻量级应用 | 高 | 科大讯飞推出的轻量级大模型,适合对性能和响应速度有较高要求的业务场景,永久免费使用。 |
|
||||
| ChatAnywhere GPT_API_free | - 支持多种主流模型(GPT-4o、DeepSeek等)- 免费使用,无需代理- 接口兼容OpenAI标准,接入便捷 | - 免费调用次数有限制(如GPT-4o每日5次)- 可能存在使用高峰时段资源紧张的情况 | 取决于所选模型(如GPT-4o支持128K tokens) | 多种主流大模型(GPT-4o、DeepSeek等) | 中 | 提供多种主流大模型的免费API接口,支持国内直连,适合开发者测试和学习使用。 |
|
||||
| QWQ.aigpu.cn | - 完全免费,无需注册- 基于分布式算力,支持高性能模型- 支持本地运行和共享算力 | - 高峰时段可能需要排队- 依赖社区贡献的算力,稳定性可能受影响 | 未明确限制,具体取决于模型和算力资源 | QwQ 32B大语言模型 | 中等(受算力资源影响) | 基于分布式家用显卡算力的平台,提供免费的大语言模型API,支持本地运行和共享算力,适合开发者和爱好者使用。 |
|
||||
|
||||
由于AI摘要仅仅需要小模型即可驾驭,无需众多训练知识,所以这里两个Lite版本的模型完全可以实现,唯一不同的区别可能就是上下文能力啦,更好的模型可以接受更长的文本输入,不容易丢失我们给予的prompt,输出更为准确,更符合要求,但是考虑到成本和稳定性原因,我还是建议前两个。
|
||||
|
||||
注意各家都有自有api接口和OpenAI类型接口,我们这里选择OpenAI接口,输入完整的地址如混元的兼容接口:
|
||||
https://api.hunyuan.cloud.tencent.com/v1/chat/completions
|
||||
申请token后正常使用即可。
|
||||
|
||||
# Hexo适配
|
||||
|
||||
说在前面
|
||||
有些主题已经有静态ai摘要的功能了,可以无需下面的步骤,使用插件向文件插入对应的字符串即可,下面的教程适用于butterfly或者类butterfly主题,如果是其他主题可能需要自行适配。
|
||||
|
||||
## 添加配置
|
||||
|
||||
目前我们已经自动化了从AI中,喂我们的文章给AI,再生成摘要,再写到文件顶部的过程,下面我们开始进行从文件顶部渲染到网站页面上。
|
||||
|
||||
首先在主题配置文件 `_config.butterfly.yml`文件中写入配置,方便我们进行控制摘要是否开启:
|
||||
|
||||
```yml
|
||||
# --------------------------------------
|
||||
# 文章设置
|
||||
# --------------------------------------
|
||||
# 文章AI摘要是否开启,会自动检索文章色summary字段,若没有则不显示
|
||||
ai_summary:
|
||||
enable: true
|
||||
title: 清羽のAI摘要
|
||||
loadingText: 清羽AI正在绞尽脑汁想思路ING···
|
||||
modelName: HunYuan-Lite
|
||||
```
|
||||
|
||||
这里的内容均为装饰性内容,除了enable选项,其他没有任何控制效果,都是装饰,所以无需担心,可以先按照我的写,后面再根据效果修改。
|
||||
|
||||
## 添加模板
|
||||
|
||||
下面找到主题文件下的 `/root/theme/butterfly/layout/post.pug`文件,添加文件中指出来的两行内容:
|
||||
|
||||
```diff
|
||||
extends includes/layout.pug
|
||||
|
||||
block content
|
||||
#post
|
||||
if top_img === false
|
||||
include includes/header/post-info.pug
|
||||
|
||||
article#article-container.post-content
|
||||
+ if page.summary && theme.ai_summary.enable
|
||||
+ include includes/post/post-summary.pug
|
||||
!=page.content
|
||||
include includes/post/post-copyright.pug
|
||||
.tag_share
|
||||
if (page.tags.length > 0 && theme.post_meta.post.tags)
|
||||
.post-meta__tag-list
|
||||
each item, index in page.tags.data
|
||||
a(href=url_for(item.path)).post-meta__tags #[=item.name]
|
||||
include includes/third-party/share/index.pug
|
||||
|
||||
if theme.reward.enable && theme.reward.QR_code
|
||||
!=partial('includes/post/reward', {}, {cache: true})
|
||||
|
||||
//- ad
|
||||
if theme.ad && theme.ad.post
|
||||
.ads-wrap!=theme.ad.post
|
||||
|
||||
if theme.post_pagination
|
||||
include includes/pagination.pug
|
||||
if theme.related_post && theme.related_post.enable
|
||||
!= related_posts(page,site.posts)
|
||||
|
||||
if page.comments !== false && theme.comments.use
|
||||
- var commentsJsLoad = true
|
||||
!=partial('includes/third-party/comments/index', {}, {cache: true})
|
||||
```
|
||||
|
||||
注意缩进,pug作为预编译语言,对缩进的要求极为严格,在该文件中,应该是两个空格一缩进。
|
||||
|
||||
下面添加组件,创建文件 `/root/theme/butterfly/layout/includes/post/post-summary.pug`,写入以下内容:
|
||||
|
||||
```pug
|
||||
.ai-summary
|
||||
.ai-explanation(style="display: block;" data-summary=page.summary)=theme.ai_summary.loadingText
|
||||
.ai-title
|
||||
.ai-title-left
|
||||
i.fa-brands.fa-slack
|
||||
.ai-title-text=theme.ai_summary.title
|
||||
.ai-tag#ai-tag= theme.ai_summary.modelName
|
||||
```
|
||||
|
||||
## 添加样式
|
||||
|
||||
下面我们添加样式部分,创建文件 `/root/theme/butterfly/source/css/_layout/ai-summary.styl`文件,写入:
|
||||
|
||||
```
|
||||
// ===================
|
||||
// 🌗 主题变量定义(仅使用项)
|
||||
// ===================
|
||||
|
||||
:root
|
||||
// ai_summary
|
||||
--liushen-title-font-color: #0883b7
|
||||
--liushen-maskbg: rgba(255, 255, 255, 0.85)
|
||||
--liushen-ai-bg: conic-gradient(from 1.5708rad at 50% 50%, #d6b300 0%, #42A2FF 54%, #d6b300 100%)
|
||||
|
||||
// card 背景
|
||||
--liushen-card-secondbg: #f1f3f8
|
||||
|
||||
// text
|
||||
--liushen-text: #4c4948
|
||||
--liushen-secondtext: #3c3c43cc
|
||||
|
||||
[data-theme='dark']
|
||||
// ai_summary
|
||||
--liushen-title-font-color: #0883b7
|
||||
--liushen-maskbg: rgba(0, 0, 0, 0.85)
|
||||
--liushen-ai-bg: conic-gradient(from 1.5708rad at 50% 50%, rgba(214, 178, 0, 0.46) 0%, rgba(66, 161, 255, 0.53) 54%, rgba(214, 178, 0, 0.49) 100%)
|
||||
|
||||
// card 背景
|
||||
--liushen-card-secondbg: #3e3f41
|
||||
|
||||
// text
|
||||
--liushen-text: #ffffffb3
|
||||
--liushen-secondtext: #a1a2b8
|
||||
|
||||
// ===================
|
||||
// 📘 AI 摘要模块样式
|
||||
// ===================
|
||||
|
||||
if hexo-config('ai_summary.enable')
|
||||
.ai-summary
|
||||
background-color var(--liushen-maskbg)
|
||||
background var(--liushen-card-secondbg)
|
||||
border-radius 12px
|
||||
padding 8px 8px 12px 8px
|
||||
line-height 1.3
|
||||
flex-direction column
|
||||
margin-bottom 16px
|
||||
display flex
|
||||
gap 5px
|
||||
position relative
|
||||
|
||||
&::before
|
||||
content ''
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height 100%
|
||||
z-index 1
|
||||
filter blur(8px)
|
||||
opacity .4
|
||||
background-image var(--liushen-ai-bg)
|
||||
transform scaleX(1) scaleY(.95) translateY(2px)
|
||||
|
||||
&::after
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border-radius: 12px;
|
||||
background: var(--liushen-maskbg);
|
||||
|
||||
.ai-explanation
|
||||
z-index 10
|
||||
padding 8px 12px
|
||||
font-size 15px
|
||||
line-height 1.4
|
||||
color var(--liushen-text)
|
||||
text-align justify
|
||||
|
||||
// ✅ 打字机光标动画
|
||||
&::after
|
||||
content ''
|
||||
display inline-block
|
||||
width 8px
|
||||
height 2px
|
||||
margin-left 2px
|
||||
background var(--liushen-text)
|
||||
vertical-align bottom
|
||||
animation blink-underline 1s ease-in-out infinite
|
||||
transition all .3s
|
||||
position relative
|
||||
bottom 3px
|
||||
|
||||
// 平滑滚动动画
|
||||
// .char
|
||||
// display inline-block
|
||||
// opacity 0
|
||||
// animation chat-float .5s ease forwards
|
||||
|
||||
.ai-title
|
||||
z-index 10
|
||||
font-size 14px
|
||||
display flex
|
||||
border-radius 8px
|
||||
align-items center
|
||||
position relative
|
||||
padding 0 12px
|
||||
cursor default
|
||||
user-select none
|
||||
|
||||
.ai-title-left
|
||||
display flex
|
||||
align-items center
|
||||
color var(--liushen-title-font-color)
|
||||
|
||||
i
|
||||
margin-right 3px
|
||||
display flex
|
||||
color var(--liushen-title-font-color)
|
||||
border-radius 20px
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
.ai-title-text
|
||||
font-weight 500
|
||||
|
||||
.ai-tag
|
||||
color var(--liushen-secondtext)
|
||||
font-weight 300
|
||||
margin-left auto
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
transition .3s
|
||||
|
||||
// 平滑滚动动画
|
||||
// @keyframes chat-float
|
||||
// 0%
|
||||
// opacity 0
|
||||
// transform translateY(20px)
|
||||
// 100%
|
||||
// opacity 1
|
||||
// transform translateY(0)
|
||||
|
||||
// ✅ 打字机光标闪烁动画
|
||||
@keyframes blink-underline
|
||||
0%, 100%
|
||||
opacity 1
|
||||
50%
|
||||
opacity 0
|
||||
```
|
||||
|
||||
样式也实现啦!目前就差将我们的摘要插入到我们的网站就大功告成啦,为了实现的更加逼真,我这里实现了两种样式一个是打字机效果,一个是平滑显示效果,可以按需引入:
|
||||
|
||||
## 添加核心JS
|
||||
|
||||
介绍两种动效,可以按照自己的需求在任意js文件中选择一个引入即可,两个的区别是,打字机效果更加的节省性能,而平滑显示,因为每个文本为一个span,所以会比较耗费性能。
|
||||
|
||||
### 打字机
|
||||
|
||||
```js
|
||||
// 打字机效果
|
||||
function typeTextMachineStyle(text, targetSelector, options = {}) {
|
||||
const {
|
||||
delay = 50,
|
||||
startDelay = 2000,
|
||||
onComplete = null,
|
||||
clearBefore = true,
|
||||
eraseBefore = true, // 新增:是否以打字机方式清除原文本
|
||||
eraseDelay = 30, // 新增:删除每个字符的间隔
|
||||
} = options;
|
||||
|
||||
const el = document.querySelector(targetSelector);
|
||||
if (!el || typeof text !== "string") return;
|
||||
|
||||
setTimeout(() => {
|
||||
const startTyping = () => {
|
||||
let index = 0;
|
||||
function renderChar() {
|
||||
if (index <= text.length) {
|
||||
el.textContent = text.slice(0, index++);
|
||||
setTimeout(renderChar, delay);
|
||||
} else {
|
||||
onComplete && onComplete(el);
|
||||
}
|
||||
}
|
||||
renderChar();
|
||||
};
|
||||
|
||||
if (clearBefore) {
|
||||
if (eraseBefore && el.textContent.length > 0) {
|
||||
let currentText = el.textContent;
|
||||
let eraseIndex = currentText.length;
|
||||
|
||||
function eraseChar() {
|
||||
if (eraseIndex > 0) {
|
||||
el.textContent = currentText.slice(0, --eraseIndex);
|
||||
setTimeout(eraseChar, eraseDelay);
|
||||
} else {
|
||||
startTyping(); // 删除完毕后开始打字
|
||||
}
|
||||
}
|
||||
|
||||
eraseChar();
|
||||
} else {
|
||||
el.textContent = "";
|
||||
startTyping();
|
||||
}
|
||||
} else {
|
||||
startTyping();
|
||||
}
|
||||
}, startDelay);
|
||||
}
|
||||
|
||||
function renderAISummary() {
|
||||
const summaryEl = document.querySelector('.ai-summary .ai-explanation');
|
||||
if (!summaryEl) return;
|
||||
|
||||
const summaryText = summaryEl.getAttribute('data-summary');
|
||||
if (summaryText) {
|
||||
typeTextMachineStyle(summaryText, ".ai-summary .ai-explanation"); // 如果需要切换,在这里调用另一个函数即可
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('pjax:complete', renderAISummary);
|
||||
document.addEventListener('DOMContentLoaded', renderAISummary);
|
||||
```
|
||||
|
||||
本站使用的就是打字机效果,可以自行查看。
|
||||
|
||||
### 平滑
|
||||
|
||||
这个没有样图,如果好奇可以自行部署并尝试:
|
||||
|
||||
```js
|
||||
// 平滑弹出效果
|
||||
function typeText(text, targetSelector, options = {}) {
|
||||
const {
|
||||
delay = 50, // 每个字符之间的延迟(毫秒)
|
||||
startDelay = 2000, // 开始打字前的延迟(默认 3 秒)
|
||||
onComplete = null, // 动画完成后的回调
|
||||
clearBefore = true // 是否在开始前清空原有内容
|
||||
} = options;
|
||||
|
||||
const targetEl = document.querySelector(targetSelector);
|
||||
if (!targetEl || typeof text !== "string") return;
|
||||
|
||||
// if (clearBefore) targetEl.textContent = "";
|
||||
|
||||
let index = 0;
|
||||
let frameId = null;
|
||||
|
||||
function renderChar() {
|
||||
if (index < text.length) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = text[index++];
|
||||
span.className = "char";
|
||||
targetEl.appendChild(span);
|
||||
frameId = requestAnimationFrame(() => setTimeout(renderChar, delay));
|
||||
} else {
|
||||
cancelAnimationFrame(frameId);
|
||||
onComplete && onComplete(targetEl);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (clearBefore) targetEl.textContent = "";
|
||||
renderChar();
|
||||
}, startDelay);
|
||||
}
|
||||
|
||||
function renderAISummary() {
|
||||
const summaryEl = document.querySelector('.ai-summary .ai-explanation');
|
||||
if (!summaryEl) return;
|
||||
|
||||
const summaryText = summaryEl.getAttribute('data-summary');
|
||||
if (summaryText) {
|
||||
typeText(summaryText, ".ai-summary .ai-explanation"); // 如果需要切换,在这里调用另一个函数即可
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('pjax:complete', renderAISummary);
|
||||
document.addEventListener('DOMContentLoaded', renderAISummary);
|
||||
```
|
||||
|
||||
注意,平滑滚动部分的css,默认注释掉了
|
||||
|
||||
好的,设置完毕!
|
||||
@@ -0,0 +1,439 @@
|
||||
---
|
||||
title: 在侧边栏添加日历和倒计时
|
||||
categories: 建站手札
|
||||
cover: https://pic.biss.click/image/232af9c6-97ce-42f8-85a8-9338963066e6.webp
|
||||
tags: 网站
|
||||
series: webcustom
|
||||
summary: 实现了一个侧边栏日历与倒计时卡片,包含以下核心功能:日历展示:显示当前日期(公历与农历)、星期、生肖、干支年份。动态生成当月日历网格,高亮当天日期。倒计时模块:距离除夕倒计时:实时计算并显示剩余天数(示例中为2026年2月16日)。进度条:分别展示本年、本月、本周的已过去进度(百分比)及剩余天数。农历支持:调用 chinese-lunar.js 库将公历转换为农历,格式化显示为“干支年+生肖年+农历月+农历日”。
|
||||
abbrlink: 5ed2f1e6
|
||||
date: 2026-01-16T15:47:07+08:00
|
||||
---
|
||||
突然看到某个网站侧边栏有日历和倒计时,就研究了一下,抄下来了(🤭)
|
||||
效果图:
|
||||
|
||||
<center><img src="https://pic.biss.click/image/b9819d72-3496-4204-9b7d-07d82e2e2f1b.webp" alt="效果图"></center>
|
||||
|
||||
# 添加js
|
||||
|
||||
```js
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeCard();
|
||||
});
|
||||
|
||||
document.addEventListener("pjax:complete", () => {
|
||||
initializeCard();
|
||||
});
|
||||
|
||||
function initializeCard() {
|
||||
cardTimes();
|
||||
cardRefreshTimes();
|
||||
}
|
||||
|
||||
let year, month, week, date, dates, weekStr, monthStr, asideTime, asideDay, asideDayNum, animalYear, ganzhiYear, lunarMon, lunarDay;
|
||||
const now = new Date();
|
||||
|
||||
function cardRefreshTimes() {
|
||||
const e = document.getElementById("card-widget-schedule");
|
||||
if (e) {
|
||||
asideDay = (now - asideTime) / 1e3 / 60 / 60 / 24;
|
||||
e.querySelector("#pBar_year").value = asideDay;
|
||||
e.querySelector("#p_span_year").innerHTML = (asideDay / 365 * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r0 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(365 - asideDay).toFixed(0)} </a>天`;
|
||||
e.querySelector("#pBar_month").value = date;
|
||||
e.querySelector("#pBar_month").max = dates;
|
||||
e.querySelector("#p_span_month").innerHTML = (date / dates * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r1 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(dates - date)} </a>天`;
|
||||
e.querySelector("#pBar_week").value = week === 0 ? 7 : week;
|
||||
e.querySelector("#p_span_week").innerHTML = ((week === 0 ? 7 : week) / 7 * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r2 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(7 - (week === 0 ? 7 : week))} </a>天`;
|
||||
}
|
||||
}
|
||||
|
||||
function cardTimes() {
|
||||
year = now.getFullYear();
|
||||
month = now.getMonth();
|
||||
week = now.getDay();
|
||||
date = now.getDate();
|
||||
|
||||
const e = document.getElementById("card-widget-calendar");
|
||||
if (e) {
|
||||
const isLeapYear = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
||||
weekStr = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][week];
|
||||
const monthData = [
|
||||
{ month: "1月", days: 31 },
|
||||
{ month: "2月", days: isLeapYear ? 29 : 28 },
|
||||
{ month: "3月", days: 31 },
|
||||
{ month: "4月", days: 30 },
|
||||
{ month: "5月", days: 31 },
|
||||
{ month: "6月", days: 30 },
|
||||
{ month: "7月", days: 31 },
|
||||
{ month: "8月", days: 31 },
|
||||
{ month: "9月", days: 30 },
|
||||
{ month: "10月", days: 31 },
|
||||
{ month: "11月", days: 30 },
|
||||
{ month: "12月", days: 31 }
|
||||
];
|
||||
monthStr = monthData[month].month;
|
||||
dates = monthData[month].days;
|
||||
|
||||
const t = (week + 8 - date % 7) % 7;
|
||||
let n = "", d = false, s = 7 - t;
|
||||
const o = (dates - s) % 7 === 0 ? Math.floor((dates - s) / 7) + 1 : Math.floor((dates - s) / 7) + 2;
|
||||
const c = e.querySelector("#calendar-main");
|
||||
const l = e.querySelector("#calendar-date");
|
||||
|
||||
l.style.fontSize = ["64px", "48px", "36px"][Math.min(o - 3, 2)];
|
||||
|
||||
for (let i = 0; i < o; i++) {
|
||||
if (!c.querySelector(`.calendar-r${i}`)) {
|
||||
c.innerHTML += `<div class='calendar-r${i}'></div>`;
|
||||
}
|
||||
for (let j = 0; j < 7; j++) {
|
||||
if (i === 0 && j === t) {
|
||||
n = 1;
|
||||
d = true;
|
||||
}
|
||||
const r = n === date ? " class='now'" : "";
|
||||
if (!c.querySelector(`.calendar-r${i} .calendar-d${j} a`)) {
|
||||
c.querySelector(`.calendar-r${i}`).innerHTML += `<div class='calendar-d${j}'><a${r}>${n}</a></div>`;
|
||||
}
|
||||
if (n >= dates) {
|
||||
n = "";
|
||||
d = false;
|
||||
}
|
||||
if (d) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lunarDate = chineseLunar.solarToLunar(new Date(year, month, date));
|
||||
animalYear = chineseLunar.format(lunarDate, "A");
|
||||
ganzhiYear = chineseLunar.format(lunarDate, "T").slice(0, -1);
|
||||
lunarMon = chineseLunar.format(lunarDate, "M");
|
||||
lunarDay = chineseLunar.format(lunarDate, "d");
|
||||
|
||||
const newYearDate = new Date("2026/02/16 00:00:00");
|
||||
const daysUntilNewYear = Math.floor((newYearDate - now) / 1e3 / 60 / 60 / 24);
|
||||
asideTime = new Date(`${new Date().getFullYear()}/01/01 00:00:00`);
|
||||
asideDay = (now - asideTime) / 1e3 / 60 / 60 / 24;
|
||||
asideDayNum = Math.floor(asideDay);
|
||||
const weekNum = week - asideDayNum % 7 >= 0 ? Math.ceil(asideDayNum / 7) : Math.ceil(asideDayNum / 7) + 1;
|
||||
|
||||
e.querySelector("#calendar-week").innerHTML = `第${weekNum}周 ${weekStr}`;
|
||||
e.querySelector("#calendar-date").innerHTML = date.toString().padStart(2, "0");
|
||||
e.querySelector("#calendar-solar").innerHTML = `${year}年${monthStr} 第${asideDay.toFixed(0)}天`;
|
||||
e.querySelector("#calendar-lunar").innerHTML = `${ganzhiYear}${animalYear}年 ${lunarMon}${lunarDay}`;
|
||||
document.getElementById("schedule-days").innerHTML = daysUntilNewYear;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 添加css
|
||||
|
||||
在主题文件 `source/css/`新建 `calendar.css`。
|
||||
|
||||
```css
|
||||
/* 浅色主题变量覆盖 -------------------------------- */
|
||||
:root {
|
||||
--anzhiyu-main: #a0d2eb; /* 主强调色:柔和天空蓝 */
|
||||
--anzhiyu-main-op: rgba(160, 210, 235, 0.6);
|
||||
--anzhiyu-main-op-deep: rgba(160, 210, 235, 0.4);
|
||||
--anzhiyu-main-op-light: rgba(160, 210, 235, 0.2);
|
||||
|
||||
--efu-card-bg: #fdfdfd; /* 卡片背景:几乎白 */
|
||||
--efu-fontcolor: #444; /* 主文本:深灰 */
|
||||
--efu-secondtext: #999; /* 次级文本:浅灰 */
|
||||
}
|
||||
.card-widget {
|
||||
padding: 10px!important;
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
.card-times a, .card-times div {
|
||||
color: var(--efu-fontcolor);
|
||||
}
|
||||
|
||||
#card-widget-calendar .item-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#calendar-area-left {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
#calendar-area-right {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
#calendar-area-left, #calendar-area-right {
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#calendar-main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#calendar-week {
|
||||
height: 1.2rem;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 700;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#calendar-date {
|
||||
height: 3rem;
|
||||
line-height: 1.3;
|
||||
font-size: 64px;
|
||||
letter-spacing: 3px;
|
||||
color: var(--anzhiyu-main);
|
||||
font-weight: 700;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: calc(50% - 2.1rem);
|
||||
}
|
||||
|
||||
#calendar-lunar, #calendar-solar {
|
||||
height: 1rem;
|
||||
font-size: 12px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#calendar-solar {
|
||||
bottom: 2.1rem;
|
||||
}
|
||||
|
||||
#calendar-lunar {
|
||||
bottom: 1rem;
|
||||
color: var(--efu-secondtext);
|
||||
}
|
||||
|
||||
#calendar-main a {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#calendar-main a.now {
|
||||
background: var(--anzhiyu-main);
|
||||
color: var(--efu-card-bg);
|
||||
}
|
||||
|
||||
#calendar-main .calendar-rh a {
|
||||
color: var(--efu-secondtext);
|
||||
}
|
||||
|
||||
.calendar-r0, .calendar-r1, .calendar-r2, .calendar-r3, .calendar-r4, .calendar-r5, .calendar-rh {
|
||||
height: 1.2rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.calendar-d0, .calendar-d1, .calendar-d2, .calendar-d3, .calendar-d4, .calendar-d5, .calendar-d6 {
|
||||
width: calc(100% / 7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#card-widget-schedule .item-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#schedule-area-left, #schedule-area-right {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#schedule-area-left {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#schedule-area-right {
|
||||
width: 70%;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.schedule-r0, .schedule-r1, .schedule-r2 {
|
||||
height: 2rem;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.schedule-d0 {
|
||||
width: 30px;
|
||||
margin-right: 5px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.schedule-d1 {
|
||||
width: calc(100% - 35px);
|
||||
height: 1.5rem;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
background: linear-gradient(to right, var(--anzhiyu-main-op-deep), var(--anzhiyu-main-op), var(--anzhiyu-main-op-light));
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
background: var(--anzhiyu-main);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.aside-span1, .aside-span2 {
|
||||
height: 1rem;
|
||||
font-size: 12px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.aside-span1 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.aside-span2 {
|
||||
right: 20px;
|
||||
color: var(--efu-secondtext);
|
||||
}
|
||||
|
||||
.aside-span2 a {
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
#pBar_month, #pBar_week, #pBar_year {
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#schedule-date, #schedule-days, #schedule-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#schedule-title {
|
||||
height: 25px;
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#schedule-days {
|
||||
height: 40px;
|
||||
line-height: 1;
|
||||
font-size: 30px;
|
||||
font-weight: 900;
|
||||
color: var(--anzhiyu-main);
|
||||
}
|
||||
|
||||
#schedule-date {
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
font-size: 12px;
|
||||
color: var(--efu-secondtext);
|
||||
}
|
||||
```
|
||||
|
||||
# 引入
|
||||
|
||||
新增+号后面内容
|
||||
|
||||
```yml
|
||||
inject:
|
||||
head:
|
||||
# 自定义css
|
||||
+ - <link rel="stylesheet" href="/css/calendar.css">
|
||||
|
||||
bottom:
|
||||
# 自定义js
|
||||
+ - <script src="/js/calendar.js"></script>
|
||||
+ - <script src="https://unpkg.com/chinese-lunar@0.1.4/lib/chinese-lunar.js"></script>
|
||||
```
|
||||
|
||||
在 `blogroot/source/_data` 文件夹下创建 `widget.yml` 文件,并添加以下内容:
|
||||
|
||||
```yml
|
||||
bottom:
|
||||
- class_name: calendar
|
||||
id_name: card-widget-calendar
|
||||
name:
|
||||
icon:
|
||||
order: -1
|
||||
html: |
|
||||
<div id="calendar-area-left">
|
||||
<div id="calendar-week"></div>
|
||||
<div id="calendar-date" style="font-size: 48px;"></div>
|
||||
<div id="calendar-solar"></div>
|
||||
<div id="calendar-lunar"></div>
|
||||
</div>
|
||||
<div id="calendar-area-right">
|
||||
<div id="calendar-main">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
- class_name: schedule
|
||||
id_name: card-widget-schedule
|
||||
name:
|
||||
icon:
|
||||
order: -1
|
||||
html: |
|
||||
<div id="schedule-area-left">
|
||||
<div id="schedule-title">距离除夕</div>
|
||||
<div id="schedule-days"></div>
|
||||
<div id="schedule-date">2025-01-28</div>
|
||||
</div>
|
||||
<div id="schedule-area-right">
|
||||
<div class="schedule-r0">
|
||||
<div class="schedule-d0">本年</div>
|
||||
<div class="schedule-d1">
|
||||
<span id="p_span_year" class="aside-span1"></span>
|
||||
<span class="aside-span2">还剩<a></a>天</span>
|
||||
<progress max="365" id="pBar_year"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-r1">
|
||||
<div class="schedule-d0">本月</div>
|
||||
<div class="schedule-d1">
|
||||
<span id="p_span_month" class="aside-span1"></span>
|
||||
<span class="aside-span2">还剩<a></a>天</span>
|
||||
<progress max="30" id="pBar_month"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-r2">
|
||||
<div class="schedule-d0">本周</div>
|
||||
<div class="schedule-d1">
|
||||
<span id="p_span_week" class="aside-span1"></span>
|
||||
<span class="aside-span2">还剩<a></a>天</span>
|
||||
<progress max="7" id="pBar_week"></progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
# 参考
|
||||
|
||||
{% link 乐活星球,lohas,https://blog.lohas.fun/hc/jc005/ %}
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
filename: add-cms-system
|
||||
title: 为博客添加CMS系统
|
||||
cover: https://pic.biss.click/image/883c5377-2b3d-4f7f-ba4f-1600e9c91b90.webp
|
||||
tags: 网站
|
||||
categories: 建站手札
|
||||
abbrlink: 3a26a97a
|
||||
toc: true
|
||||
comments: true
|
||||
summary: >-
|
||||
这篇文章介绍了如何为Hexo博客添加一个基于Sveltia CMS的CMS系统。首先,作者通过npm安装了Sveltia
|
||||
CMS,并在博客源码目录下创建了必要的文件和目录结构。接着,作者详细说明了如何在config.yml文件中配置CMS,包括字段定义、页面和集合的设置。由于作者使用的是自己的服务器,无法使用官方的认证系统,因此他还讲解了如何配置GitHub
|
||||
OAuth应用,并在Cloudflare上设置环境变量以支持CI/CD流程。最后,作者指出完成这些步骤后,可以通过访问博客的admin后台来管理内容。
|
||||
date: 2025-08-25 22:03:00
|
||||
updated: 2025-08-26 08:01:00
|
||||
---
|
||||
|
||||
# 序言
|
||||
|
||||
因为喜欢wordpress之类动态博客的在线编辑功能,但是又不想抛弃hexo,特别是这个花里胡哨的模板,所以想着给博客添加一个cms系统,在搜索一番后发现这种CMS系统叫无头CMS,然后找到一个很好的系统:sveltia-cms
|
||||
|
||||
|
||||
{% link sveltia-cms,sveltia,https://github.com/sveltia/sveltia-cms %}
|
||||
|
||||
# 安装
|
||||
|
||||
## 安装包
|
||||
|
||||
在博客根目录下运行`npm i @sveltia/cms`
|
||||
|
||||
## 创建必要文件
|
||||
|
||||
然后在博客`source`目录下创建`admin`目录
|
||||
并在其下新建`index.html`
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>博客管理</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
新建`config.yml`
|
||||
|
||||
```yml
|
||||
# https://decapcms.org/docs/configuration-options/
|
||||
backend:
|
||||
name: github
|
||||
repo: #博客存储库
|
||||
branch: #分支
|
||||
auth_type:
|
||||
app_id:
|
||||
media_folder: "folder"
|
||||
public_folder: "folder"
|
||||
site_url: ""
|
||||
logo_url: ""
|
||||
locale: "zh_Hans"
|
||||
common_col_conf: &common_col_conf
|
||||
create: true
|
||||
slug: "{{fields.filename}}"
|
||||
sortable_fields:
|
||||
- "commit_date"
|
||||
- "title"
|
||||
- "date"
|
||||
- "updated"
|
||||
# https://decapcms.org/docs/widgets/
|
||||
fields:
|
||||
- label: "文件名"
|
||||
name: "filename"
|
||||
widget: "string"
|
||||
- label: "标题"
|
||||
name: "title"
|
||||
widget: "string"
|
||||
- label: "发表日期"
|
||||
name: "date"
|
||||
widget: "datetime"
|
||||
format: "YYYY-MM-DD HH:mm:ss"
|
||||
date_format: "YYYY-MM-DD"
|
||||
time_format: "HH:mm:ss"
|
||||
- label: "更新日期"
|
||||
name: "updated"
|
||||
widget: "datetime"
|
||||
format: "YYYY-MM-DD HH:mm:ss"
|
||||
date_format: "YYYY-MM-DD"
|
||||
time_format: "HH:mm:ss"
|
||||
required: false
|
||||
- label: "封面"
|
||||
name: "cover"
|
||||
widget: "image"
|
||||
required: false
|
||||
- label: "标签"
|
||||
name: "tags"
|
||||
widget: "select"
|
||||
multiple: true
|
||||
required: false
|
||||
options:
|
||||
- "amazon"
|
||||
- "android"
|
||||
- "angularjs"
|
||||
- "azure"
|
||||
- "cdn"
|
||||
- "chartjs"
|
||||
- "chrome"
|
||||
- "csharp"
|
||||
- "css"
|
||||
- "devops"
|
||||
- "diary"
|
||||
- "docker"
|
||||
- "edge"
|
||||
- "git"
|
||||
- "github"
|
||||
- "hexo"
|
||||
- "html"
|
||||
- "icarus"
|
||||
- "java"
|
||||
- "js"
|
||||
- "life"
|
||||
- "material"
|
||||
- "mysql"
|
||||
- "nodejs"
|
||||
- "onedrive"
|
||||
- "oneindex"
|
||||
- "php"
|
||||
- "restapi"
|
||||
- "security"
|
||||
- "serverless"
|
||||
- "shadowdefender"
|
||||
- "tool"
|
||||
- "twikoo"
|
||||
- "ubuntu"
|
||||
- "vagrant"
|
||||
- "vb"
|
||||
- "vite"
|
||||
- "vue"
|
||||
- "webpack"
|
||||
- "windows"
|
||||
- "xlsx"
|
||||
- "小程序"
|
||||
- label: "分类"
|
||||
name: "categories"
|
||||
widget: "select"
|
||||
multiple: true
|
||||
required: false
|
||||
options:
|
||||
- "Diary"
|
||||
- "Tool"
|
||||
- "Tech"
|
||||
- "FrontEnd"
|
||||
- "BackEnd"
|
||||
- "Windows"
|
||||
- "Android"
|
||||
- "Linux"
|
||||
- "Serverless"
|
||||
- label: "正文"
|
||||
name: "body"
|
||||
widget: "markdown"
|
||||
- label: "原创"
|
||||
name: "toc"
|
||||
widget: "boolean"
|
||||
default: true
|
||||
- label: "评论"
|
||||
name: "comments"
|
||||
widget: "boolean"
|
||||
default: true
|
||||
collections:
|
||||
- name: "2023"
|
||||
label: "2023"
|
||||
folder: "posts/2023"
|
||||
preview_path: "2023/{{filename}}/"
|
||||
<<: *common_col_conf
|
||||
- name: "pages"
|
||||
label: "Pages"
|
||||
files:
|
||||
- name: "friends"
|
||||
label: "友链"
|
||||
file: "source/friends/index.md"
|
||||
preview_path: "friends/"
|
||||
<<: *common_col_conf
|
||||
- name: "about"
|
||||
label: "关于"
|
||||
file: "source/about/index.md"
|
||||
preview_path: "about/"
|
||||
<<: *common_col_conf
|
||||
```
|
||||
|
||||
需要自己修改一些配置;
|
||||
一般到这一步就可以了,但是因为我使用自己的服务器,没办法用官方的auth系统,所以还要进一步配置。
|
||||
|
||||
## 创建auth系统、授权
|
||||
|
||||
{% link Sveltia CMS Authenticator,Sveltia CMS Authenticator,https://github.com/sveltia/sveltia-cms-auth %}
|
||||
比较简单,直接Cloudflare一键部署就可以
|
||||
然后要在github新建一个oauth应用,就不写了,可以自己查找。
|
||||
然后在cloudflare新建环境变量:
|
||||
`GITHUB_CLIENT_ID`:
|
||||
`GITHUB_CLIENT_SECRET`:
|
||||
然后在`config.yml`添加
|
||||
|
||||
```yml
|
||||
backend:
|
||||
name: github # or gitlab
|
||||
repo: username/repo
|
||||
branch: main
|
||||
base_url: <YOUR_WORKER_URL>
|
||||
```
|
||||
|
||||
# 大功告成
|
||||
|
||||
访问博客的admin就可以了
|
||||
Loaded 3 of 74 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user