Qexo 本地私有化部署教程及优化 前言:
之前部署了自己的hexo博客,一直用的是 hexo-admin
插件或者本地 typora
写文章。苦苦寻找,终于看到了一个不错的后台管理页面 qexo
。本文将要介绍的是,如何将 qexo
本地化部署及避坑 ,以及修改 qexo
源码进行美化 ,不会详细介绍 hexo
的部署流程(参见其他文章)。
每个人的博客部署方式都不一样,Github
,Vercel
,Cloudflare
等等,都能满足自动化部署的需求,这些方式我都尝试过,本次将采用私有化部署方式,大致需求如下。
需要一台主机(云主机/本机),本文以云主机进行演示
准备一个github、gitee、gitcode、云效等Git账号(非必须)
接下来,看看我是如何用华为云+宝塔面板+qexo+hexo+云效,搭建一个自动化博客管理流水线。
(华为云、宝塔、云效的教程不再赘述)
一、拉取Qexo的代码,把它运行起来 1、 拉取qexo代码,存储到任意目录。例如:/home/www/Qexo-master/
2、 根据官网教程,在根目录新建 configs.py
,写入官网教程给的内容,看代码注释,看代码注释🙂 *本来想用sqlite存储,但是项目没运行起来,mysql吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import pymysqlpymysql.install_as_MySQLdb() DATABASES = { 'default' : { 'ENGINE' : 'django.db.backends.mysql' , 'NAME' : 'qexo' , 'USER' : 'root' , 'PASSWORD' : 'password' , 'HOST' : '127.0.0.1' , 'PORT' : '3306' , 'OPTIONS' : { "init_command" : "SET sql_mode='STRICT_TRANS_TABLES'" } } } DOMAINS = ["127.0.0.1" , "yoursite.com" ]
3、执行python运行命令,如果使用宝塔面板则跳过这一步 *此处pip或pip3 *
1 2 3 4 pip3 install -r requirements.txt python3 manage.py makemigrations python3 manage.py migrate python3 manage.py runserver 0.0 .0 .0 :8000 --noreload
此时,已经可以打开qexo后台页面,如果打不开就多看看文档。
4、 如果你跟我一样,用的是宝塔面板,则进行这一步 4.1 在宝塔面板的网站中新增一个python项目,填写下面内容
4.2 然后在终端中执行第3步的命令,因为宝塔和宿主机的python环境是隔离的。
执行完毕,点击运行,你的qexo项目就能启动了。(云主机记得修改安全组规则)
4.3 在配置中填写你的域名,更好的访问 qexo
后台
还可以配置自动申请SSL证书等,自己动手尝试吧。
二、Qexo的优化操作
优化图床,使 Qexo
支持本地图片上传,将图片保存到云主机,满足安全性。
优化文章列表,使文章按创建时间排序,并显示创建时间。
优化图片列表,图片直接显示缩略图,不必手动点击。
优化写文章自动加载标签和分类(巨好用)。
开搞!
1、本地图床 效果图
1.1 在Qexo源码目录 hexoweb/libs/image/providers
下新增 local.py
文件,可以看到该目录下还有 ftp
,github
等图床配置。
local.py文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 """ @Project : custom @Author : GG @Blog : https://www.oplog.cn """ import datetimeimport osimport uuidfrom ..core import Providerdef delete (config ): path = config.get("delete_url" ) if os.path.exists(path): os.remove(path) return "OK" class Main (Provider ): name = '本地服务' params = { 'api' : {'description' : '本地路径' , 'placeholder' : '图床图片上传的 目录' }, 'custom_url' : {'description' : '自定义域名' , 'placeholder' : '返回带自定义域名的URL,如果没有则返回全路径' }, } def __init__ (self, api, custom_url ): self .path = api self .custom_url = custom_url self .delete_url = None def upload (self, file ): old_name = os.path.splitext(file.name) content = file.read() date = str (datetime.date.today().strftime("%Y-%m" )) file_name = date + "/" + str (uuid.uuid4()).replace("-" , "" ) + old_name[1 ] save_path = self .path + "/" + file_name parent_dir = os.path.dirname(save_path) os.makedirs(parent_dir, exist_ok=True ) with open (save_path, 'wb' ) as target_file: target_file.write(content) return [self .custom_url + '/' + file_name, {"provider" : Main.name, "delete_url" : save_path}]
1.2 同时,在 hexoweb/libs/image/providers
目录下的 __init__.py
文件中,新增一行代码。
1 2 3 4 5 6 7 8 9 10 11 _all_providers = { local.Main.name: local, custom.Main.name: custom, s3.Main.name: s3, ftp.Main.name: ftp, dogecloudoss.Main.name: dogecloudoss, alioss.Main.name: alioss, gitHub.Main.name: gitHub, upyun_storage.Main.name: upyun_storage, }
完成后,打开即可在配置中看到本地服务图床类型,添加本地路径和自定义域名。
1.3 修改python项目的配置文件
添加了自定义域名后,还需要配置nginx反向代理,才能访问图片。如果是单独装的nginx ,直接修改 nginx.conf
文件即可。
增加一个location配置项
1 2 3 location /file { alias /home/www/file/; }
宝塔配置文件
完成后,即可使用本地图床,访问图片。
2、文章列表时间倒序 效果图
1、首先添加创建时间字段 ctime
在 hexoweb/libs/platforms/providers/local.py
文件,找到 get_path
函数,修改43行,增加代码。注意 要在上一行的末尾加个逗号(, )。
1 "ctime" : os.stat(filedir).st_ctime
2、然后将 ctime
保存到数据库
在 hexoweb/libs/platforms/core.py
文件中,增加一行代码,注意 要在上一行的末尾加个逗号(, )。
1 "ctime" : posts[i]["ctime" ]
3、数据回显时根据 ctime
进行排序
在 hexoweb/views.py
文件中,搜索 elif "posts" in load_template:
这个判断语句,增加下面代码
1 posts.sort(key=lambda x: x.get("ctime" , float (1 )), reverse=True )
4、修改文章列表模板
找到 templates/home/posts.html
文件,这是文章列表的模板文件。
先在上面的html部分,加入 创建时间
标题,一个 th
标签。
1 2 3 <th class ="text-secondary text-xxs font-weight-bolder opacity-7" > <span class ="text-secondary" > 创建时间</span > </th >
然后在下面的JavaScript函数部分,返回的html文本中,加入 td
标签。
1 2 3 4 <td class ="text-xs font-weight-bold" > @@ctime@@ </td >
最后,在函数中的数据处理部分,添加对 ctime
字段的数据转换操作。
小心,不要把代码改出BUG
1 2 3 4 5 6 7 8 9 <!-- # fix add date column --> let ctime = page_posts[i].ctime * 1000 ;let formattedDate = new Date (ctime).toLocaleString ();list += post_temp.replaceAll ("@@name@@" , excerpt_by_local (page_posts[i].name , 50 )) .replaceAll ("@@size@@" , page_posts[i].size ) .replaceAll ("@@ctime@@" , formattedDate) .replaceAll ("@@status@@" , status) .replaceAll ("@@path@@" , encodeURIComponent (page_posts[i].path )) .replaceAll ("@@fullname@@" , encodeURIComponent (page_posts[i].name ));
3、图片列表缩略图 效果图
1、修改图片列表模板文件
在 templates/home/images.html
文件的 181行
写入一个 img
标签,用来显示缩略图
1 2 <img src ="@@url@@" class ="avatar avatar-sm me-3" alt ="xd" > </img >
4. 自动加载标签和分类 效果图
1. 要改这几个文件
2. 修改api.py
导入yaml包
增加 refresh_tags_categories
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @login_required(login_url="/login/" ) def refresh_tags_categories (request ): try : provider = json.loads(get_setting("PROVIDER" )) path = provider['params' ]['path' ] + "/source/_posts" tag_list = set () categories_list = set () for root, dirs, files in os.walk(path): for file in files: file_path = os.path.join(root, file) try : categories, tag = extract_categories_and_tags(file_path) tag_list.update(tag) categories_list.update(categories) except Exception as e: logging.error(f'----------extract_categories_and_tags failed on file:{file_path} ' ) context = {"msg" : gettext("SAVE_SUCCESS" ), "status" : True , "data" : {"tags" : sorted (list (tag_list)), "categories" : sorted (list (categories_list))}} except Exception as e: logging.error(e) context = {"msg" : gettext("CAPTCHA_GET_FAILED" ), "status" : False } return JsonResponse(safe=False , data=context) def extract_categories_and_tags (file_path ): pattern = r'^---\s*([\s\S]*?)\s*---' with open (file_path, 'r' , encoding="utf-8" ) as f: content = f.read(500 ) pattern_match = re.search(pattern, content, re.MULTILINE) if pattern_match: json_data = yaml.safe_load(pattern_match.group(1 )) categories = flat_map(json_data.get('categories' , [])) tags = flat_map(json_data.get('tags' , [])) return categories, tags def flat_map (arr ): flat_list = [] for a in arr: if isinstance (a, list ): flat_list.extend(a) else : flat_list.append(a) return flat_list
3.修改urls.py,声明接口
1 path('api/refresh_tags_categories/' , refresh_tags_categories, name='refresh_tags_categories' ),
4.修改new.html文件
增加刷新函数 refreshTagAndCategory
,发送ajax请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function refreshTagAndCategory ( ) { if (!_status) { return false ; } $.ajax ({ url : '/api/refresh_tags_categories/' , method : 'get' , dataType : 'JSON' , success : function (res ) { console .log (res); if (res.status ) { notyf.success ('{{ "UPDATE_SUCCESS" | gettext }}' ); let tags_list = res.data .tags ; let categories_list = res.data .categories ; let categories_str = "" ; for (let i = 0 ; i < categories_list.length ; i++) { categories_str += '<option value="' + categories_list[i] + '">' + categories_list[i] + '</option>' } $("#category_select_list" ).html (categories_str); let tags_str = "" ; for (let i = 0 ; i < tags_list.length ; i++) { tags_str += '<option value="' + tags_list[i] + '">' + tags_list[i] + '</option>' } $("#tag_select_list" ).html (tags_str); } else { notyf.error ('{{ "SAVE_FAILED" | gettext }}' ); } }, error : function (res ) { loading.destroy (); notyf.error ("{{ " NETWORK_ERROR " | gettext }}" ); } }) }
修改 showSidebar
函数,内容较复杂,下面直接贴我的整个函数
—————-这里是改动说明—————–
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 function showSidebar (first = false ) { let html = "" , content, front = front_matter, ifTag = false , ifCategory = false ; if (!first) { front = get_front_matter (); save_bottom (); OrgSidebar = [...Sidebar ]; } for (let i = 0 ; i < OrgSidebar .length ; i++) { if (OrgSidebar [i]["search" ] === "tags" ) { html += `<div class="form-group"> <label class="form-control-label"><i class="${OrgSidebar[i]["icon" ]} "></i>${OrgSidebar[i]["name" ]} </label> <div class="input-group mb-0"> <input type="text" class="form-control" placeholder="{{ "ADD_TAG" | gettext }}" name="qexo-tag" id="qexo-tag" list="tag_select_list"> <datalist id="tag_select_list"></datalist> <button class="btn mb-0 childButton" type="button" onclick="add_tag($('#qexo-tag') .val());$('#qexo-tag').val('')">+</button> <button class="btn mb-0 childButton" type="button" onclick="refreshTagAndCategory()"> <svg class="bi bi-arrow-clockwise" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" d="M3.17 6.706a5 5 0 0 1 7.103-3.16.5.5 0 1 0 .454-.892A6 6 0 1 0 13.455 5.5a.5.5 0 0 0-.91.417 5 5 0 1 1-9.375.789z"/> <path fill-rule="evenodd" d="M8.147.146a.5.5 0 0 1 .707 0l2.5 2.5a.5.5 0 0 1 0 .708l-2.5 2.5a.5.5 0 1 1-.707-.708L10.293 3 8.147.854a.5.5 0 0 1 0-.708z"/> </svg> </button> </div> <div id="tag-container"></div> </div>` ; ifTag = true ; } else if (OrgSidebar [i]["search" ] === "categories" ) { html += `<div class="form-group"> <label class="form-control-label"><i class="${OrgSidebar[i]["icon" ]} "></i>${OrgSidebar[i]["name" ]} </label> <div class="input-group mb-0"> <input type="text" class="form-control" placeholder="{{ "ADD_CATEGORY" | gettext }}" name="qexo-category" id="qexo-category" list="category_select_list"> <datalist id="category_select_list"></datalist> <button class="btn mb-0 childButton" type="button" onclick="add_category($ ('#qexo-category').val());$('#qexo-category').val('')">+</button> <button class="btn mb-0 childButton" type="button" onclick="refreshTagAndCategory()"> <svg class="bi bi-arrow-clockwise" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" d="M3.17 6.706a5 5 0 0 1 7.103-3.16.5.5 0 1 0 .454-.892A6 6 0 1 0 13.455 5.5a.5.5 0 0 0-.91.417 5 5 0 1 1-9.375.789z"/> <path fill-rule="evenodd" d="M8.147.146a.5.5 0 0 1 .707 0l2.5 2.5a.5.5 0 0 1 0 .708l-2.5 2.5a.5.5 0 1 1-.707-.708L10.293 3 8.147.854a.5.5 0 0 1 0-.708z"/> </svg> </button> </div> <div id="category-container"></div> </div>` ; ifCategory = true ; } else { if (OrgSidebar [i]["search" ] === "updated" || OrgSidebar [i]["search" ] === "lastmod" ) { content = getRFC3339 (); } else if (front[OrgSidebar [i]["search" ]] !== undefined ) { content = front[OrgSidebar [i]["search" ]]; } else content = "" ; content = (typeof content) === "object" ? JSON .stringify (content).replaceAll ("\"" , "" ") : content.toString().replaceAll(" \"" , "" ") html += `<div class=" form-group"> <label class=" form-control-label"><i class=" ` + OrgSidebar[i]["icon"] + ` "></i> ` + OrgSidebar[i][" name"] + `</label> <input type=" text" name=" title" class=" form-control" placeholder=" ` + OrgSidebar[i]["name"] + ` " id=" ` + OrgSidebar[i]["search"] + ` " value=" ` + content + ` "></div>`; } delete front[OrgSidebar[i][" search"]]; } $(" #sidebar-container").html(html); if (ifTag) show_tags(); if (ifCategory) show_categories(); front_matter = front; show_bottom(); set_vditor_size(); // 监听分类、标签回车键 $('#qexo-tag').bind('keypress', function (event) { if (event.keyCode === 13) { event.preventDefault(); add_tag($('#qexo-tag').val()); $('#qexo-tag').val('') } }); $('#qexo-category').bind('keypress', function (event) { if (event.keyCode === 13) { event.preventDefault(); add_category($('#qexo-category').val()); $('#qexo-category').val('') } }); }
另外两个文件改动差不多,其中 edit.html
基本一致,而 edittalk.html
文件是只需要增加标签的改动,因为 说说
没有分类选项。
5.修改edit.html
也是先添加函数 refreshTagAndCategory
改动html,代码不贴了
6.修改 edit_talk.html
修改html部分,这里只有标签的输入框
修改函数部分,粘贴 refreshTagAndCategory
函数
三、通过阿里云效,完成流水线部署 因为华为云28块只有一年有效期,为了到时候方便迁移博客,所以我不是直接通过本地运行命令将hexo打包部署的,而是加入了云效流水线,只要提交代码后,就能自动完成部署,类似 github
的 Action
。不需要科学上网,方便操作。
1、注册阿里云账号,进入云效,创建一个Git仓库存放hexo源码(略过)
2、进入流水线,根据指引完成云主机的绑定操作(略过)
3、创建云效流水线
构建阶段选择node.js构建,选择对应node版本,输入构建命令,此处基本与hexo安装时一样。
部署阶段,选择你的主机,然后将打包好的html文件通过命令拷贝至nginx指向的目录。
4、完成Qexo的自动部署配置
Qexo本地部署配置
自动部署命令 建议通过 shell
脚本执行,在博客路径 (你的hexo源码目录 )下新建 gitpush.sh
文件
1 2 3 4 5 # 新建文件的linux命令 touch gitpush.sh # 赋予权限的linux命令 chmod 777 ./gitpush.sh
在 gitpush.sh
文件中写入下面命令
1 2 3 4 5 6 7 8 9 10 11 12 # ! /bin/bash # 执行$HOME 环境变量,否则Git可能无法运行 export HOME=/root echo "=================" git config --global user.name git config --global user.email echo "===================" git add . git commit -m "qexo push" git push
完成以上配置,在使用Qexo编辑完文章后,自动部署阶段会通过Git将hexo源文件上传到云效,由云效进行自动化打包部署。