blog.itcode.devblog.itcode.dev

On Realizing I've Overhauled the Blog Four Times

It feels like it wasn't long ago that I overhauled the blog for the third time because I didn't like how it looked, and yet here I am, before I knew it, already overhauling it for the fourth time. My last blog post was from almost half a year ago, since I hardly paid any attention to the blog while developing a toy project. What's more, since the 3rd renewal I've picked up a variety of frontend skills. Because my standards rose along with that, part of the reason was that I just couldn't stand the blog's UI anymore. Fortunately, I had quite a few posts already written, and a few of them turned out to be reliable earners, so visitor numbers didn't decline.

On Realizing I've Overhauled the Blog Four Times

It feels like it wasn't long ago that I overhauled the blog for the third time because I didn't like how it looked, and yet here I am, before I knew it, already overhauling it for the fourth time. My last blog post was from almost half a year ago, since I hardly paid any attention to the blog while developing a toy project. What's more, since the 3rd renewal I've picked up a variety of frontend skills. Because my standards rose along with that, part of the reason was that I just couldn't stand the blog's UI anymore. Fortunately, I had quite a few posts already written, and a few of them turned out to be reliable earners, so visitor numbers didn't decline.
RWB0104
@RWBwritten at 2023-09-08 18:00:59

🖼️ Under construction...

It feels like it wasn't long ago that I overhauled the blog for the third time because I didn't like how it looked, and yet here I am, before I knew it, already overhauling it for the fourth time.

My last blog post was from almost half a year ago, since I hardly paid any attention to the blog while developing a toy project. What's more, since the 3rd renewal I've picked up a variety of frontend skills. Because my standards rose along with that, part of the reason was that I just couldn't stand the blog's UI anymore.

Fortunately, I had quite a few posts already written, and a few of them turned out to be reliable earners, so visitor numbers didn't decline.

Looking back, the 3rd renewal was really a renewal that "had to happen." Up through the 2nd renewal, my React skills were still in a transitional phase. Even while building it, there were a lot of parts that felt off, and even though I suspected there must be cleaner, more sophisticated patterns out there, I had no idea what to even search for since I didn't know what I didn't know. It wasn't until the 3rd renewal that both the UI and the underlying structure got a solid foundation.

But strictly speaking, only the foundation was solid — once you actually looked underneath, there were more than a couple of shaky spots, like a Jenga tower midway through a game. Even though the gaping holes were visible, I didn't have the ability to fill them in at the time. Since the blog ran fine even with a few holes here and there, I never really found the motivation or the time to fix them.

The main focus of the 4th renewal was improving the UI along with filling in those holes. Here's a rough overview of the renewal.

This blog had been using Next.js from the start. With Next.js bumping up to version 13, there were a few notable changes.

  • Adoption of the app folder structure
  • Explicit distinction between client / server components

The app folder structure, which was still an experimental feature as recently as last year, seems to have become the mainstream approach. I remember, back when I was setting up a toy project, running the create-next-app script and being asked whether to enable that option.

Back then it was still labeled experimental, so I didn't use it, which means this is actually the first time I've used it directly. Under app, page components live, and the structure is such that the file itself becomes the specification for the page. For example, like this.

TXT

🏠 project
├─ 📂 app/
│   ├─ layout.tsx
│   ├─ template.tsx
│   ├─ error.tsx
│   ├─ loading.tsx
│   ├─ page.tsx
│   └─ 📂 home/
│       ├─ layout.tsx
│       ├─ template.tsx
│       ├─ error.tsx
│       ├─ loading.tsx
│       └─ page.tsx
└─ ...

Specification is done in this way, and each file is given a corresponding role. This is why the file structure alone acts as a specification.

The above structure gets applied internally as follows.

TSX

// root
<Layout>
    <Template>
        <ErrorBoundary fallback={<Error />}>
            <Suspense fallback={<Loading />}>
                <ErrorBoundary fallback={<NotFound />}>
                    <Page />
                </ErrorBoundary>
            </Suspense>
        </ErrorBoundary>
    </Template>
</Layout>

The root page is implemented as above. A sub-page like home in the example above is applied as a nested structure, as shown below.

TSX

// home
<Layout>
    <Template>
        <ErrorBoundary fallback={<Error />}>
            <Suspense fallback={<Loading />}>
                <ErrorBoundary fallback={<NotFound />}>
                    <Layout>
                        <Template>
                            <ErrorBoundary fallback={<Error />}>
                                <Suspense fallback={<Loading />}>
                                    <ErrorBoundary fallback={<NotFound />}>
                                        <Page />
                                    </ErrorBoundary>
                                </Suspense>
                            </ErrorBoundary>
                        </Template>
                    </Layout>
                </ErrorBoundary>
            </Suspense>
        </ErrorBoundary>
    </Template>
</Layout>

