原本,我利用 Hexo 搭建了一个个人博客,刚开始还挺新鲜的,但慢慢就觉得样式单调,而且我想加入评论以及流量监控很麻烦。所以想着要不自己手写一个,更好控制也能自定义样式。于是 yyblog 就孕育而生
整个博客,不仅仅由 yyblog 组成,还有 ybg-cli 脚手架,用于自动创建删除文章以及编译文章。
yyblog
ybg-cli
采用Nextjs
+Typescript
+Tailwindw
为主要技术。
效仿 Hexo 采用纯前端,文章编写删除编译都在本地运行,对前端工程师更友好。 Nextjs 同时也支持全栈开发。简单的 sql 语句也能够对文章进行增删改查。
Next 提供的文件路由同时也可以帮我们自定义一些特殊页面,常见的 layout 页面,404 页面以及错误页面等等。
避免中文命名文件!!
除了文件路由以外,Next 也提供了很多组件以及优化,比如 font
,<Image>
标签还有<Script>
标签。 这里说一下<Script>
,博客网站上我放了一些动画(来源于 codepen),而这些动画script
脚本,放入 tsx(jsx)
中只能通过dangerouslySetInnerHTML
这个极具风险的 attribute 但正好 Nextjs 提供了一个<Script>
,能够让我们使用脚本组件。
采用纯前端的重点就在于文章的生成删除以及编译,麻烦就麻烦在编译成 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)的转化。
更新了英文版,主要采用的是Next的中间件-middleware.js
以及react-i18next
和i18next
,这两个库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
匹配器用于使得中间件在特定的路径上执行。 这个字符串是负向预测先行,用于匹配不包含以上内容的字符串片段,你需要稍微修改正则以匹配你的文件目录。
一键部署
选择一个静态页面管理网站,比如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
进行部署。
一些使用到的库或者代码:
md
文件为 HTML