Private Content with Git Submodules

gitsubmodulesdeploymentprivacy

When building sites with personal content — journal entries, portfolio projects, work history — you face a dilemma: you want the code public on GitHub, but the content private. Here's how I solved it with Git submodules.

The Problem

Both my projects (react-playground and digital-handshake) use MDX files for content. A build script copies these files into src/app/ before Next.js builds the static site. Initially, real content was simply gitignored — it lived on my machine and nowhere else.

This had two issues:

  1. No version history for real content
  2. Running the build script deleted entries that only existed in src/app/ but not in the source directories — a nasty bug when the content folders were empty

The Solution: Private Submodules

The idea is simple: keep example content in the public repo as a fallback, and store real content in a separate private repository added as a Git submodule.

Structure

project/
├── journal-examples/          # public repo (tracked)
│   └── example-entry/
├── journal/                   # private submodule
│   ├── real-entry-one/
│   └── real-entry-two/
├── scripts/
│   └── prepare-content.mjs    # copies entries into src/app/
└── src/app/journal/
    ├── page.tsx               # list page (tracked)
    └── */                     # entries (generated, gitignored)

The Build Script

The prepare-content.mjs script uses a fallback strategy:

  1. If the private submodule (journal/) is cloned and has content → use it
  2. Otherwise → fall back to journal-examples/
  3. The --examples flag forces the use of examples
if (forceExamples) {
  sourceDir = EXAMPLES_DIR
} else if (hasEntries(JOURNAL_DIR)) {
  sourceDir = JOURNAL_DIR
} else {
  sourceDir = EXAMPLES_DIR
}

The generated entries in src/app/journal/*/ are gitignored since they're recreated on every npm run dev or npm run build.

New Machine Workflow

Setting up on a fresh machine:

# Option 1: Clone with submodules in one go
git clone --recurse-submodules git@github.com:user/react-playground.git

# Option 2: If already cloned without submodules
git submodule update --init

After that, npm run dev automatically picks up the real content from the submodule.

Without access to the private repo, the site still builds with the example entries — useful for contributors or CI previews.

Key Takeaway

Git submodules give you the best of both worlds: a public repo that anyone can clone and build, with private content that's versioned, backed up, and automatically included when you have access. The fallback to examples means the project always works, even without the private data.

Resources