关于 Jekyll TeXt 博客搭建

在此记录一些与 Jekyll TeXt 主题相关的配置细节, 以及问题的解决办法, 不定期更新。

之前用 Hexo 搭 Git Pages 的经历实在是惨痛, 个人对前端没什么研究配置非常痛苦, 尤其是还需要装大量的依赖库尝试解决 图片显示Markdown显示数学公式显示 等问题, Node.js 也非常不省心, 库版本冲突也很严重。 咬咬牙用了一段时间博客园, 不简洁, 不优雅, 不顺我心。 偶然间在知乎上看到了一些关于 Hugo, Jekyll, Hexo 相关的比较, Jekyll 虽然用 Ruby 构建速度稍慢 (实际感觉还挺快), 但关键是 Github 的支持非常友好, 能自动编译并部署网站, 而且相关的社区建设也很不错, 对于只想专注记录并做点代码微调的人而言显得非常良心。 偶然间发现了 TeXt 这款主题, 文档很新而且很全面, 样式也吸引人, 就此入坑了。 前后花了两天去踩坑配自己心仪的样式, 在此记录一些配置细节, 不定期更新。

Warning

_data/variables.yml 中的配置对 _config.yml 会进行覆写有更高的优先级。

给自己的网站增加一个独一无二的 logo 是很多人第一件要做的事, 但这个 logo 尺寸也是有要求的, 例如 Bing 网页抓取的页面不能超过 125 KB。 因而 logo.svg 尺寸尽可能要小, 否则会导致页面尺寸过大出现网页抓取的 SEO 问题。

bootcdn

BootCDN 域名已经迁移到 https://cdn.bootcdn.net/, 以 https://cdn.bootcss.com/ 开头的链接已经全部不可用, 需要在新的 BootCDN 往网站中找到相应的 js 文件进行更新。 需要在 _data/variables.yml 更新这些链接并适当更新版本, 老旧版本可能会造成一些问题。

Info

需要注意的是, mathjax 需要更换为 https://unpkg.com/ 源, BootCDN 源出现 Uncaught ReferenceError: MathJax is not defined

leancloud

使用 leancloud 国际版需要更新 _includes/pageview-providers/leancloud.jsserverURLs 为如下形式。

serverURLs: https://xxxxxxxx.api.lncldglobal.com # 把xxxxxxxx替换成你自己AppID的前8位字符

这个问题是在 valine 的 issue 版面找到的 国际版域名问题 #340

pageview

1. 自动生成文章标识 key

按照文档配置好 leancloud 之后发现文章的 views 一直都没有变化, 后检查文档说明发现需要给每篇文章增加一个 unique key 才能进行阅读量的统计。 找到 _includes/pageview-providers/leancloud/post.html 发现几个 page.key 的变量, 以及 _includes/article-info.html 中有这么一句代码给 data-page-key 进行了赋值。 data-page-keypost.html 用以判断是否对 pageview 进行增加的依据。

<li><i class="far fa-eye"></i> <span class="js-pageview" data-page-key="{{ include.article.key }}">0</span> {{ _locale_views }}</li>

对于 Jekyll 而言, page.id 以及 page.url 都是内置的变量, 且对于一个网页而言, 这两个值都可以作为唯一标识, 其作用与 unique key 的作用别无二致。 那么可以选择其中一个对原有的 page.key 进行替代。 但是, page.id 当时产生了一个问题, 虽然文章中的 views 部分会增加了, 但文章列表中仍然显示为 0, 我怀疑当时将 data-page-key 直接改为 page.id, 并且没有对 / 进行处理有关系。 实际的更改如下所示:

  • post.html

      { page.key } ->  { page.url }
      {{ page.key }} -> {{ page.url | replace:'/', 's' }}
    
  • article.html

      {{ include.article.key }} -> {{ include.article.url | replace: '/', 's' }}
    

该部分有两条在 TeXt theme 的 issues 均有提及

2. Home 界面的多余的 views

开启 pageview 之后在 Home/Archive/About 界面的右上角也能看到 views 这个字样, 感觉非常多余, 其实只需要在上述的那句 data-page-key 的代码前加一些判断, 让这个字样在 Home/Archive/About 界面不显示即可。 对于 Archive 以及 About 两个界面, 需要判断 page.url != '/archive.html' 以及 page.url != '/about.html', 但对于 Home 界面就有些麻烦, 这些 views 在 Home 界面是全关联的, 如果右上角的 views 消失了, 后面的 article list 中的也会跟着消失, 所以需要区分 Home 界面本身的 article 和我们的在 _posts 文件夹下的 article。

发现 home.html 实际也是一种特殊的 post, 只是它的 title 被认为设置隐藏了。 所以, 只需要给这篇特殊的文章设定 pageview: false 就能让它的 pageview 字样消失而不影响 Home 界面其他文章的 pageview 字样的显示。

后来想想对于 Archive 和 About 这两个界面而言也是同样的道理, 都应该能用 pageview: false 将 pageview 字样去除。 实践证明确实可行!

