Moving Forth, Moving Fourth

Yet another rewriting of my personal blog, explained in details.

No, not rewritten in Rust, yet.

History

As you may notice, most of my blog-rewrites follow the pace of Allan🫠. I've been trying to convince myself that writing blog should be prioritized before rewriting blog, but as Allan argued:

❎ A website to log something
βœ… A testing field for new techs

Well, that makes sense. I am interested in topics like designing, user interface and user experience since junior high, which contributes a lot to my current interest in web frontend development. I do have a strong personal taste, and that urges me to build my own stuff to satisfy my own aesthetics and standards.

v-1

My negative-first version (v-1), just like everyone else, is powered by Hexo and equipped with the renowned NeXT theme. It quickly fell out of my favour, because it was simply too fancy and too popular.

v0

Then Hugo came into view, because it was adopted by Allan then, and most of its themes were breathtakingly minimalistic. I chose Noteworthy for my zeroth version (v0), and began to wrote some stuff, like the one on KMP and the one on Manchu inscription analysis. That was around summer 2020, I suppose.

Hugo is powered by Go, which ensures a decent speed, but brings loads of problems when I tried to tweak the site on my own, as I argued in a previous post on blogging:

  1. The API is quite complex yet poorly documented.
  2. The html/template package provided by Go standard library is hard to learn.
  3. The community is not vibrant (then), and there aren't much plugins (now I would just write my own) and tutorials, especially Chinese ones (now I'm perfectly comfortable with the global lingua franca).
  4. Its Markdown parser. The Markdown parsers in Go is of terrible quality and stability, and at some time, Hugo switched a parser and broke all my hand-written HTML and CSS. I had to switch to Pandoc.

As I became more familiar with JavaScript, I decide I should use a JavaScript based generator. Although JavaScript is slow, the JavaScript ecosystem provides better support for web related stuff, as that's its primary focus.

v1

By winter 2021, Allan moved his blog to Gridsome, and the OI-Wiki team said they would move to Gatsby. So I decided to rewrite a v1 in Gatsby, as it seems too have a larger community. It turned out to be a nightmare, as React just sucks. I tried to implement the default theme of Gridsome on my own in Gatsby, and the work is really tedious. My codebase is filled with JSX class components loosely typed by Flow.js and abomination-like SCSS stylesheets.

One of the major breakthough during v1 was the introduction of blogging with GitHub issues, as you may check Allan's original idea. And a CI pipeline is introduced to build the blog automatically.

v2-v3

I always rewrite blogs during winter. A new version, v2, is introduced during winter 2022. The Webpack powered Gatsby is too slow to bear, and the ecosystem was on a steady decline (acquired by Netlify now, as of 2023), Vue 3 went into stable and the new bundler, Vite, was really a hit. So I rewrote the whole unmaintainable codebase in Vue + TypeScript, based on vite-ssg. But I stumbled upon the bundling pipeline, some of the build-time data were leaked into the bundle, which resulted in a huge client payload. Some of the features introduced in v2, like customizable themes and i18n, also impacted the reading experience. They changed DOM or styling after hydration, causing apparent glitches.

I played with Astro (recommended by Allan of course), but it was quite nascent and unusable. So I introduced v3 to remove i18n completely, move the light/dark mode logic to the unbundled <script> tag on index.html. But the problem remained. Vue has a runtime, and each of the blog post is considered a component, so the bundle contains two copies of each post, one is the pre-rendered HTML, one is the component for hydration. This is quite absurd. Vue is quite suitable for writing interactive logic and SPAs, which is quite different from a static blog. Also, I would like to implement my own logic of Markdown compilation (to intergrate UnoCSS in Markdown with CommonMark attributes extension, use custom server-rendable components in Markdown, compile-time TOC generation and so on), so a rewrite is only a matter of time.

And here comes v4. May the fourth be with me, before another move.

Compilation

JavaScript is all about transpiling and compiling. The blog itself can be described as a compiler that eats Markdown as input and produces web pages, so I choose the title of compilation.

