跳到主要内容

魔改 Docusaurus,给博客文章页面增加了几个功能

· 阅读需 12 分钟
编程范儿

一直认为 Docusaurus 默认的博客文章页面太过单调,可能对于只是在网上找一地方单纯记录文字的人来说已经够了。但是我是把它打造成一个用户体验很棒的知识分享产品来打造。

首先我们来看看一个好的博客或者也可以扩大点范围叫 Web 信息资讯类产品都有哪些功能?

  • 社交平台转发分享
  • 点赞,鼓励
  • 评论互动
  • 快速复制文章链接
  • 二维码扫描手机端阅读

还有一个我觉得很好的最近几年才在各大产品频繁出现的文字的收听功能,比如微信公众号的“听全文”,国外的 Medium 也有播放音频。好处很多,对于很多不愿意或者不方便看文字的人来说他可以通过另外一条途径 来了解你分享的东西。

我很喜欢 Medium 简洁大气的设计,两边大量的留白,中间以文章内容为中心,而且该有的功能又很丰富。我直接参考了它的样式。然后花了一天的时间把这几个功能实现了。

下面简单说下这几个功能的实现思路,不是很难,给需要的人一个参考:

给作者一个鼓励

由于本网站是纯静态的,没法收集用户的点赞(喜欢)数据,只能借助第三方的服务实现。比如评论用的 Giscus 就是用的 Github Discussions 功能集成的,这个详细的放到下面再说。 这里我用了一个偷懒的方式,当你点击鼓掌的图标时,会喷发出彩带的效果。为了让阅读文章的人有个表达的地方,至于收不收集这个数量我觉得是其次。

可以点击下面的按钮体验下实际效果:

代码的实现,彩带效果这里借助了 canvas-confetti 这个库,你可以发现本站很多地方都有使用到。

import confetti from 'canvas-confetti';

function onClap (e) {
confetti({
particleCount: 200, // 默认值:50,要发射的五彩纸屑的数量。
startVelocity: 30, // 默认值:45,五彩纸屑开始移动的速度,以像素为单位
angle: 90, // 发射的角度,0 表示水平向右;90 表示垂直向上;180 表示水平向左;270 表示垂直向下
spread: 120, //默认值:45,五彩纸屑在垂直方向扩散的角度,45 表示五彩纸屑以垂直方向正负 22.5 度角发射
ticks: 300, // 默认值: 200 ,值越小粒子消失得越快,值越大粒子消失得越慢
gravity: 0.5, // 默认值:1,粒子下落的速度。1 是全重力,0.5 是半重力,0 表示无重力;大于 1 表示加速下落,负值表示粒子会向上升起。
origin: { // 彩带起点位置,0.5表示位于页面的中心
x: 0.5,
y: 0.5
}
})
}

评论互动

上面已经提到,评论功能使用的是 Giscus 开源方案,类似的还有很多,它们都是借助 Github 提供的 API 实现。

  • giscus - 可借助组件库在 React、Vue 和 Svelte 中使用,支持多种语言
  • gitalk - 基于 Github Issue 和 Preact 开发的评论插件
  • utterances - 借助 Github issues 实现的轻量的评论组件,giscus 灵感就是来源于它

评论区代码我封装成了一个组件,外层包裹了一个 idComment 的 DIV,为了上面的评论图标通过锚点点击快速跳转到达底部评论区。

import React from 'react';
import Giscus from "@giscus/react";
import { useColorMode } from '@docusaurus/theme-common';
import styles from './style.module.scss';

export default function GiscusComponent() {
const { colorMode } = useColorMode();

return (
<div className={styles.commentWrapper} id="Comment">
<Giscus
repo="fantingsheng/spacexcode-discus"
repoId="R_kgDOJo****"
category="General"
categoryId="DIC_kwDOJoGL984CWxiW" // E.g. id of "General"
mapping="url" // Important! To map comments to URL
term="Welcome to @giscus/react component!"
strict="0"
reactionsEnabled="1"
emitMetadata="1"
inputPosition="top"
theme={colorMode}
lang="zh-Hans"
loading="lazy"
crossorigin="anonymous"
async
/>
</div>
);
}

社交平台转发分享

社交平台我选了我日常经常维护的微博和 X,这两个平台都可以通过链接直达,通过相关的参数携带分享的文字,链接和图片。

关于博客文章中要分享的信息,我们可以从 Docusaurus 提供了 API 中获取到;

import { useBlogPost } from '@docusaurus/theme-common/internal';
import useBaseUrl from '@docusaurus/useBaseUrl';