<!--  article-info.html -->
{%- if page.layout != "404" -%}
    {%- if page.url != '/about.html' and page.url != '/archive.html' -%}
        <li><i class="far fa-eye"></i> <span class="js-pageview" data-page-key="{{ include.article.url | replace: '/', 's' }}">0</span> {{ _locale_views }}</li>
    {%- endif -%}
{%- else -%}
    <li><i class="far fa-eye"></i> 404 {{ _locale_views }}</li>
{%- endif -%}

gitalk

从博客园换成 Git Pages 的另外一个原因是评论系统的便利性, Github 账号登陆省去了评论需要重新创建账号的麻烦。 尝试过使用 valine 作为评论系统, 虽然可以和 leancloud 公用 key, 但界面有些复杂不简洁不如 gitalk 赏心悦目。

Note

gitalk 在 2023-05 时的最新版本是 1.7.2, 使用前记得在 _include/variable.yml 中更新 bootcdn 地址。

1. 让 gitalk 摆脱 page.key

这里就不能直接改成把 _includes/comments-providers/gitalk.html 中的 page.key 直接改成 page.url, 不然在 Home 和 About 这两个界面也会出现评论框, 我改成了 page.id 就神奇的解决了问题。

另外在 issue 版面我看到一条 发布的新文章提示“未找到相关的 Issues 进行评论,请联系xxx初始化创建” #63 的问题, 如果自己是管理员的话登陆一下就能自动创建评论区解决问题。 关键的是这一句, 文章链接过长 (超过 50 个字符) 也会导致创建失败出现 Error: Validation Failed., 按照 Prevent comments initialize issue 中的记录, 只需要将 page.id 改为 page.id | truncate: 50, '' 即可解决问题。

2. Error: Request failed with status code 403

