# 搜索功能

# algolia 搜索功能

需要安装 hexo-algoliasearch 插件。

  1. 注册 algolia,创建 Index:

    image-20220530210920658

  2. 获取 Key,修改站点配置:

    • 新建 API Key:image-20220530212532566

    • 获取相应的 Key,并填入配置信息:

      image-20220530212807371

  3. 在配置完 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 插件。

  1. 修改主题文件夹下的 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,
        }
      }
  2. 在路径 themes\shoka\source\js\_app\ 下,找到 pjax.js 搜索 algoliaSearch() 函数,该函数是用来实现 algolia 搜索的,由于现在要实现本地搜索,因此屏蔽掉该函数,添加并使用实现本地搜索的函数 localSearch()

  3. 实现 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();
        }
      });
    }
  4. 配置信息添加参考:https://www.npmjs.com/package/hexo-generator-search

# valine 评价功能

# 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 对应的值。
tagMetatagColor 中,除了 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 配置手册

以下作相关重要部分补充,以 LeanCloud 国际版为准。

# 自定义环境变量

点击 云引擎 - -WEB - 设置 - 添加 , 添加一些如下变量, 可以用来自定义一些邮件通知、通知模板、消息回复等个性化的设置。

变量示例说明
ADMIN_URLhttps://xxx.example.com/[建议] Web 主机二级域名(云引擎域名),用于自动唤醒
AKISMET_KEYxxxxxxx[可选] 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_EMAILxxxxxx@qq.com[必填] 发件邮箱
SENDER_NAMEuser_name[必填] 发件人
SITE_NAMExxx's blog[必填] 博客名称
SITE_URLhttps://example.com/[必填] 博客首页地址
SMTP_HOSTsmtp.qq.com[可选] SMTP_SERVICE 留空时,自定义 SMTP 服务器地址
SMTP_PASSxxxxxxx[必填] SMTP 登录密码(QQ 邮箱需要获取 SMTP 授权码,参照客户端设置
SMTP_PORT465 或 587,参照 POP3 与 SMTP[可选] SMTP_SERVICE 留空时,自定义 SMTP 端口
SMTP_SECUREtrue[可选] 使用 TLS
SMTP_SERVICEQQ[新版支持] 邮件服务提供商,支持 QQ、163、126、Gmail 以及 更多
SMTP_USERxxxxxx@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 这个域名被弃用了,因而会发生报错提示遇到跨域访问的问题。

解决方法:

  1. 前往 https://console.leancloud.app/
  2. 选择并点击你的应用
  3. 前往 设置 > 应用凭证
  4. 然后你会看到一个 REST API 服务器地址 把那个链接复制起来
  5. 最后把它放入配置档案 valineserverURLs
    serverURLs 通常是这样的 https://[appId 前八位].api.lncldglobal.com ;或者对其进行域名绑定: 设置 -> 域名绑定 -> API 访问域名 -> 绑定新域名 ,然后使用该域名配置到 serverURLs 中。

最后提醒:

请确保你的 appKeyappId 是正确的,这样你的评论功能就能正常使用了。

# 定时唤醒

关于自动休眠的官方说法:点击查看。由于目前 leancloud 发布了 [关于对体验版云引擎定时任务进行适当流控的说明](https://forum.leancloud.cn/t/topic/22595) 一则文章,导致 leancloud 的定时任务使用需要升级至标准版云引擎才能避免休眠;因此,在这里放弃 leancloud 的定时任务,利用 github actions 中的 workflow 定时执行命令访问 leancloud 的 web 域名,即可解决 leancloud 平台因为流控原因无法激活定时唤醒任务的问题:

  1. 主分支上创建 github actions 的 workflow 任务。

  2. 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-->
  3. 添加私钥 DOMAIN (域名):

    仓库 -> settings -> Secrets and variabled -> Actions -> New repository secret

    • Name:DOMAIN
    • Value:你的 web 域名

# 评价头像

  1. 注册:进入 Gravatar 网站,点击左上角菜单里的 Sign In 进行登录 / 注册。

  2. 绑定邮箱及头像:点击 My Gravatar -> Add email address -> Add a new image

  3. 点击 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."

启用则需要找到 footerpost 的两处 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 文件中,添加并替换即可。如下图:

image-20221115121446459

# 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 主题上也有提供,只需要如下调用模板即可:

image-20221115164425176

最后,在需要获取 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:

image-20221126231952585

# Google 收录

谷歌收录相对简单,只需要准备一个谷歌账号,然后访问 Google Search Console 如下图:

image-20221126232456573

先登录账号,然后再输入个人博客网站域名。然后弹出验证网站所有权窗口,这里一般选择 CNAME验证 ,接着根据提示操作即可,这里就不贴图,并且在你的网站管理那里添加给出来的 DNS 解析;最后等 DNS 更改生效,验证通过即可。完成后进入配置,添加站点地图链接,可能添加完后刷新会显示 无法获取 的状态,但其实是已经配置完成了。

# Bing 收录

进入 Bing Webmaster Tools ,这个可以选择使用 GSC 导入网站,只需要授权一下即可;或者手动添加通过 CNAME验证 添加,如下图:

image-20221126234903144

通过后,同样的添加站点地图链接。

# Baidu 收录

访问 百度搜索资源平台 ,点击 用户中心 -> 站点管理 ,然后添加网站,一路到验证通过,这里同样的使用 CNAME验证 即可,当然选择其他的也行,哪种方便用哪种:

image-20221127000022983

验证完毕后,找到 普通收录 ,选择 sitemap ,接着添加站点地图链接:

image-20221127000321081

# 看板卡通模型

这里使用 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
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

夏沫の浅雨 微信支付

微信支付

夏沫の浅雨 支付宝

支付宝