Version 4 is powered by Astro. As always, the site is completely statically generated, there is no backend. Astro provides the ability of partial hydration, that different parts on the page can follow diffent strategy of rendering. The post is still rendered into a component, but Astro components disintergrate into vanilla HTML during compile time, so there is no runtime hydration at all. However, some components should exist in runtime (browsers) to provide decent interactivity. Astro allows you to decide how and when to load these components (during client idle, after the DOM element becomes visible, etc.). Some examples include the dark mode toggler, the theme tweaker and the wrapper of Giscus comment pane. These components are implemented in Svelte, as they don't require a runtime (a heavy virtual-DOM).

My blog contains two sources of contents, the file system (a git repo at Yixuan-Wang/blog-contents), and a GitHub issues page (at Yixuan-Wang/blog-contents/issues). These sources are implemented as two configurable modules with a unified interface. The fs module will read the content from local file system, while the gh module will request the GitHub API to fetch all issues and its content. Why would I use two different sources? I think serious or long posts are worthwhile for a file, but serial or short ones don't. Meanwhile, complex posts with raw HTML or fancy custom components can be easily debugged and previewed with a file system source.

A Vite plugin is used to pull contents from configured source modules, and then compile the Markdown contents into an Astro component. The official Markdown plugin of Astro is not used, so I can implement my own compilation pipeline, use custom syntax and transform the content as I wish. The Markdown compilation utilizes parsers and AST tooling provided by unified.js. The compiler also analyzes the custom Astro components used in the Markdown. Thanks to the fact that Astro's component syntax is a strict superset of HTML (neither JSX or Vue templates guarantee that, although Astro's own component compiler may fail on marginal cases), we can feed the HTML (with minor tweaks) into Astro's Vite pipeline, instead of embedding it as innerHTML which makes custom components impossible. And because of partial hydration, these components will be compiled back into HTML again during the final build (An astro build), but the custom components embedded in them can contain components that need runtime execution. The whole process is similar to Astro's MDX plugin, but using an extended CommonMark syntax.

Another Vite plugin is capable of gathering metadata from these posts and emitting a virtual ES module for compile time queries. Astro can read data from this module, and render subsidary pages like categories, tags and the archive. These metadata are swept away during the final build.

Some logic is now implemented in vanilla JavaScript, like color themes and the dark mode. They are injected into the HTML, which will execute immediately, so the glitches of colors in v2 and v3 are gone. The color theme toggle and the dark mode toggle is implemented in Svelte, which will be loaded after the first render, providing correct interactive logic without blocking the rendering of static contents.

Syntax

Of course, Markdown is the top choice for markup language when writing blogs. But Markdown has a myriad of extensions like MDX or Markdoc. I choose to base my syntax on CommonMark plus GFM. The CommonMark standard has a formalized specification, is well acknowledged and supported, and is considered default for remark, while GFM is a de facto extension to the CommonMark standard.

Why not MDX? MDX allows you to use JSX-like expressions in Markdown files, which enables components naturally, and is officially supported by Astro. But MDX is not fully compatible with Markdown. Raw HTML tags and HTML-style comments are supported in Markdown, but forbidden by MDX. I have posts with embedded raw HTML tags, and in many cases (SVGs, embedded videos, etc.), raw HTMLs can fulfill the job without the hassle of components. And I have adopted the <!-- more --> comment to mark out the excerpt since v0, but that is not allowed in MDX. MDX also treats {} as variable interpolations. Using variables in static blog posts doesn't make sense, and this syntax may collide with normal contents.

Template languages like Markdoc or Go text/template enjoy better compatibility, but are mostly restricted to the official implementation. I have rewrote posts that contained extended syntax twice (migrating from v0 to v1, v3 to v4), and that is not enjoyable. I have to stick to a well-formalized specification.

The answer is the Generic Directives and Attributes syntax extensions for CommonMark. They don't make into the standard, but enjoy an official plugin from remark, a well maintained markdown-it plugin (markdown-it was used in v2 and v3), and built-in support (as divs and spans and attributes) by Pandoc (Pandoc was used in v0). The remark implementation is a stricter subset (to be exact, it forbids whitespaces) of the other two implementations.

