# 搜索功能
# algolia 搜索功能
algolia官方地址
需要安装 hexo-algoliasearch
插件。
注册 algolia,创建 Index:
获取 Key,修改站点配置:
新建 API Key:
获取相应的 Key,并填入配置信息:
在配置完 Key 后,回到终端控制台,键入以下命令上传数据到
algolia
:hexo algolia
配置参考:
algolia: | |
appId: "Application ID对应码" | |
apiKey: "API Keys页面的All API Keys中刚刚新建的API key的对应码" | |
adminApiKey: "Admin API Key对应码" | |
chunkSize: 5000 | |
indexName: "你填写的Indices部分" | |
fields: | |
- title #必须配置 | |
- path #必须配置 | |
- categories #推荐配置 | |
- content:strip:truncate,0,4000 | |
- gallery | |
- photos | |
- tags |
注:每次更新修改文章后还需要执行 hexo algolia
的指令,否则,搜索数据没有或者对不上。
# 本地搜索功能
奈何本人对前端不熟悉,主要参考应用了该博主的教程。
需要安装 hexo-generator-search
插件。
修改主题文件夹下的
themes\shoka\scripts\generaters\script.js
文件,定位到if(config.algolia)
判断的位置,然后追加配置赋值:if(config.search.enable) {
siteConfig.search = {
path : config.search.path,
field : config.search.post,
format: config.search.format,
limit: config.search.limit,
content: config.search.content,
unescape: config.search.unescape,
preload: config.search.preload,
trigger: config.search.trigger,
top_n_per_article: config.search.top_n_per_article,
article_per_page: config.search.article_per_page,
}
}
在路径
themes\shoka\source\js\_app\
下,找到pjax.js
搜索algoliaSearch()
函数,该函数是用来实现 algolia 搜索的,由于现在要实现本地搜索,因此屏蔽掉该函数,添加并使用实现本地搜索的函数localSearch()
。实现
localSearch()
函数需要在themes\shoka\source\js\_app\page.js
文件下找个地方定义如下内容:function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
var localSearch = function localSearch(pjax) {
// 参考 hexo next 主题的配置方法
if (CONFIG.search === null) return;
if (!siteSearch) {
siteSearch = BODY.createChild('div', {
id: 'search',
innerHTML: "<div class=\"inner\"><div class=\"header\"><span class=\"icon\"><i class=\"ic i-search\"></i></span><div class=\"search-input-container\"><input class=\"search-input\"\nautocomplete=\"off\"\nplaceholder=\"".concat(LOCAL.search.placeholder, "\"\nspellcheck=\"false\"\ntype=\"text\"\nid=\"local-search-input\"></div><span class=\"close-btn\"><i class=\"ic i-times-circle\"></i></span></div><div class=\"results\" id=\"search-results\"><div class=\"inner\"><div id=\"search-stats\"></div><div id=\"search-hits\"></div><div id=\"search-pagination\"></div></div></div></div></div>")
});
}
var isFetched = false;
var datas;
var isXml = true;
var current_page = 0;
var article_per_page = parseInt(CONFIG.search.article_per_page, 10);
var total_pages = 0;
var max_page_on_show = 7; // 一次最多显示 7 个页码
var start_page = 0;
var end_page = 0;
var resultItems = [];
// search DB path
var searchPath = CONFIG.search.path;
if (searchPath.length === 0) {
searchPath = 'search.xml';
} else if (searchPath.endsWith('json')) {
isXml = false;
}
var input = $('.search-input'); // document.querySelector('.search-input');
var resultContent = document.getElementById('search-hits');
var paginationContent = document.getElementById('search-pagination');
var getIndexByWord = function getIndexByWord(word, text, caseSensitive) {
if (CONFIG.search.unescape) {
var div = document.createElement('div');
div.innerText = word;
word = div.innerHTML;
}
var wordLen = word.length;
if (wordLen === 0) {
return [];
}
var startPosition = 0;
var position = [];
var index = [];
if (!caseSensitive) {
text = text.toLowerCase();
word = word.toLowerCase();
}
while ((position = text.indexOf(word, startPosition)) > -1) {
index.push({
position: position,
word: word
});
startPosition = position + wordLen;
}
return index;
};
// Merge hits into slices
var mergeIntoSlice = function mergeIntoSlice(start, end, index, searchText) {
var item = index[index.length - 1];
var _item = item,
position = _item.position,
word = _item.word;
var hits = [];
var searchTextCountInSlice = 0;
while (position + word.length <= end && index.length !== 0) {
if (word === searchText) {
searchTextCountInSlice++;
}
hits.push({
position: position,
length: word.length
});
var wordEnd = position + word.length;
// Move to next position of hit
index.pop();
while (index.length !== 0) {
item = index[index.length - 1];
position = item.position;
word = item.word;
if (wordEnd > position) {
index.pop();
} else {
break;
}
}
}
return {
hits: hits,
start: start,
end: end,
searchTextCount: searchTextCountInSlice
};
};
// Highlight title and content
var highlightKeyword = function highlightKeyword(text, slice) {
var result = '';
var prevEnd = slice.start;
slice.hits.forEach(function (hit) {
result += text.substring(prevEnd, hit.position);
var end = hit.position + hit.length;
result += "<mark>".concat(text.substring(hit.position, end), "</mark>");
prevEnd = end;
});
result += text.substring(prevEnd, slice.end);
return result;
};
var pagination = function pagination() {
var addPrevPage = function addPrevPage(current_page) {
var classContent = '';
var numberContent = '';
if (current_page === 0) {
classContent = '#search-pagination pagination-item disabled-item';
numberContent = '<span class="#search-pagination page-number"><i class="ic i-angle-left"></i></span>';
} else {
classContent = '#search-pagination pagination-item';
numberContent = "<a class=\"#search-pagination page-number\" aria-label=\"Prev\" href=\"#\"><i class=\"ic i-angle-left\"></i></a>";
}
var prevPage = "<li class=\"".concat(classContent, "\" id=\"prev-page\">").concat(numberContent, " </li>");
return prevPage;
};
var addNextPage = function addNextPage(current_page) {
var classContent = '';
var numberContent = '';
if (current_page + 1 === total_pages) {
classContent = '#search-pagination pagination-item disabled-item';
numberContent = '<span class="#search-pagination page-number"><i class="ic i-angle-right"></i></span>';
} else {
classContent = '#search-pagination pagination-item';
numberContent = "<a class=\"#search-pagination page-number\" aria-label=\"Next\" href=\"#\"><i class=\"ic i-angle-right\"></i></a>";
}
var nextPage = "<li class=\"".concat(classContent, "\" id=\"next-page\">").concat(numberContent, " </li>");
return nextPage;
};
var addPage = function addPage(index, current_page) {
var classContent = '';
var numberContent = "<a class=\"#search-pagination page-number\" aria-label=\"".concat(index + 1, "\" href=\"#\">").concat(index + 1, "</a>");
if (index === current_page) {
classContent = '#search-pagination pagination-item current';
} else {
classContent = '#search-pagination pagination-item';
}
var page = "<li class=\"".concat(classContent, "\" id=\"page-").concat(index + 1, "\">").concat(numberContent, " </li>");
return page;
};
var addPaginationEvents = function addPaginationEvents(start_page, end_page) {
if (total_pages <= 0) {
return;
}
var onPrevPageClick = function onPrevPageClick(event) {
if (current_page > 0) {
current_page -= 1;
}
if (current_page < start_page) {
start_page = current_page;
end_page = Math.min(end_page, start_page + max_page_on_show);
}
pagination();
};
var onNextPageClick = function onNextPageClick(event) {
if (current_page + 1 < total_pages) {
current_page += 1;
}
if (current_page > end_page) {
end_page = current_page;
start_page = Math.max(0, end_page - max_page_on_show);
}
pagination();
};
var onPageClick = function onPageClick(event) {
var page_number = parseInt(event.target.ariaLabel);
current_page = page_number - 1; // note minus 1 here
pagination();
};
var prevPage = document.getElementById('prev-page');
prevPage.addEventListener('click', onPrevPageClick);
var nextPage = document.getElementById('next-page');
nextPage.addEventListener('click', onNextPageClick);
for (var i = start_page; i < end_page; i += 1) {
var page = document.getElementById("page-".concat(i + 1));
page.addEventListener('click', onPageClick);
}
};
paginationContent.innerHTML = ''; // clear
var begin_index = Math.min(current_page * article_per_page, resultItems.length);
var end_index = Math.min(begin_index + article_per_page, resultItems.length);
resultContent.innerHTML = "".concat(resultItems.slice(begin_index, end_index).map(function (result) {
return result.item;
}).join(''));
start_page = Math.max(0, total_pages - max_page_on_show);
end_page = start_page + Math.min(total_pages, max_page_on_show);
var pageContent = '<div class="#search-pagination">';
pageContent += '<div class="#search-pagination pagination">';
pageContent += '<ul>';
if (total_pages > 0) {
// add prev page arrow, when no prev page not selectable
pageContent += addPrevPage(current_page);
for (var i = start_page; i < end_page; i += 1) {
pageContent += addPage(i, current_page);
}
// add next page arrow, when no next page not selectable
pageContent += addNextPage(current_page);
}
pageContent += '</ul>';
pageContent += '</div>';
pageContent += '</div>';
paginationContent.innerHTML = pageContent;
addPaginationEvents(start_page, end_page);
resultContent.scrollTop = 0; // scroll to top
window.pjax && window.pjax.refresh(resultContent);
};
var inputEventFunction = function inputEventFunction() {
if (!isFetched) {
console.log("Data not fetched.");
return;
}
var searchText = input.value.trim().toLowerCase();
var keywords = searchText.split(/[-\s]+/);
if (keywords.length > 1) {
keywords.push(searchText);
}
resultItems = [];
if (searchText.length > 0) {
// Perform local searching
datas.forEach(function (_ref, index) {
var categories = _ref.categories,
title = _ref.title,
content = _ref.content,
url = _ref.url;
var titleInLowerCase = title.toLowerCase();
var contentInLowerCase = content.toLowerCase();
var indexOfTitle = [];
var indexOfContent = [];
var searchTextCount = 0;
keywords.forEach(function (keyword) {
indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false));
indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false));
});
// Show search results
if (indexOfTitle.length > 0 || indexOfContent.length > 0) {
var hitCount = indexOfTitle.length + indexOfContent.length;
// Sort index by position of keyword
[indexOfTitle, indexOfContent].forEach(function (index) {
index.sort(function (itemLeft, itemRight) {
if (itemRight.position !== itemLeft.position) {
return itemRight.position - itemLeft.position;
}
return itemLeft.word.length - item.word.length;
});
});
var slicesOfTitle = [];
if (indexOfTitle.length !== 0) {
var tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText);
searchTextCount += tmp.searchTextCountInSlice;
slicesOfTitle.push(tmp);
}
var slicesOfContent = [];
while (indexOfContent.length !== 0) {
var _item2 = indexOfContent[indexOfContent.length - 1];
var position = _item2.position,
word = _item2.word;
// Cut out 100 characters
var start = position - 20;
var end = position + 30;
if (start < 0) {
start = 0;
}
if (end < position + word.length) {
end = position + word.length;
}
if (end > content.length) {
end = content.length;
}
var _tmp = mergeIntoSlice(start, end, indexOfContent, searchText);
searchTextCount += _tmp.searchTextCountInSlice;
slicesOfContent.push(_tmp);
}
// Sort slices in content by search text's count and hits' count
slicesOfContent.sort(function (sliceLeft, sliceRight) {
if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) {
return sliceRight.searchTextCount - sliceLeft.searchTextCount;
} else if (sliceLeft.hits.length !== sliceRight.hits.length) {
return sliceRight.hits.length - sliceLeft.hits.length;
}
return sliceLeft.start - sliceRight.start;
});
// Select top N slices in content
var upperBound = parseInt(CONFIG.search.top_n_per_article, 10);
if (upperBound >= 0) {
slicesOfContent = slicesOfContent.slice(0, upperBound);
}
var resultItem = '';
resultItem += '<div class="#search-hits item">';
resultItem += '<li>';
var cats = categories !== undefined ? '<span>' + categories.join('<i class="ic i-angle-right"></i>') + '</span>' : '<span>No categories</span>';
resultItem += "<a href=\"".concat(url, "\">") + cats;
if (slicesOfTitle.length !== 0) {
resultItem += "<b>".concat(highlightKeyword(title, slicesOfTitle[0]), "</b><br>");
} else {
resultItem += "<b>".concat(title, "</b><br>");
}
slicesOfContent.forEach(function (slice) {
resultItem += "<li class=\"#search-hits subitem\">".concat(highlightKeyword(content, slice), " ...</li>");
});
resultItem += '</a>';
resultItem += '</li>';
resultItem += '</div>';
resultItems.push({
item: resultItem,
id: resultItems.length,
hitCount: hitCount,
searchTextCount: searchTextCount
});
}
});
}
if (keywords.length === 1 && keywords[0] === '') {
resultContent.innerHTML = '<div id="no-result"><i></i></div>';
} else if (resultItems.length === 0) {
resultContent.innerHTML = '<div id="no-result"><i></i></div>';
} else {
resultItems.sort(function (resultLeft, resultRight) {
if (resultLeft.searchTextCount !== resultRight.searchTextCount) {
return resultRight.searchTextCount - resultLeft.searchTextCount;
} else if (resultLeft.hitCount !== resultRight.hitCount) {
return resultRight.hitCount - resultLeft.hitCount;
}
return resultRight.id - resultLeft.id;
});
}
// Do pagination
total_pages = Math.ceil(resultItems.length / article_per_page);
pagination();
};
var fetchData = function fetchData() {
fetch(CONFIG.root + searchPath).then(function (response) {
return response.text();
}).then(function (res) {
// Get the contents from search data
isFetched = true;
datas = isXml ? _toConsumableArray(new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')).map(function (element) {
return {
title: element.querySelector('title').textContent,
content: element.querySelector('content').textContent,
url: element.querySelector('url').textContent
};
}) : JSON.parse(res);
// Only match articles with not empty titles
datas = datas.filter(function (data) {
return data.title;
}).map(function (data) {
data.title = data.title.trim();
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '';
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/');
return data;
});
// Remove loading animation
document.getElementById('search-hits').innerHTML = '<i></i>';
inputEventFunction();
});
};
if (CONFIG.search.preload) {
console.log("fetch data.");
fetchData();
}
if (CONFIG.search.trigger === 'auto') {
input.addEventListener('input', inputEventFunction);
} else {
document.querySelector('.search-icon').addEventListener('click', inputEventFunction);
input.addEventListener('keypress', function (event) {
if (event.key === 'Enter') {
inputEventFunction();
}
});
}
// Handle and trigger popup window
document.querySelectorAll('.popup-trigger').forEach(function (element) {
element.addEventListener('click', function () {
document.body.style.overflow = 'hidden';
document.querySelector('.search-pop-overlay').classList.add('search-active');
input.focus();
if (!isFetched) fetchData();
});
});
// Handle and trigger popup window
$.each('.search', function (element) {
element.addEventListener('click', function () {
document.body.style.overflow = 'hidden';
transition(siteSearch, 'shrinkIn', function () {
$('.search-input').focus();
}); // transition.shrinkIn
});
});
// Monitor main search box
var onPopupClose = function onPopupClose() {
document.body.style.overflow = ' ';
transition(siteSearch, 0); // "transition.shrinkOut"
};
siteSearch.addEventListener('click', function (event) {
if (event.target === siteSearch) {
onPopupClose();
}
});
$('.close-btn').addEventListener('click', onPopupClose);
window.addEventListener('pjax:success', onPopupClose);
window.addEventListener('keyup', function (event) {
if (event.key === 'Escape') {
onPopupClose();
}
});
}
配置信息添加参考:https://www.npmjs.com/package/hexo-generator-search
# valine 评价功能
# leancloud 配置
leancloud官方地址
1、获取 LeanCloud 的 appld 和 appkey 。
valine: | |
appId: #Your_appId | |
appKey: #Your_appkey | |
placeholder: ヽ(○´∀`)ノ♪ # Comment box placeholder | |
avatar: mp # Gravatar style : mp, identicon, monsterid, wavatar, robohash, retro | |
pageSize: 10 # Pagination size | |
lang: zh-CN | |
visitor: true # 文章访问量统计 | |
NoRecordIP: false # 不记录 IP | |
serverURLs: # When the custom domain name is enabled, fill it in here (it will be detected automatically by default, no need to fill in);当使用 LeanCloud 共享域名时填入 LeanCloud 服务器地址 (REST API 服务器地址) | |
powerMode: true # 默认打开评论框输入特效 | |
tagMeta: | |
visitor: 新朋友 | |
master: 主人 | |
friend: 小伙伴 | |
investor: 金主粑粑 | |
tagColor: | |
master: "var(--color-orange)" | |
friend: "var(--color-aqua)" | |
investor: "var(--color-pink)" | |
tagMember: | |
master: | |
# - hash of master@email.com | |
# - hash of master2@email.com | |
friend: | |
# - hash of friend@email.com | |
# - hash of friend2@email.com | |
investor: | |
# - hash of investor1@email.com |
2、tagMeta 标签
tag 标签显示在评论者名字的后面,默认是 tagMeta.visitor
对应的值。
在 tagMeta
和 tagColor
中,除了 visitor
这个 key 不能修改外,其他 key 都可以换一换,但需要保证一致性。
tagMeta: | |
visitor: 游客 | |
admin: 管理员 | |
waifu: 我老婆 | |
tagColor: | |
visitor: "#855194" | |
admin: "#a77c59" | |
waifu: "#ed6ea0" | |
tagMember: | |
admin: | |
# - hash of admin@email.com | |
waifu: | |
# - hash of waifu@email.com |
3、单篇文章评价配置
在文章 Front Matter 中也可以配置上述参数,访问该文章页面时,将覆盖全局配置。
尤其可以用来配置一个特殊的 placeholder
:
--- | |
valine: | |
placeholder: "💪请遵守评价礼仪, 禁止恶意评价\n🤣一起来玩啊, 留下你的足迹吧\n⚠️公开网络, 注意隐私信息" | |
--- |
如果某一篇文章需要关闭评论功能,则在文章 Front Matter 中配置:
--- | |
title: 关闭评论 | |
comment: false | |
--- |
# Valine Admin 配置
Valine-Admin网页地址
评论通知与管理工具建议使用这个 Valine-Admin ;大致教程可参考:Valine Admin 配置手册 。
以下作相关重要部分补充,以 LeanCloud 国际版为准。
# 自定义环境变量
点击 云引擎
- -WEB
- 设置
- 添加
, 添加一些如下变量, 可以用来自定义一些邮件通知、通知模板、消息回复等个性化的设置。
变量 | 示例 | 说明 |
---|---|---|
ADMIN_URL | https://xxx.example.com/ | [建议] Web 主机二级域名(云引擎域名),用于自动唤醒 |
AKISMET_KEY | xxxxxxx | [可选] Akismet Key 用于垃圾评论检测,设为 MANUAL_REVIEW 开启人工审核,留空不使用反垃圾 |
BLOGGER_EMAIL | ${SENDER_EMAIL} | [可选] 博主通知收件地址,默认使用 SENDER_EMAIL |
MAIL_SUBJECT | ${PARENT_NICK} ,您在 ${SITE_NAME} 上的评论收到了回复 | [可选] 访客邮件接收主题 |
MAIL_SUBJECT_ADMIN | ${SITE_NAME} 上有新评论了 | [可选] 管理员邮件接收主题 |
MAIL_TEMPLATE | 代码块_1 | [可选] 访客邮件内容模板 |
MAIL_TEMPLATE_ADMIN | 代码块_2 | [可选] 管理员邮件内容模板 |
SENDER_EMAIL | xxxxxx@qq.com | [必填] 发件邮箱 |
SENDER_NAME | user_name | [必填] 发件人 |
SITE_NAME | xxx's blog | [必填] 博客名称 |
SITE_URL | https://example.com/ | [必填] 博客首页地址 |
SMTP_HOST | smtp.qq.com | [可选] SMTP_SERVICE 留空时,自定义 SMTP 服务器地址 |
SMTP_PASS | xxxxxxx | [必填] SMTP 登录密码(QQ 邮箱需要获取 SMTP 授权码,参照客户端设置) |
SMTP_PORT | 465 或 587,参照 POP3 与 SMTP | [可选] SMTP_SERVICE 留空时,自定义 SMTP 端口 |
SMTP_SECURE | true | [可选] 使用 TLS |
SMTP_SERVICE | [新版支持] 邮件服务提供商,支持 QQ、163、126、Gmail 以及 更多 | |
SMTP_USER | xxxxxx@qq.com | [必填] SMTP 用户名 |
注意 SITE_URL
需要以 /
结尾,否则在评价后台的 查看评价
的链接会对不上文章链接(缺 /
分割符)。
# MAIL_TEMPLATE
变量实现:
<div style="border-radius: 10px 10px 10px 10px;font-size:13px; color: #555555;width: 666px;font-family:'Century Gothic','Trebuchet MS','Hiragino Sans GB',微软雅黑,'Microsoft Yahei',Tahoma,Helvetica,Arial,'SimSun',sans-serif;margin:50px auto;border:1px solid #eee;max-width:100%;background: #ffffff repeating-linear-gradient(-45deg,#fff,#fff 1.125rem,transparent 1.125rem,transparent 2.25rem);box-shadow: 0 1px 5px rgba(0, 0, 0, 0.15);"><div style="width:100%;background:#49BDAD;color:#ffffff;border-radius: 10px 10px 0 0;background-image: -moz-linear-gradient(0deg, rgb(67, 198, 184), rgb(255, 209, 244));background-image: -webkit-linear-gradient(0deg, rgb(67, 198, 184), rgb(255, 209, 244));height: 66px;"><p style="font-size:15px;word-break:break-all;padding: 23px 32px;margin:0;background-color: hsla(0,0%,100%,.4);border-radius: 10px 10px 0 0;">您在<a style="text-decoration:none;color: #ffffff;" href="${SITE_URL}"> ${SITE_NAME}</a>上的留言有新回复啦!</p></div><div style="margin:40px auto;width:90%"><p>${PARENT_NICK} 同学,您曾在文章上发表评论:</p><div style="background: #fafafa repeating-linear-gradient(-45deg,#fff,#fff 1.125rem,transparent 1.125rem,transparent 2.25rem);box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);margin:20px 0px;padding:15px;border-radius:5px;font-size:14px;color:#555555;">${PARENT_COMMENT}</div><p>${NICK} 给您的回复如下:</p><div style="background: #fafafa repeating-linear-gradient(-45deg,#fff,#fff 1.125rem,transparent 1.125rem,transparent 2.25rem);box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);margin:20px 0px;padding:15px;border-radius:5px;font-size:14px;color:#555555;">${COMMENT}</div><p>您可以点击<a style="text-decoration:none; color:#12addb" href="${POST_URL}#comments">查看回复的完整內容</a>,欢迎再次光临<a style="text-decoration:none; color:#12addb" href="${SITE_URL}"> ${SITE_NAME}</a>。</p><style type="text/css">a:link{text-decoration:none}a:visited{text-decoration:none}a:hover{text-decoration:none}a:active{text-decoration:none}</style></div></div> |
# MAIL_TEMPLATE_ADMIN
变量实现:
<div style="border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;"><h2 style="border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;">您在<a style="text-decoration:none;color: #12ADDB;" href="${SITE_URL}" target="_blank">${SITE_NAME}</a>上的文章有了新的评论</h2><p><strong>${NICK}</strong>回复说:</p><div style="background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;"> ${COMMENT}</div><p>您可以点击<a style="text-decoration:none; color:#12addb" href="${POST_URL}" target="_blank">查看回复的完整內容</a><br></p></div></div> |
# 访问域名设置
在配置环境变量的下方,有以下两个的配置:
使用共享域名(目前国际版需要自定义域名才运作)
这里可以自己定义,使用一个容易记忆的名称即可。在填写
ADMIN_URL
时,需要注意在域名前添加stg
前缀。例如,我自己定义的名称为xxxblog
,此时完整的域名包含了固定的后缀.avosapps.us
,即完整的域名是https://xxxblog.avosapps.us
。而ADMIN_URL
中需要填写https://stg-xxxblog.avosapps.us
。自定义域名
该域名需要二级域名,然后按正常的配置 DNS 解析,等通过就行了;如果绑定了域名,那么在变量中的
ADMIN_URL
则填写该域名。
# 管理后台
当部署完成后,首先需要设置管理员信息。访问管理员注册页面 https://云引擎域名/sign-up
,注册管理员登录信息,如:https://xxx.example.com//sign-up 。
# 解决跨域问题
在非国际版中,他们的 API 链接是会自动检测的;但当你使用的是国际版,它并不会自动检测。并且由于 us.avoscloud.com 这个域名被弃用了,因而会发生报错提示遇到跨域访问的问题。
解决方法:
- 前往 https://console.leancloud.app/
- 选择并点击你的应用
- 前往
设置
>应用凭证
- 然后你会看到一个
REST API 服务器地址
把那个链接复制起来 - 最后把它放入配置档案
valine
的serverURLs
serverURLs
通常是这样的https://[appId 前八位].api.lncldglobal.com
;或者对其进行域名绑定:设置
->域名绑定
->API 访问域名
->绑定新域名
,然后使用该域名配置到serverURLs
中。
最后提醒:
请确保你的 appKey
和 appId
是正确的,这样你的评论功能就能正常使用了。
# 定时唤醒
关于自动休眠的官方说法:点击查看。由于目前 leancloud 发布了 [关于对体验版云引擎定时任务进行适当流控的说明](https://forum.leancloud.cn/t/topic/22595) 一则文章,导致 leancloud 的定时任务使用需要升级至标准版云引擎才能避免休眠;因此,在这里放弃 leancloud 的定时任务,利用 github actions 中的 workflow 定时执行命令访问 leancloud 的 web 域名,即可解决 leancloud 平台因为流控原因无法激活定时唤醒任务的问题:
主分支上创建 github actions 的 workflow 任务。
在
xxx.yml
文件中修改如下代码:name: 'wake comment system'
on:
push:
schedule:
- cron: '7,33,53 0-15,23 * * *'
jobs:
bot:
runs-on: ubuntu-latest
steps:
- run: curl -sLo /dev/null $<!--swig3-->
添加私钥
DOMAIN
(域名):仓库
->settings
->Secrets and variabled
->Actions
->New repository secret
- Name:DOMAIN
- Value:你的 web 域名
# 评价头像
Gravatar官方地址
注册:进入 Gravatar 网站,点击左上角菜单里的 Sign In 进行登录 / 注册。
绑定邮箱及头像:点击 My Gravatar -> Add email address -> Add a new image
点击
View rating
可查看邮箱的hash
码。hash 码可填入 shoka 主题的_config.yml
文件中的valine.tagMember
其中一处(评论区对应显示的 tag 标签)。配置好后,有缓冲时间,最慢有 7 天缓冲时间。
# 字数及阅读时间统计
需要安装 hexo-symbols-count-time
插件。
安装后不需要修改站点配置文件,直接使用插件默认配置就行,如需进行配置,可参考:
# hexo-symbols-count-time | |
## shoka 主题默认采取了默认的配置,所以覆盖相应配置就行了 | |
symbols_count_time: | |
symbols: true | |
time: true | |
total_symbols: true | |
total_time: true | |
exclude_codeblock: false | |
awl: 4 | |
wpm: 245 | |
suffix: "mins." | |
#exclude_codeblock: 是否排除代码块区域的字数统计 | |
#awl: 平均字符长度 (多少符字算一个中文或英文) 中文约是 2,英文约是 5,其他约是 6 | |
#wpm: 每分钟阅读字数 正常值约为 275 字,较小值约为 200 字,较快值约 350 字 | |
#suffix: 当总字数小于每分钟阅读字数时,默认采取的时间类型,不填写默认类型采取 "mins." |
启用则需要找到 footer
和 post
的两处 count
,修改为 true
:
# 页尾全站统计 | |
footer: | |
since: 2010 | |
count: true | |
# 文章界面统计 | |
post: | |
count: true |
# 访问及阅读统计
LeanCloud 评价里面是有阅读访问统计,但是,目前由于 LeanCloud 国际版不对国内用户提供服务了,因此改用其它记录统计,这里可以使用简单的不蒜子计数,操作更改也比较简单: 引脚本 + 写标签
1、调用不蒜子的官方脚本,这个比较简单,在你需要的地方调用如下代码
# 引脚本 | |
<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script> | |
# 写标签 | |
<span id="busuanzi_value_page_pv"> # 当前访问页面次数 | |
<span id="busuanzi_container_site_uv"> # 站点访客次数 | |
<span id="busuanzi_container_site_pv"> # 站点访问总量 |
2、静态部署调用,这个需要在 <root>/source/js
路径下创建 busuanzi.pure.min.js
文件,如果 js
目录不存在则自己新建,并且把如下代码添加进去,并保存:
var bszCaller,bszTag;!function(){var c,d,e,a=!1,b=[];ready=function(c){return a||"interactive"===document.readyState||"complete"===document.readyState?c.call(document):b.push(function(){return c.call(this)}),this},d=function(){for(var a=0,c=b.length;c>a;a++)b[a].apply(document);b=[]},e=function(){a||(a=!0,d.call(window),document.removeEventListener?document.removeEventListener("DOMContentLoaded",e,!1):document.attachEvent&&(document.detachEvent("onreadystatechange",e),window==window.top&&(clearInterval(c),c=null)))},document.addEventListener?document.addEventListener("DOMContentLoaded",e,!1):document.attachEvent&&(document.attachEvent("onreadystatechange",function(){/loaded|complete/.test(document.readyState)&&e()}),window==window.top&&(c=setInterval(function(){try{a||document.documentElement.doScroll("left")}catch(b){return}e()},5)))}(),bszCaller={fetch:function(a,b){var c="BusuanziCallback_"+Math.floor(1099511627776*Math.random());window[c]=this.evalCall(b),a=a.replace("=BusuanziCallback","="+c),scriptTag=document.createElement("SCRIPT"),scriptTag.type="text/javascript",scriptTag.defer=!0,scriptTag.src=a,scriptTag.referrerPolicy="no-referrer-when-downgrade",document.getElementsByTagName("HEAD")[0].appendChild(scriptTag)},evalCall:function(a){return function(b){ready(function(){try{a(b),scriptTag.parentElement.removeChild(scriptTag)}catch(c){bszTag.hides()}})}}},bszCaller.fetch("//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback",function(a){bszTag.texts(a),bszTag.shows()}),bszTag={bszs:["site_pv","page_pv","site_uv"],texts:function(a){this.bszs.map(function(b){var c=document.getElementById("busuanzi_value_"+b);c&&(c.innerHTML=a[b])})},hides:function(){this.bszs.map(function(a){var b=document.getElementById("busuanzi_container_"+a);b&&(b.style.display="none")})},shows:function(){this.bszs.map(function(a){var b=document.getElementById("busuanzi_container_"+a);b&&(b.style.display="inline")})}}; |
然后更改上面第一条代码的引用路径为本地路径,例如: <script async src="https://xxx.example.com/js/busuanzi.pure.min.js"></script>
, xxx.example.com
为你的部署网站。后面三行的代码的 <span id="xxx">
不用变动,只需在合适的地方调用想用的计数代码即可。
3、举个例子,替换 shoka 主题原有的页面访问计数。在 <root>/themes/shoka/layout/_partials/post/footer.njk
文件中,添加并替换即可。如下图:
# RSS 订阅
需要安装 hexo-generator-feed
插件。
安装完成后,需要在 <root>/_config.yml
配置中添加如下信息,并填写自己需要的信息:
feed: | |
enable: true | |
type: atom | |
path: atom.xml | |
limit: 20 | |
hub: | |
content: | |
content_limit: 140 | |
content_limit_delim: ' ' | |
order_by: -date | |
icon: icon.png | |
autodiscovery: true | |
template: |
具体内容可参看:https://github.com/hexojs/hexo-generator-feed
当然,如果有能力的,可以自建 rss 模板,shoka 主题上也有提供,只需要如下调用模板即可:
最后,在需要获取 RSS 链接的地方添加 /atom.xml
即可。
# 站点运行时间
在配置站点页脚的 <root>/themes/shoka/layout/_partials/footer.njk
文件上,选择合适的地方添加如下代码:
<!--swig5--> | |
<div class="create_time"> | |
<span id="timeDate">加载日期...</span> | |
<span id="times">加载时间...</span> | |
<script> | |
var now = new Date(); | |
function createtime() { | |
var grt = new Date(''); | |
now.setTime(now.getTime() + 250); | |
days = (now - grt) / 1000 / 60 / 60 / 24; | |
dnum = Math.floor(days); | |
hours = (now - grt) / 1000 / 60 / 60 - (24 * dnum); | |
hnum = Math.floor(hours); | |
if (String(hnum).length == 1) { | |
hnum = "0" + hnum; | |
} | |
minutes = (now - grt) / 1000 / 60 - (24 * 60 * dnum) - (60 * hnum); | |
mnum = Math.floor(minutes); | |
if (String(mnum).length == 1) { | |
mnum = "0" + mnum; | |
} | |
seconds = (now - grt) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum); | |
snum = Math.round(seconds); | |
if (String(snum).length == 1) { | |
snum = "0" + snum; | |
} | |
document.getElementById("timeDate").innerHTML = " 本站存活 " + dnum + " 天 "; | |
document.getElementById("times").innerHTML = hnum + " 小时 " + mnum + " 分 " + snum + " 秒"; | |
} | |
setInterval("createtime()", 250); | |
</script> | |
</div> | |
<!--swig7--> |
再在主题配置文件 <root>/themes/shoka/_config.yml
中添加:
# Runing Time | |
running_time: | |
enable: true | |
create_time: "01/01/1945 19:00:00" #此处修改你的建站时间或者网站上线时间 |
# SEO 优化及站点收录
辛辛苦苦搭好网站,当然是想跟其他人一起分享博文啦;但是,对于个人博客,如果没有被搜索引擎收录的话,别人在搜索引擎基本上是看不到的。那么如何查看个人博客网站是否被收录?只需要在对应的搜索引擎搜索框上输入:
site:your_website |
eg:
# Google 收录
谷歌收录相对简单,只需要准备一个谷歌账号,然后访问 Google Search Console 如下图:
先登录账号,然后再输入个人博客网站域名。然后弹出验证网站所有权窗口,这里一般选择 CNAME验证
,接着根据提示操作即可,这里就不贴图,并且在你的网站管理那里添加给出来的 DNS 解析;最后等 DNS 更改生效,验证通过即可。完成后进入配置,添加站点地图链接,可能添加完后刷新会显示 无法获取
的状态,但其实是已经配置完成了。
# Bing 收录
进入 Bing Webmaster Tools ,这个可以选择使用 GSC 导入网站,只需要授权一下即可;或者手动添加通过 CNAME验证
添加,如下图:
通过后,同样的添加站点地图链接。
# Baidu 收录
访问 百度搜索资源平台 ,点击 用户中心
-> 站点管理
,然后添加网站,一路到验证通过,这里同样的使用 CNAME验证
即可,当然选择其他的也行,哪种方便用哪种:
验证完毕后,找到 普通收录
,选择 sitemap
,接着添加站点地图链接:
# 看板卡通模型
这里使用 live2d模块
,比较简单。
安装插件 npm install --save hexo-helper-live2d
,选则安装所需的卡通模型:https://github.com/xiazeyu/live2d-widget-models ,例如,安装名为 unitychan 的模型:
npm install live2d-widget-model-unitychan |
配置插件,在 <root>/_config.yml
中添加及更改如下信息:
live2d: | |
enable: true | |
scriptFrom: local # 默认 | |
pluginRootPath: live2dw/ # 插件在站点上的根目录 (相对路径) | |
pluginJsPath: lib/ # 脚本文件相对与插件根目录路径 | |
pluginModelPath: assets/ # 模型文件相对与插件根目录路径 | |
tagMode: false # 标签模式,是否仅替换 live2d tag 标签而非插入到所有页面中 | |
debug: false # 调试,是否在控制台输出日志 | |
model: | |
use: live2d-widget-model-unitychan | |
display: | |
position: right #动画位置 | |
width: 210 | |
height: 380 | |
# 位置配置,这个在左侧边栏位置很居中 | |
hOffset: 50 # 调节水平位置 | |
vOffset: -25 # 调节垂直位置 | |
mobile: | |
show: false # 是否在移动设备上显示 | |
scale: 0.5 # 移动设备上的缩放 | |
react: | |
opacityDefault: 0.7 | |
opacityOnHover: 0.8 |