Markdown is a pleasure to work with, especially for developers. This is why it has become the default language for document writing among software engineers. Its design emphasizes readability in plain text, allowing you to read and edit raw text without relying on a parser like Pandoc to convert it into HTML or a renderer such as a browser to view the formatted output.
The widespread adoption of Markdown speaks for itself; it has become so prevalent that its usage hardly needs further explanation. The vast ecosystem offers almost everything I need through Remark and Rehype plugins. I’ve even created custom plugins for this blog due to Markdown’s ease of use and straightforward approach to modifying its syntax tree.
This post demonstrates my customized Markdown-to-post process for this blog, serving as both documentation and a guide for future reference. This saves me from having to revisit the code to figure out what I implemented.
GitHub Flavored Markdown (GFM) enhances standard Markdown by incorporating additional features such as tables, task lists, and automatic conversion of plain text URLs into clickable links. I’m so accustomed to writing Markdown on GitHub that I didn’t realize these were extensions, not part of standard Markdown. GFM is the default formatting in Astro, the framework powering this site.
Beyond Markdown, there’s MDX, a superset that supports embedded components from various frameworks. This flexibility ensures I’m well-equipped, especially when creating interactive posts.
As a developer, code snippets are essential in my blog posts. I want them to be well-highlighted, similar to how they appear in my IDEs. I opted for rehype-pretty-code, a wrapper for shiki, so I don’t have to worry much about syntax highlighting.
In addition to syntax highlighting, it offers some nice-to-have features. For
example, inline code can also be colored: let x = Ok(());
. With code
blocks, you can add titles, show line numbers, or highlight specific lines or
words.
I also added a code group component to display multiple code blocks as tabs. This is a great way to show related code together without taking up too much space.
// This is the main function.
fn main() {
// Statements here are executed when the compiled binary is called.
// Print text to the console.
println!("Hello World!");
}
package main
func main() {
panic("Nah, don't use Go")
}
Adding images in Markdown is straightforward and supported by default. However, ensuring they are optimized and performant on the web is a challenge. I don’t want to serve a full-fledged 4K uncompressed image to a phone user, and it probably wouldn’t look good either. The ideal image resolution should match the width and height of the rendered element in the browser. For example, if you set an image to display at 1600px wide, it looks sharpest when the actual image’s width is exactly 1600px. This method is called responsive images, and MDN has a great article about it.
There’s also something called Cumulative Layout Shift. To prevent this, you need to specify the image’s width and height beforehand. This allows the browser to reserve space for the image while it fetches it, ensuring a smooth layout without sudden shifts.
This is where I needed to write my own Markdown plugin to replace the default
image with a custom responsive image component. This component generates
different resolutions for an image at build time and provides instructions on
which resolution to use for specific screen sizes. It would be easier to use
image hosting services, where you can simply append a query parameter to the
URL, like image.service/my-image.png?width=1600
, and let the service handle
resizing and caching. However, since this is just a personal blog, I don’t want
to pay extra for a service that might not even see much traffic.
Fortunately, Astro can pre-generate images for multiple screen sizes at build time. They even expose an API that optimizes images behind the scenes, making it easy to create a custom image component tailored to my needs. My site is now 100% static, and I can deploy it almost anywhere to start receiving traffic.
There’s one more aspect to consider: I want the optimization to apply to remote images as well. This way, I can avoid committing large image files to my code repository. During the build process, Astro downloads remote images and transforms them into custom resolutions. However, the API requires width and height attributes for remote images. To address this, I customized Markdown’s image title syntax to allow the injection of attributes.
Local image, supports paths relative to the markdown file:

Remote image, width and height attributes are required:

Another way to do remote image, same end result as above:
<img src="https://share.hhai.dev/IMG_1203.png" width="1920" height="816" />

Text right after the image is automatically recognized as the image caption
(without additional line breaks).
<img src="https://share.hhai.dev/IMG_1203.png" width="1920" height="816" />
> With the <img> tag, you need to put the caption inside a block quote.
Keeping everything within the standard reading width can be boring. I want
certain elements, like images, to stand out more. To achieve this, I created a
Rehype plugin to enclose regular parts of the post within a standard reading
width container, while special feature elements are wrapped in a larger width
container. These feature elements are identified by a specific featuretype
attribute and can be added similarly to the width and height attributes
mentioned earlier. Additionally, tables are featured by default because they
tend to be wide and require ample horizontal space to display properly. You’ll
need to be on desktop to see the examples below in full effect.
I have a love-hate relationship with math. I hate it because I’m not good at it, but it has the capability to solve many problems related to my work. It feels great when I finally solve something using math, like this piece of asymptotic analysis I did for a blog post:
This is enabled by the KaTeX library.
This post might never truly be finished. As I continue to introduce new features, I’ll revisit this space, editing and documenting those additions.
Here are the features I want to add in the future: