How I Built My Portfolio with Next.js, Contentlayer, and Git Submodules

Table of Contents(13 sections)
Building a personal developer portfolio is one of the most rewarding engineering projects you can undertake. It is your technical sandbox: a place to experiment with bleeding-edge web standards, share deep technical guides, and build a high-performance digital presence that you own completely.
When architecting locionic.com, I established three strict engineering constraints:
- Zero Database & Zero Headless CMS: No monthly SaaS database fees, no external API downtime, and no vendor lock-in.
- Content as Code: Every technical article, code snippet, and tutorial must live as plain Markdown/MDX in Git version control.
- Sub-Second Global Performance: Flawless 100/100 Core Web Vitals, pre-rendered static HTML, and zero client-side layout shifts.
In this guide, I break down the exact production architecture behind locionic.com - from Git submodule content isolation and Contentlayer build schemas to React Server Component (RSC) payload optimization.
1. The Content as Code Architecture: Git Submodule Isolation
Most developers building a blog either store their markdown files directly inside the main application repository or connect to an external headless CMS like Sanity or Strapi. Both approaches have notable flaws:
- Monolithic App Repo: As your blog expands to 200+ articles with images and localized translations (
en,vi,ja), your code repository git history bloats with thousands of content commits. - Headless CMS: Requires continuous network calls during builds, complex webhook sync pipelines, and monthly subscription tiers.
The Submodule Solution
I separated the codebase into two distinct repositories:
- Application Shell (
locionic/blog-and-projects): Contains Next.js routes, Tailwind styling, React components, and build scripts. - Content Core (
locionic/projectsmounted atcontents/): Contains only pure.mdxfiles, technical tutorials, interactive cheatsheets, and localized translations.
/home/developer/blog-and-projects/
├── app/ # Next.js 14 App Router (RSC Layouts, API routes)
├── components/ # UI components, MDX custom widgets
├── contents/ # Git Submodule pointing to locionic/projects
│ ├── blog_dev/ # 200+ Production Technical Guides (.mdx)
│ ├── cheatsheets/ # Interactive CLI & Git Reference Guides
│ └── courses/ # Multi-lesson curriculum files
└── package.json
This decoupled architecture allows me to write, edit, and publish technical guides from any Markdown editor or mobile git client without triggering application rebuilds or touching React code.
2. Type-Safe Content Processing with Contentlayer
To turn raw .mdx files into strongly typed TypeScript objects at build time, the site utilizes Contentlayer:
// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrismPlus from 'rehype-prism-plus';
export const Blog = defineDocumentType(() => ({
name: 'Blog',
filePathPattern: 'blog_dev/**/*.mdx',
contentType: 'mdx',
fields: {
title: { type: 'string', required: true },
date: { type: 'date', required: true },
summary: { type: 'string', required: true },
tags: { type: 'list', of: { type: 'string' }, default: [] },
draft: { type: 'boolean', default: false },
images: { type: 'list', of: { type: 'string' }, default: [] },
},
computedFields: {
slug: {
type: 'string',
resolve: (doc) => doc._raw.flattenedPath.replace(/^blog_dev\//, '').replace(/\/[^/]+$/, ''),
},
readingTime: {
type: 'json',
resolve: (doc) => readingTime(doc.body.raw),
},
},
}));
Contentlayer reads every .mdx file, parses YAML frontmatter, validates types, and generates static JSON documents in .contentlayer/generated. If I commit an article with a missing summary or malformed date, the build fails immediately in CI with an exact line number.
3. The RSC Serialization Trap: Slashing 2MB of Hidden Page Weight
One of the most critical performance bugs uncovered during production profiling involved React Server Components (RSC) payload serialization.
In early builds, our blog post layout displayed a "Related Posts" widget at the bottom of each article:
// ❌ DANGEROUS: Leaking 2MB of MDX body code into every page's RSC payload
import { allBlogs } from 'contentlayer/generated';
export default function BlogPostPage({ params }) {
const post = allBlogs.find((p) => p.slug === params.slug);
// Passing full Contentlayer objects to a Client Component!
return <PostLayout post={post} allPosts={allBlogs} />;
}
Because allBlogs contains the raw compiled JavaScript code (body.code) for all 200+ articles, Next.js serialized the entire 200-article catalog into the __NEXT_DATA__ / RSC JSON payload of every single blog post. A simple article page was downloading 2.3 MB of hidden JSON on initial load!
The Fix: Server-Side Stripping with coreContent()
We refactored the data layer to strip all build-time fields before serializing props across the server-client boundary:
// ✅ OPTIMAL: Only ship lightweight metadata to client components
export function coreContent<T extends { body: unknown; _raw: unknown }>(content: T) {
const { body, _raw, _id, type, ...rest } = content;
return rest;
}
// Pass only 3 slim related post summaries
const relatedPosts = getRelatedPosts(post, allBlogs, 3).map(coreContent);
This single optimization reduced initial page weight from 2.3 MB down to 82 KB - a 96% bandwidth reduction that skyrocketed mobile Lighthouse scores to 99.
4. Algorithmic Internal Linking: The 10-Cluster Hamiltonian Ring
To avoid search engine orphan penalties and maximize topical authority, the site runs an automated graph-theory script (scripts/relink_cluster_mesh.py):
- TF-IDF Semantic Clustering: Classifies all 219 articles across 10 distinct technical clusters (
nextjs_react,ai_llm_rag,devops_cloud,systems_wasm). - Hamiltonian 4-Chord Ring: Forms a continuous closed ring within each cluster connecting each article to its 4 closest semantic neighbors.
- Guaranteed Topology: Guarantees that every article has an in-degree and out-degree of at least 4, maintaining exactly 0 orphan articles across the entire domain.
Technical Architecture Summary
| Architectural Layer | Technology Selected | Primary Engineering Rationale |
|---|---|---|
| Framework | Next.js 14 App Router | Static Site Generation (SSG) + Server Components |
| Styling | Tailwind CSS + Typography | Zero runtime CSS-in-JS overhead |
| Content Engine | Contentlayer 0.3.4 | Build-time type validation for MDX |
| Search Engine | FlexSearch / Statically Built | 100% client-side search without Algolia costs |
| Hosting & CDN | Vercel Edge Network | Global HTTP/3 caching and Edge OG image rendering |
| Analytics & Privacy | Google Analytics + CookieConsent | GDPR-compliant cookie consent flags |
Test Your Knowledge
Frequently Asked Questions
How does client-side search work without a database?
During the post-build step, scripts/generate-search.mjs compiles a minified public/search.json file containing the titles, summaries, and tags of all published articles. When a user presses Cmd+K, the search modal loads this index in-memory and searches instantly with sub-millisecond latency.
How are social OpenGraph preview images generated?
We utilize Next.js Edge route /api/og/[locale]/[slug]. Using @vercel/og (backed by Satori), the server generates dynamic, high-resolution 1200x630 PNG graphics containing the post title, author name, and branding on the fly, with aggressive 1-year immutable edge caching.
How do you handle code syntax highlighting?
Code blocks in MDX are pre-rendered at build time using rehype-prism-plus with custom Prism CSS themes. This means zero syntax-highlighting JavaScript is shipped to the browser; syntax tokens are pre-formatted into native HTML <span> elements.
You Might Also Like
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Next.js Hydration Error: Fix React 418, 423 & Text Content Mismatch (2026)
Instant copy-paste fixes for Next.js hydration errors: Minified React Error #418 & #423, text content mismatch, browser extensions, suppressHydrationWarning, and interactive error debugger.
Read more
Next.js 14: Debugging the Server-Client Boundary
Practical guide to diagnosing and debugging Server and Client Component boundaries in Next.js 14+ App Router - with concrete error messages, RSC payload inspection, server-only guards, and React 19 Server Actions error handling.
Read more
Next.js App Router Dynamic Revalidation Guide
Master Next.js App Router caching architecture: fetch request memoization, data cache invalidation with revalidateTag, and on-demand ISR revalidation.
Read more