这是 Next.js 构建博客的第五篇文章,上一篇文章介绍了 Next.js 如何处理常见的问题,这一篇主要介绍给博客进行功能增强。
图片放大缩小
在详情页面会经常遇到图片,很多时候为了考虑排版只会放一个等比例缩小的图片,而不是任由图片展示初始尺寸,这个时候为了为了查看图片就需要考虑功能的增强了。
这里介绍一下怎么来进行添加,不过在添加之前需要想一下,图片放大缩小这个功能,我们需要用服务器渲染还是客户端渲染?
我的建议是客户端渲染即可,因为服务器渲染一方面适合比较通用的部分,另外则是 seo 抓取跟图片本身其实关联不大,默认情况下 html 携带图片 alt 属性就足够了。
博客的渲染使用了 @bytemd/react,看一个官方文档的示例
js
import gfm from "@bytemd/plugin-gfm";
import { Editor, Viewer } from "@bytemd/react";
const plugins = [
gfm(),
// Add more plugins here
];
const App = () => {
const [value, setValue] = useState("");
return (
<Editor
value={value}
plugins={plugins}
onChange={(v) => {
setValue(v);
}}
/>
);
};
传递 value 属性就得到一个完整的 view,不过这里不太符合我们要求,因为我们需要给 <img />
添加 onClik
事件,有两种思路可以做到:
- 一种是@bytemd/react 自定义插件
- 另外一种就是拦截整体整体区域的点击,利用冒泡机制即可。
下面是一个示例,使用第二种方式
js
const [visible, setVisible] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
useEffect(() => {
const dom = document.querySelector(".markdown-body") as HTMLDivElement;
const callback = (e: MouseEvent) => {
const dom = e.target as HTMLImageElement;
if (!/img/i.test(dom.nodeName)) {
return;
}
const index = imgAll.indexOf(dom.src);
setActiveIndex(index);
setVisible(true);
};
dom.addEventListener("click", callback);
return () => {
dom.removeEventListener("click", callback);
};
}, [imgAll]);
之后简单封装一下预览图片组件,这里使用了 react-viewer
js
"use client";
import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
import Viewer from "react-viewer";
interface Props {
imgAll: string[];
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
activeIndex: number;
}
export function Preview({ visible, imgAll, setVisible, activeIndex }: Props) {
const images = useMemo(() => {
return imgAll.map((f) => {
return {
src: f,
alt: f.split("/").at(-1),
};
});
}, [imgAll]);
// 防止点开抖动
const id = "article_style";
useEffect(() => {
if (!visible) {
const dom = document.querySelector(`#${id}`);
if (dom) {
// 延迟去除,防止抖动
setTimeout(() => {
document.head.removeChild(dom);
}, 500);
}
return;
}
const { clientWidth } = window.document.documentElement;
const screenDifference = window.innerWidth - clientWidth;
const content = `
html body{
overflow-Y:hidden;
${
screenDifference > 0 ? `width:calc(100% - ${screenDifference}px)` : ""
}
}
`;
const style =
document.querySelector(`#${id}`) || document.createElement("style");
style.id = id;
style.innerHTML = content;
document.head.appendChild(style);
}, [visible]);
return (
<Viewer
visible={visible}
activeIndex={activeIndex}
onClose={() => {
setVisible(false);
}}
onMaskClick={() => {
setVisible(false);
}}
images={images}
/>
);
}
之后引入
js
const Preview = dynamic(() => import("./preview").then((e) => e.Preview), {
ssr: false,
});
点击量
很多时候需要对文章点击量进行一个整体衡量,包括站点访问量之类的,这里用的是 不蒜子 - 极简网页计数器
首先安装依赖
sh
pnpm i busuanzi.pure.js
之后在每次路由发生变化的时候进行监听
js
"use client";
import { usePathname } from "next/navigation";
import { fetch } from "busuanzi.pure.js";
import { useUpdateEffect } from "ahooks";
// 给文章添加点击量
export default function Statistics() {
const pathname = usePathname();
useUpdateEffect(() => {
fetch();
}, [pathname]);
return null;
}
在 app/page.tsx 页面引入
js
const Statistics = dynamicNext(() => import("./statistics"), { ssr: false });
return () => {
<Statistics></Statistics>;
};
在需要地方引入,例如我需要某一篇文章的访问量,那我就
html
<span>
<i className="qzf qzf-eye" />
<span id="busuanzi_value_page_pv" suppressHydrationWarning> 0 </span>次
</span>
其他的方式可以看一下文章,最后需要注意一下,需要设置 meta 属性为 no-referrer-when-downgrade
,具体原因看不蒜子在 Chrome 85 版本后所有页面统计是同一个数据
js
export const metadata: Metadata = {
referrer: "no-referrer-when-downgrade",
};
添加收录
这里以 Google 为例,可以访问此网站,根据示例一步步来。
然后在 app/layout.tsx 中添加下面这样的代码
js
return (
<html lang="zh">
<meta
name="google-site-verification"
content="4FVbyJeMZIl9kKhdo9gaJLqZviP6Z5En9GbS5VD8g6w"
/>
</head>
<body>
</body>
</html>
);
给代码添加复制和在线运行功能
这块因为这段时间心情很糟糕,代码并没有写完,所以下面全部都是伪代码形式。
因为渲染 md 用的组件是 @bytemd/react,它其实是支持插件拓展的,放一张官方的图
有两个步骤可以完成添加的功能
- The HTML AST could be manipulated by several rehype plugins
- Some extra DOM manipulation after the HTML being rendered
最后一种对 ssr 没有帮助,相当于客户端渲染了,不过对 dom 添加之类的操作十分方便我们做一些定制,例如添加复制,我们可以写一个 copy 组件,然后在 dom 元素出现的时候直接使用 react-dom
render()
指定元素就下可以了。
结合一个官方给出的示例来进行看下
js
export default function mathPlugin(): BytemdPlugin {
return {
remark: (processor) => processor.use(remarkMath),
+ viewerEffect({ markdownBody }) {
+ const renderMath = async (selector: string, displayMode: boolean) => {
+ const katex = await import('katex').then((m) => m.default)
+
+ const els = markdownBody.querySelectorAll<HTMLElement>(selector)
+ els.forEach((el) => {
+ katex.render(el.innerText, el, { displayMode })
+ })
+ }
+
+ renderMath('.math.math-inline', false)
+ renderMath('.math.math-display', true)
+ },
}
}
相当于就是操作 dom 元素了。
最后
如果文章有错误之类的欢迎指出,顺便下一篇文章就是收尾了,主要介绍使用 Github Actions 来完成自动发布,在使用的时候只需要监听 issues 的变化就行。