LogoCésar Alberca
August 26, 2024 - 10 minutes

Blog Redesign

I recently became a Freelancer and Digital Nomad traveling the world. This kilometerstone could only be celebrated by redesigning my blog.

You are not a true Web Developer if you don't redesign your blog every year.

Being a freelancer means that I not only need to be technically proficient, but also need to think about how I can effectively communicate what I offer so people will want to book my services.

A powerful way of conveying your message—whatever that might be—is to make sure the medium in which your message is transmitted is attractive enough. Designing is something that we humans do quite often: we design our outfits, we design how we talk, and as a web designer, I designed and built this very same site you are experiencing.

Technologies

Technology needs to serve a purpose. Sometimes—most times, let's be honest—we web developers focus too much on the technologies and frameworks we use. We should talk about them, be proud of what we use, and maybe even poke fun at others' choices, but only to a certain extent. Here, I'm mentioning some of the technologies I've used, what I've learned from them, and how they've empowered me to focus on what really matters: the message.

A good starting point is to use starters. I used the Next.js starter portfolio. It provided me with some initial configuration for SEO, so I wouldn't have to worry about it (for now). For instance, it included sitemap.ts, robots.ts, rss/route.ts, and og/route.ts.

When using a starter, what I do is generate the template in a temporary folder and then choose what to copy over to my project. This ensures that I don't take more than I need and that I can check exactly what I want to bring over.

I also configured the project as I usually do, with Prettier, ESLint, Husky, and Lint Staged. How I configure the projects I work on could definitely be another post at some point.

I made a conscious—perhaps also debatable—choice not to delve too deep into best practices, testing, and architecture. Why? Because I want to create a blog series where I can improve the codebase bit by byte. But hey, it's not like I coded the thing with my eyes closed (which could be an interesting challenge, to be honest). You can check the source code here at the time of publishing this post. I think it's pretty neat.

What I decided to focus on instead was creating a clean design with a big focus on content and some subtle animations that would make the site b·e·a·u·t·i·f·u·l. Here are some:

Animations

Background (more hover!)
Background with image (still hover!)
3D card (you guessed it, hover!)

I'm always on the lookout for inspiration. Seeing how others have tackled similar problems reassures me that I'm not alone in my quest for my solution. Some great places I stole ideas from looked for inspiration were Codrops and Awwwards.

MDX

As you can see, all the components I'm developing for the website can be reused in blog posts, and that's thanks to MDX. MDX is like Markdown on Kombucha (meaning super powerful). Why? Because in MDX, you can import React components, which is a crazy thing to do.

**Foo** <Background> <div className="p-xl">Bar</div> </Background> > Baz

Which will render this:

Foo

Bar

Baz

Cool, right?

Next.js comes with support for MDX using some extra libraries. I started configuring the project following the Next.js MDX guide, which at some point suggests using next-mdx-remote. I found it a little cumbersome and noticed that it provided some functionality I don't really need (YAGNI).

After searching for simpler solutions, I stumbled upon this fantastic post, which showed how to simplify the setup using just next-mdx.

The only thing I still need to figure out is how to generate SEO-compliant metadata for each article and talk. But that will be another POST.

The structure for the posts is quite simple. We just tell Next.js, "please, render .mdx files when asked to":