<!-- inline (span) -->
:inline[content]{#id .class key="val"}

<!-- leaf (element) -->
::leaf[content]{#id .class key="val"}

<!-- container -->
:::container[inline-content]{#id .class key="val"}
<!--
  content
-->
:::

Unlike raw HTML tags, generic directives allow Markdown inside themselves. This improves writing experiences a lot.

Besides, we also introduced a special syntax for rubys, which is a hack of the Markdown link syntax and a delight for an otaku like me.

[ε€’](-ゆめ)に[僕](-ぼく)らで[εΈ†](-ほ)γ‚’[εΌ΅](-は)って

What about components then? We adopted the <component is> syntax from standard HTML. During compilation, we traverse the AST tree to replace all <component is="component-a"> tags with <ComponentA> (Astro component syntax), and inject imports to the generated virtual Astro component, so there's no need to declare imports explicitly in the frontmatter. Combined with the generic directives, ::component{is="component-a"} is also supported.

Besides, as the compilation target is the Astro component, <script> and <style> tags are allowed (unlike in Vue). UnoCSS will also properly scan these components and inject the right styling for them.

You may check the Tests on Advanced Contents post (in Chinese) for demos.

Colors

March 21 is the International Color Day, a day for humankind to celebrate the diversity our eye cells can capture. Playing with colors sounds pretty fun to me, and as you may have noticed, my blog provides a customizable color palette.

Why do I allow you, the reader, to customize the look of my own blog? To me, although the writings are likely personal and opinionated, the reading experience is indisuptably yours, and I want you to feel comfortable. Of course, my blog has few readers, but at least when I want to revisit my writings, I want to have a comfortable reading experience. This applies to the color system, and other typesetting issues, which I may talk about later.

Colors can reflect ambience and emotions, which can be fluctuant. As of version 4.0.4, 7 hue options are built-in, which means you can pick your own choice with one click, just like Independent Voices palettes of Firefox.

This feature has been around since v2. The full-fledged color tuning in v2-v3 is not reimplemented yet, because of the underlying mechanics have changed. Although we call it a color palette, only two accent colors (called one and two) can be freely chosen. Too many accents may cause distraction, so I delibrately avoid that. All prebuilt palettes use two similar colors for two accents, aiming to keep a consonance. We would like to explore more creative combinations in the future.

The key feature of v4 colors is color calculation, which is heavily inspired by Dynamic color of Google's Material You. For each accent, 6 variants are calculated from the base. To take the default blue palette as an example of the calculation:

One Light
One Dark
Two Light
Two Dark

The accent color under dark theme is lightened a little, then a dimmed and a faded variant are generated from the light / dark accent. Specifically, the dimmed version of the dark accent is desaturated. Under light theme, the dimmed version is used as the foreground color, and the faded version is used as the background color, while the opposite applies to the dark theme. This mechanism ensured a consistent and harmonic color experience. The color calculation is based on a new color format, OKLCH, which makes its way into the CSS Colors Module 4 standards and lands in all major browsers recently (March 2023). This color format is based on lightness, chroma and hue, and fixes the inconsistency of lightness under different hues found in earlier formats like HSL. OKLCH is also capable of encoding the more vibrant colors outside of the sRGB space. You can read more about OKLCH at this Evil Martians's post. Currently, colors are defined, stored in your local storage, and calculated under OKLCH format, but polyfilled to RGB colors when injected as CSS variables. Under the hood, all of the UI components are colored solely with these CSS variables to create a unified experience.

I hope these colors will add some spice to your reading on my blog with a minimalist design.

Fonts

Unlike colors, the final decision is that v4 will have an opinionated font pick. Previously, we decided to enforce an opinionated font pick. The pick was: Inter for UI and post, Source Serif 4 for post headings, JetBrains Mono for monospace and codes, Noto Sans SC and Noto Serif SC for Simplified Chinese (also known as Source Han Sans and Source Han Serif). Although I enjoy Iosevka very much, I feel like JetBrains Mono might be less agressive for the general public. The fonts are loaded from Google Fonts CDN, so every device should enjoy the same look.

The main concern was, although serif fonts will probably give you a better reading experience, the glyph coverage of serif fonts is insufficient. The linguistic parts of this blog make heavy use of rare Unicode codepoints.

However, we move on to support full font customization. An independent typesetting page can be found under the lab section, where you can pick the fontfaces of headings and contents, and pick your own serif, sans-serif or monospace fonts. The opinionated look will be treated as the default look.

We simply cannot resist a reading experience comparable to newspapers or academic papers. If you have a high-end device, feel free to toggle serif fonts on.