Building a Modern Solarpunk Blog with Next.js 15 and TypeScript
J

Joe

November 20, 2025

Building a Modern Solarpunk Blog with Next.js 15 and TypeScript

Building a Modern Solarpunk Blog with Next.js 15 and TypeScript

Building a sustainable, performant, and beautiful blog platform using cutting-edge web technologies

Introduction

In today's world of bloated websites and over-engineered solutions, I wanted to create something different: a blog platform that's fast, sustainable, and aesthetically aligned with the solarpunk vision—where technology and nature exist in harmony. This article walks through the technical architecture of my blog frontend, built with Next.js 15, TypeScript, and a custom solarpunk-inspired design system.

The Tech Stack

Core Technologies

Next.js 15 with App Router - The backbone of the project. Next.js 15's App Router provides:

  • Server Components by default for optimal performance
  • Streaming and Suspense support for better UX
  • Built-in route handlers for API integration
  • Server Actions for clean data fetching

TypeScript - Full type safety across the entire application ensures fewer runtime errors and better developer experience.

Tailwind CSS 4 - Utility-first styling with custom solarpunk color palette:

colors: { 'solarpunk-forest': '#1a3a2e', 'solarpunk-moss': '#4f7c5f', 'solarpunk-glow': '#00ff88', 'solarpunk-sage': '#95cf9e', }

HeroUI Component Library - A modern, accessible component system built on React Aria, providing beautiful pre-built components without sacrificing customization.

Why This Stack?

  1. Performance: Server Components reduce JavaScript bundle size by rendering on the server
  2. SEO: SSR and dynamic metadata generation ensure great search engine visibility
  3. Developer Experience: TypeScript + modern tooling = fewer bugs, faster development
  4. Sustainability: Smaller bundles = less energy consumed by users' devices

Architecture Decisions

Server Actions for API Integration

Instead of traditional REST client libraries, I leveraged Next.js Server Actions:

'use server'; export async function getBlogPosts( page: number = 1, perPage: number = 15 ): Promise<BlogPostsResponse> { const response = await fetch( `${API_BASE_URL}/posts?page=${page}&per_page=${perPage}`, { next: { revalidate: 60 }, headers: { 'Accept': 'application/json' }, } ); return await response.json(); }

Benefits:

  • Security: API keys and sensitive logic stay server-side
  • Performance: No client-side API libraries needed
  • Simplicity: Direct API access without exposing internals
  • Caching: Built-in Next.js caching with revalidate

Dynamic Rendering Strategy

Initially, I used static generation, but this caused issues during Docker builds when the external API wasn't available. The solution:

// Force dynamic rendering to fetch fresh data at runtime export const dynamic = 'force-dynamic';

This ensures:

  1. Build time: Can use mock data (SKIP_EXTERNAL_FETCH=true)
  2. Runtime: Always fetches fresh content from the API
  3. No stale content: Users always see the latest posts

Docker Multi-Stage Builds

The deployment strategy uses multi-stage Docker builds:

# Build stage - optionally skip external API calls FROM node:20-alpine AS builder ARG SKIP_EXTERNAL_FETCH=false ENV SKIP_EXTERNAL_FETCH=${SKIP_EXTERNAL_FETCH} RUN npm run build # Production stage - clean runtime environment FROM node:20-alpine AS runner ENV NODE_ENV=production # Note: SKIP_EXTERNAL_FETCH NOT set here COPY --from=builder /app/.next ./.next

This allows CI/CD pipelines to build without API dependencies while ensuring production always fetches real data.

Key Features

1. Markdown Rendering with Syntax Highlighting

Blog posts are written in Markdown and rendered with full GitHub Flavored Markdown support:

import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, rehypeSanitize]} components={{ code({ inline, className, children }) { const language = /language-(\w+)/.exec(className)?.[1]; return !inline && language ? ( <SyntaxHighlighter language={language}> {children} </SyntaxHighlighter> ) : <code>{children}</code>; }, }} > {post.content} </ReactMarkdown>

2. Tag-Based Filtering

Users can filter blog posts by tags using both single and multiple tag searches:

export async function searchPostsByTag( tagSlug: string, page: number = 1, perPage: number = 15 ): Promise<BlogPostsResponse> { const hasMultipleTags = tagSlug.includes(' ') || tagSlug.includes(','); const url = hasMultipleTags ? `${API_BASE_URL}/posts?tag=${encodeURIComponent(tagSlug)}` : `${API_BASE_URL}/tags/${tagSlug}/posts`; return fetch(url).then(r => r.json()); }

3. Responsive Image Handling

Using Next.js Image component with Azure Blob Storage for optimal loading:

<Image src={post.featured_image_url} alt={post.title} fill className="object-cover" priority />

4. Automated CI/CD Pipeline

GitHub Actions workflow for automated deployments:

  1. Build: Docker image with SKIP_EXTERNAL_FETCH=true
  2. Push: To GitHub Container Registry (GHCR)
  3. Deploy: SSH to server, pull latest image, restart containers
  4. Nginx: Reverse proxy with Let's Encrypt SSL

Design Philosophy: Solarpunk Aesthetic

The visual design embraces solarpunk principles:

  • Nature-inspired colors: Deep forest greens, bioluminescent glows
  • Dark mode first: Reduces screen energy consumption
  • Glassmorphism: Subtle transparency effects for depth
  • Smooth animations: Framer Motion for delightful micro-interactions
.glow-text { text-shadow: 0 0 20px rgba(0, 255, 136, 0.3), 0 0 40px rgba(0, 255, 136, 0.2); }

Lessons Learned

1. Static vs Dynamic Rendering

Problem: Static Site Generation (SSG) baked build-time data into HTML
Solution: Use export const dynamic = 'force-dynamic' for data-driven pages

2. Build-Time API Dependencies

Problem: CI/CD builds failed when API was unavailable
Solution: Conditional mock data during builds, real data at runtime

3. Environment Variable Pitfalls

Problem: SKIP_EXTERNAL_FETCH leaked from build to production
Solution: Only set environment variables in the appropriate Docker stage

Future Enhancements

some features I plan on adding in the future.

  1. Reading progress indicator
  2. Related posts recommendation engine

Conclusion

Building a modern blog platform doesn't require complex frameworks or bloated dependencies. By leveraging Next.js 15's Server Components, Server Actions, and a thoughtful architecture, I can create fast, sustainable, and beautiful web experiences.

The key takeaways:

  • Server Components reduce client-side JavaScript
  • Server Actions simplify API integration
  • Dynamic rendering ensures fresh content
  • Docker multi-stage builds enable flexible deployment
  • TypeScript provides confidence and maintainability

Technical Specifications

  • Framework: Next.js 15.3.1
  • Runtime: Node.js 20 (Alpine Linux)
  • Deployment: Docker + Nginx + Let's Encrypt
  • Hosting: Self-hosted VPS
  • CI/CD: GitHub Actions
  • Registry: GitHub Container Registry (GHCR)
  • Database: PostgreSQL (backend API)
  • Assets: Azure Blob Storage

Resources

Built with 💚 for a sustainable future

#nextjs
#azure