'Building This Blog: A Technical Deep Dive into My Next.js AI-Powered Publishing [post] deterministic
An in-depth look at the technical architecture behind this blog - how
Building This Blog: A Technical Deep Dive into My Next.js AI-Powered Publishing Platform
I've been meaning to write this post for a while now. After all those blog posts about AI agents, local LLMs, RAG systems, and the Model Context Protocol, it seems only fitting to turn the lens inward and explain how this very blog actually works. This isn't just navel-gazing - understanding your tools deeply makes you a better developer, and I think there's genuine value in sharing the architectural decisions that make this system tick.
What makes this blog unique isn't just that it's a markdown-powered publishing platform - it's that the blog itself demonstrates the very AI technologies I write about. The site features an AI assistant with tool calling, MCP integration, semantic search powered by local embeddings, and an interactive knowledge graph. It's a working demonstration of local-first, sovereign AI infrastructure.
The Foundation: Why Next.js?
When I set out to build this blog, I had several requirements in mind:
1. **Static site generation (SSG)** for performance and SEO 2. **Markdown support** because I wanted to write posts in plain text 3. **Type safety** given my background in TypeScript projects 4. **Easy deployment** with Vercel or similar platforms 5. **AI integration capabilities** to demonstrate agentic workflows 6. **Flexibility** to add features like semantic search and knowledge graphs later
Next.js checked all these boxes. The App Router provides excellent SSG support, and the React foundation means I can embed interactive components when needed. With Next.js 16 and React 19, we're at the cutting edge of React Server Components architecture.
The Tech Stack
Here's what this blog is built on:
json
{
"framework": "Next.js 16.1.6",
"language": "TypeScript (strict mode)",
"ui": "React 19 + Tailwind CSS v4",
"animations": "Framer Motion 12",
"ai": "Vercel AI SDK 4.3",
"llm": "Ollama + OpenAI + Anthropic",
"protocol": "MCP (Model Context Protocol)",
"markdown": "gray-matter + react-markdown",
"visualization": "react-force-graph-3d + Three.js",
"deployment": "Vercel"
}
The key differentiator from a typical blog is the AI layer. This isn't just a static site - it's an agentic platform that can search its own content, answer questions about my work, and demonstrate MCP in action.
The File Structure
Let me walk you through how this blog is organized:
a01/
├── blog/ # All markdown blog posts live here (100+ posts!)
│ ├── 2024-10-04-detailed-description-of-insight-journal.md
│ ├── 2025-03-24-model-context-protocol.md
│ ├── 2026-01-25-synthetic-intelligence.md
│ └── ... (many more posts on AI, LLMs, autonomous agents)
├── public/
│ ├── images/ # Blog post images
│ └── art/ # AI-generated artwork (ComfyUI)
├── src/
│ ├── app/ # Next.js app router pages
│ │ ├── api/
│ │ │ ├── chat/ # AI Chat API endpoint
│ │ │ └── search/ # Semantic search API
│ │ └── blog/ # Blog listing and post pages
│ ├── components/
│ │ ├── ai/ # AI chat components with personas
│ │ ├── knowledge-graph.tsx # 3D interactive knowledge graph
│ │ └── related-posts.tsx # AI-powered recommendations
│ └── lib/
│ ├── blog.ts # Core blog API with reading time & TOC
│ ├── semantic-search.ts # Ollama-powered embeddings
│ ├── ai/
│ │ ├── tools.ts # Tool definitions for AI agent
│ │ └── types.ts # Persona definitions & schemas
│ └── mcp/
│ └── server.ts # MCP server integration
└── package.json
The simplicity is intentional. Every markdown file in the blog/ directory automatically becomes a blog post. No database, no CMS, no external dependencies. Just files - embodying the local-first philosophy I advocate for in my writing.
The Core: blog.ts
The heart of this system is src/lib/blog.ts. Let me walk you through the key components:
The BlogPost Interface
First, I defined a TypeScript interface that captures everything we need for a blog post:
typescript
export interface BlogPost {
slug: string;
title: string;
date: string;
description?: string;
categories?: string[];
tags?: string[];
author?: string;
image?: string;
content: string;
layout?: string;
canonical_url?: string;
readingTime?: number; // Auto-calculated
tableOfContents?: TableOfContentsItem[];
og?: { /* Open Graph metadata */ };
twitter?: { /* Twitter Card metadata */ };
}
This interface handles not just the basics (title, date, content) but also SEO metadata, reading time estimation, and auto-generated table of contents. The reading time is calculated based on an average reading speed of 200 words per minute:
typescript
export function calculateReadingTime(content: string): number {
const wordsPerMinute = 200;
const wordCount = content.trim().split(/\s+/).length;
return Math.max(1, Math.ceil(wordCount / wordsPerMinute));
}
Parsing Markdown with gray-matter
The magic happens through the gray-matter library, which parses YAML frontmatter from markdown files:
typescript
const { data, content } = matter(fileContents);
datacontains the frontmatter (title, date, tags, etc.)contentcontains the actual markdown body
This separation is elegant because it lets me write metadata alongside content without any special syntax beyond standard YAML.
Auto-Generating Table of Contents
For a technical blog, having a table of contents is essential. I extract headings from the markdown content automatically:
```typescript export function extractTableOfContents(content: string): TableOfContentsItem[] { const headingRegex = /^(#{1,3})\s+(.+)$/gm; const headings: TableOfContentsItem[] = []; let match;
while ((match = headingRegex.exec(content)) !== null) { const level = match[1].length; const title = match[2].trim(); const id = title.toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-');
headings.push({ id, title, level }); }
return headings; } ```
This creates clickable anchor links for each heading, allowing readers to jump to specific sections.
The AI Layer: Vercel AI SDK with Tool Calling
This is where the blog becomes more than a static site. I integrated the Vercel AI SDK to create an interactive AI assistant that can answer questions about the blog, search content, and demonstrate agentic workflows.
The Chat API (src/app/api/chat/route.ts)
The chat endpoint handles streaming responses with tool calling support:
typescript
export async function POST(req: Request) {
const body = await req.json();
const { messages, personaId } = body;
// Get the selected persona
const persona = personas.find(p => p.id === personaId);
// Build system prompt with persona context
const systemPrompt = buildSystemPrompt(defaultAgent, persona);
// Stream the response back to the client
const stream = new ReadableStream({
async start(controller) {
// ... streaming logic
}
});
return new Response(stream, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
Multiple AI Personas
The blog features four distinct AI personas, each tailored to different visitor needs:
| Persona | Description | Best For | |---------|-------------|----------| | **Technical Engineer** | Deep technical details, code examples, architecture diagrams | Developers | | **Recruiter/HR** | High-level overview, business value, measurable achievements | Recruiters | | **Researcher** | Academic depth, citations, theoretical foundations | Researchers | | **General** | Balanced, accessible responses | General visitors |
Each persona has its own system prompt that guides the AI's tone and depth:
```typescript export const personas: Persona[] = [ { id: 'engineer', name: 'Technical Engineer',