Next.js 构建博客之功能拓展

544 阅读5分钟

image.png

  1. Next.js 构建博客之资源抓取
  2. Next.js 构建博客之博客搭建
  3. Next.js 构建博客之打包 SSG
  4. Next.js 构建博客之常见问题处理
  5. Next.js 构建博客之功能拓展
  6. Next.js 构建博客之自动构建

这是 Next.js 构建博客的第五篇文章,上一篇文章 Next.js 构建博客之常见问题处理 介绍了 Next.js 如何处理常见的问题,这一篇主要介绍给博客进行功能增强。

如果你想看已经部署博客的地址可以点击查看,代码仓库地址点击查看

图片放大缩小

在详情页面会经常遇到图片,很多时候为了考虑排版只会放一个等比例缩小的图片,而不是任由图片展示初始尺寸,这个时候为了为了查看图片就需要考虑功能的增强了。

这里介绍一下怎么来进行添加,不过在添加之前需要想一下,图片放大缩小这个功能,我们需要用服务器渲染还是客户端渲染?

我的建议是客户端渲染即可,因为服务器渲染一方面适合比较通用的部分,另外则是 seo 抓取跟图片本身其实关联不大,默认情况下 html 携带图片 alt 属性就足够了。

博客的渲染使用了 @bytemd/react,看一个官方文档的示例

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 事件,有两种思路可以做到:

  1. 一种是@bytemd/react 自定义插件
  2. 另外一种就是拦截整体整体区域的点击,利用冒泡机制即可。

下面是一个示例,使用第二种方式

 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

"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}
    />
  );
}

之后引入

const Preview = dynamic(() => import("./preview").then((e) => e.Preview), {
  ssr: false,
});

点击量

很多时候需要对文章点击量进行一个整体衡量,包括站点访问量之类的,这里用的是 不蒜子 - 极简网页计数器

首先安装依赖

pnpm i busuanzi.pure.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 页面引入

const Statistics = dynamicNext(() => import("./statistics"), { ssr: false });

return () => {
  <Statistics></Statistics>;
};

在需要地方引入,例如我需要某一篇文章的访问量,那我就

<span>
  <i className="qzf qzf-eye" />
  <span id="busuanzi_value_page_pv" suppressHydrationWarning> 0 </span></span>

其他的方式可以看一下文章,最后需要注意一下,需要设置 meta 属性为 no-referrer-when-downgrade,具体原因看不蒜子在 Chrome 85 版本后所有页面统计是同一个数据

export const metadata: Metadata = {
  referrer: "no-referrer-when-downgrade",
};

添加收录

这里以 Google 为例,可以访问此网站,根据示例一步步来。

然后在 app/layout.tsx 中添加下面这样的代码

  return (
    <html lang="zh">
        <meta
          name="google-site-verification"
          content="4FVbyJeMZIl9kKhdo9gaJLqZviP6Z5En9GbS5VD8g6w"
        />
      </head>
      <body>

      </body>
    </html>
  );

给代码添加复制和在线运行功能

这块因为这段时间心情很糟糕,代码并没有写完,所以下面全部都是伪代码形式。

因为渲染 md 用的组件是 @bytemd/react,它其实是支持插件拓展的,放一张官方的图

example

有两个步骤可以完成添加的功能

  • 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() 指定元素就下可以了。

结合一个官方给出的示例来进行看下

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 元素了。

添加搜索功能

文章搜索也是一个很重要功能,这边分享一下我是怎么做的,首先对于关键词搜索啥的 SSG 是没办法收录的,因为你也不知道到底会有多少关键词。

分享一下我的做法

<form
  id="search-form"
  method="get"
  action={`${process.env.NEXT_PUBLIC_BASE_PATH}/page/1`}
  className="uk-search uk-search-navbar uk-width-1-1 qzhai_so uk-visible@s"
>
  <input
    className="uk-search-input"
    type="search"
    name="s"
    id="s"
    placeholder="搜索"
    defaultValue=""
  />
</form>

当输入之后 enter 按键被触发会在 page 后面携带 s?=xxx 的参数,之后在客户端组件里面通过

const searchParams = useSearchParams();
const search = searchParams.get("s");

来取到对应的值,之后就是对数据进行渲染加红即可,这里说下加红的做法,可以通过正则来替换,把搜索文本替换成

<span style="color:red">关键词</span>

之后 React 直接渲染 html 字符串就达到了一个高亮的效果。

这里贴一下我的不完整代码

<Link
  href={path}
  dangerouslySetInnerHTML={{
    __html: s
      ? title.replace(new RegExp(s, "ig"), (value: string) => {
          return `<span style="color: red">${value}</span>`;
        })
      : title,
  }}
></Link>

最后

如果文章有错误之类的欢迎指出,顺便下一篇文章 Next.js 构建博客之自动构建 就是收尾了,主要介绍使用 Github Actions 来完成自动发布,在使用的时候只需要监听 issues 的变化就行。