纯JS实现WordPress简单文章目录功能

导语

文章目录这种东西应该是每个(WordPress)站点都具备的功能,尤其是阅读长文章的时候,在用户触手可及的地方提供一个目录,既可以方便用户快捷跳转,又可以快速了解文章概要,是一件一举两得的事情。然而现实情况却不尽人意,大部分CMS只关注文章本身,而把其他部分交给主题和用户来定制,导致这些”其他部分”一直没有一个统一的标准,主题和插件实现的五花八门。加之中外审美的巨大差异,找一个和自己主题搭配且好用的文章目录方案,确实是一件十分困难的事情。

相信折腾到最后,大部分人都会选择使用带文章目录功能的主题,或是文章目录插件。我自己找了一圈,发现大部分插件不是审美严重不同(违和感极强),就是价格昂贵,十分划不来,并且我个人也不喜欢使用不必要插件。

最终,我决定使用纯JavaScript实现一个文章目录功能。这样既不会影响后续更新,也免去了使用不搭配的插件带来的一系列烦恼,实现效果可以参考右侧侧边栏

设计思路

在页面加载时通过JavaScript提取页面中的标题信息,随后创建并插入文章目录。

大致分为三个步骤

  1. 判断是否为文章页面
  2. 获取标题信息
  3. 生成文章目录

文章标题的结构

通常情况下我们使用h1-6标签显示标题,h1标签显示文章标题,因此我们在正文使用的标签就从h2开始。一般标题分级不会超过三级,h2、h3、h4再往后就几乎不用了。观察维基百科的目录结构,一般是显示三级,但我们通常不会写那么长的大型文章,分二级显示就足够了。也就是说,我们只需要获取文章页面的h2、h3标签

获取标题信息

大部分WordPress主题的文章主体部分通常包含在一个div元素中,div里包含着各种文章元素,标题h2 h3、段落p、图片img等等,不过这个div的class是什么由开发者决定。按下你浏览器的F12键,打开审查元素,可以轻松找到该div元素。

文章容器的class是entry-inner

判断是否为文章页面

由于WordPress的限制,我们在不修改模板/使用插件的前提下插入JS代码只能插入到整个网站。不过解决方法也很简单,只要检索页面中有没有文章主体的div即可判断出当前页面是不是文章页面,进而自适应生成目录。

let articleContent = document.getElementsByClassName('entry-inner');
if (articleContent.length !== 1) {
    return null;
}

寻找并记录标题

遍历这个容器的所有元素,找出二级标题h2和三级标题h3即可。为了实现目录层次,我们使用一个数组来记录一个二级标题下的所有三级标题。为了实现跳转功能,我们为每个标题标签分配唯一id

let catalog = [];
let header = {};

let elements = articleContent[0].childNodes;
// 遍历所有元素
for (let i = 0; i < elements.length; i++) {
    if (elements[i].nodeName === 'H2') {
        // 为二级标题分配ID以供锚点跳转,下同
        elements[i].id = 'h2-' + catalog.length;
        // 记录此二级标题和其所有的三级子标题
        header = {
            name: elements[i].innerText,
            childHeaders: []
        };
        catalog.push(header);

    } else if (elements[i].nodeName === 'H3') {
        elements[i].id = 'h2-' + (catalog.length - 1) + '-h3-' + header.childHeaders.length;
        // 记录此三级标题到二级标题下
        header.childHeaders.push(elements[i].innerText);
    }
}

生成文章目录

有了目录信息,并且在上一步已经为标签添加了ID,只需要简单的拼接即可实现自动生成文章目录

let catalog = '<div style="text-align: center; margin-top: 10px;">文章目录</div>';

for (let i = 0; i < catalogData.length; i++) {
    let target = '#h2-' + i; // 跳转目标
    let index = (i + 1) + '. '; // 标题索引
    let name = catalogData[i].name; // 标题
    catalog += '<a href="' + target + '">' + index + name + '</a><br/>';

    for (let i2 = 0; i2 < catalogData[i].childHeaders.length; i2++) {
        target = '#h2-' + i + '-h3-' + i2;
        index = (i + 1) + '.' + (i2 + 1) + '. ';
        name = catalogData[i].childHeaders[i2];
        catalog += '  <a href="' + target + '">' + index + name + '</a><br/>'
    }
}