export default function BlogToolBar({}) {
const { metadata, isBlogPostPage } = useBlogPost();
const { permalink } = metadata;
const blogUrl = siteConfig.url + '' + useBaseUrl(permalink);

return (
....
)
}

标题和描述可以从 metadata 中拿到,图片我们在撰写文章的时候,选择一个封面图放在 front matter 的信息里。

文章的链接我们并不能直接拿到,需要拿到后缀后和配置信息里的 url 进行拼接。

  • 微博
<a 
className={styles.shareLink}
href={`https://service.weibo.com/share/share.php?url=${encodeURIComponent(blogUrl)}&title=${encodeURIComponent(metadata.title + "|" + metadata.description)}&pic=${metadata.frontMatter.image}`}
target="_blank"
>分享到微博</a>
  • X(原 Twitter)
<a 
className={styles.shareLink}
href={`https://twitter.com/share?url=${encodeURIComponent(blogUrl)}&text=${encodeURIComponent(metadata.title + "|" + metadata.description)}&image=${metadata.frontMatter.image}`}
target="_blank"
>Share to X</a>

快速复制文章链接

如果让你选择通过一个按钮点击和从浏览器地址栏选中后再复制操作,你会选哪个?当然是通过按钮点击,这样会轻松方便很多。

得益于浏览器提供了直接的接口,复制操作实现起来很简单:

const onCopy = () => {
navigator.clipboard.writeText(blogUrl)
.then(() => {
console.log("复制链接成功");
})
.catch((error) => {
console.error("复制时出错: ", error);
});
};

或者使用 useCopyToClipboard Hooks 实现。两种不同的实现方法。

二维码扫描手机端阅读

为了便于在手机端阅读,或者通过二维码分享文章,使用 qrcode.react 库为每篇文章的 URL 生成对应的二维码。

npm install qrcode.react

安装好之后,在页面中调用

import QRCode from 'qrcode.react';

<QRCode value="https://spacexcode.com/blog" renderAs="svg" />

更多使用参数详见:https://www.npmjs.com/package/qrcode.react

文字的音频收听

这里的核心实现是将文字转成音频,有在线转和提前准备音频文件两种,在线转有点难,暂时没实现思路,可能市场上相关的服务。我本着尽快实现的原则,首先我将博客文章中的文字通过 腾讯的一款叫 腾讯智影 的产品,它提供了从文字转音频的能力,而且是免费的。

我会把转好的 mp3 格式的音频文件放到我的阿里云存储上。然后同样在文章的 front matter 里放上该音频链接。

import React from 'react';

export default function () {
const [paused, setPaused] = React.useState(true);
const audioRef = React.useRef();

const onPlayAudio = () => {
if (metadata.frontMatter.audio) {
audioRef.current.play();
paused ? audioRef.current.play() : audioRef.current.pause();
setPaused(!paused)
} else {
console.log("本文未上传音频");
}
}

return (
<IconButton aria-label="听全文" onClick={onPlayAudio}>
{paused ?
(<svg width="24" height="24" viewBox="0 0 24 24" fill="#6B6B6B"><path fillRule="evenodd" clipRule="evenodd" d="M3 12a9 9 0 1 1 18 0 9 9 0 0 1-18 0zm9-10a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm3.38 10.42l-4.6 3.06a.5.5 0 0 1-.78-.41V8.93c0-.4.45-.63.78-.41l4.6 3.06c.3.2.3.64 0 .84z" fill="currentColor"></path></svg>)
:
(<svg viewBox="0 0 256 256" width="24" height="24"><rect width="256" height="256" fill="none"></rect>
<circle cx="128" cy="128" r="96" fill="none" stroke="#6B6B6B" strokeLinecap="round" strokeLinejoin="round" strokeWidth="8"></circle>
<line x1="104" x2="104" y1="96" y2="160" fill="none" stroke="#6B6B6B" strokeLinecap="round" strokeLinejoin="round" strokeWidth="8"></line>
<line x1="152" x2="152" y1="96" y2="160" fill="none" stroke="#6B6B6B" strokeLinecap="round" strokeLinejoin="round" strokeWidth="8"></line>
</svg>)
}
</IconButton>
<audio ref={audioRef} src={metadata.frontMatter.audio}></audio>
)
}

好了,关于这几个功能的实现都讲完了。虽然功能都很简单,但是却能大大地提升博客的浏览体验。何乐而不为。

就写到这里,感谢你的阅读:),我们下一篇见。

太空编程
分享硬核的前端编程知识。
想及时了解前端相关资讯,请关注作者公众号“太空编程”,回复关键字,获取丰富的学习资料。