Since this post isn't about explaining Next.js 13, I'll just note that this much of a change exists and move on. The important point is that, unlike the older version where only page code was written, the page's layout or template can now also be managed at the page level.

If this structure is followed precisely, you should be able to intuitively grasp the structure of a page. Of course, it's not enforced, so you can still just write pages and manage layout or templates the old way if you prefer. I myself still take the approach of separating things like layout into their own components and importing them the old way, out of convenience.

More details can be found in the official Next.js routing documentation.

A somewhat unfamiliar change: components must now explicitly declare whether they are client or server components. The default appears to be a server component. What functionality is available differs depending on the component's classification.

Featureserverclient
Data fetching
Direct access to backend resources
Managing sensitive server-side info (tokens, etc.)
Keeping large dependencies on the server / reducing client-side JS code
Using event listeners (onChange, etc.)
State management and lifecycle usage (useEffect, etc.)
Using browser-only APIs (Navigator API, etc.)
Using custom Hooks
Using React class components

These are the capabilities for each classification. Nothing new here. In the old version, these features were all bundled together within a component, but starting with 13, they're now explicitly separated.

Features like getServerProps can be used in server components. Conversely, to use things like useState, cookies, or localStorage, you need a client component.

Usage for each is as follows.

TSX

// Server component (no need to declare it explicitly)

export default function Component(): ReactNode
{
    // ...
}

TSX

// Client component
'use client'

export default function Component(): ReactNode
{
    // ...
}

If you use a feature that violates its component type, it'll kindly throw an error letting you know, so you can just check it and fix it accordingly. If server/client functionality ends up mixed together, that's structurally incorrect, so the component needs to be split apart.

More details on this can be found in the official Next.js rendering documentation.

I remember that, back when I was designing the component structure, I liked splitting things up and managing them separately. That mindset got applied in a somewhat bizarre way, resulting in the structure below.

TSX

🏠 project
├─ 📂 src/
│   ├─ 📂 components/
│   │   ├─ 📂 Card/
│   │   │   ├─ Card.tsx
│   │   │   └─ index.ts
│   └─ 📂 styles/
│       └─ 📂 Card/
│            └─ Card.module.scss
└─ ...

I'm not sure why I did it, but I ended up going with that kind of structure at the time. Looking at it now, it's a bizarre structure no matter how you slice it.

TSX

🏠 project
├─ 📂 src/
│   └─ 📂 components/
│       ├─ 📂 atom/
│       │   └─ 📂 Card/
│       │       ├─ Card.tsx
│       │       ├─ Card.module.scss
│       │       └─ index.ts
│       ├─ 📂 molecule/
│       ├─ 📂 organism/
│       └─ 📂 template/
└─ ...

I applied an atomic-based folder structure as shown above.

This is the part I liked best about this renewal. The existing markdown conversion logic had quite a few problems. It was built around the marked library, but since rendering was string-based, it had the downside of making it very difficult to apply React components.

Because of this, markdown alone had to be built purely out of HTML, JavaScript, and CSS. Since the blog's foundation is React, it always bothered me that the most important content ended up being handled separately, like it was in a different world entirely. What's more, managing HTML tags as raw strings hurt maintainability, and honestly, it just looked ugly too.

TS

// Rendering code blocks
renderer.code = (code: string, lang: string = 'txt'): string =>
{
    // When there's a valid language
    if (lang && renderer?.options?.highlight)
    {
        // Block equation case
        if (lang === 'latex-block')
        {
            const katexText = katex.renderToString(code, { output: 'html', throwOnError: true });

            return `<div class="katex-block">${katexText}</div>`;
        }

        // Otherwise
        code = renderer.options.highlight(code, lang as string) as string;

        const langClass = `language-${lang}`;

        while (COMMENT_REGX.test(code))
        {
            const [ origin, target ] = COMMENT_REGX.exec(code) as string[];

            const newer = target.split('\n').map((item) => `<span class="token comment" data-tag="new">${item}</span>`).join('\n');

            code = code.replace(origin, newer);
        }

        const line = code.split('\n').map((item, index) => `<tr data-number=${index}><td class="line-number" data-number="${index}">${index}</td><td class="line-code" data-number=${index}>${item}</td></tr>`).join('\n').replace(/\t|\\n/, '');

        return `
            <div class="block-code">
                <div class="top">
                    <p>${lang.toUpperCase()}</p>
                    <div></div>
                    <div></div>
                    <div></div>
                </div>

                <button onclick="copyCode(this);"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard"><path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path></svg></button>

                <pre class="${langClass}"><table><tbody>${line}</tbody></table></pre>
            </div>
        `;
    }

    return '';
};

Of all the markdown conversion logic, this code-block logic was the largest and most important. Not only is it obviously ugly code at a glance, but a structure like this would be a headache to customize down the line.

