AHdark

AHdark Blog

Senior high school student with a deep passion for coding. Driven by a love for problem-solving, I’m diving into algorithms while honing my skills in TypeScript, Rust, and Golang.
telegram
tg_channel
twitter
github

Reconstructing the Blog System with Next.js and Hexo

In the years from 2019 to 2022, many of my projects relied on servers (or VPS). I had used Alibaba Cloud, Tencent Cloud, Vultr, Digital Ocean, GCP, AWS, Azure, and so on, but in the end, I found that none of these servers were what I wanted. The server configurations were bloated and complex, especially when I had dozens of servers, and maintaining them became a painful task.
Trying to migrate several applications from an expiring server to another server, after struggling with LNMP for several hours, I discovered that I was operating on the wrong server. This incident has likely left a psychological shadow on me for life.

Therefore, after gradually improving my Go development skills and containerization solutions in 2023, I migrated almost all my applications to Kubernetes. Naturally, I no longer needed to hold any servers.
However, my blog is built on WordPress, and the RWX PVC prices on GCP are extremely high, so my blog remained on the server (I had tried before, but it failed).

In mid-2023, I began to contemplate whether I should refactor my blog system to make it containerized as well, achieving cost reduction and efficiency improvement. By the end of 2023, I gave up my job due to academic reasons, and I had much more free time. This gave me the opportunity to refactor the blog system, so I began this refactoring.

Why Refactor#

It's not just because I don't want to continue holding a VPS as mentioned above; there are many reasons for truly refactoring and choosing to use such a technology stack. You might ask why I can't use a SaaS version of WordPress services? Why do I want to use Hexo? Why did I start contemplating this in the middle of the year and only began refactoring now?

First of all, WordPress is an excellent CMS system, and its ecosystem is very complete. However, WordPress itself is built on PHP, and its biggest selling point is its extensibility, so I cannot rewrite it, and it's also very difficult to optimize its performance in a low-cost environment.

I had used WordPress before and struggled with optimization for a long time. I even published an article on how to optimize WordPress1, but in the end, I still gave up on WordPress. The optimization of WordPress is endless, lacks technical content, and the computational bottleneck of low-cost environments is really hard to break through for PHP applications. It was just a continuous cycle of caching, trading space complexity for time complexity, and nothing more. The inherent problems in its program design still remain unresolved.

Even if WordPress is containerized, it still faces many issues:

  1. WordPress relies on file storage, operating the file system, and must use RWX (Read-Write Many) mode PVCs. Its price is exorbitantly high.
  2. WordPress relies on MySQL, while the Cloud SQL I use is Postgres, so I need an additional database to run MySQL.

Moreover, due to historical reasons, WordPress is more suitable for traditional websites. As a deep React developer, I prefer to use React to develop themes. This has led me to be unable to develop themes for my own blog and instead use ready-made themes like Argon, MDx, etc., although I have also participated in their maintenance.

Dependency Libraries and Framework Selection#

The framework I chose is Next.js, a React framework developed by Vercel, while the storage of articles relies on Hexo Warehouse. You might be curious why I chose these two among many options. This was also a well-considered choice.
At the same time, I can also explain why I delayed the refactoring until the end of the year.

Next.js#

The initial reason I chose Next.js is simple: it is a React framework. As a React developer, I prefer to use React. Next.js itself is efficient, easy to use, and has a very complete ecosystem. I believe every React developer would choose Next.js when faced with the need for Server-Side Rendering. Moreover, I have rich development experience with Next.js; you can find many related open-source projects on my GitHub, including the V2Board frontend refactoring project I developed at the beginning of this year, which is also based on Next.js.

Next.js is at the forefront of the times, and its support for React is the best. Vercel has developed projects like SWC, Turborepo, and Turbopack, and as Next.js is their own T0 product, it naturally supports these right away. The emergence of these projects has given Next.js higher performance. After the introduction of React Server Components, Next.js was also the first to release the App Router, allowing the use of Server Components in Next.js. (The usage is somewhat unintuitive; I had to split a file into several just to use that "use client";)

RSC allows us to directly call Hexo in Components and pass its internal data types. Most of the API returned by Hexo is of the Document or Query class. To optimize performance, much of its content is getter/setter, which cannot be passed. In the previous Pages Router, we needed to convert it into a structure that could be JSON serialized before passing it to Components. Therefore, under the App Router, we can write many fewer data passing steps. 2

Hexo Warehouse#

Hexo Warehouse is a JSON-based data storage created for the building process of Hexo. However, as a data storage, it does not rely on Hexo itself and can be used as a database. In the second half of this year, Hexo Warehouse completed its TypeScript refactoring, making it easier for me to use. Hexo also released version 7.0.0, further improving API and TypeScript support. I finally no longer need to write AnyScript for Hexo!

In fact, I started the refactoring in the middle of this year, choosing to use Next.js (Pages Router) + Material UI + Hexo (v6), and was tormented for a while before ultimately giving up. I later waited for Warehouse and Hexo to complete their adaptation to TypeScript before deleting the project and starting over. The new project uses Tailwind CSS, which is also a change for me.

Although I used Hexo Warehouse, it's hard to find traces of Hexo in my code. It is only used on the server side and merely serves as a data storage. I did not use Hexo's own features, and Markdown is parsed using a different library. So even though I initialized a Hexo instance, I primarily used its config and warehouse.

Yes, I use Hexo Config to configure most content settings, such as Title, Description, Author, etc. Because I don't want to define more configs and don't want to define these in the Next.js ENV.

Tailwind CSS#

Tailwind CSS is a CSS framework that is not a UI framework in itself. Its characteristic is using a Utility-First approach rather than using something like Bootstrap. If you have installed Wappalyzer, you might find that I also use shadcn/ui, because since this year, I have been pursuing minimalism in design. This kind of monochromatic design allows readers to focus more on the article itself rather than flashy designs.

Moreover, it is well known that when the component reuse rate is high enough, writing with Tailwind CSS is very comfortable. I don't need to write a bunch of CSS; I just need to write a bunch of Utility Classes. My habit is to avoid repetition as much as possible, so my reuse rate is also very high, which is one of the reasons.

Of course, as a CSS framework, Tailwind CSS is not a UI framework in itself, which also imposes some limitations on me. For example, I cannot obfuscate CSS, and the data passed by RSC for CSS takes up a lot of space (due to escaping), which is also one of the points I find most troublesome. I will explain in detail the many problems I currently face later.

Turborepo & pnpm workspace#

Since my project is essentially a Next.js web app calling an instance of Hexo, Hexo also needs to be used as a package. However, I find it quite inelegant to have one package inside another, so I used a monorepo solution to keep these two packages separate.

pnpm is my commonly used dependency management tool, which supports monorepo with pnpm-workspace.yaml. Turbopack has more features, and Vercel provides excellent support for it, so I added Turbopack to manage tasks, parallel builds, and remote caching. In this case, pnpm workspace serves only as a dependency management tool for the monorepo, while the build aspect is handled by Turbopack.

  1. Task management: Using turbo.json to manage and define tasks, including relationships between tasks, can further improve code reuse.
  2. Parallel builds: Using the turbo build command to build the project, which will automatically build in parallel.
  3. Remote caching: Turbo will cache the results of your local builds to Vercel, and when I submit code, Vercel will perform incremental builds based on the cached results, significantly improving deployment speed.
  4. Hash awareness: Turbo will determine whether a file has changed based on the file's hash value rather than its modification time.

Of course, my actual repository does not only contain these two packages; it also includes some dependency libraries, utility libraries, and eslint configurations, etc.

Problems Encountered During Development#

Despite having references from previous articles and my personal experience, I still encountered many problems during development. Some of these problems were my own issues, while others were caused by the technology stack I used. These problems are pitfalls I encountered during development, and I hope they can provide some reference for future developers.

Distinction Between pre > code and code in Code Block Rendering#

I use react-markdown to render Markdown, which renders code blocks as <pre><code></code></pre>. However, I need to perform some additional processing on code blocks, such as simply highlighting the content in <code> with background and font, while the content in <pre> needs to be styled richly.

In this case, the style system of Tailwind CSS cannot support complex and strict styles. Therefore, I need to use other methods to provide CSS. Currently, I directly imported Scss, which is also compatible with PostCss, but I am still preparing to switch it to Style9 or Emotion.

.article {
  pre {
    @apply pt-6 px-4 pb-2 overflow-visible relative;

    tab-size: 2;

    :before {
      content: "";
      position: absolute;
      background: url(data:image/svg+xml;base64,...) no-repeat;
      background-position-y: center;
      top: 22px;
      left: 20px;
      height: 1rem;
      width: 3.5rem;
      margin-left: 0.3rem;
      display: block;
    }

    code {
      @apply px-2 py-4 mt-4 block leading-normal;
    }
  }
}

Ultimately, the convenience of Tailwind CSS is also its downside; its style system cannot support complex styles.

Automatically Generating Excerpt#

Since my articles are written in Markdown, obtaining excerpts is quite troublesome. Many of my early articles did not strictly follow the format, and many lacked an Abstract section. Some even had images or similar content at the beginning. Therefore, I cannot use conventional replacement methods to obtain excerpts. Currently, my solution is to add an excerpt field in the Front Matter section of each article and manually fill in the excerpt.

