hexo 添加文章搜索功能

官方有插件 hexo-generator-search,不过这插件有一下几点问题:

  1. 没移除特殊字符串,如标签,html字符,代码等,导致生成的搜索文件很大

  2. 所有文章内容按照 .md 源码文件原样输出到 content 字段,如果要存心拷贝盗取文章,直接复制 content 就可得到文章源码。

  3. 不方便自定义搜索字段

为解决以上问题,根据官方插件源码改造了一下,代码如下

  1. 添加文件 themes/landscape/scripts/generator_search_db.js

    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
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    const { deepMerge, stripHTML } = require('hexo-util');

    hexo.config.search = deepMerge({
    path: 'search.xml',
    field: 'post'
    }, hexo.config.search);

    // 移除 hexo 标签,如 {% xxx %}
    const stripHexoTag = (str) => str.replace(/\{%[\S\s]+?%\}/mg, '');

    // 移除 代码,如 ` ` ` xxx ` ` `
    const stripCode = (str) => str.replace(/```[\S\s]+?```/mg, '');

    // 移除 换行符,如 \n\n
    const stripLineChar = (str) => str.replace(/[\r\n]+?/mg, '');

    // 移除 style 标签内容,如 < style>xxxx</>
    const stripStyle = (str) => str.replace(/<style.*?>.+<\/style>/img, '');

    // 移除 script 标签内容,如 <script>xxxx</script>
    const stripScript = (str) => str.replace(/<script.*?>.+<\/script>/img, '');

    // 移除 超链接,如 [github](https:www.github.com/xxx)
    const stripUrl = (str) => str.replace(/(\[.+?\])\((http)?s?:[^)]+?\)/img, '$1');

    // 移除特殊字符串
    const stripSpecialString = (str) => (
    stripUrl(
    stripCode(
    stripHexoTag(
    stripHTML(
    stripScript(
    stripStyle(
    stripLineChar(str)
    )
    )
    )
    )
    )
    )
    );

    hexo.extend.generator.register('json', function (locals) {
    var config = this.config;
    var searchConfig = config.search;
    var searchField = searchConfig.field;
    var content = searchConfig.content;

    var posts, pages;

    if (searchField.trim() != '') {
    searchField = searchField.trim();
    if (searchField == 'post') {
    posts = locals.posts.sort('-date');
    } else if (searchField == 'page') {
    pages = locals.pages;
    } else {
    posts = locals.posts.sort('-date');
    pages = locals.pages;
    }
    } else {
    posts = locals.posts.sort('-date');
    }

    var res = new Array();
    var index = 0;

    if (posts) {
    posts.each(function (post) {
    if (post.indexing != undefined && !post.indexing) return;
    var temp_post = new Object();
    if (post.title) {
    temp_post.title = post.title;
    }
    if (post.path) {
    temp_post.url = config.root + post.path;
    }
    if (content != false && post._content) {
    temp_post.content = stripSpecialString(post._content);
    }
    if (post.tags && post.tags.length > 0) {
    var tags = [];
    post.tags.forEach(function (tag) {
    tags.push(tag.name);
    });
    temp_post.tags = tags;
    }
    if (post.categories && post.categories.length > 0) {
    var categories = [];
    post.categories.forEach(function (cate) {
    categories.push(cate.name);
    });
    temp_post.categories = categories;
    }
    res[index] = temp_post;
    index += 1;
    });
    }
    if (pages) {
    pages.each(function (page) {
    if (page.indexing != undefined && !page.indexing) return;
    var temp_page = new Object()
    if (page.title) {
    temp_page.title = page.title;
    }
    if (page.path) {
    temp_page.url = config.root + page.path;
    }
    if (content != false && page._content) {
    temp_page.content = stripSpecialString(page._content);
    }
    if (page.tags && page.tags.length > 0) {
    var tags = new Array();
    var tag_index = 0;
    page.tags.each(function (tag) {
    tags[tag_index] = tag.name;
    });
    temp_page.tags = tags;
    }
    if (page.categories && page.categories.length > 0) {
    temp_page.categories = [];
    (page.categories.each || page.categories.forEach)(function (item) {
    temp_page.categories.push(item);
    });
    }
    res[index] = temp_page;
    index += 1;
    });
    }


    var json = JSON.stringify(res);

    return {
    path: searchConfig.path,
    data: json
    };
    });
  2. 修改主题模版 themes/landscape/layout/_partial/header.ejs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    -      <div id="search-form-wrap">
    - <%- search_form({button: '&#xF002;'}) %>
    - </div>
    + <div class="search-form-wrap">
    + <form class="search-form">
    + <input type="search" name="q" class="search-form-input" id="js_site_search_input" placeholder="Search" autocomplete="off">
    + <button type="submit" class="search-form-submit">&#xF002;</button>
    + </form>
    + <div id="js_site_search_result" class="local-search-result">
    + </div
  3. 添加文件 themes/landscape/source/js/search.js

    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
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    // 获取搜索文件
    const getSearchFile = function () {
    return new Promise((resolve, reject) => {
    if (getSearchFile.ret) {
    return resolve(getSearchFile.ret);
    }
    const path = '/search.json';
    $.ajax({
    url: path,
    dataType: 'json',
    success: function (data) {
    getSearchFile.ret = data;
    resolve(getSearchFile.ret);
    },
    error: function () {
    resolve(null);
    },
    });
    });
    };

    // 获取搜搜结果
    const getSearchResult = (key) => {
    const keywords = key.toLowerCase().split(/[\s\-]+/);
    return getSearchFile().then(data => {
    if (!data) {
    return '';
    }
    let str = '<ul class="search-result-list">';
    const beforeLength = 10; // content 关键词之前显示数量
    const contentLength = 30; // content 显示长度
    data.forEach(function ({ title = 'Untitled', content = '', url = '' }) {
    title = title.trim().toLowerCase();
    content = content.trim().toLowerCase().replace(/<[^>]+>/g, '');
    let isMatch = true;
    let indexTitle = -1;
    let indexContent = -1;
    let firstOccur = -1;
    keywords.forEach(function (wd, i) {
    indexTitle = title.indexOf(wd);
    indexContent = content.indexOf(wd);

    if (indexTitle < 0 && indexContent < 0) {
    isMatch = false;
    } else {
    if (indexContent < 0) {
    indexContent = 0;
    }
    if (i == 0) {
    firstOccur = indexContent;
    }
    }
    });
    if (isMatch && content) {
    str += '<li class="search-result-item"><a href=' + url + ' class="search-result-title" target="_blank">' + title + '</a>';
    if (firstOccur >= 0) {
    let start = Math.max(firstOccur - beforeLength, 0);

    let match_content = content.substr(start, contentLength);

    // highlight all keywords
    keywords.forEach(function (wd) {
    const reg = new RegExp(wd, 'gi');
    match_content = match_content.replace(reg, '<em class="search-wd">' + wd + '</em>');
    });

    str += '<p class="search-result-content">' + match_content + '...</p>'
    }
    str += '</li>';
    }
    });
    str += '</ul>';
    if (str.indexOf('</li>') === -1) {
    return '';
    }
    return str;
    });
    };

    const event = () => {
    const $resultContent = $('#js_site_search_result');
    $('#js_site_search_input').on('input', function () {
    const val = $(this).val().trim();
    $resultContent.html('');
    if (val.length <= 0) {
    return;
    }
    $resultContent.html('<div><span class="local-search-empty">正在载入索引文件,请稍后……<span></div>');
    getSearchResult(val).then(ret => {
    if (!ret) {
    return $resultContent.html('<div><span class="local-search-empty">没有找到内容,请尝试更换检索词。<span></div>');
    }
    $resultContent.html(ret);
    });
    });
    };

    export default event;
  4. 引入js并执行,修改文件 themes/landscape/source/js/search.js

    1
    2
    + import search from './search';
    + search();
  5. 添加样式 themes/landscape/source/css/_partial/search.styl

    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
    .local-search-result
    position: absolute
    z-index: 99
    width: 240px
    top: 30px
    right: 0
    .local-search-empty
    color: #888
    line-height: 1.6em
    text-align: center
    display: block
    font-size: 16px
    font-weight: normal
    background: #fff
    padding: 5px 8px
    border-radius: 3px
    ul
    width: 100%
    max-height: 450px
    min-height: 0
    height: auto
    overflow-y: auto
    border: 1px solid color-border
    padding: 10px
    background: #FFF
    box-sizing: border-box
    border-radius: 3px

    // 滚动条样式
    &::-webkit-scrollbar
    width: 5px
    height: 5px
    /*滑块*/
    &::-webkit-scrollbar-thumb
    background-color: #666
    &::-webkit-scrollbar-thumb:hover
    background-color: #999
    /*滑道*/
    &::-webkit-scrollbar-track
    background-color: #ccc

    .search-result-item
    text-align: left
    border-bottom: 1px solid color-border
    padding: 10px 0
    font-weight: normal
    &:last-child
    border-bottom: none
    margin-bottom: 0
    .search-wd
    color: #e58c7c
    .search-result-title
    line-height: 1.4em
    font-size: 16px
    color: color-default
    display block
    ell()
    .search-result-content
    margin-top: 10px
    font-size: 14px
    overflow: hidden
  6. 引入样式,修改文件 themes/landscape/source/css/style.styl

    1
    + @import "_partial/search"
  7. 重启项目,如果不出意外,顶部导航栏输入字符串应该可搜索,效果如下图:

本文由 linx(544819896@qq.com) 创作,采用 CC BY 4.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。本文链接为: https://blog.jijian.link/2020-01-22/hexo-search/

如果您觉得文章不错,可以请我喝一杯咖啡!