Part 2 of 5. ← Part 1: The Why · Part 3: AI Curation Workflows →
The structural core of this system is deliberately simple. Hugo already does most of the work. The job is to wire it together correctly.
The visibility toggle: draft as a first-class concept
Hugo’s draft frontmatter field has existed forever, but it’s usually treated as a staging flag, something you flip to false before you publish. Here it’s the permanent state of inner content. draft: true means inner mind. draft: false means outer mind. That’s the entire visibility model.
The inner mind adds one extra field:
mind: inner # or: outer
This mirrors draft and is there for AI tooling. It’s easier to read and write mind: inner in a prompt than to remember that draft: true means private.
Config environments
Hugo’s environment configuration lets you layer config files. The directory structure:
config/
_default/
hugo.yaml ← base config (public build)
private/
hugo.yaml ← overlay (private build)
The private overlay is minimal:
buildDrafts: true
baseURL: 'http://localhost:1313'
params:
privateMode: true
navbar:
brandName: "Steph Locke · Inner Mind"
buildDrafts: true is what makes drafted content appear. privateMode: true is a custom param that layouts check to decide whether to render the amber banner and INNER badges.
Two build commands, one codebase:
hugo # public: no drafts, deploys to stephlocke.com
hugo server --environment private # private: everything, localhost only
The GitHub Actions deploy workflow runs Hugo in public mode with production flags (--gc, --minify, --baseURL, and --cacheDir). It does not enable the private environment, so drafts never reach the public site.
Why this matters for sharing: built-in RSS
Keeping everything in Hugo means the outer mind is not only human-readable on the site, it’s machine-readable as a feed. People who want to stay up to date can subscribe in any feed reader and get new public posts automatically, without depending on social platforms or algorithms.
Because inner content stays drafted, only outer-mind content is syndicated. The same visibility model that protects private notes also cleanly powers distribution: private stays private, public flows out via RSS.
The amber banner and INNER badge
A layouts/_default/baseof.html theme override injects the banner in private mode only:
{{ if .Site.Params.privateMode }}
{{- partial "inner-banner.html" . -}}
{{ end }}
The banner is sticky at the top so it’s impossible to miss:
<div style="background:#b45309;color:#fff;text-align:center;padding:0.4rem 1rem;
font-size:0.85rem;font-weight:600;position:sticky;top:0;z-index:9999;">
◉ Inner Mind · Private build, content with a draft badge is not published
</div>
Each draft page gets a small INNER badge next to the title, wired into layouts/_default/single.html:
<h1>{{ .Title }}{{ if and .Site.Params.privateMode .Draft }}
{{- partial "inner-badge.html" . -}}
{{ end }}</h1>
Archetypes with sensible defaults
Each content type has an archetype that pre-fills the right defaults, so hugo new links/my-link.md immediately produces a correctly-structured inner mind file:
---
title: "My Link"
date: 2026-04-06T00:00:00Z
draft: true
mind: inner
source_url: ""
no_promote: false
tags: []
related:
- "blog/second-mind-why-p1/index.md"
- "blog/second-mind-ai-p3/index.md"
---
The no_promote field is for the auto-promote workflow. Set it to true to permanently lock something in the inner mind regardless of age.
Resources default to draft: false and mind: outer, since they’re reference material you’d normally want public.
The connection graph
Every page can declare explicit connections via related frontmatter:
related:
- "blog/second-mind-why-p1/index.md"
- "blog/second-mind-ai-p3/index.md"
At build time, a Python script (`scripts/build-graph.py`) scans all published markdown files, collects nodes and two types of edges:
1. **Explicit links**: from `related` frontmatter
2. **Implicit links**: any two pages sharing at least one tag
The output is `static/graph.json`, which the `/graph/` page loads via Cytoscape.js, a browser library for interactive node-link diagrams. Node size scales with degree (more connections = bigger node). Clicking a node navigates to that page. In the private build, the script can be run with `GRAPH_DRAFTS=true` to include inner nodes, which appear in amber.
The graph is generated fresh at each CI build and excluded from git (`static/graph.json` is in `.gitignore`). To build it locally:
```bash
python3 scripts/build-graph.py
Pagefind search
Pagefind handles keyword search on the public site. It runs after Hugo builds, indexes the public/ directory, and produces a fully client-side search UI with no server required.
In CI, this is a single step after the Hugo build:
- name: Build Pagefind index
run: pagefind --site public
Locally, the index needs to go into static/pagefind/ so that hugo server can serve it:
./scripts/build-search.sh
That script builds Hugo, then runs pagefind --site public --output-path static/pagefind. After that, hugo server serves the search UI at /search/ without any further steps.
What this looks like in practice
Starting a local session:
# Build the generated files (needed once, or when content changes a lot)
python3 scripts/build-graph.py
./scripts/build-search.sh
# Browse everything
hugo server --environment private
The amber banner confirms you’re in the inner mind. Draft content shows the INNER badge. The /graph/ page shows the content network. The /search/ page does keyword search across published content.
For readers, the public build also exposes RSS feeds (for example the site feed and blog feed), so following ongoing work is as simple as subscribing once.
The next post covers the AI layer: the Claude Code slash commands that automate capture and curation, the Android-to-GitHub-Issues pipeline, and the ChromaDB semantic search index.
Next: AI Curation Workflows →