Hexo-NexT主题优化记录

Hexo-NexT主题优化记录

NexT主题的常用优化,在网上已经有很多朋友们分享了,常见都有的,我就不再重复造轮子了。在使用的过程中,渐渐的会有一些自己想要的功能和想法。所以在优化的过程中,记录下来,以作备忘。顺便也可以给其他的朋友们做个参考。

本人是做后台服务端的,并不擅长于前端内容。如有错误或不清晰的地方,欢迎批评指正。

  • 我的Hexo-NexT主题的版本是5.1.3

自定义文章更新时间

主题内本身自带了文章更新时间的功能,如果是在本机上做发布的话,用自带应该就可以了。以下基本不用看了。

由于我的博客是做了CI,放在daocloud上。 当持续集成在执行Hexo命令hexo g 生成页面的时候,没改动的文章的更新之间也被重置为了当前时间,导致所有没单独指定updated 值的文章的更新时间都是当前时间。忍不了,看了下db.json 文件,modified 字段是被重置为了当前时间。应该是导致我用不了该功能的原因。

所以想到避开自带功能引用db.jsonmodified 字段的的变量updated 。 自定义个变量updated_at ,来实现文章更新时间的功能。

  • 效果图

  • 步骤

    1. 自定义文件变量updated_at :在md 源文件的头部Front-matter什么是Front-matter?

    2. 修改文章页渲染逻辑,themes\next\layout\_macro\post.swig文件。

      代码

      <div class="post-meta">
      <span class="post-time">
      {% if theme.post_meta.created_at %}
      <span class="post-meta-item-icon">
      <i class="fa fa-calendar-o"></i>
      </span>
      {% if theme.post_meta.item_text %}
      <span class="post-meta-item-text">{{ __('post.posted') }}</span>
      {% endif %}
      <time title="{{ __('post.created') }}" itemprop="dateCreated datePublished" datetime="{{ moment(post.date).format() }}">
      {{ date(post.date, config.date_format) }}
      </time>
      {% endif %}

      {% if theme.post_meta.created_at and page.updated_at %}
      <span class="post-meta-divider">|</span>
      {% endif %}

      {% if theme.post_meta.updated_at and page.updated_at %}
      <span class="post-meta-item-icon">
      <i class="fa fa-calendar-check-o"></i>
      </span>
      {% if theme.post_meta.item_text %}
      <span class="post-meta-item-text">{{ __('post.modified') }}</span>
      {% endif %}
      <time title="{{ __('post.modified') }}" itemprop="dateModified" datetime="{{ moment(page.updated_at).format() }}">
      {{ moment(page.updated_at).format("YYYY-MM-DD") }}
      </time>
      {% endif %}
      </span>
    3. 修改主题配置文件 (_config.yml),将updated_at 设置为true

自定义版权信息

  • 效果图

  • 修改步骤

    1. 打开路径:themes\next\layout\_macro

    2. 修改文件:post-copyright.swig

    3. 参考代码。注:updated_at 字段见上 自定义文章更新时间 中自定义的变量。

      <ul class="post-copyright">
      <li class="post-copyright-author">
      <strong>{{ __('post.copyright.author') + __('symbol.colon') }}</strong>
      {{ post.author | default(config.author) }}
      </li>

      <li class="post-copyright-link">
      <strong>本文标题:</strong>
      <a href="{{ url_for(post.permalink) }}" title="{{ post.title }}">{{ post.title }}</a>
      </li>

      <li class="post-copyright-link">
      <strong>{{ __('post.copyright.link') + __('symbol.colon') }}</strong>
      <a href="{{ post.url | default(post.permalink) }}" title="{{ post.title }}">{{ post.url | default(post.permalink) }}</a>
      </li>

      <li class="post-copyright-date">
      <strong>发布时间:</strong>{{ post.date.format("YYYY年MM月DD日 - HH时mm分") }}
      </li>

      {% if post.updated_at %}
      <li class="post-copyright-date">
      <strong>最后更新:</strong>{{ moment(page.updated_at).format("YYYY年MM月DD日 - HH时mm分") }}
      </li>
      {% endif %}

      <li class="post-copyright-author">
      <strong>联系邮箱:</strong>
      <a href="mailto:lautayfir@163.com">lautayfir@163.com</a>
      </li>

      <li class="post-copyright-license">
      <strong>{{ __('post.copyright.license_title') + __('symbol.colon') }} </strong>
      {{ __('post.copyright.license_content', theme.post_copyright.license_url, theme.post_copyright.license) }}
      </li>
      </ul>