这个问题就是 gitalk 被墙了, 加一个 proxy_config.yml 就行, 但也要记得同步更新 gitalk.html 中的配置。 proxy 的值如何获取我找到了 解决 Gitalk 无法获取 Github Token 问题 文章, fork 这个作者的仓库然后按照他文中给的步骤注册 netlify账号并编译部署 cors-server 仓库, 它会生成一个 [https://xxxx.netlify.app] 的 site 地址, 如果在网页中输入 [https://xxxx.netlify.app/github_access_token] 会出现 a cors proxy by netlify! 那就是成功了。 proxy 部分用这个地址即可。 懒的话直接拿这个作者已经建好的 github_access_token 也是可以的。

vercel: https://vercel.prohibitorum.top/github_access_token
netlify: https://strong-caramel-969805.netlify.app/github_access_token

3. Error: u.concat(…).join is not a function

在 gitalk repository 的 issue 界面就可以找到这条问题的处理办法, Error: u.concat(…).join is not a function! #114_config.yml 配置界面给 gitalk 加上如下代码。

gitalk:
  labels:
    - gitalk

另外在 _includes/comments-providers/gitalk.html 中进行修改。

const gitalk = new Gitalk({
    // ...
    labels: '{{ theme.gitalk.labels }}'.split(',').filter(l => l),
    //...
});
gitalk.render('gitalk-container');

site exposure

注册 google analytics 并且配置好 _config.yml 文件中的 analytics 部分的信息, 会发现 google analytics 好像并没有开始工作。 实际上是 google 并没有将网站纳入搜索索引中, 在 google 搜索中输入 site:YOUR_GIT_PAGES_ADDRESS 会发现没有任何内容。 要么等着 google 的搜索引擎来抓网页, 要么就自己主动先进行配置申请。

让Google搜索到用Jekyll搭建在Github Pages上的博客
自己建网站怎么添加Google Analytics统计代码查看每日流量

1. sitemap

站点地图(Site Map)是用来注明网站结构的文件,我们希望搜索引擎的爬虫了解我们的网站结构,以便于高效爬取内容,快速建立索引。 使用 XML-Sitemaps 生成我们的 Git Pages 的 sitemap.xml 文件, 然后将其放在自己网站文件的根目录。 之后就需要在对应的 Google Search Console 中增加 sitemap URL 进行验证。 sitemap.xml 需要在我们增添新的博文或内容后手动进行更新, 以使得搜索引擎能够获取网站的最新内容。 或者通过 jekyll-sitemap 插件自动生成更新。

2. Google Search Console

资源类型选择 “网站前缀”, 提交我们的 sitemap.xml 并对需要的网址进行网站检查, 然后申请编入索引。

3. Bing Webmasters

如果想要在 Bing 中增加自己的网站索引也是类似的操作, 只是我们需要用到 Bing Webmaster Tools。 如果用 Google Search Console 顺利的话, 可以用同一个 Google 账号导入相关的信息, 但 sitemap.xml 验证好像还是需要自己添加。

4. URL 上传搜索引擎索引

这篇 文章 介绍了如何通过 API 接口提交 URL 申请, 以加快文章列表被搜索引擎的收录。

  • 生成 URL 列表手动递交

    在提交 URL 之前需要准备好 URL 列表, 形如

      http://www.your-site.com/1.html
      http://www.your-site.com/2.html
    

    我们可以手动通过 XML-Sitemaps 网站生成 sitemap, 或者通过 Github 支持的 jekyll-sitemap 插件在部署后自动生成, 这个插件可以在 _config.yml 中看到是已经被添加了的。 那么之后就可以用以下 Linux 命令提取 sitemap.xml 文件中的 URL, 筛选我们最新添加的部分。

    可以使用诸如 compareit 这类插件进行更高效的对比筛选

  • API 推送递交

    Bing 以及 Baidu 都提供了 API 接口进行提交。 根据 Bing Webmasters 以及 Baidu linksubmit 提供的 API 结构, 可以更改如下 commit_urls.py 的相应信息, 使用 python commit_urls.py 提交网站, 该办法是通过 Git Log 获取最新一条更改信息, 并将其中与 URL 对应部分进行裁剪组合获取所需的 URLs。

    commit_urls.py
      import os
      import subprocess
      import requests
      import json
    
      def commit_urls():
          print("Submit newest URLs to Baidu and Bing")
          os.system("git checkout master")
          urls = []
    
          # 生成url列表
          ret = subprocess.run(
              "git rev-parse --short HEAD", stdout=subprocess.PIPE, stderr=subprocess.PIPE
          )
          if ret.returncode == 0:
              commit_id = str(ret.stdout, "utf_8").strip()
              ret = subprocess.run(
                  "git show --pretty=" " --name-only " + commit_id,
                  stdout=subprocess.PIPE,
                  stderr=subprocess.PIPE,
              )
              if ret.returncode == 0:
                  changes = str(ret.stdout, "utf-8").split("\n")
                  for change in changes:
                      ''' 使用 Jekyll 主题直接上传源码至 Git Pages 版本
                      # change[7:] 去掉最前面的 _post/
                      change = change[7:]
                      post_name = change.split("-")
                      if len(post_name) > 3:
                          year = str(post_name.__getitem__(0))
                          month = str(post_name.__getitem__(1))
                          day = str(post_name.__getitem__(2))
                          if len(year) == 4 and int(year) >= 2023:
                              if len(month) == 2 and int(month) >= 1 and int(month) <= 12:
                                  if len(day) == 2 and int(day) >= 1 and int(day) <= 31:
                                      if change.endswith(".md"):
                                          # change[:-3] 是为了去掉末尾的 .md
                                          prefix = year + "/" + month + "/" + day + "/"
                                          article_path = prefix + change[len(prefix):-3]
                                          urls.append("https://your-site.com/{}".format(article_path))
                      '''
                      # 文章为 .html 结尾的编译后的静态网站, 自行更改相关配置
                      if change.endswith(".html"):
                          # change[:-5] 是为了去掉末尾的 .html
                          urls.append("https://your-site.com/{}".format(change[:-5]))
              else:
                  print("subprocess run error:{}".format(ret.stderr))
          else:
              print("subprocess run error:{}".format(ret.stderr))
    
          print("Current submitted URLs:", urls)
    
          # 提交到 Bing
          headers = {
              "Content-Type": "application/json; charset=utf-8",
              "Host": "ssl.bing.com",
          }
          data = {"siteUrl": "https://your-site.com", "urlList": urls}
          response = requests.post(
              url="https://www.bing.com/webmaster/api.svc/json/SubmitUrlbatch?apikey=your-apikey",
              headers=headers,
              data=json.dumps(data)
          )
          print("Bing's response: ", response.content)
    
          # 提交到百度
          headers = {
              "User-Agent": "curl/7.12.1",
              "Host": "data.zz.baidu.com",
              "Content-Type": "text/plain"
          }
          response = requests.post(
              url="http://data.zz.baidu.com/urls?site=your-site.com&token=your-token",
              headers=headers,
              data="\n".join(urls)
          )
          print("Baidu's response: ", response.content)
    

    如果成功则会有类似如下的打印

      Submit newest URLs to Baidu and Bing
      Switched to branch 'master'
      Your branch is up to date with 'origin/master'.
      Current submitted URLs: ['https://hangx-ma.github.io/2023/05/18/Jekyll-TeXt-config']
      Bing's response:  b'{"d":null}'
      Baidu's response:  b'{"remain":99,"success":1}'
    

复制博客内容的时候增加版权信息感觉也是挺有用的, 需要确认版权声明的位置, 对于文章而言, 主题内容肯定在 content 中。 F12 使用开发者工具可以确定 TeXt 主题的文章主题内容是在 <div class="page__content"> 中。 我在 _includes/custom 中创建了一个 copyright.js 监听版权声明, 之后只需要在 _layouts/page.html 中引用该 js 文件即可。 当然为了灵活处理, 可以在 _config.yml 增加 copyright 的开关并在引用时加入条件语句。

copyright.js
function setClipboardText(event){
    // clipboardData 对象是为通过编辑菜单、快捷菜单和快捷键执行的编辑操作所保留的,也就是你复制或者剪切内容
    let clipboardData = event.clipboardData || window.clipboardData;
    // 如果未复制或者未剪切,则return出去
    if (!clipboardData) { return; }
    event.preventDefault();
    // Selection 对象,表示用户选择的文本范围或光标的当前位置。
    //     声明一个变量接收 -- 用户输入的剪切或者复制的文本转化为字符串
    let text = window.getSelection().toString();

    if (text) {
        // 如果文本存在则先取消文本默认事件
        event.preventDefault();
        // 通过调用常clipboardData对象的 setData(format, data) 方法;来设置相关文本
        // format: 一个DOMString 表示要添加到 drag object的拖动数据的类型。
        // data: 一个 DOMString表示要添加到 drag object的数据。
        var copyright = '\n\n'
        + '\nAuthor: HangX-Ma(一只豆腐)'
        + '\nEmail: m-contour@qq.com'
        + '\nArticle Address: ' + window.location.hostname + window.location.pathname
        + '\nCopyright Notice: '
        + 'The copyright belongs to the author. '
        + 'For commercial reproduction, please contact the author for authorization. '
        + 'For non-commercial reproduction, please indicate the source.'

        clipboardData.setData('text/plain', text + copyright);

    }
};
var contents = document.getElementsByClassName("page__content");
// 监听文章内容的copy事件
contents[0].addEventListener('copy',function(e){
    setClipboardText(e);
});
  • 通过 F12 开发者工具查阅自己的网站主题内容所属位置。

      <!-- F12 查阅地址 -->
      <body>
      <div class="root" data-is-touch="false">
          <div class="layout--page js-page-root">
          <div class="page__main js-page-main page__viewport cell cell--auto">
              <div class="page__main-inner">
              <div class="page__header d-print-none">...</div>
              <div class="page__content">...</div>
              <div class="page__header d-print-none">...</div>
              </div>
          </div>
      </div>
      </body>
    
  • copyright.js 监听 javascript 文件在 head 中引用。

      <!-- _layouts/page.html 底部增添 copyright.js -->
      {%- if page.copyright -%}
      <script>
      {%- include custom/copyright.js -%}
      </script>
      {%- endif -%}
    

新手如何给Hexo博客在复制时添加版权声明

markdown

1. 代码块 Liquid 代码误解析

如果页面是通过Jekyll引擎进行渲染的, 那么在文章中写了 Liquid 代码, 引擎也会将其解析。 找了一圈发现最方便的是针对块内容进行特殊的解释。 这样在 {% raw %} 以及 {% endraw %} 之间的代码就不会被解析了, 但也需要注意空白符此时也会被作为 raw 的一部分显示在代码块中。

{% raw %}
{% this %}
{% endraw %}

作为替代,如果 Jekyll 的版本在 4.0 及以上,也可以在 post 顶部的 YAML 区域指定:

---
render_with_liquid: false
---

设置Markdown中展示Liquid(Jekyll)但不解析的方式
Jekyll 代码块展示

2. 代码块折叠

{::options parse_block_html="true" /}

<details><summary markdown="span"> `commit_urls.py` <i class="fas fa-file-code" style="color: #ff4040;"></i></summary>

YOUR_CODE_HERE

</details>

{::options parse_block_html="false" /}

Collapsible Code Blocks in GitHub Pages

个性化配置

1. Back To Top

给网页增加返回顶部的功能能提供阅读的便利, 尝试过很多自己添加 js 代码的方式都没成功, 或者是中间没有过渡动画。 Github 上的一个开源的库倒是非常好用, 配置也很简单。 我在 _layouts/base.html 的 body 部分增加了配置代码, 毕竟所有的页面都是继承自 base, 这样这个 Back-To-Top 按钮就能全局可见了。

<!-- _layouts/base.html -->
<body>
    ...
    <script src="https://unpkg.com/vanilla-back-to-top@7.2.1/dist/vanilla-back-to-top.min.js"></script>
    <script>addBackToTop({
        diameter: 30,
        backgroundColor: '#FF4040',
        textColor: '#FFFFF0',
    })</script>
</body>

vfeskov/vanilla-back-to-top: Simple and tiny Back To Top button with no dependencies.

2. 隐藏 AddToAny 悬浮按钮

摸索了有一阵子, 发现 AddToAny 的按钮实际上是通过 <a class="a2a_button_copy_link"></a> 这样的语句添加的, 那我们只需要对这些语句段进行控制就能控制 AddToAny 的按钮的隐藏和显示了。 这对于手机阅读非常重要, 读者也不希望一个特占位置的分享按钮影响阅读体验, 把它缩小成一个小三角就很优雅了。

主要通过切换 display: blockdisplay: none 两种状态完成显示和隐藏。

3. 访客与阅读统计

用 “不蒜子”, 非常简易! 只需要在 _includes/footer.html 中加入如下代码, 就能获得和咱一样的显示效果了。

    <div class="site-info mt-2">
      ...
      <div align="center">
        <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
        <span id="busuanzi_container_site_pv">
            Total <span id="busuanzi_value_site_pv"><i class="fa fa-spinner fa-pulse"></i></span> views,
        </span>
        <span id="busuanzi_container_site_uv">
            <span id="busuanzi_value_site_uv"><i class="fa fa-spinner fa-pulse"></i></span> visitors.
        </span>
      </div>
    </div>

4. 个性化页签

个性化 Tab 标签。 相信很多人都看过 Tab 标签显示突然崩溃, 然后点回去又是好的。 我创建了 _include/custom/funny-title.js 文件, 并在 _layouts/base.html 中以 script 的形式引用了该文件。 这样我的网站的 Tab 也能跟读者开个小小的玩笑了 :stuck_out_tongue_closed_eyes:

// funny-title.js
(function () {
    var OriginTitle = document.title;
    var titleTime;
    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            $('[rel="icon"]').attr('href', "/favicon.ico");
            document.title = '╭(°A°`)╮ 页面崩溃啦 ~';
            clearTimeout(titleTime);
        }
        else {
            $('[rel="icon"]').attr('href', "/favicon.ico");
            document.title = '(ฅ>ω<*ฅ) 噫又好啦 ~' + OriginTitle;
            titleTime = setTimeout(function () {
                document.title = OriginTitle;
            }, 2000);
        }
    });
})();

5. 增加 last_modified 标签

想给有些文章加上 最后的修改日期 以方便标识和记忆, 但这个主题的 last_date 的功能是在文章末尾添加一句最后修改日期, 我想自己加一个和文章日期非常像的样式, 也在文章列表中显示出来的那种。 既然之前操作过 pageview, 已经知道是在 article-info.html 修改文章样式, 那就模仿已有格式增加。

  • 增加 _show_last_modified 标识以判断是否显示最后修改日期。

      {%- assign _show_last_modified = include.article.last_modified -%}
    
  • 增加新的文章信息。

      {%- if _show_author or _show_date or _pageview or _show_last_modified -%}
        {%- if _show_last_modified -%}
          <li>
          {%- include snippets/get-locale-string.html key='ARTICLE_DATE_FORMAT' -%}
          <i class="far fa-edit"></i> <span>{{ include.article.last_modified | date: __return }}</span>
          </li>
        {%- endif -%}
      {%- endif -%}
    
  • 最后只要在想增加该信息的文章的头部增加如下样式信息就能显示了。

      - last_modified: 2023-05-20 # 需要和日期的格式一致
    

6. 增加 jekyll-seo-tag

按照官网给的指导书就行 jekyll/jekyll-seo-tag

7. 一键代码复制

文章倒是没咋看, 主要看 PR 的 commit 部分更改了啥自己加上了, 感觉效果还挺不错的。

