Tire Kicking (Test Post Please Ignore)

January 1, 2025
000

Building a Minimalist Blog with Bluesky and Next.js

I recently built a new personal blog platform that combines the decentralized nature of the Bluesky social network with the modern developer experience of Next.js. Here's how I put it together and what I learned along the way.

The Stack

The blog is built with:

The Concept

I wanted to create a minimalist, terminal-inspired blog that would:

  1. Store all content on the Bluesky network using their custom protocol
  2. Render content statically for optimal performance
  3. Support full markdown with code highlighting
  4. Have a clean, distraction-free reading experience
  5. Include dark mode support

Key Features

Terminal-Inspired Design

The UI is built around a terminal window metaphor, complete with:

  • Window controls (red, yellow, green dots)
  • Title bar showing current "directory"
  • Monospace font (Geist Mono)
  • Command-line inspired navigation
export function Terminal({ children, title = "terminal" }: TerminalProps) {
  return (
    <div className="w-full max-w-4xl mx-auto overflow-hidden rounded-lg border border-gray-200 dark:border-gray-800 shadow-lg">
      {/* Title bar */}
      <div className="relative flex items-center justify-between px-4 py-2 bg-gray-100 dark:bg-gray-800/50">
        {/* ... */}
      </div>
      <div className="p-6 bg-white dark:bg-gray-900 min-h-[200px]">
        {children}
      </div>
    </div>
  );
}

Content Storage

All blog posts are stored as custom records on the Bluesky network using their com.whtwnd.blog.entry record type. This means the content is:

  • Decentralized
  • Portable
  • Accessible via the AT Protocol
  • Versioned

The API integration is handled through a simple client wrapper:

export async function getPosts(options: GetPostsOptions = {}) {
  const posts = await bsky.get("com.atproto.repo.listRecords", {
    params: {
      repo: process.env.ACCOUNT_DID!,
      collection: "com.whtwnd.blog.entry",
      limit: options.limit,
      cursor: options.cursor,
      reverse: options.reverse,
    },
  });
  // ...
}

Markdown Rendering

The blog supports full GitHub-flavored markdown with some nice extras:

  • Syntax highlighting for code blocks
  • Custom components for headings, links, and blockquotes
  • Footnotes support
  • Table formatting
  • Custom prose styles via Tailwind
<Markdown
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[rehypeRaw, rehypeSanitize]}
  className="prose dark:prose-invert max-w-none"
  components={{
    code({ node, inline, className, children, ...props }) {
      // Custom code block rendering
    },
    // Other custom components...
  }}
>
  {post.value.content}
</Markdown>

Dark Mode

The site includes a fully-featured dark mode that:

  • Respects system preferences
  • Can be toggled manually
  • Persists user choice
  • Transitions smoothly between modes

This is implemented using next-themes and custom Tailwind styles:

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <button
      onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
      className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
    >
      {theme === "dark" ? <Sun /> : <Moon />}
    </button>
  );
}

Lessons Learned

Building this project taught me several valuable lessons:

  1. AT Protocol Integration: Working with Bluesky's API required understanding their custom protocols and record types. While different from traditional REST APIs, it offers interesting possibilities for decentralized content.

  2. Static Generation: Next.js's static generation capabilities work great with decentralized content, allowing for fast page loads while still maintaining dynamic content updates.

  3. Design Systems: Creating a cohesive design system using Tailwind requires careful planning of components and consistent styling patterns.

Future Improvements

Some features I'm considering adding:

  • [ ] RSS feed generation
  • [ ] Image optimization and lazy loading
  • [ ] Comment system using Bluesky replies
  • [ ] Improved search functionality
  • [ ] Post series/categorization

Bonus Bluesky Embed?

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:gttrfs4hfmrclyxvwkwcgpj7/app.bsky.feed.post/3lc2sisixx22i" data-bluesky-cid="bafyreidsmd23ihrh5yulahid54mmj3gf5j66qrx5tzclb6yqkswzxjavxm"><p lang="en">personal websites are the millennial version of a project car</p>&mdash; austin (<a href="https://bsky.app/profile/did:plc:gttrfs4hfmrclyxvwkwcgpj7?ref_src=embed">@aparker.io</a>) <a href="https://bsky.app/profile/did:plc:gttrfs4hfmrclyxvwkwcgpj7/post/3lc2sisixx22i?ref_src=embed">Nov 29, 2024 at 12:03 AM</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>