YYGod0120
NewBlogCategories: Project     2024-02-03

前言

原本,我利用 Hexo 搭建了一个个人博客,刚开始还挺新鲜的,但慢慢就觉得样式单调,而且我想加入评论以及流量监控很麻烦。所以想着要不自己手写一个,更好控制也能自定义样式。于是 yyblog 就孕育而生

结构

整个博客,不仅仅由 yyblog 组成,还有 ybg-cli 脚手架,用于自动创建删除文章以及编译文章。

  • yyblog

    • Nextjs
    • typescript
    • tailwind
    • react-syntax-highlighter
  • ybg-cli

    • cac
    • gray-matter
    • he

yyblog

采用Nextjs+Typescript+Tailwindw为主要技术。

效仿 Hexo 采用纯前端,文章编写删除编译都在本地运行,对前端工程师更友好。 Nextjs 同时也支持全栈开发。简单的 sql 语句也能够对文章进行增删改查。

Next 提供的文件路由同时也可以帮我们自定义一些特殊页面,常见的 layout 页面,404 页面以及错误页面等等。

避免中文命名文件!!

除了文件路由以外,Next 也提供了很多组件以及优化,比如 font<Image>标签还有<Script>标签。 这里说一下<Script>,博客网站上我放了一些动画(来源于 codepen),而这些动画script脚本,放入 tsx(jsx) 中只能通过dangerouslySetInnerHTML这个极具风险的 attribute 但正好 Nextjs 提供了一个<Script>,能够让我们使用脚本组件。

ybg-cli

采用纯前端的重点就在于文章的生成删除以及编译,麻烦就麻烦在编译成 TSX 代码插入到 yyblog 中形成页面。

光看文字不如上点代码:

1export async function compileFile(): Promise<mdFile[]> {
2  let compiledFiles: mdFile[] = [];
3  const fileList = fs.readdirSync(_postFolder);
4  for (const file of fileList) {
5    const filePath = path.join(_postFolder, file);
6    const fileContent = fs.readFileSync(filePath, "utf-8");
7    const parsedFile = matter(fileContent);
8    const newMatter = {
9      ...parsedFile,
10      data: { ...parsedFile.data, date: UTCToString(parsedFile.data.date) },
11    };
12    const picPath = makeImportPic(await marked(parsedFile.content));
13    const htmlText = HtmlToNext(await marked(parsedFile.content));
14    compiledFiles.push(
15      picPath
16        ? {
17            mdMatter: newMatter,
18            mdHtml: htmlText,
19            other: {
20              picPath: picPath,
21            },
22          }
23        : { mdMatter: newMatter, mdHtml: htmlText },
24    );
25  }
26  return compiledFiles;
27}
28

读取文件并且用 gray-matter 解析 md 文件生成对应的内容。mdMatter是文章头部yaml格式内容的解析。mdHtml是文章主题部分的解析。other是解析成 Html 后需要插入到最后 TSX 内容的东西。

重点在于HtmlToNext函数:

1import he from "he";
2function ImageRepimg(html: string) {
3  const processedHtml = html.replace(
4    /<img\s+src="(.*?)"\s+alt="(.*?)".*?\/>/g,
5    function (match, src, alt) {
6      const modifiedSrc = src.split("/");
7      const newSrc = modifiedSrc[modifiedSrc.length - 1]; //修改后的SRC
8
9      const modifiedAlt = alt; // 修改后的alt
10
11      return `<Image src={${newSrc.slice(
12        0,
13        newSrc.lastIndexOf("."),
14      )}} alt="${modifiedAlt}" 
15      sizes="100vw"
16      style={{
17        width: '100%',
18        height: 'auto',
19      }} />`;
20    },
21  );
22  return processedHtml;
23}
24function replaceClassName(html: string) {
25  const processedHtml = html.replace(/className=/g, "className=");
26  return processedHtml;
27}
28function highLightHtml(html: string) {
29  // 在代码块内的特殊字符前加上 \
30  const replacedString1 = html.replace(
31    /<pre><code className="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g,
32    (_, language, codeContent) => {
33      //转义符删除
34      const decodeCode = he.decode(codeContent);
35      const codeWithBackslash = decodeCode.replace(/([^\w\s"'])/g, "\\$1");
36      return `<SyntaxHighlighter language="${language}" style={oneLight} showLineNumbers>{ \`${codeWithBackslash}\` }</SyntaxHighlighter>`;
37    },
38  );
39
40  return replacedString1;
41}
42export function HtmlToNext(html: string) {
43  //替换img标签
44  const step1Html = ImageRepimg(html);
45  //替换class为className
46  const step2Html = replaceClassName(step1Html);
47  //高亮代码
48  const step3Html = highLightHtml(step2Html);
49  //闭合分割线
50  const step4Html = step3Html.replace(/<hr>/g, "<hr />");
51  return step4Html;
52}
53

通过对解析后 html 的修改,主要是用正则,实现 html 向 TSX(Next)的转化。

3月4号更新:

更新了英文版,主要采用的是Next的中间件-middleware.js以及react-i18nexti18next,这两个库i18n转化库。

配置的教程在这里

1import { NextResponse } from "next/server";
2import acceptLanguage from "accept-language";
3import { fallbackLng, languages, cookieName } from "@/app/i18n/setting";
4
5acceptLanguage.languages(languages);
6
7export const config = {
8  // matcher: '/:lng*'
9  matcher: ["/((?!api|_next/static|_next/image|imgs|favicon.ico|sw.js).*)"],
10};
11export function middleware(req) {
12  let lng;
13  if (req.cookies.has(cookieName))
14    lng = acceptLanguage.get(req.cookies.get(cookieName).value);
15  if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
16  if (!lng) lng = fallbackLng;
17
18  // Redirect if lng in path is not supported
19
20  if (
21    !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
22    !req.nextUrl.pathname.startsWith("/_next")
23  ) {
24    return NextResponse.redirect(
25      new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
26    );
27  }
28  if (req.headers.has("referer")) {
29    const refererUrl = new URL(req.headers.get("referer"));
30    const lngInReferer = languages.find((l) =>
31      refererUrl.pathname.startsWith(`/${l}`),
32    );
33    const response = NextResponse.next();
34    if (lngInReferer) response.cookies.set(cookieName, lngInReferer);
35    return response;
36  }
37
38  return NextResponse.next();
39}
40

这一段是中间件主要的代码,作用是用户使用不支持语言时自动跳转到默认语言,记住用户每次结束后使用的语言。

注意:

1export const config = {
2  // matcher: '/:lng*'
3  matcher: ["/((?!api|_next/static|_next/image|imgs|favicon.ico|sw.js).*)"],
4};
5

matcher匹配器用于使得中间件在特定的路径上执行。 这个字符串是负向预测先行,用于匹配不包含以上内容的字符串片段,你需要稍微修改正则以匹配你的文件目录。

3月24号更新:

一键部署

选择一个静态页面管理网站,比如githubpage或者vercrel,绑定自己的博客仓库。 在根目录下设置_blog.json,配置文件:

1{
2  "deployCon": {
3    "commitMessage": "docs: new essay", //每次commit的message
4    "remote_store_url": "https://github.com/YYGod0120/yyblog.git", //远程仓库url
5    "remote_store_name": "origin", //别名
6    "branch": "main" //想要提交的分支
7  }
8}
9

然后使用ybg init进行初始化。 最后每次写完文章执行pnpm d进行部署。

附件

一些使用到的库或者代码:

  • gray-matter:解析静态网页元数据
  • marked:转化md文件为 HTML
  • rimraf:快捷删除文件夹及其内容
  • tsup:快速构建 TypeScript 项目的工具
  • picocolors:命令行颜色
  • cac:构建命令行工具的 JavaScript/TypeScript 框架
  • codepen: 动画及特别的 404 页面都来源于此
  • react-i18next i18n for react
© 2023 - 2024
githubYYGod0120