copy-to-clipboard.js
(function() {
    // This script is built on clipboard.js, cf: https://clipboardjs.com/
    // 1. find all code blocks defined under snippet class
    var snippets = document.querySelectorAll('pre');
    [].forEach.call(snippets, function(snippet) {
        if (snippet.closest('.copyable') !== null) {
        snippet.firstChild.insertAdjacentHTML('beforebegin', '<button class="btn" data-clipboard-snippet><i class="far fa-copy"></i></button>');
        }
    });
    var clipboardSnippets = new ClipboardJS('[data-clipboard-snippet]', {
        target: function(trigger) {
            return trigger.nextElementSibling;
        }
    });
    // ref: https://www.freecodecamp.org/chinese/news/copy-text-to-clipboard-javascript/
    clipboardSnippets.on('success', function(e) {
        e.clearSelection();
        var res = "";
        var regex_terminal = /^\$\s{2}/g;
        var split_text = e.text.split('\n');
        split_text.forEach((element, index) => {
            // replace heading '$' and space
            if (regex_terminal.test(element)) {
                res += element.replace(regex_terminal, '') + "\n";
            } else {
                // replace heading number and space
                res += element.replace(/^([1-9]\s{3}|[1-9][0-9]\s{2}|[1-9][0-9][0-9]\s{1})/g, '') + "\n";
            }
        })
        navigator.clipboard.writeText(res).then(() => {
            showTooltip(e.trigger, 'Copied!');
        },() => {
            showTooltip(e.trigger, fallbackMessage(e.action));
        });
    });
    clipboardSnippets.on('error', function(e) {
        showTooltip(e.trigger, fallbackMessage(e.action));
    });

    // 2. add event listener for all created copy button
    var btns = document.querySelectorAll('.btn');
    for (var i = 0; i < btns.length; i++) {
        btns[i].addEventListener('mouseleave', clearTooltip);
        btns[i].addEventListener('blur', clearTooltip);
        btns[i].addEventListener('mouseenter', showToolhint);
    }

    function clearTooltip(e) {
        e.currentTarget.setAttribute('class', 'btn');
        e.currentTarget.removeAttribute('aria-label');
    }

    function showTooltip(elem, msg) {
        elem.setAttribute('class', 'btn tooltipped tooltipped-s');
        elem.setAttribute('aria-label', msg);
    }

    function showToolhint(e) {
        e.currentTarget.setAttribute('class', 'btn tooltipped tooltipped-s');
        e.currentTarget.setAttribute('aria-label', 'copy to clipboard');
    }

    function fallbackMessage(action) {
        var actionMsg = '';
        var actionKey = (action === 'cut' ? 'X' : 'C');
        if (/iPhone|iPad/i.test(navigator.userAgent)) {
            actionMsg = 'No support :(';
        } else if (/Mac/i.test(navigator.userAgent)) {
            actionMsg = 'Press ⌘-' + actionKey + ' to ' + action;
        } else {
            actionMsg = 'Press Ctrl-' + actionKey + ' to ' + action;
        }
        return actionMsg;
    }
})();