import withMDX from '@next/mdx' /** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], } export default withMDX()(nextConfig)

And then it's a matter of specifying in what route would the post be rendered.

src/ └── app/ └── blog/ └── (posts)/ └── blog-redesign/ └── page.mdx

Since we want to customize the components that MDX renders, we can create a file under src called mdx-components to do so:

import type { MDXComponents } from 'mdx/types' import Link from 'next/link' import { type ComponentProps, createElement, type LinkHTMLAttributes, type PropsWithChildren, type ReactNode, } from 'react' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import dark from 'react-syntax-highlighter/dist/esm/styles/prism/synthwave84' import { cn } from '@/lib/utils' function CustomLink(props: LinkHTMLAttributes<HTMLAnchorElement> & PropsWithChildren<{ href: string }>) { let href = props.href if (href.startsWith('/')) { return <Link {...props}>{props.children}</Link> } if (href.startsWith('#')) { return <a {...props} /> } return <a target="_blank" rel="noopener noreferrer" {...props} /> } function Code({ children, ...props }: { children: string; className: string }) { const className = props?.className ?? '' const match = /language-(\w+)/.exec(className ?? '') return match ? ( <SyntaxHighlighter {...props} language={match[1]} PreTag="div" style={dark} codeTagProps={{ style: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', }, }} > {children} </SyntaxHighlighter> ) : ( <code {...props} className={cn(className, 'bg-muted px-[4px] py-[2px] mx-[2px] rounded not-prose text-base font-mono')} > {children} </code> ) } function slugify(str: string) { 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 - } function createHeading(level: number) { const Heading = ({ children }: { children: string }) => { const slug = slugify(children) return createElement( `h${level}`, { id: slug }, [ createElement('a', { href: `#${slug}`, key: `link-${slug}`, className: 'anchor', }), ], children, ) } Heading.displayName = `Heading${level}` return Heading } const customComponents: ComponentProps<any>['components'] = { h1: createHeading(1), h2: createHeading(2), h3: createHeading(3), h4: createHeading(4), h5: createHeading(5), h6: createHeading(6), a: CustomLink, code: Code, pre: ({ children }: { children: ReactNode }) => <pre className="p-0 font-mono">{children}</pre>, } export function useMDXComponents(components: MDXComponents): MDXComponents { return { ...customComponents, ...components, } }

Each article should also have the same layout as the website. To achieve this, you can export a default function that uses a custom component. I would have liked it if this were integrated into the Next.js layout system. Maybe in the future?

import { PostLayout } from '@/features/posts/delivery/post-layout' # My blog content export default function Page({ children }) { return <PostLayout slug="blog-redesign">{children}</PostLayout> }

The PostLayout component has the following content:

import { Page } from '@/core/components/page/page' import type { ReactNode } from 'react' import { PostPage } from '@/features/posts/delivery/post.page' import type { PostMetadata } from '@/post-metadata' export async function PostLayout({ children, slug }: { children: ReactNode; slug: string }) { const { metadata } = (await import(`./(posts)/${slug}/page.mdx`)) as { metadata: PostMetadata } return ( <Page top> <PostPage metadata={metadata} slug={slug}> {children} </PostPage> </Page> ) }

There's some metadata in each article that is used to generate the SEO metadata. This metadata is exported as a const. For example, the metadata for this very article is:

export const metadata = { slug: 'blog-redesign', title: 'Blog Redesign', date: '2024-08-25', image: 'blog-redesign/medusa.jpg', categories: ['software-development', 'branding', 'design'], readTime: 15, summary: 'After quitting my job, I decided to redesign my blog. This is the story of how I did it.', }

This removes the need of adding front-matter and a subsequent parser. Simpler is better.

TaildwindCSS

In case you missed it, there's some code that might initially look aberrant. If so, it's because you haven't tried yet TailwindCSS:

<code {...props} className={cn(className, 'bg-muted px-[4px] py-[2px] mx-[2px] rounded not-prose text-base font-mono')}> {children} </code>

To me, CSS is part of the front-end architecture, and it's something I don't overlook nor hate. I wanted to incorporate TailwindCSS to see if it's scalable and provides a good framework for development. Conclusion: it does.

However, there are two changes I made:

  1. Use named spacing units instead of arbitrary numbers.
  2. Use theme-oriented names instead of specific colors.

It's important to me to abstract the spacing units. I don't want to use numbers such as p-2, m-8, or w-4. Instead, I prefer using p-s, m-m, or w-xl. This way, I can change the spacing units in one place, and it will be reflected throughout the entire project.

The same goes for colors. You typically won't see specific colors in the code (unless I want to invariably use a particular color). Instead, you'll see text-primary, bg-secondary, border-destructive, etc. This approach provides more flexibility and maintainability to the project, especially if you want to change the primary color down the line.

import type { Config } from 'tailwindcss' const config = { theme: { extend: { spacing: { xxs: '8px', xs: '12px', s: '16px', m: '24px', l: '32px', xl: '48px', xxl: '56px', }, colors: { border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))', }, destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))', }, muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))', }, accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))', }, popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))', }, card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))', }, }, }, }, } satisfies Config export default config

ShadcnUI

ShadcnUI introduces a concept that might initially make us shudder in fear: copying and pasting code. It does this by generating code for your components. But why would I want that? Well, it serves as a base for lightweight components, and from there, I can customize them as much as I want. The code is mine after all.

Whenever I need a new component, I head over to their documentation and find the component I need. Then, I generate it using the CLI:

npx shadcn-ui@latest add button

The button will be generated in src/components/ui/button.tsx and will look something like this:

import * as React from 'react' import { Slot } from '@radix-ui/react-slot' import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const buttonVariants = cva( 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean } const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button' return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> }, ) Button.displayName = 'Button' export { Button, buttonVariants }

As you can see, it uses TailwindCSS and RadixUI under the hood to do all the heavy lifting. class-variance-authority is a library that makes it easy to create variants of a component.

Another reason I chose ShadcnUI is its integration with Vercel's new AI tool to generate pieces of UI: v0. Is web design already doomed? Well, when wasn't it?

What's Next?

I need want to get back to blog writing, covering everything from technical topics to more personal stories. I want to share my projects, my experiences as a Digital Nomad, and even the recipe I use to make my deodorant. My goal is to create a site that I cherish visiting and enjoy reading. And if you do too, well, that would be amazing!