文章链接唯一永久化

Hexo默认的链接生成规则为 :year/:month/:day/:title ,生成出来的链接效果为:https://www.liutf.com/2017/11/14/hello-world/ 这样的。

这样的链接首先在结构中分为了4层,对于SEO中,层级超过两层了,就不容易被收录。于是,我就修改了生成规则为:year:month:day/:title.html,效果为:https://www.liutf.com/20171114/hello-world.html 这样的。

嗯?这个看起来还像这么回事,日期+标题,看起来凑合。初步符合了我预期的逼格。

But?

我的创建日期改了下?或者我的title 改了下呢?生成出来的链接就随之而改变了,生成了一个新的链接。之前的链接就变成了无效链接,404 在向你招手。

于是,我认为给文章链接做唯一化和永久化还是有必要的。不要随之Front-matter中的变量改变而改变了。我们用到的方案是hexo-abbrlink

  • 效果示例

    https://www.liutf.com/1243066710.html

  • 步骤

    1. 安装插件

      npm install hexo-abbrlink --save
    2. 修改根目录配置文件config.yml ,改为:permalink: :abbrlink.html

    3. 配置文件底部添加

      # abbrlink config
      abbrlink:
      alg: crc32 # 算法:crc16(default) and crc32
      rep: dec # 进制:dec(default) and hex
    4. 你也可以设值为其他,看个人喜好。生成效果如下:

      crc16 & hex
      https://post.zz173.com/posts/66c8.html

      crc16 & dec
      https://post.zz173.com/posts/65535.html

      crc32 & hex
      https://post.zz173.com/posts/8ddf18fb.html

      crc32 & dec
      https://post.zz173.com/posts/1690090958.html

注意事项

生成完后,原md文件的Front-matter 内会增加abbrlink 字段,值为生成的ID ,这个字段确保了在我们修改了Front-matter 内的博客标题title 或 创建日期date 字段之后而不会改变链接地址。

首页、正文、畅言评论边框模块化

NexT 主题自带的模板是透明的,总觉得有种模糊感。看到一位美女博主的博客 的设计后,觉得特别好看。于是也照着优化了下。觉得看起来舒服多了。

先看效果:

  • 首页

  • 详情页

  • 评论页

实现步骤

  1. 进入themes\next\source\css\_custom 目录,打开custom.styl 文件

  2. 添加下面自定义样式即可


    .post {
    margin-bottom: 100px;
    padding: 25px;
    -webkit-box-shadow: 0 0 14px #cacbcb;
    -moz-box-shadow: 0 0 14px #cacbcc;
    background: #fff;
    }

    .comments {
    -webkit-box-shadow: 0 0 14px #cacbcb;
    -moz-box-shadow: 0 0 14px #cacbcc;
    background: #fff;
    padding: 25px;
    margin-bottom: 100px;
    margin: 60px 20px 0;
    }

去除Coding Pages的跳转页

托管在Coding Pages 服务上,是银牌会员的话,默认会有一个跳转页。非常恶心。

图中,这是我放置了广告(在底部添加了Hosted by Coding Pages),已经审核通过了。才去掉了跳转页。

添加方法

  1. 进入目录themes\next\layout\_partials ,打开文件footer.swig

  2. 添加代码

    <!-- coding page start -->
    <span class="post-meta-divider">|</span>
    <span>Hosted by <a href="https://pages.coding.me" style="font-weight: bold">Coding Pages</a></span>
    <!-- coding page end -->

    具体位置见图