为博客添加代码块一键复制功能 - Yuze Zou
feat: copy to clipboard for code blocks #218

8. 代码自动换行与行号设置

8.1 自动换行

_sass/custom.scss 中加入如下代码即可, 但一般不建议使用, 代码多了容易臃肿混乱。

/* auto wrap code */
pre {
    white-space: pre-wrap;
    word-wrap: break-word;
}

让pre标签的代码块自动换行

8.2 代码行号设置

按这种方法可以对常规的 Markdown 的代码段自动显示行号, 在 _config.yml 中加入以下代码即可。 但这种方式会在 code 外层套上一个 table 的框, 不美观。

# More › http://kramdown.gettalong.org/quickref.html
# Options › http://kramdown.gettalong.org/options.html
kramdown:
  input:          GFM
  # https://github.com/jekyll/jekyll/pull/4090
  syntax_highlighter: rouge

  # Rouge Highlighter in Kramdown › http://kramdown.gettalong.org/syntax_highlighter/rouge.html
  # span, block element options fall back to global
  syntax_highlighter_opts:
    # Rouge Options › https://github.com/jneen/rouge#full-options
    default_lang: c
    css_class: 'highlight'
    #line_numbers: true # bad idea, spans don't need linenos and would inherit this option
    span:
      line_numbers: false
    block:
      line_numbers: true
      start_line: 1

Enabling line numbers with rouge #4619
Jekyll 代码块展示

偶然间发现了一个 jekyll-code-style 的仓库, 作者用 js 实现了对行号的加载, 对 Terminal 类型的代码则在前添加 ~$ 符号。 对于前端小白而言, 这几个功能真的是直击我心。 如此一来, 就不用管 Jekyll 原生的 highlight 语法导致无法正常缩进代码块的问题, 使用的时候也按照原来的 Markdown 的代码块风格, 减少了使用者的负担。 更重要的是, 这与我之前添加的代码一键复制的功能不冲突, 这是 Jekyll 的代码渲染所做不到的。

Conflict (2023-12-03)

而后测试发现, 如果使用这套脚本, 虽然在样式上能够表现得比较完美, 手动复制时也不会选中行号, 但使用 clipboard.js 进行一键复制则会出现行号被拷贝的情况。 究其原因, 这个 js 脚本是重构了文本内容, 在文本前添加了特殊的字符串。 我尝试过自己重构一个类似 clipboard.js 的脚本, 但是个人功力不足, 总有一些不合心意的地方, 而这个打包好的脚本也没有提供更改结果字符串的接口。 因而, 我参考了 CSDNBilibili - 给你的网页代码框添加一个行号吧!!! 的写法, 利用 ulli 元素实现了行号, 但由于没有利用更为合适的 table, 行对齐显得极为困难, 调参花费了不少时间, 并且容易在缩放时产生问题。

Solver (2023-12-04)

经过尝试, 也找到了解决思路, 我发现 clipboard.js 会在最后暴露一个 Event 接口, Event.text 存储了拷贝的结果。 既然有这个结果, 就能够通过正则表达式匹配去删除之前添加在每行开头的数字或美元符号以及多余的空格了。 于是我更改了 copy-to-clipboard.js 文件中的一些内容, 使得目前的拷贝支持 1000 行以下的数据。 虽然这种办法需要重写剪贴板, 但好在博客的中的代码量并没有太多, 而且重写剪贴板有一个意外之喜, 这番更改将原来复制增加的 Copyright 内容给去掉了, 这样拷贝代码的时候就能直接使用了! 相关代码可以看前面的 7.一键代码复制

相关的代码就参照仓库的指导, 把 put-code-elements.js 脚本文件放在 <head></head> 块之间:

  <head>
    <script type="text/javascript" src='{{ "/js/put-code-elements.js" }}'></script>
  </head>

如果你参照了我之前的配置希望用 {:.copyable} 给想要的代码块增加一键复制的按钮, 还需要对这个脚本文件以及与 highlight syntax 相关的文件进行一些更改:

  • put-code-elements.js 中需要添加对 .copyable class 的筛选, 增加拷贝按钮之后 children 的起始位置需要向后延一个, copy button 占用了一个位置。

    window.addEventListener('DOMContentLoaded', () => {
      var codeBlocks = document.querySelectorAll("pre.highlight")
      codeBlocks.forEach((element) => {
        var self = element.children[0]
        var highlighterClassName = element.parentElement.parentElement.className
    
        // Added By HangX-Ma(2023-12-01): fix copy-to-clipboard element influence
        if (element.closest('.copyable') !== null) {
            self = element.children[1]
        }
        ...
      })
    })
    
  • _highlight.scss 这部分是这个 Jekyll 主题的 highlight syntax 文件, 当然如果不想自定义的话可以直接用代码仓库提供的 jekyll-code-style.css 文件。 可以看到样例给的 language 出现的地方在最右侧, 这会与 copy button 出现重合。 可以通过更改此处的代码调整位置, 其他的属性可以自定义。

    /* Language parser */
    pre.highlight::before {
      margin-top: -15px;
      margin-right: 20px;
      float: right;
    }
    