Since I deleted many articles that I found rather weak after migration, manual operation is not too troublesome. However, I still hope to automatically obtain excerpts, as that would seem more automated.

Currently, I still haven't found a solution, and I don't want to use Hexo's API to fetch article content. This is precisely why the refactoring of the Markdown AST parsing has been put on the agenda.

Article Directory Generation and Heading Component IDs#

Currently, my article directory is generated using the remark-toc plugin of react-markdown. This plugin recognizes the content in headings and generates a directory when it matches. However, the anchor hash part carried by the directory generated by remark-toc cannot adapt to special symbols, so you can see that some directories in this article are not quite normal...

In my view, I should use a separate component to generate the directory and place it in the appropriate position. But this also means that I still need to parse the Markdown AST myself.

Hexo Cannot Call fs After Webpack Bundle#

In the early stages of developing my blog, when introducing Hexo dependencies, as long as I used Hexo in Server Components, errors would occur:

../../node_modules/.pnpm/fsevents@2.3.3/node_modules/fsevents/fsevents.node
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)

There were also errors in the Console:

@ahdark-blog/web:dev:  ⨯ ../../node_modules/.pnpm/fsevents@2.3.3/node_modules/fsevents/fsevents.node
@ahdark-blog/web:dev: Module parse failed: Unexpected character '�' (1:0)
@ahdark-blog/web:dev: You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
@ahdark-blog/web:dev: (Source code omitted for this binary file)

By consulting relevant information, I found that this was due to Hexo being unable to call fs after the Webpack bundle. Next.js provides an experimental setting option serverComponentsExternalPackages, which can exclude Node.js environment packages. 3

Based on the error content, I added hexo, hexo-fs, and hexo-util to serverComponentsExternalPackages to resolve this issue.

Vercel Caching Packages Under Monorepo Causes Articles to Not Update Correctly#

After I adopted a monorepo structure, Vercel would cache the packages under the monorepo, causing my articles to not update correctly after modification. This is due to Vercel's build caching mechanism.

Currently, my solution is to set the environment variable VERCEL_FORCE_NO_BUILD_CACHE to true, so that Vercel does not cache build results. However, in some cases, Vercel still does not synchronize changes to the hexo package. This issue has not been completely resolved, and the current solution will overall slow down the build speed, requiring further optimization.

To this end, I raised this issue in Vercel's GitHub Discussions, but I have not received a response yet. 4

Rendering Issues with ApexCharts#

I use ApexCharts for chart rendering, which is inherited from Mantis's solution. However, there are certain issues with ApexCharts rendering in Next.js, especially regarding SSR.

First, due to the use of ApexCharts's API, it must be rendered correctly in a non-SSR environment. Therefore, I need to use Next.js's dynamic import:

import dynamic from "next/dynamic";

const ReactApexCharts = dynamic(() => import("react-apexcharts"), {
  ssr: false,
});

Moreover, this import is in Client Components, and my actual call should be to this component rather than using dynamic import every time. Furthermore, although charts do not significantly impact SEO, rendering them on the client can reduce some performance overhead, but it is still not elegant enough.

Secondly, since RSC cannot pass functions, and ApexCharts's API requires passing functions for formatting and other tasks, I also had to place the entire data part and rendering part of the chart on the client, using API Route to call data and then render the chart.

Inability to Access Hexo Instance in Static Rendering Under Vercel Environment#

Due to Vercel's static builds, I cannot access the Hexo instance in pages rendered statically and in routes with POST methods. When I am on pages that do not use SSG, Hexo cannot access the instance, so I cannot obtain the article list. In routes with POST methods, Hexo faces the same issue.

Currently, the solution is to use SSG in dynamic routes (pages), which is feasible in my application scenario and indeed improves performance.

In Route Handlers, I define the rendering mode of components by exporting the dynamic variable while avoiding the use of POST methods.

export const dynamic = "force-dynamic"; // defaults to auto, or 'force-static'

Additionally, I recommend defining appropriate Cache-Control in the Response, which can allow Vercel to cache the page and reduce unnecessary calculations.

Background Color Cannot Use Transition#

Since my theme is provided by shadcn/ui, which uses CSS Variables to define colors under different themes (Light / Dark), changing CSS Variables does not trigger transitions, so I cannot use transitions to achieve animation for theme switching. Therefore, you might see that my theme switching happens instantly, without any animation to transition.

I do not want to use JavaScript to manipulate the DOM to achieve this animation; instead, I want to use CSS as much as possible. Currently, I have not found a solution. I might use Style9 in the future to replace Tailwind CSS's class handling solution, allowing Tailwind CSS to be used only as a macro.