For this renewal, I put a lot of thought into changing this part as much as possible. I could either keep using marked and look for a better approach, or switch to a similar library, unified. I'd gone through this same deliberation before — back then, I felt marked's official documentation was more intuitive, and I was actually able to implement what I wanted with it. But unified's extensive resources and third-party ecosystem drew me in, so I decided to look into that route this time.

At first I tried to do the conversion using unified directly, but I ran into various obstacles, including third-party version issues. Rather than keep struggling like that, I figured it might be better to just look for a suitable React-specific library. That's how I found react-markdown. Being React-based, I could use it as follows.

TSX

export default function Viewer(): ReactNode
{
    return (
        <ReactMarkdown
            className={cn('markdown')}
            data-component='MarkdownViewer'
            rehypePlugins={[[ rehypeKatex, { output: 'mathml' }], rehypeRaw ]}
            remarkPlugins={[ remarkGfm, remarkMath ]}
            components={{
                a: MarkdownA,
                blockquote: MarkdownBlockquote,
                code: handleCode,
                h1: MarkdownHeading,
                h2: MarkdownHeading,
                h3: MarkdownHeading,
                h4: MarkdownHeading,
                h5: MarkdownHeading,
                h6: MarkdownHeading,
                img: MarkdownImg,
                table: MarkdownTable,
                td: MarkdownCell,
                th: MarkdownCell,
                tr: MarkdownTr
            }}
        >
            {text}
        </ReactMarkdown>
    );
}

As shown above, I created and applied a matching component for each tag. Since these are React components, they blended easily into the blog's design, and applying state management like useState was also convenient. In other words, direct interaction between the markdown content and the blog is now possible. This advantage really shone with the image component, where I implemented an image modal that pops up when an image is clicked.

More than anything, it's now far cleaner and nicer-looking than the old string-based approach. It feels like one of the blog's biggest gaps has finally been properly filled.

Up through the 2nd renewal, I was still using Material UI. That theme was gorgeous, but also quite heavy, so even the slightest misuse would cause a serious hit to performance. On top of that, combined with my own dismal React skills at the time, the performance was seriously bad. The version at the time was 4, and apparently it had a lot more performance issues compared to the current version 5.

Because of this, for the 3rd renewal I didn't use a design system and instead built every component myself, which I initially thought looked great, but the further along I got, the more it started feeling somehow tacky. I improved the blog's UI by referencing various sites and resources.

Also, having grown familiar with Material UI again through several toy projects, I brought it back to strengthen the design foundation.

🖼️ Change to popular posts

🖼️ Change to posts and categories

I changed the fonts and layout to look cleaner and simpler. I switched posts to a grid layout so more posts can be seen at once per row. The old post cards were needlessly long.

I also implemented category selection so that, via CSS's filter, the selected category is highlighted clearly while still giving it a distinctive feel.

I also set up state management so that things like post page changes, category selection, and keyword search are faithfully reflected in URL parameters. This way, users can see the same currently-selected category, keywords, and so on just by entering the URL.

I also made active use of framer-motion, which I learned at work, to implement subtle animations. It was a great help not just for the post feed, but for implementing a good portion of the blog's animations overall.

It might not be a dazzling leap forward, but I think it's much more polished compared to before.

The existing comment system used Utterances. It worked by treating GitHub Issues like comments, and while it was a fairly decent commenting library, it had the following problems.

  • Commenting forces a GitHub account requirement. Non-members can't comment
  • Not very React-friendly. In particular, theme switching required separate logic
  • No support for nested replies

Since GitHub Issues weren't originally built for this purpose, there was a fair amount of divergence from a typical comment UX. Whenever a reply was needed, mentioning a GitHub ID had to stand in for a reply.

I came across this service by chance while looking around — GitHub added the Discussions feature to meet exactly this kind of need. This one is, quite literally, built for comments, so it was far better than GitHub Issues.

A GitHub account is still required, but every other issue was cleanly resolved. Migrating from Utterances to Discussions was also simple, so it didn't take much effort to move over. That said, bulk migration isn't supported, so it would be a hassle if you have a large volume of comments.

You can check out Giscus's various features in the official Giscus documentation. The docs are written clearly and easy to follow, and it offers a lot more functionality than Utterances.

I'm glad the blog keeps getting better with each successive renewal. Having patched things up to some degree, I'm hopeful I'll be able to stay attached to the blog for a while.

I need to start writing posts again soon too. In particular, I'd like to add a bit more content to the OpenLayers guide. Sometimes when I skim through it, there are parts that bug me. On top of that, I have plenty of topics I want to write about in mind, but writing posts turns out to be way more of a hassle than I expected...

The period between the 3rd and 4th renewal was about a year. I wonder how long this renewal will last.

# React# Next.js# Material UI# Giscus
ship
blog.itcode.dev

Notes from the π-th Alpaca

7.0.1
Developed by RWB since 2021.057th upgraded at 2026.08