我个人用了另一种左对齐的样式规避了这个问题, 另外由于 language 可能会对代码产生遮挡, 可以选择在 code 区块增加 margin-top 解决该问题。

  code {
    margin-top: 10px;
  }

9 滚动条设置

9.1 取消滚动条但不影响滚动

使用 ::webkit-scrollbar 伪元素选择器,不过这个选择器只在 webkit 核心的浏览器中有效,例如 Chrome、新 Edge、Safari 等。::web-kit-scrollbar 可以直接选择滚动条元素,把它的 display 属性设置为 none 就可以隐藏滚动条了:

// custom.scss
::-webkit-scrollbar {
  display: none;
}

main {
  width: 100vw;
  height: 100vh;
}

section {
  width: 100%;
  height: 100%;
}

CSS 如何隐藏滚动条,但不影响内容滚动

9.2 更优雅的滚动条

按照上述设置可以取消滚动条的显示, 但对用户的使用产生了一定的障碍, 很可能用户不会注意到滚动条的存在而遗漏更多的被隐藏的内容。 因而可以对代码块部分的滚动条进行定制化, hover 和 normal 两种情况显示不同的颜色, 并让底色与代码块保持一致。

/* scroll bar */
pre {
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  &::-webkit-scrollbar {
    display: inline-flexbox;
    height: 5px;
    width: 5px;
    background-color: #282c34;
    border-radius: 5px;
  }
  /* Handle */
  &::-webkit-scrollbar-thumb {
    background: #555;
    border-radius: 5px;
  }

  /* Handle on hover */
  &::-webkit-scrollbar-thumb:hover {
    background: #888;
    border-radius: 5px;
  }
}
# This is a test
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean interdum, ligula in ultrices sodales, ante enim scelerisque diam, nec molestie lorem nulla sit amet dolor. Aenean id augue ante. Duis ut mi faucibus, pellentesque sem quis, gravida nisi. Nam cursus.        -- Mark Twain

我的代码块也能显示行数,滑动,高亮和一键复制 - hoyouly

10. 提示框优化

对于这套 Jekyll 主题自带的诸如 {:.info}{:.error} 等改变文字的 background 以及 border color 的形式, 我一直想找一个替代品, 偶然间发现了 H2O-ac 这套主题, 其中介绍了 lazee/premonition 这个库, 能够通过简易的设置改变原有的 Quote 的形式, 实现多用途且兼具美观的目的。

Note

The body of the note goes here. Premonition allows you to write any Markdown inside the block. [Link]

Info

The body of the info goes here. Premonition allows you to write any Markdown inside the block. [Link]

Warning

The body of the warning goes here. Premonition allows you to write any Markdown inside the block. [Link]

Error

The body of the error goes here. Premonition allows you to write any Markdown inside the block. [Link]

Look, this is a Citation style. [Link]

------ Citation