压缩网站源文件

hexo-filter-cleanup 插件方式

使用hexo-filter-cleanup 插件来压缩网站源文件,十分简单方便。一行安装命令即可搞定。

# 在站点根目录下执行以下命令
npm install hexo-filter-cleanup --save

对比起gulp 的方式,是要简单得多了。但是我托管在DaoCloud 上做CI ,主机用不了这个插件,无奈只能继续用gulp 方式。

gulp方式

  1. 在根目录新建文件gulpfile.js ,和_config.yml 平级。

  2. gulpfile.js 文件添加内容

    var gulp = require('gulp')
    var cleancss = require('gulp-clean-css')
    var uglify = require('gulp-uglify')
    var htmlmin = require('gulp-htmlmin')
    var htmlclean = require('gulp-htmlclean')
    var prettyData = require('gulp-pretty-data')
    var dom = require('gulp-dom')

    gulp.task('css', function () { //處理css
    return gulp.src('./public/**/*.css')
    // .pipe(cleancss({compatibility: 'ie8'}))
    .pipe(gulp.dest('./public'))
    })
    gulp.task('html', function () { //處理html
    return gulp.src('./public/**/*.html')
    .pipe(dom(function () { //這會把每個外連的連結加上rel="noopener noreferrer"(為了安全性)
    var links = Array.from(this.querySelectorAll('a'))
    links.filter(link => link.target === '_blank')
    .forEach(link=>link.rel='noopener noreferrer')
    return this
    }))
    .pipe(htmlclean())
    .pipe(htmlmin({
    removeComments: true,
    minifyJS: true,
    minifyCSS: true,
    minifyURLs: true,
    }))
    .pipe(gulp.dest('./public'))
    })
    gulp.task('js', function () { //處理javascript
    return gulp.src('./public/**/*.js')
    .pipe(uglify())
    .pipe(gulp.dest('./public'))
    })
    gulp.task('xml-json', function () { //處理xml與json(選擇性,如果不要的話就把這一段移除掉並把下面'xml-json'也移除)
    return gulp.src('./public/**/*.(xml|json)')
    .pipe(prettyData({
    type: 'minify',
    preserveComments: true,
    extensions: {
    'xlf': 'xml',
    'svg': 'xml'
    }
    }))
    .pipe(gulp.dest('public'))
    })
    gulp.task('default', [ //執行tasks
    'css', 'html', 'js', 'xml-json'
    ])

  3. 修改package.json 文件,添加build 命令脚本

    {
    "name": "hexo-site",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "build": "hexo clean && hexo g && gulp"
    },
    "hexo": {
    "version": "3.4.1"
    },
    "dependencies": {
    "hexo": "^3.4.2",
    "hexo-baidu-url-submit": "^0.0.5",
    "hexo-generator-archive": "^0.1.4",
    "hexo-generator-category": "^0.1.3",
    "hexo-generator-index": "^0.2.0",
    "hexo-generator-searchdb": "^1.0.8",
    "hexo-generator-tag": "^0.2.0",
    "hexo-renderer-ejs": "^0.3.0",
    "hexo-renderer-marked": "^0.3.0",
    "hexo-renderer-stylus": "^0.3.1",
    "hexo-server": "^0.2.0"
    }
    }

  4. 执行命令

    npm i -S gulp gulp-clean-css gulp-uglify gulp-htmlmin gulp-htmlclean gulp-pretty-data
    gulp-dom --registry=https://registry.npm.taobao.org

    npm run build

    hexo d

    执行npm run build 的时候,就会执行package.json 文件内的hexo clean && hexo g && gulp 命令了。

使用上面方式压缩完后,发现页面里还有一段js 脚本没有压缩到,看起来略不爽。

找了很久,原来是local search 的一段脚本放在localsearch.swig 文件里了,没处理到。

压缩步骤:

  1. 打开路径themes\next\layout\_third-party\search ,文件localsearch.swig

  2. 看到文本内容为未压缩的脚本内容

    {% if theme.local_search.enable %}
    <script type="text/javascript">
    // Popup Window;
    var isfetched = false;
    var isXml = true;
    // Search DB path;
    var search_path = "{{ config.search.path }}";
    if (search_path.length === 0) {
    search_path = "search.xml";
    } else if (/json$/i.test(search_path)) {
    isXml = false;
    }
    var path = "{{ config.root }}" + search_path;
    // monitor main search box;

    var onPopupClose = function (e) {
    $('.popup').hide();
    $('#local-search-input').val('');
    $('.search-result-list').remove();
    $('#no-result').remove();
    $(".local-search-pop-overlay").remove();
    $('body').css('overflow', '');
    }

    function proceedsearch() {
    $("body")
    .append('<div class="search-popup-overlay local-search-pop-overlay"></div>')
    .css('overflow', 'hidden');
    $('.search-popup-overlay').click(onPopupClose);
    $('.popup').toggle();
    var $localSearchInput = $('#local-search-input');
    $localSearchInput.attr("autocapitalize", "none");
    $localSearchInput.attr("autocorrect", "off");
    $localSearchInput.focus();
    }

    // search function;
    var searchFunc = function(path, search_id, content_id) {
    'use strict';

    // start loading animation
    $("body")
    .append('<div class="search-popup-overlay local-search-pop-overlay">' +
    '<div id="search-loading-icon">' +
    '<i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>' +
    '</div>' +
    '</div>')
    .css('overflow', 'hidden');
    $("#search-loading-icon").css('margin', '20% auto 0 auto').css('text-align', 'center');

    $.ajax({
    url: path,
    dataType: isXml ? "xml" : "json",
    async: true,
    success: function(res) {
    // get the contents from search data
    isfetched = true;
    $('.popup').detach().appendTo('.header-inner');
    var datas = isXml ? $("entry", res).map(function() {
    return {
    title: $("title", this).text(),
    content: $("content",this).text(),
    url: $("url" , this).text()
    };
    }).get() : res;
    var input = document.getElementById(search_id);
    var resultContent = document.getElementById(content_id);
    var inputEventFunction = function() {
    var searchText = input.value.trim().toLowerCase();
    var keywords = searchText.split(/[\s\-]+/);
    if (keywords.length > 1) {
    keywords.push(searchText);
    }
    var resultItems = [];
    if (searchText.length > 0) {
    // perform local searching
    datas.forEach(function(data) {
    var isMatch = false;
    var hitCount = 0;
    var searchTextCount = 0;
    var title = data.title.trim();
    var titleInLowerCase = title.toLowerCase();
    var content = data.content.trim().replace(/<[^>]+>/g,"");
    var contentInLowerCase = content.toLowerCase();
    var articleUrl = decodeURIComponent(data.url);
    var indexOfTitle = [];
    var indexOfContent = [];
    // only match articles with not empty titles
    if(title != '') {
    keywords.forEach(function(keyword) {
    function getIndexByWord(word, text, caseSensitive) {
    var wordLen = word.length;
    if (wordLen === 0) {
    return [];
    }
    var startPosition = 0, position = [], 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;
    }

    indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false));
    indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false));
    });
    if (indexOfTitle.length > 0 || indexOfContent.length > 0) {
    isMatch = true;
    hitCount = indexOfTitle.length + indexOfContent.length;
    }
    }

    // show search results

    if (isMatch) {
    // 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;
    } else {
    return itemLeft.word.length - itemRight.word.length;
    }
    });
    });

    // merge hits into slices

    function mergeIntoSlice(text, start, end, index) {
    var item = index[index.length - 1];
    var position = item.position;
    var 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;
    }
    }
    }
    searchTextCount += searchTextCountInSlice;
    return {
    hits: hits,
    start: start,
    end: end,
    searchTextCount: searchTextCountInSlice
    };
    }

    var slicesOfTitle = [];
    if (indexOfTitle.length != 0) {
    slicesOfTitle.push(mergeIntoSlice(title, 0, title.length, indexOfTitle));
    }

    var slicesOfContent = [];
    while (indexOfContent.length != 0) {
    var item = indexOfContent[indexOfContent.length - 1];
    var position = item.position;
    var word = item.word;
    // cut out 100 characters
    var start = position - 20;
    var end = position + 80;
    if(start < 0){
    start = 0;
    }
    if (end < position + word.length) {
    end = position + word.length;
    }
    if(end > content.length){
    end = content.length;
    }
    slicesOfContent.push(mergeIntoSlice(content, start, end, indexOfContent));
    }

    // 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;
    } else {
    return sliceLeft.start - sliceRight.start;
    }
    });

    // select top N slices in content

    var upperBound = parseInt('{{ theme.local_search.top_n_per_article }}');
    if (upperBound >= 0) {
    slicesOfContent = slicesOfContent.slice(0, upperBound);
    }

    // highlight title and content

    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 += '<b class="search-keyword">' + text.substring(hit.position, end) + '</b>';
    prevEnd = end;
    });
    result += text.substring(prevEnd, slice.end);
    return result;
    }

    var resultItem = '';

    if (slicesOfTitle.length != 0) {
    resultItem += "<li><a href='" + articleUrl + "' class='search-result-title'>" + highlightKeyword(title, slicesOfTitle[0]) + "</a>";
    } else {
    resultItem += "<li><a href='" + articleUrl + "' class='search-result-title'>" + title + "</a>";
    }

    slicesOfContent.forEach(function (slice) {
    resultItem += "<a href='" + articleUrl + "'>" +
    "<p class=\"search-result\">" + highlightKeyword(content, slice) +
    "...</p>" + "</a>";
    });

    resultItem += "</li>";
    resultItems.push({
    item: resultItem,
    searchTextCount: searchTextCount,
    hitCount: hitCount,
    id: resultItems.length
    });
    }
    })
    };
    if (keywords.length === 1 && keywords[0] === "") {
    resultContent.innerHTML = '<div id="no-result"><i class="fa fa-search fa-5x" /></div>'
    } else if (resultItems.length === 0) {
    resultContent.innerHTML = '<div id="no-result"><i class="fa fa-frown-o fa-5x" /></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;
    } else {
    return resultRight.id - resultLeft.id;
    }
    });
    var searchResultList = '<ul class=\"search-result-list\">';
    resultItems.forEach(function (result) {
    searchResultList += result.item;
    })
    searchResultList += "</ul>";
    resultContent.innerHTML = searchResultList;
    }
    }

    if ('auto' === '{{ theme.local_search.trigger }}') {
    input.addEventListener('input', inputEventFunction);
    } else {
    $('.search-icon').click(inputEventFunction);
    input.addEventListener('keypress', function (event) {
    if (event.keyCode === 13) {
    inputEventFunction();
    }
    });
    }

    // remove loading animation
    $(".local-search-pop-overlay").remove();
    $('body').css('overflow', '');

    proceedsearch();
    }
    });
    }

    // handle and trigger popup window;
    $('.popup-trigger').click(function(e) {
    e.stopPropagation();
    if (isfetched === false) {
    searchFunc(path, 'local-search-input', 'local-search-result');
    } else {
    proceedsearch();
    };
    });

    $('.popup-btn-close').click(onPopupClose);
    $('.popup').click(function(e){
    e.stopPropagation();
    });
    $(document).on('keyup', function (event) {
    var shouldDismissSearchPopup = event.which === 27 &&
    $('.search-popup').is(':visible');
    if (shouldDismissSearchPopup) {
    onPopupClose();
    }
    });
    </script>
    {% endif %}

  3. 打开oschina 在线JS/CSS/HTML压缩 ,复制<script type="text/javascript"></script> 节点内的脚本内容,将内容进行压缩。

  4. 拷贝压缩后的内容到<script type="text/javascript"></script> 节点内即可。

参考链接

分 享