Building Portfolio Blogs with Wisp CMS and Next.js
Independent developers need to demonstrate their technical knowledge and project experience. A well-designed blog serves as both a portfolio showcase and a platform for technical writing. Wisp CMS, paired with Next.js, provides a lightweight, performant approach to building content-driven developer blogs without the overhead of traditional headless CMS platforms.
This article examines the Wisp CMS Next.js blog template, focusing on server-side optimizations, dark mode implementation, and RSS feed generation. These features solve specific problems that arise when independent developers need to maintain a production-grade blog while keeping complexity low and build times fast.
Understanding Wisp CMS Architecture
Wisp CMS operates as a file-based content management system designed specifically for Next.js applications. Rather than storing content in a database, Wisp uses the filesystem as its primary content store. This approach eliminates the need for external services, reduces infrastructure costs, and makes content portable and version-controllable.
The system reads Markdown and MDX files from a designated content directory, parses frontmatter metadata, and exposes this data through a JavaScript API that Next.js can consume during build time or request time. Content files live alongside application code in the repository, making the entire project self-contained.
Wisp integrates directly with Next.js's file-based routing and build process. When a developer adds a new post to the content directory, Wisp automatically detects it and makes it available through API functions. This means there is no separate deployment process for content—pushing code to production automatically publishes new posts.
Server Components and Performance Optimization
Next.js server components represent a shift in how React applications render content. Server components execute on the server and send only the rendered HTML to the browser, rather than shipping JavaScript that runs client-side. This reduces the JavaScript payload sent to users and improves initial page load time.
Wisp blog templates leverage server components by rendering blog list pages and individual post pages entirely on the server. When a user requests the blog homepage, the server fetches all posts from the filesystem, renders the HTML, and sends the complete page to the browser. The browser receives no React component tree to hydrate, no state management code, and no unnecessary JavaScript.
Consider the practical difference. A traditional client-side rendered blog loads a JavaScript bundle that contains the entire React framework, routing logic, state management, and component code. The browser must parse and execute this bundle before rendering anything. A server component-based blog sends only the HTML the user needs to see. The bundle size difference is substantial: server-rendered blog pages ship 40-60% less JavaScript than their client-rendered equivalents, measured using standard bundling tools.
The performance gain extends beyond initial load. Server components avoid the "JavaScript execution cost" that impacts slower devices. On a mid-range Android phone with limited CPU power, executing 200KB of JavaScript takes measurable time. The same content delivered as pre-rendered HTML displays instantly.
Implementing this in a Wisp Next.js blog requires marking post list and detail pages as server components. This is the default behavior in the Next.js app directory—components are server components unless explicitly marked as client components with the "use client" directive.
// app/blog/page.js - rendered on server
import { getPosts } from '@wisp-cms/client';
import PostCard from '@/components/PostCard';
export default async function BlogPage() {
const posts = await getPosts();
return (
<div className="blog-list">
{posts.map((post) => (
<PostCard key={post.slug} post={post} />
))}
</div>
);
}
This page executes entirely on the server. The getPosts() function reads from the filesystem, and the component tree renders to HTML without any client-side JavaScript. The returned HTML includes the complete blog list structure, ready for the browser to display.
Individual post pages follow the same pattern. The server fetches post metadata and content, renders the Markdown or MDX to HTML, and sends the final document to the browser.
// app/blog/[slug]/page.js
import { getPostBySlug, getPosts } from '@wisp-cms/client';
import MDXContent from '@/components/MDXContent';
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<MDXContent source={post.content} />
</article>
);
}
The generateStaticParams() function tells Next.js to pre-render every post at build time. When the build process runs, it fetches all posts, renders each one as static HTML, and outputs files that serve instantly when users request them. This is static site generation—every post becomes a pre-built HTML file.
Static generation provides the best performance for blogs. The server does no work at request time. Users receive cached HTML files served by a CDN. Load times drop to single-digit milliseconds.
Markdown and MDX Processing
Wisp supports both Markdown and MDX—a format that combines Markdown syntax with embedded React components. This distinction matters for blog content.
Markdown files contain only text, headings, code blocks, and standard formatting. A Markdown processor converts this to HTML. This approach works well for most blog posts: the content is safe, fast to parse, and produces semantically clean HTML.
MDX extends Markdown by allowing developers to embed React components directly in content files. This enables dynamic interactivity within posts: interactive code editors, data visualizations, embedded forms, and custom components that respond to user input.
The trade-off exists in build complexity and client-side JavaScript. A post written in Markdown renders completely on the server—no client-side code is needed. A post written in MDX must include the React component definitions and any client state management those components require. This increases the JavaScript bundle size.
For developer portfolios, the choice depends on the content style. Educational posts with code examples work fine in Markdown. Interactive posts that showcase specific functionality benefit from MDX components.
Wisp handles both formats transparently. The blog template can include posts in either format in the same blog directory. The system detects the file type and processes it accordingly.
---
title: "Building a Smart Contract Auditor"
description: "Implementing automated security checks"
published: true
---
# Building a Smart Contract Auditor
When building production smart contracts, security analysis must be systematic. This post walks through the architecture of an automated auditor.
## The Audit Pipeline
The auditor processes contracts in stages:
1. Lexical analysis tokenizes the Solidity code
2. Syntax analysis builds an abstract syntax tree
3. Semantic analysis performs type checking and flow analysis
4. Pattern matching identifies known vulnerabilities
This Markdown file contains standard prose and code blocks. When Wisp processes it, the frontmatter metadata (title, description, published status) becomes accessible through the API. The Markdown content converts to HTML.
An MDX equivalent could include interactive components:
---
title: Building a Smart Contract Auditor
description: Implementing automated security checks
published: true
---
import { CodeDifference } from '@/components/CodeDifference';
# Building a Smart Contract Auditor
<CodeDifference
before={vulnerableCode}
after={fixedCode}
/>
When building production smart contracts...
Here, the CodeDifference component renders interactively, allowing users to toggle between vulnerable and fixed versions of code. This requires React on the client to function, increasing the JavaScript bundle size.
For independent developer portfolios, Markdown alone provides sufficient functionality in most cases. The focus should remain on clear technical writing rather than interactive components. Interactive elements distract from the content and increase maintenance burden—components must remain compatible with the blog template across framework versions.
Dark Mode Implementation
Dark mode has become standard for developer-focused applications. Implementing it properly requires handling both server-side and client-side rendering, persisting user preference, and ensuring all text and components maintain sufficient contrast in both themes.
The Wisp Next.js template implements dark mode using CSS custom properties (variables) that change based on a theme class on the document root element. This approach separates theme logic from component styling.
/* styles/theme.css */
:root {
--color-background: #ffffff;
--color-text: #000000;
--color-border: #e5e7eb;
--color-code-bg: #f3f4f6;
}
[data-theme="dark"] {
--color-background: #0a0a0a;
--color-text: #ffffff;
--color-border: #2d2d2d;
--color-code-bg: #1a1a1a;
}
body {
background-color: var(--color-background);
color: var(--color-text);
}
code {
background-color: var(--color-code-bg);
border: 1px solid var(--color-border);
}
Components reference these variables through standard CSS. This means a single CSS file controls theme colors across the entire application.
The critical challenge with dark mode in Next.js is preventing flash of unstyled content (FOUC) during page load. When a user visits the site with dark mode enabled, the initial HTML sent by the server renders in light mode (the default). The JavaScript then loads, detects the user's preference, and switches to dark mode. The user sees a flash of light mode before dark mode appears.
Solving this requires detecting the theme preference before rendering any content. Wisp's Next.js template uses a small script injected into the document head, before any styling or component rendering:
<head>
<script>
(function() {
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
</head>
This script runs synchronously before the page renders. It checks localStorage for a stored theme preference. If no preference exists, it checks the system's color scheme preference using the media query API. Then it sets the theme attribute on the document root immediately.
Because this script runs before any visual content renders, the correct theme applies from the start. No flash occurs.
A client-side component handles theme switching when the user clicks a toggle button:
// components/ThemeToggle.js
'use client';
import { useEffect, useState } from 'react';
export default function ThemeToggle() {
const [theme, setTheme] = useState('light');
const [mounted, setMounted] = useState(false);
useEffect(() => {
const currentTheme = document.documentElement.getAttribute('data-theme');
setTheme(currentTheme);
setMounted(true);
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
setTheme(newTheme);
};
if (!mounted) return null;
return (
<button onClick={toggleTheme} aria-label="Toggle theme">
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
The component must only render after mounting (the if (!mounted) return null check). This prevents server-side rendering the theme toggle in one state while the client expects another. The toggle updates both the DOM attribute and localStorage, so the preference persists across sessions.
RSS Feed Generation
RSS feeds provide a subscription mechanism for blog readers. Developers often subscribe to relevant blogs using RSS readers, making RSS support valuable for reaching technical audiences.
Wisp generates RSS feeds by collecting all published posts, formatting them according to RSS specification, and serving them from a well-known URL (typically /feed.xml or /rss.xml).
RSS feed generation happens at build time using a simple Node.js script. The script fetches all posts from Wisp, iterates through them, and generates valid RSS XML:
// scripts/generate-rss.js
import { getPosts } from '@wisp-cms/client';
import fs from 'fs';
import path from 'path';
const SITE_URL = 'https://yourdomain.com';
async function generateRSS() {
const posts = await getPosts();
const published = posts.filter(post => post.published === true);
const rssItems = published.map(post => {
return `
<item>
<title>${escapeXml(post.title)}</title>
<description>${escapeXml(post.description || '')}</description>
<link>${SITE_URL}/blog/${post.slug}</link>
<guid>${SITE_URL}/blog/${post.slug}</guid>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
</item>
`;
}).join('');
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Your Blog Title</title>
<link>${SITE_URL}</link>
<description>Your blog description</description>
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${rssItems}
</channel>
</rss>`;
const publicDir = path.join(process.cwd(), 'public');
fs.writeFileSync(path.join(publicDir, 'feed.xml'), rss);
console.log('RSS feed generated at /feed.xml');
}
function escapeXml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
generateRSS().catch(console.error);
This script integrates into the Next.js build process by adding it to the package.json build script:
{
"scripts": {
"build": "node scripts/generate-rss.js && next build"
}
}
Every time the application builds, the RSS feed regenerates with the latest posts. The generated XML file sits in the public directory, where Next.js serves it as a static asset.
RSS feeds require proper XML formatting and date handling. The script escapes special XML characters in post titles and descriptions to prevent invalid XML. It formats publication dates in RFC 2822 format (the RSS standard), using toUTCString() to ensure consistency regardless of the server's timezone.
For maximum discoverability, the blog layout template should include a link tag in the document head:
<head>
<link rel="alternate" type="application/rss+xml" href="/feed.xml" title="Your Blog RSS Feed" />
</head>
This tells feed readers where to find the RSS feed automatically. When readers visit the site, their reader application detects the feed link and offers to subscribe.
Advantages Over MDX-Based Systems
Several blog systems use MDX as their primary content format, combining Markdown writing with embedded React components. While flexible, this approach introduces complexity that doesn't serve independent developer portfolios well.
MDX-based systems require the entire React framework to ship to the browser for each post. Interactive components need client-side JavaScript to function, increasing bundle size. A post with a single interactive code block still requires loading the full React runtime and all component dependencies. This overhead accumulates across pages.
The Wisp template keeps client-side JavaScript to a minimum. Most pages render entirely on the server. Only interactive elements like the theme toggle require client-side code. A simple blog post is pure HTML—no JavaScript executes on the user's browser.
Build time differs significantly. MDX compilation adds processing overhead during the build. Every MDX file must be parsed, AST-transformed, and compiled. For a blog with 50-100 posts, this can extend build times to several seconds or more. Wisp's filesystem approach with standard Markdown processes faster, typically completing blog builds in under one second.
Content migration becomes simpler with Markdown. If an independent developer needs to switch blog platforms in the future, Markdown files remain compatible across systems. MDX files tie content to the React ecosystem, making portability harder.
For most independent developer blogs, the advantages of simplicity and performance outweigh the flexibility of embedded interactive components. Technical writing benefits from focused, readable prose and clear code examples rather than interactive distractions.
Setting Up a Blog with the Wisp Template
Creating a new blog with Wisp and Next.js starts with scaffolding a project from the official template:
npx create-next-app@latest my-blog --template https://github.com/wisp-cms/nextjs-blog-template
cd my-blog
npm install
npm run dev
This command clones the template repository, installs dependencies, and starts the development server. The template includes a content/blog directory with example posts in Markdown format.
Adding a new post requires creating a Markdown file in the content/blog directory:
---
title: Understanding Async/Await in Solidity
description: A guide to asynchronous patterns in smart contracts
date: 2024-01-15
published: true
---
# Understanding Async/Await in Solidity
Solidity smart contracts operate in a synchronous execution model...
Wisp detects the new file immediately in development mode. Refreshing the browser shows the new post in the blog list. The post is accessible at /blog/understanding-async-await-in-solidity (the slug is derived from the filename).
For deployment, the typical approach is pushing the repository to a hosting service like Vercel, GitHub Pages, or a self-hosted server. The build process generates static HTML files for all posts and deploys them alongside the Next.js application code.
Vercel provides the smoothest deployment experience for Next.js applications. Connecting a GitHub repository to Vercel enables automatic deployments: every push to the main branch triggers a build, runs the RSS generation script, and deploys the updated site. New blog posts published via a repository push go live within seconds.
The combination of server components, static generation, and filesystem-based content storage makes Wisp blogs ideal for independent developers. The setup requires minimal configuration, scales to thousands of posts without performance degradation, and keeps deployment simple.
For Web3 documentation needs or full-stack Next.js development work, I'm available for consulting. Visit my Fiverr profile at https://fiverr.com/meric_cintosun to discuss your project.
Top comments (0)