当然配置上不是特别简易, premonition 库的 svg 不能够在这款 Jekyll theme 正常显示, 如果可以正常显示就直接按照上述仓库的指导配置即可。 如果不行, 按照如下方式进行配置(魔改自 zhonger/jekyll-theme-H2O-ac):

  • 按照仓库所给指导, 在 Gemfile_config.yml 处增添 premonition 插件, 并用 bundle install 进行安装。
  • _sass/additional 目录下增加 _premonition.scss 文件, 拷贝如下代码:

    _premonition.scss
    // https://github.com/zhonger/jekyll-theme-H2O-ac/blob/551145e14e46ab62cfdca5eb2dc90510b98f8e02/dev/sass/premonition.scss
    $color-info-dark: #009400;
    $color-info-darker: #008b00;
    $color-info-darkest: #007300;
    $color-info-light: #26b226;
    $color-info-lighter: #4dbf4d;
    $color-info-lightest: #80d280;
    $color-info-contrast-background: #e6f6e6;
    $color-info-contrast-foreground: #003100;
    $color-info-code-background: rgba(0,164,0,.15);
    $color-note-dark: #4cb3d4;
    $color-note-darker: #47a9c9;
    $color-note-darkest: #3b8ba5;
    $color-note-light: #6ecfef;
    $color-note-lighter: #87d8f2;
    $color-note-lightest: #aae3f6;
    $color-note-contrast-background: #eef9fd;
    $color-note-contrast-foreground: #193c47;
    $color-note-code-background: rgba(84,199,236,.15);
    $color-warning-dark: #e6a700;
    $color-warning-darker: #d99e00;
    $color-warning-darkest: #b38200;
    $color-warning-light: #ffc426;
    $color-warning-lighter: #ffcf4d;
    $color-warning-lightest: #ffdd80;
    $color-warning-contrast-background: #fff8e6;
    $color-warning-contrast-foreground: #4d3800;
    $color-warning-code-background: rgba(255,186,0,.15);
    $color-error-dark: #e13238;
    $color-error-darker: #d53035;
    $color-error-darkest: #af272b;
    $color-error-light: #fb565b;
    $color-error-lighter: #fb7478;
    $color-error-lightest: #fd9c9f;
    $color-error-contrast-background: #fdf7f7;
    $color-error-contrast-foreground: #4b1113;
    $color-error-code-background: rgba(250,56,62,.15);
    $color-citation-darkest: #495057;
    $color-citation-contrast-background: #f8f9fa;
    $color-citation-contrast-foreground: #323940;
    $color-citation-code-background: #fdfdfe;
    
    .premonition {
        p {
            margin-bottom: 0;
        }
        &.note, &.info, &.warning, &.error, &.citation{
            border-radius: 6.4px;
            padding: 1rem;
            margin-bottom: 1em;
        }
        &.note{
            background-color: $color-note-contrast-background;
            border-left: 5px solid $color-note-darkest;
            color: $color-note-darkest;
            code{
                background-color: $color-note-code-background;
                color: $color-note-darkest!important;
            }
            a{
                color: $color-note-darkest!important;
            }
        }
        &.info{
            background-color: $color-info-contrast-background;
            border-left: 5px solid $color-info-dark;
            color: $color-info-dark;
            code{
                background-color: $color-info-code-background;
                color: $color-info-dark!important;
            }
            a{
                color: $color-info-dark!important;
            }
        }
        &.warning{
            background-color: $color-warning-contrast-background;
            border-left: 5px solid $color-warning-dark;
            color: $color-warning-dark;
            code{
                background-color: $color-warning-code-background;
                color: $color-warning-dark!important;
            }
            a{
                color: $color-warning-dark!important;
            }
        }
        &.error{
            background-color: $color-error-contrast-background;
            border-left: 5px solid $color-error-dark;
            color: $color-error-dark;
            code{
                background-color: $color-error-code-background;
                color: $color-error-dark!important;
            }
            a{
                color: $color-error-dark!important;
            }
        }
        &.citation{
            background-color: $color-citation-contrast-background;
            border-left: 5px solid $color-citation-darkest;
            color: $color-citation-darkest;
            code{
                background-color: $color-citation-code-background;
                color: $color-citation-darkest!important;
            }
            a{
                color: $color-citation-darkest!important;
            }
            .ref{
                text-align: right;
            }
        }
        .header{
            font-weight: 700;
            line-height: 17.5px;
            margin-bottom: 0.3rem;
            background-color: transparent !important;
        }
        code{
            text-shadow: none;
        }
        .icon{
            width: 18px;
            height: 18px;
            margin-bottom: -2px;
            margin-right: 5px;
        }
        .title{
            display: inline-block;
        }
    }
    
  • _config.yml 中增加如下代码, 将默认的 premonition svg 图片替换为 font-awesome 中的图标, 注意使用的 font-awesome 的版本需要与自己的 Jekyll 主题使用的保持一致, 我的主题用的使 v5.15.4 版本:

    _config.yml
    # premonition
    premonition:
      default:
        template: '<div class="premonition {{type}}">
              <div class="header">
                <i class="fas fa-quote-left" style="color: #acb1b9;"></i>
                <div class="title"> {{title}} </div>
              </div>
              <div class="content">
                {{content}}
              </div>
            </div>'
      types:
        citation:
          template: '<div class="premonition {{type}}">
              <div class="header">
                <i class="fas fa-quote-left" style="color: #acb1b9;"></i>
              </div>
              <div class="content">
                {{content}}
              </div>
              {% if title %}<div class="ref"> ------ {{title}} </div>{% endif %}
            </div>'
          default_title: ''
        note:
          template: '<div class="premonition {{type}}">
                <div class="header">
                  <i class="fas fa-sticky-note" style="color: #87d8f2;"></i>
                  <div class="title"> {{title}} </div>
                </div>
                <div class="content">
                  {{content}}
                </div>
              </div>'
          default_title: 'Note'
        info:
          template: '<div class="premonition {{type}}">
                <div class="header">
                  <i class="fas fa-exclamation-circle" style="color: #4dbf4d;"></i>
                  <div class="title"> {{title}} </div>
                </div>
                <div class="content">
                  {{content}}
                </div>
              </div>'
          default_title: 'Info'
        warning:
          template: '<div class="premonition {{type}}">
                <div class="header">
                  <i class="fas fa-exclamation-triangle" style="color: #ffcf4d;"></i>
                  <div class="title"> {{title}} </div>
                </div>
                <div class="content">
                  {{content}}
                </div>
              </div>'
          default_title: 'Warning'
        error:
          template: '<div class="premonition {{type}}">
                <div class="header">
                  <i class="fas fa-times-circle" style="color: #fb7478;"></i>
                  <div class="title"> {{title}} </div>
                </div>
                <div class="content">
                  {{content}}
                </div>
              </div>'
          default_title: 'Error'
    

11. OneDark 主题配置

感觉原版的 Tomorrow 主题的 syntax highlight 不咋好看, 自己替换成了 OneDark(可参考HangX-Ma/onedark.scss) 但不知咋滴部分渲染还是不正常, 应该是 rouge 本身的问题。 后来又折腾了一波 GungnirHighlight.js 配置, 效果也不尽如人意。 如果有小伙伴喜欢 Gungnir 的主题可以参考这个 jekyll-theme-gungnir

OneDark 主题是参考 bryanchapel/jekyllHighlighterAtomOneDarkTheme 做的更改, 我调成了 Vivid 配色, 跟 VSCode 中使用的更接近。