2024 Blog Update
I updated my blog a couple of days ago and wanted to share some of the changes I made. Especially because I have been doing this for a while now and I frequently encounter issues of some sort. Most of them beeing related to the fact that packages that were hot and trendy a couple of months ago are now deprecated and I have to update them. Over time this lead me to rebuild the tech stack of my blog from scratch. I will go over the changes I made and the reasons behind them.
Shoutout: As my blog is a next.js project I often refer to leerob.io as a source of inspiration. I saw him doing a "blog refresh" and I thought I could do my own version of that. So here I am putting my own spin on it. Thanks Lee! 🙏
Content Management
I started my web dev journey on PHP and designing the entire site myself back in 2017 then iteratet to content management on Wordpress and designed the website with the internal style sheet and a base theme. So yeah pretty basic for a long time. But, I was frustrated! This just never looked the way I wanted it to look. Not to mention the countless ads and plugins I had to run and install, that all would at some point break my site.
The first time I was able to feel like I had a chance to get this right is when I switched to Next.js and Contentful(CMS). Not long after that I transitioned my content to Markdown but both Contentful and .md
would not allow me to use Components inside my blog. Thats why I moved to MDX, and then to Contentlayer with MDX. And now I'm back to MDX without contentlayer because it would basically not allow me to update next.js to the latest version without removing it.
The good thing is. I actually don't need Contentlayer. I can do everything I need with just MDX and a few small functions. I'll go over the changes I made and the reasons behind them.
- Markdown - with support for JSX
- Syntax Highlighting
- Version Control
- Minimal External Dependencies
Retrieving Content
I followed the same approach as leerob and used the following code to retrieve my blog posts:
I removed the following libraries:
contentlayer
next-contentlayer
rehype-autolink-headings
rehype-pretty-code
rehype-slug
remark-gfm
I was still able to maintain almost all of my content requirements with not much code. For example, here's how I'm able to retrieve all of my blog posts:
import fs from "fs";
import path from "path";
function getMDXFiles(dir) {
return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx");
}
function readMDXFile(filePath) {
let rawContent = fs.readFileSync(filePath, "utf-8");
return parseFrontmatter(rawContent);
}
function getMDXData(dir) {
let mdxFiles = getMDXFiles(dir);
return mdxFiles.map((file) => {
let { metadata, content } = readMDXFile(path.join(dir, file));
let slug = path.basename(file, path.extname(file));
return {
metadata,
slug,
content,
};
});
}
export function getBlogPosts() {
return getMDXData(path.join(process.cwd(), "content/blog"));
}
That's not too bad. We are missing fast refresh for content changes now, but to be honest that was not a big deal for me. I'm the only one who is updating the content and I can live with a 2 second refresh. Maybe I can look into how to implement a workaround for that in the future.
Gray-matter
On this I was kind of undicided. I liked the idea of having the frontmatter in my markdown files but I also liked the idea of having it in a separate file. I ended up with the following solution:
- remove
gray-matter
- implemented
parseFrontmatter
function - implemented features that I needed like arrays and booleans
function parseFrontmatter(fileContent: string) {
let frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
let match = frontmatterRegex.exec(fileContent);
let frontMatterBlock = match![1];
let content = fileContent.replace(frontmatterRegex, '').trim();
let frontMatterLines = frontMatterBlock.trim().split('\n');
let metadata: Partial<Metadata> = {};
frontMatterLines.forEach((line) => {
let [key, ...valueArr] = line.split(': ');
let value: string | boolean | string[] | undefined = valueArr.join(': ').trim();
value = value.replace(/^[''"](.*)['"]$/, '$1')
if (value === 'true') value = true;
if (value === 'false') value = false;
if (value.toString().includes("[") && value.toString().includes("]")) {
if (typeof value !== 'string') return;
// remove brackets and quotes
value = value.replace(/[\[\]]+/g, '').replace(/^[''"](.*)['"]$/, '$1')
value = value.split(',').map((tag) => tag.trim())
}
if (value === undefined) return;
metadata[key.trim() as keyof Metadata] = value;
});
return { metadata: metadata as Metadata, content };
}
Remark and Rehype
What about the AST (abstract syntax tree) modifications for auto-linking headings, adding IDs, and supporting syntax highlighting? I thought about this for quite some time. The functionality that you need is fairly minimal. I ended up writing a few small functions to replace the functionality of the plugins I was using.
Note: Be sure your line endings are set to LF or you might run into issues with the highlight
function.
import { highlight } from "sugar-high"; // 1KB new dependency
// This replaces rehype-pretty-code
function Code({ children, ...props }) {
let codeHTML = highlight(children);
return <code dangerouslySetInnerHTML={{ __html: codeHTML }} {...props} />;
}
// This replaces rehype-slug
function slugify(str) {
return str
.toString()
.toLowerCase()
.trim() // Remove whitespace from both ends of a string
.replace(/\s+/g, "-") // Replace spaces with -
.replace(/&/g, "-and-") // Replace & with 'and'
.replace(/[^\w\-]+/g, "") // Remove all non-word characters except for -
.replace(/\-\-+/g, "-"); // Replace multiple - with single -
}
// This replaces rehype-autolink-headings
function createHeading(level) {
return ({ children }) => {
let slug = slugify(children);
return React.createElement(
`h${level}`,
{ id: slug },
[
React.createElement("a", {
href: `#${slug}`,
key: `link-${slug}`,
className: "anchor",
}),
],
children
);
};
}
Oh and that remark-gfm
I was using for the GitHub style Markdown tables? Again, you can use a React component for that.
function Table({ data }) {
let headers = data.headers.map((header, index) => <th key={index}>{header}</th>);
let rows = data.rows.map((row, index) => (
<tr key={index}>
{row.map((cell, cellIndex) => (
<td key={cellIndex}>{cell}</td>
))}
</tr>
));
return (
<table>
<thead>
<tr>{headers}</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
Dependency Minimalism
Why go through all of this work to delete dependencies?
Damn it, I'm tired of updating them. I'm tired of the breaking changes. I'm tired of the security vulnerabilities that I don't know if I should care about them. I'm tired of the performance implications. I'm tired of the bundle size. I'm tired of the complexity. I'm tired of the cognitive overhead. I'm tired of the lack of control.
Performance
Earlier this year, I moved this blog to the Next.js App Router. That came with a subtle but important change: React Server Components by default. I actually installed the Next.js 12 beta the very day it was released after watching the Next.js Conf and I was really excited to try out the new features.
At first I was a bit confused on how you implement these components. The documentation just was not yet there and I also encountered some issues with the beta version too. But after a couple of days I was able to get it to work and I'm really happy with the results.
Server Components
Server Components are awesome! Finally you can read data from anywhere and directly use it in your components. This makes the entire data fetching process much easier and more efficient. I'm using it for my blog views, guestbook entries, and redirects.
Suspense
It enables Next.js to prerender as much of the page as possible to static, leaving holes for dynamic components.
For example, 98% of this blog post page is work that can be prerendering during the build. However, those view counts at the top of the page should be dynamic on every request. With Partial Prerendering, the code for that looks like:
async function Views({ slug }: { slug: string }) {
let views = await getViewsCount();
incrementViews(slug);
// ...
}
export default function Blog({ params }) {
let post = getBlogPosts().find((post) => post.slug === params.slug);
// ...
return (
<section>
<Suspense fallback={<p className="h-5" />}>
<Views slug={post.slug} />
</Suspense>
<article>{post.content}</article>
</section>
);
}
Everything up to the Suspense
boundary, including the fallback
, can be prerendered. Then, when the request happens, the static shell is instantly shown, followed by the dynamic content (views) streaming in after the fact.
getViewsCount
signals to Next.js that it's dynamic through noStore()
:
export async function getViewsCount() {
noStore();
let data = await sql`
SELECT slug, count
FROM views
`;
return data.rows as { slug: string; count: number }[];
}
And incrementViews
is a Server Action that can be called like any JavaScript function:
"use server";
import { sql } from "@vercel/postgres";
export async function increment(slug: string) {
await sql`
INSERT INTO views (slug, count)
VALUES (${slug}, 1)
ON CONFLICT (slug)
DO UPDATE SET count = views.count + 1
`;
}
I'm really happy with this approach. In the past, I had to include more dependencies like swr
and add additional client-side JavaScript to achieve this. Partial Prerendering feels fast, without giving up the dynamic components like projects numbers or view counts.
Opinions
There's an assortment of other opinions I've included here now:
- Next.js is the best framework at the moment: I have experience with a lot of different tools by now. With next.js it feels like the ecosystem is strong, the enviroment moves fast and the documentation is great. Nothing else comes close in my opinion. If you believe
X
then there is always a better tool for the tast at hand. But overall I think next.js is the best choice for 90% of the projects out there. - Prefer copy/paste over abstraction: I removed
clsx
/classnames
andgray-matter
for simple copy-pastable alternatives. Sure, they don't support every single feature. But, I don't need it. I'd rather have the code in my repo. - Prefer loading SVGs as images: This is an interesting take. I rarely think about where to put my svgs and inlining them came obvious to me. But starting to change my mind. Related reading here and on svg sprites.
- Prefer larger files versus many components: Just works for my brain better. Keep code that changes often close together. Your mileage may vary.
- Only using
app/
for everything: I actually liked thepages/
directory for its flexibility and also at the time for its performance. But my brain kind of has an issue to use bothpages/
andapp/
at the same time. I'm going to try to stick toapp/
for everything. - Prefer colocating styles with components: Okay, I've been using Tailwind now for for quite some time and it still feels nice to use. It achives everything I need and I rather have my styles right in my components instead of searching for classes in my CSS files. I did not always do it this way though, I went from CSS → Sass → styled-components → Chakra → Tailwind. For right now I'm happy with Tailwind.
Conclusion
I really had fun doing this one! It makes me happy knowing that I have my personal blog on the latest tech stack. If you made it this far. Thanks for reading! 🙏
- Affiliate Disclaimer
- Disclaimer:
Links on the site might be affiliate links, so if you click them I might earn a small commission.