•8 min read

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

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

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:

  1. Zero Database & Zero Headless CMS: No monthly SaaS database fees, no external API downtime, and no vendor lock-in.
  2. Content as Code: Every technical article, code snippet, and tutorial must live as plain Markdown/MDX in Git version control.
  3. 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.


Next.js Architecture Illustration
Audio Briefing
0:00 / 0:00

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:

  1. Application Shell (locionic/blog-and-projects): Contains Next.js routes, Tailwind styling, React components, and build scripts.
  2. Content Core (locionic/projects mounted at contents/): Contains only pure .mdx files, 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.


Advertisement

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):

  1. TF-IDF Semantic Clustering: Classifies all 219 articles across 10 distinct technical clusters (nextjs_react, ai_llm_rag, devops_cloud, systems_wasm).
  2. Hamiltonian 4-Chord Ring: Forms a continuous closed ring within each cluster connecting each article to its 4 closest semantic neighbors.
  3. 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.

Advertisement

Technical Architecture Summary

Architectural LayerTechnology SelectedPrimary Engineering Rationale
FrameworkNext.js 14 App RouterStatic Site Generation (SSG) + Server Components
StylingTailwind CSS + TypographyZero runtime CSS-in-JS overhead
Content EngineContentlayer 0.3.4Build-time type validation for MDX
Search EngineFlexSearch / Statically Built100% client-side search without Algolia costs
Hosting & CDNVercel Edge NetworkGlobal HTTP/3 caching and Edge OG image rendering
Analytics & PrivacyGoogle Analytics + CookieConsentGDPR-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

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Next.js 14: Debugging the Server-Client Boundary
nextjs

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