How I Built My Blog Section – Next.js Blog with CMS & SEO
Why a Blog?
There are many good reasons to integrate a blog into a portfolio. The most important: SEO. Static portfolio pages struggle to rank for long-tail keywords. Blog posts, however, can be precisely optimized for search queries that potential clients actually type.
A blog also demonstrates expertise – *you don't just know how to build something, you can explain why.*
The Requirements
I had clear criteria for my blog section:
- No ongoing costs – No expensive CMS subscription
- Bilingual – German and English in parallel
- SEO-optimized – Server-Side Rendering, structured data, dynamic metadata
- Admin panel – Create new posts without a deployment
- Premium design – Matching the rest of the portfolio
Technical Architecture
Next.js App Router with Server Components
The blog uses exclusively Next.js App Router and Server Components for maximum SEO performance. This means:
- Every blog post is server-side rendered
- Metadata is generated dynamically via
generateMetadata() - Google immediately sees the full HTML content – no JavaScript rendering required
```typescript
// Dynamic SEO metadata per blog post
export async function generateMetadata({ params }) {
const post = getPostBySlug(params.slug)
return {
title: post.title.en,
description: post.description.en,
openGraph: { ... },
}
}
`
Bilingual Content
Each post contains title, description, and content as an object with de and en keys:
```typescript
interface BlogPost {
slug: string
title: { de: string; en: string }
description: { de: string; en: string }
content: { de: string; en: string }
date: string
tags: string[]
readTime: number
}
`
Language switching uses a LanguageContext – no routing change, no reload, just state.
The CMS: Vercel Blob
Instead of an expensive headless CMS (Sanity, Contentful, etc.), I use Vercel Blob as a free data store. Blog posts are saved as JSON files.
The architecture:
`
data/blog-posts.ts ← Static posts (existing)
+
Vercel Blob (*.json) ← New posts from admin panel
↓
getAllPosts() ← Merges both sources
`
The Admin Panel
At /admin/blog there's a complete blog management interface:
- Post list with all entries (static + CMS)
- New post with live markdown editor for DE + EN
- Edit existing CMS posts
- Delete with confirmation
- Password protection via Next.js Proxy and cookie auth
SEO: Structured Data (JSON-LD)
For each blog post, an Article JSON-LD Schema is automatically generated:
```json
{
"@type": "Article",
"headline": "Post title",
"datePublished": "2025-05-04",
"author": { "@type": "Person", "name": "Andrei P." },
"publisher": { "@type": "Organization", "name": "APsolution" }
}
`
Google can use this to display Rich Results in search – a direct SEO advantage.
XML Sitemap
All blog posts automatically appear in the dynamic sitemap.xml that Next.js generates via app/sitemap.ts. New CMS posts appear in the sitemap immediately.
Performance & Core Web Vitals
The design accounts for all three Core Web Vitals:
- LCP (Largest Contentful Paint): Hero image loaded with
priority - INP (Interaction to Next Paint): No heavy client-side processes
- CLS (Cumulative Layout Shift): Image dimensions always explicitly specified
Lessons Learned
- Server Components first – Use SSR whenever possible, Client Components only where needed
- Plan bilinguality from day one – Adding it later is expensive
- Avoid CMS complexity – For a portfolio, Vercel Blob is completely sufficient
- SEO is not an add-on – It must be built into the architecture
Conclusion
The blog section consists of ~15 new files and is fully integrated into the existing portfolio infrastructure. No external services, no subscription costs, maximum control.
Want a system like this for your own website? Email me: apsolution.at@gmail.com