完整代码

使用时记得根据你的实际情况修改dynamic-wrapperentry-inner两处,目录的样式随意发挥。

为了平滑跳转,可以为htmlbody设置 scroll-behavior: smooth

已添加详细注释,方便修改使用

<script>
    /*
    Author: Azure99
    WebSite: https://www.rainng.com/
    GitHub: https://github.com/Azure99
     */

    let catalogData = getArticleCatalog();
    // 自适应文章目录
    if (catalogData != null) {
        // dynamic-wrapper换成你的目录容器
        let wrapper = document.getElementById('dynamic-wrapper');
        wrapper.innerHTML = generateCatalog(catalogData);
    }

    // 获取本页面的文章目录信息
    function getArticleCatalog() {
        // entry-inner换成你使用主题的文章容器
        let articleContent = document.getElementsByClassName('entry-inner');
        if (articleContent.length !== 1) {
            return null;
        }

        let catalog = [];
        let header = {};

        let elements = articleContent[0].childNodes;
        // 遍历所有元素
        for (let i = 0; i < elements.length; i++) {
            if (elements[i].nodeName === 'H2') {
                // 为二级标题分配ID以供锚点跳转,下同
                elements[i].id = 'h2-' + catalog.length;
                // 记录此二级标题和其所有的三级子标题
                header = {
                    name: elements[i].innerText,
                    childHeaders: []
                };
                catalog.push(header);

            } else if (elements[i].nodeName === 'H3') {
                elements[i].id = 'h2-' + (catalog.length - 1) + '-h3-' + header.childHeaders.length;
                // 记录此三级标题到二级标题下
                header.childHeaders.push(elements[i].innerText);
            }
        }

        return catalog;
    }

    // 根据目录信息生成文章目录代码
    function generateCatalog(catalogData) {
        let catalog = '<div style="text-align: center; margin-top: 10px;">文章目录</div>';

        for (let i = 0; i < catalogData.length; i++) {
            let target = '#h2-' + i; // 跳转目标
            let index = (i + 1) + '. '; // 标题索引
            let name = catalogData[i].name; // 标题
            catalog += '<a href="' + target + '">' + index + name + '</a><br/>';

            for (let i2 = 0; i2 < catalogData[i].childHeaders.length; i2++) {
                target = '#h2-' + i + '-h3-' + i2;
                index = (i + 1) + '.' + (i2 + 1) + '. ';
                name = catalogData[i].childHeaders[i2];
                catalog += '  <a href="' + target + '">' + index + name + '</a><br/>'
            }
        }

        return catalog;
    }
</script>

Azure99

底层码农,休闲音游玩家,偶尔写写代码

看看这些?

6 条评论

  1. 捉急说道:

    有个wordpress网站,文章容器是嵌套起来的,像下面这样,有办法解决吗?直接用大佬的模板实现不了..

    <div data-sek-level="module" data-sek-id="_nimble_422" "class="sek-row sa-dflu"



    ……

  2. Luyee说道:

    非常不错,感谢分享。不过在我添加此HTML代码到自定义的HTML小工具里没法实现,不知道原因。尝试多次修改了对应主题的dynamic-wrapper、entry-inner,也可能是我找的不对导致的。另外我还使用了构建器来编辑文章和页面的,也可能和这个有关。

  3. 鱼丸粗面说道:

    非常好用,谢谢分享。另外说一句,如果顶部有固定导航,要考虑到锚点偏移,不然会被导航遮盖住。

  4. 挖站否说道:

    可以实现渐变颜色吗?我发现阿里云的文档是有这个效果 的。

    • Azure99说道:

      可以是可以,现在想到的一个思路:计算出所有标题的绝对高度,然后监听屏幕位置。不过实现起来应该比较啰嗦,有空写一下。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注