/* globals.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 47.4% 11.2%;

    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;

    --popover: 0 0% 100%;
    --popover-foreground: 222.2 47.4% 11.2%;

    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;

    --card: 0 0% 100%;
    --card-foreground: 222.2 47.4% 11.2%;

    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;

    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;

    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;

    --destructive: 0 100% 50%;
    --destructive-foreground: 210 40% 98%;

    --ring: 215 20.2% 65.1%;

    --radius: 0.5rem;
  }

  .dark {
    --background: 224 71% 4%;
    --foreground: 213 31% 91%;

    --muted: 223 47% 11%;
    --muted-foreground: 215.4 16.3% 56.9%;

    --accent: 216 34% 17%;
    --accent-foreground: 210 40% 98%;

    --popover: 224 71% 4%;
    --popover-foreground: 215 20.2% 65.1%;

    --border: 216 34% 17%;
    --input: 216 34% 17%;

    --card: 224 71% 4%;
    --card-foreground: 213 31% 91%;

    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 1.2%;

    --secondary: 222.2 47.4% 11.2%;
    --secondary-foreground: 210 40% 98%;

    --destructive: 0 63% 31%;
    --destructive-foreground: 210 40% 98%;

    --ring: 216 34% 17%;

    --radius: 0.5rem;
  }
}

@layer base {
  * {
    @apply border-border;
  }

  body {
    @apply bg-background text-foreground;
    font-feature-settings:
      "rlig" 1,
      "calt" 1;
  }
}

Future Prospects#

Currently, my UI/UX and some features are not very complete; many of the implemented ones are not perfect, and there are many areas that need improvement. I also have some ideas, but due to time and ability constraints, I have not implemented them. I will continue to improve my blog system in the future.

Rewriting Markdown AST Parsing Based on WASM#

The current Markdown AST is provided by react-markdown, but it is not what I want. Its internal calls are overly complex, and plugins are difficult to develop, making it hard to systematically manage article content. I may introduce some external libraries through WASM for partial or complete parsing to better achieve features like automatic directory and excerpt retrieval.

Recently, I have been learning Rust, and I may use Rust to write this parser.

Using Style9 to Manage CSS#

Currently, I use Tailwind CSS to manage CSS, but its style system is not perfect:

  1. Everyone uses the same class names, making them easy to misuse.
  2. Relying on PostCss preprocessing, I find it difficult to separate the UI library into another monorepo package.
  3. Unable to obfuscate CSS, leading to large CSS files, especially in RSC.
  4. Tailwind CSS's theme is difficult to use with transitions.

Therefore, I may use Style9 to manage CSS, as its style system is more complete and can use CSS Modules for management. However, the problem I need to solve is how to use Tailwind CSS class names in Style9. A feasible solution is stailwc, but I have not been able to use it properly in Next.js 14 + App Router yet. 5

Acknowledgments#

During the development of this project, I received help from many people, and I would like to express my gratitude to them.

Articles by Predecessors#

Without the two articles by Sukka and fengkx, I might not have been able to complete this refactoring. Their articles provided me with a lot of references, enabling me to better complete this refactoring.

Additionally, I also referenced Sukka's blog for styling, especially his Layout section, and I would like to thank him for that.

Open Source Projects#

I used many open-source projects during development, which provided me with a lot of support, enabling me to better complete this refactoring.

Including Vercel, which has made significant contributions to the open-source community, allowing me to use Next.js, Turborepo, and SWC, as well as providing free build and deployment services: Vercel

Feedback Received During the Testing Period#

During the public testing period of the blog, many friends provided me with a lot of opinions and suggestions after seeing my posts on various platforms, enabling me to better improve my UI/UX Design. I would like to thank them.

UI/UX Optimization#

  • QQ 1257135905 Yui
  • QQ 2639365465 TheOrdinaryWow
  • QQ 2727341319 時间
  • QQ 970704142 青木
  • QQ 2652720816 Eacls
  • QQ 1846405136 Whitebear
  • QQ 1123801846 小桦
  • QQ 844177330 網絡乞丐
  • QQ 2076274471 原罪_超凡

84074177fac1657cc163a3f9316a2b61

Suggestions provided by Eacls ⬆️

Bug Feedback: Pagination Causing Horizontal Scroll on Page#

  • QQ 2744601427 愛ゆ
  • QQ 1214050656 玻璃晴朗
  • QQ 3216627800 哦

Footnotes#

  1. How to Effectively Improve the Access Speed of WordPress Blog

  2. https://blog.skk.moe/post/refactor-my-blog-using-nextjs-app-router

  3. https://nextjs.org/docs/app/api-reference/next-config-js/serverComponentsExternalPackages

  4. https://github.com/orgs/vercel/discussions/5277

  5. https://www.fengkx.top/post/rewrite-blog-with-next#CSS-%E6%96%B9%E6%A1%88%E9%80%89%E6%8B%A9

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.