---
title: 'The State of Static Blogs in 2024'
description: 'There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.'
date: 2024-07-25
tags: ['v1.0.0']
image: './1200x630.png'
authors: ['enscribe']
---

## Introduction

Hello! My name is enscribe ([jktrn](https://github.com/jktrn) on GitHub), and I'm a fullstack web developer who has been fiddling with blogging platforms for a couple of years now. I run a blog at [enscribe.dev](https://enscribe.dev), where I write about cybersecurity and the capture-the-flag (CTF) scene.

I have a lot of opinions about what makes a great blogging template. As a cumulative result of all the slop, bullshit, and outright terrible design decisions I've had to deal with working with various templates and frameworks, I bring you [astro-erudite](https://github.com/jktrn/astro-erudite), which should hopefully bring a better developer and user experience in terms of ease of use, customization, and performance.

astro-erudite is written in Astro, a framework hyperoptimized for static content such as blogs. Aesthetically, it is also designed to be as boring as possible while still maintaining maximum functionality, as to allow for the freedom of the developer (or the designer they hire) to make their blog uniquely their own. Within the codebase of this template I've included many nuances that, in my opinion (and there will be many, many opinions here), make the developer experience significantly more pleasant. I've also _excluded_ many features that, frankly, you don't need.

## Welcoming some DX features

This is a non-exhaustive list of features I believe are essential for a frictionless developer experience:

- [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers&mdash;however, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-stone-50 text-stone-900 dark:bg-stone-900 dark:text-stone-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utility colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors).
- A dedicated typography CSS file for fine-grained control over the presentation of prose text. Although [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) (a plugin that automatically styles any content surrounded by an `<article>{:html}` tag) offers a solution to this, you lose out on all of the control and often have to make overrides for undesirable output. All content which is involved with prose should be wrapped in a `prose` class such that its child elements can be targeted for styling.
- [Expressive Code](https://expressive-code.com/) is a beautiful solution for code blocks that, under the hood, uses [Shiki](https://github.com/shikijs/shiki) for syntax highlighting. Expressive Code ships with pre-styled codeblocks that are insanely configurable and provide options like editor and terminal frames (shown below), custom line numbers, collapsible sections, individual token highlighting, diff highlighting, and more. To use these for any provided codeblock, simply add any of the following props after the codeblock's backticks:

  ````mdx showLineNumbers=false collapse={2-42}
  ```ts title="example.ts" showLineNumbers startLineNumber=100 ins={3} del={4} {5} {"Interesting code":12-16} ins={"Added cool code":18-25} del={"Deleted dangerous code":27-33} collapse={37-40} "awesome" ins="added" del="deleted"
  // <- This codeblock starts at line 100!

  // This line should be marked as a diff addition
  // This line should be marked as a diff deletion
  // This line should be highlighted

  // The keyword "added" will be highlighted in green
  // The keyword "deleted" will be highlighted in red
  // The keyword "awesome" will be marked with gray

  // Insert an empty line above code you wish to add a note to

  function demonstrateFeatures() {
    console.log('Hello world!')
    return true
  }


  function obfuscateString(input) {
    return Buffer.from(input)
      .toString('base64')
      .replace(/[A-Za-z]/g, (c) =>
        String.fromCharCode(c.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1)),
      )
  }


  function deleteAllFiles() {
    fs.rmdirSync('/etc', { recursive: true })
    fs.rmdirSync('/usr', { recursive: true })
    fs.rmdirSync('/home', { recursive: true })
    return 'System wiped!'
  }

  // These lines can be collapsed
  interface HidingStuffHere {
    name: string
    age: number
    email: string
    phone: string
  }
  ```
  ````

  This results in a codeblock that looks like this:

  ```ts title="example.ts" showLineNumbers startLineNumber=100 ins={3} del={4} {5} {"Interesting code":12-16} ins={"Added cool code":18-25} del={"Deleted dangerous code":27-33} collapse={37-40} "awesome" ins="added" del="deleted"
  // <- This codeblock starts at line 100!

  // This line should be marked as a diff addition
  // This line should be marked as a diff deletion
  // This line should be highlighted

  // The keyword "added" will be highlighted in green
  // The keyword "deleted" will be highlighted in red
  // The keyword "awesome" will be marked with gray

  // Insert an empty line above code you wish to add a note to

  function demonstrateFeatures() {
    console.log('Hello world!')
    return true
  }


  function obfuscateString(input) {
    return Buffer.from(input)
      .toString('base64')
      .replace(/[A-Za-z]/g, (c) =>
        String.fromCharCode(c.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1)),
      )
  }


  function deleteAllFiles() {
    fs.rmdirSync('/etc', { recursive: true })
    fs.rmdirSync('/usr', { recursive: true })
    fs.rmdirSync('/home', { recursive: true })
    return 'System wiped!'
  }

  // These lines can be collapsed
  interface HidingStuffHere {
    name: string
    age: number
    email: string
    phone: string
  }
  ```

  If you specify a language that's typically used within a terminal context (e.g. `ps1`, `sh`, `console`, etc.) then the frame of the codeblock will instead look like a terminal:

  ```console title="Installing dependencies with pnpm"
  $ pnpm install @astrojs/mdx @astrojs/react @astrojs/sitemap astro-icon
  ```

- The `cn(){:js}` function is a utility function which combines [clsx](https://www.npmjs.com/package/clsx) and [tailwind-merge](https://www.npmjs.com/package/tailwind-merge), two packages which allow painless conditional class addition and concatenation:

  ```tsx title="src ＞ lib ＞ utils.ts" caption="A utility function for class name concatenation" showLineNumbers
  import { type ClassValue, clsx } from 'clsx'
  import { twMerge } from 'tailwind-merge'

  export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs))
  }
  ```

  This needs to be in every single template. This is an example of it being used in my `<Link>{:html}` component:

  ```astro showLineNumbers title="src ＞ components ＞ Link.astro" caption="A custom Link component with tailwind-merge and clsx" {10-15} "cn"
  ---
  import { cn } from '@/lib/utils'

  const { href, external, class: className, underline, ...rest } = Astro.props
  ---

  <a
    href={href}
    target={external ? '_blank' : '_self'}
    class={cn(
      'inline-block transition-colors duration-300 ease-in-out',
      underline &&
        'underline decoration-muted-foreground underline-offset-[3px] hover:decoration-foreground',
      className,
    )}
    {...rest}
  >
    <slot />
  </a>
  ```

  We were able to, in a single helper function:

  1. Concatenate whatever the user passed via the `class{:astro}` prop to our base styles
  2. Conditionally add an underline if the `underline{:astro}` prop is true

  Awesome!

## Welcoming some UX features

Within the blog itself (as in the layout, appearance, and navigation) are features that I believe are essential for a great user experience:

- Images are awesome and, by default, your blog post should have an image associated with it as part of the post's [Open Graph](https://ogp.me/) metadata. Since you can do whatever you want with the image, all of my dummy posts will have a placeholder image placed within their folder in `src/content/blog/`. Whenever you load into a blog post, splat in the middle will be the image associated with that post in its frontmatter.
- Theme selectors should be self-explanatory. I've added one on the top right of the header, which is also `sticky` and not `absolute` such that it doesn't ignore the document flow (and thus you won't have to add `mt-20` to the top of every single page).
- The table of contents of a post shouldn't be reduced to a `<details closed>{:html}` at the start of a blog post on desktop. You'd need to go to the top of the page to navigate through items. I've added a sticky `<TableOfContents>{:html}` component which always hangs out around the unused left side margin of a blog post. I also attached a very tiny client-side script using [`IntersectionObserver{:js}`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to highlight all of the headings you're viewing within the TOC as you scroll through the page&mdash;it also will handle nested headings in that the parent heading of a visible child will still be highlighted even if off-screen (see the dummy [2024 Post](/blog/2024-post) for an example of this). I'll still use a collapsible `<details>{:html}` element for the table of contents on mobile though since obviously a table of contents on the side is unfeasible for small screens.
- Every page, except the homepage, will have a `<Breadcrumb>{:html}` component which shows you your current location in the site hierarchy. I don't see these often in blog templates even though they are so amazing for both discoverability (SEO and crawling) and user experience (the user always knows how "deep" they are in the site).
- You can specify multiple post authors via frontmatter. If this post author's id is found within the `Authors` collection, then it will render particular info from that author's frontmatter file, `[author-name].md` (e.g. avatar, link to profile). For example, the previous post (2024 Post) has two authors: "enscribe" and "jktrn", where "enscribe" is the only author with a custom avatar since "jktrn" is unregistered.
  - Each author will have their own page, which lists all of their posts. If you're the only author throughout the entire blog then you can simply disregard all aspects regarding both inserting authors and the `Authors` collection.
- Each tag will also have their own page, which lists all of the posts under that tag!
- $\LaTeX$ is fully supported with [KaTeX](https://katex.org/):

  <blockquote>

  To solve the cubic equation $t^3 + pt + q = 0$ (where the real numbers
  $p, q$ satisfy ${4p^3 + 27q^2} > 0$) one can use Cardano's formula:

  $$
    \sqrt[{3}]{
      -\frac{q}{2}
      +\sqrt{\frac{q^2}{4} + {\frac{p^{3}}{27}}}
    }+
    \sqrt[{3}]{
      -\frac{q}{2}
      -\sqrt{\frac{q^2}{4} + {\frac{p^{3}}{27}}}
    }
  $$

  For any $u_1, \dots, u_n \in \mathbb{C}$ and
  $v_1, \dots, v_n \in \mathbb{C}$, the Cauchy–Bunyakovsky–Schwarz
  inequality can be written as follows:

  $$
    \left| \sum_{k=1}^n {u_k \bar{v_k}} \right|^2
    \leq
    {
      \left( \sum_{k=1}^n {|u_k|} \right)^2
      \left( \sum_{k=1}^n {|v_k|} \right)^2
    }
  $$

  Finally, the determinant of a Vandermonde matrix can be calculated
  using the following expression:

  $$
    \begin{vmatrix}
    1 & x_1 & x_1^2 & \dots & x_1^{n-1} \\
    1 & x_2 & x_2^2 & \dots & x_2^{n-1} \\
    1 & x_3 & x_3^2 & \dots & x_3^{n-1} \\
    \vdots & \vdots & \vdots & \ddots & \vdots \\
    1 & x_n & x_n^2 & \dots & x_n^{n-1} \\
    \end{vmatrix}
    = {\prod_{1 \leq {i,j} \leq n} {(x_i - x_j)}}
  $$

  —<cite>[Three famous mathematical formulas](https://developer.mozilla.org/en-US/docs/Learn/MathML/First_steps/Three_famous_mathematical_formulas) (Mozilla Docs)</cite>

  </blockquote>

## Foregoing some slop

- Goodbye, [ESLint](https://eslint.org/)! There have been so many occasions where I've had to deal with blogging templates with in-built pre-commit hooks which enforce contrived and arbitrary linting rules that, frankly, I couldn't be bothered with. Obviously, linting is awesome for ensuring consistency and best practice, but that's with shared and large codebases. You're dealing with, at most, your mediocre MDX blog posts and some interior fetching. It's just not worth the hypertension.

- You really don't need analytics via [Umami](https://umami.is) or [Plausible](https://plausible.io). Let's be realistic: for many personal blogs, unless you're an anime profile picture Twitter microcelebrity and/or the daughter of Taylor Swift you don't need to know how many of your avid fans click Big Button A versus how many click Big Button B.
- You really don't need a comments section via [Giscus](https://giscus.app). This opens up a can of worms involving the ability to spam comments and the necessity to moderate them. If you want organic discussion about your blog posts to happen, then share on social media and let people discuss there.
  - Speaking of sharing on social media, let's get rid of the share buttons. Please inform me of a single time you have used a share button on a blog post.
- You really don't need a <abbr title="Content Management System">CMS</abbr> unless you have thousands of posts and/or are willing to navigate through a clunky management interface. Markdown and folders is really all you need, which you can organize to your preference via folder or file naming conventions.
- If you have literally anything involving an `.env` file in a blogging site, please think about what you are doing very carefully.
- Please do not override the browser's <kbd>Ctrl</kbd> + <kbd>K</kbd> functionality to open up a command palette. There should not be a single reason why a user would use a small context menu to browse your blog over the `/blog` route. Most of the time, command palettes on sites do nothing more than regurgitate shortcuts that are already on the same page you're hiding with the palette's modal.

## Something important

Before we wrap up, I want to emphasize that everything that I've shared here is based on my own personal opinions and experiences. While I believe these practices and choices lead to a better blogging experience, you're absolutely free to disagree.

The web development community, especially in spaces like Twitter and various online forums, is constantly engaged in heated debates about what constitutes "best practices." You'll find a wide spectrum of viewpoints:

1. Fundamentalists who adhere strictly to established patterns and completely disregard change,
2. Accelerationists who gobble up whatever Vercel cooks as if it's the second coming of Christ,
3. and everyone in between this spectrum who just wants to ship.

I'm just another guy who loves to blog, and I wanted to share what particular technology stack worked the best for me in this particular use case. A stack for one project can be completely unusable for another. If you vehemently hate any of the design choices I've made then simply get rid of them. MIT license! Happy blogging.
