Site Redux — Phase C: Substack ↔ site integration (research & recommendation)
Status: investigation only — no build in this phase. Author: Claude (Opus 4.8), commissioned via the portfolio-redux build prompt (June 5, 2026). Scope: how to make libearden.dev and a Substack publication read as one integrated presence on GitHub Pages + Jekyll with no backend, plus a proposed design for a future “Writing” index.
Note on the missing spec. The build prompt referenced
/docs/site-redux-spec.md(the akhiltheerthala.com recon + proposal) as the source document. That file does not exist in the repo or in git history, and the Linear documents API is currently down (deprecated transport). The reconciled substance of that spec is, however, captured in Linear LIS-339 (tokens, the serif Variant A amendment, the v1a vs deferred split). This doc proceeds from LIS-339 + the build prompt. If the real spec resurfaces, re-check §”Branding parity” against it.
1. The goal, stated precisely
Two surfaces, one identity:
- libearden.dev — Jekyll, GitHub Pages, the “who I am / what I research” home. Static.
- Substack — the writing engine. Owns drafting, subscriptions, email delivery, comments, and the reader relationship. (Per
_config.yml, Mailchimp and Disqus are already disabled because Substack is meant to own subscriptions + comments — see LIS-200/LIS-202.)
“Integrated presence” decomposes into three separate problems, which is the whole trick:
- Identity / branding — same name, wordmark, accent (amber
#E8B339), type, and a way back to the site. Cheap to solve and the highest-leverage. - Navigation — the site can point into writing, and writing can point back to the site.
- Content surfacing — does the site show the latest posts, or just link to them? This is the only genuinely hard part on a no-backend stack, and the one with real tradeoffs.
Most of the felt “one presence” comes from #1 and #2, which are nearly free. #3 is where effort/risk lives, and it’s worth being deliberate about how much to spend there.
2. The binding constraint: GitHub Pages build environment
This constraint drives the entire recommendation, so it goes first.
- Native GitHub Pages builds run Jekyll in
--safemode. Only whitelisted plugins load; arbitrary gems do not; and the build has no network access. You therefore cannot fetch an external RSS feed during a native GH Pages build. Ajekyll-something-fetches-substackplugin will simply not run. jekyll-feedis whitelisted, but it generates your feed from your posts — it does not consume a remote feed. It’s the wrong direction for this use case.- Escape hatch: GitHub Actions. If you build the site yourself in an Action (
actions/jekyll-build-pagesor a plainjekyll buildstep) and deploy the artifact, you can run any plugin and make network calls — including fetching Substack’s RSS, transforming it to_data, and rendering it. This is free, well-trodden, and keeps everything static at serve time. The cost is that you now own a small build pipeline instead of relying on the zero-config GH Pages magic.
So “pull Substack RSS at build time” is achievable — but only via Actions, not via the native build. That reframes the options below.
3. The options
Option 1 — Substack on a custom subdomain (writing.libearden.dev)
Point a subdomain at Substack; Substack hosts the writing surface. The site and the blog share a registered domain, so they read as one property in the address bar.
- Mechanics: one-time $50 Substack custom-domain fee; add a CNAME record
writing → target.substack-custom-domains.com(Substack gives the exact target in its setup wizard). Subdomain only — you can’t point a root domain at Substack on most registrars, but a subdomain is exactly what we want here. Up to ~36h propagation. (Substack custom domain docs) - Branding: Substack lets you set logo, colors, and a “back to site” link in its publication settings — enough to carry the wordmark + amber and a link home. You do not control Substack’s layout/markup, so parity is approximate, not pixel.
- Content on the site: still need to link to it; this option alone doesn’t surface posts on the homepage.
- Effort: ~1 hour (mostly DNS + waiting). Reversible.
- Risk: low. The
.devapex stays on GH Pages (CNAMEfile =libearden.github.io’s domain); only thewritingsubdomain delegates to Substack. No conflict with the existing site.
Note: the repo’s
CNAMEfile currently contains the apex (libearden.dev). A subdomain CNAME is a DNS-registrar change, not a repo change — it doesn’t touch GH Pages config.
Option 2 — /writing section that pulls Substack RSS at build time (via Actions)
A GitHub Action fetches https://<pub>.substack.com/feed on each build (and on a schedule, e.g. nightly cron), writes it into _data/substack.yml (or generates stub posts), and Jekyll renders a native /writing/ index in the site’s own templates.
- Mechanics: GH Action with three steps — (1)
curl/script the feed → parse XML → emit_data/substack.json; (2)jekyll build; (3) deploy viaactions/deploy-pages. Addschedule:cron so new posts appear without a manual push. Substack’s feed includes full post content + excerpts + dates + links (Substack RSS docs). - Branding: total — rendered in our own Liquid + tokens, so it matches the site exactly (amber, serif headings, the eval-demonstrator type system).
- Content on the site: full control — show N latest, excerpts, “read on Substack →” deep links.
- Effort: ~half a day to a day (write + debug the Action and the parser; handle the feed being empty/down).
- Risk: medium. You now maintain a build pipeline; a malformed feed or Substack outage can fail a build (mitigate: cache last-good
_data, fail soft). Switching from native GH Pages build to Actions is a one-way-ish setup change. - Caveat: posts on the site are a mirror. Comments, subscribe, and the reader relationship still live on Substack — so deep-link each item back. Don’t try to reproduce the full reading experience.
Option 3 — Client-side fetch of the RSS in the browser
Ship a static /writing/ page whose JS fetches the Substack feed at page load and renders cards.
- Blocker: Substack’s
/feedendpoint does not send permissive CORS headers, so a direct browserfetch()is blocked. You’d need a third-party CORS proxy (api.rss2json.com,corsproxy.io, a Cloudflare Worker, etc.) — i.e. an external dependency with rate limits, privacy implications, and its own uptime. - Effort: ~2–3 hours.
- Risk: high relative to value. Adds runtime JS, layout shift, a spinner/empty state, and a fragile external proxy on the critical render path. Worst reliability of the four, and it undercuts the site’s “minimal JS, no layout shift” design principle. Only sensible if real-time freshness mattered, which for a low-cadence research blog it doesn’t.
Option 4 — Embed Substack (iframe / Substack embed widgets)
Drop Substack’s embeddable feed/subscribe widget or an <iframe> of the publication into a page.
- Branding: worst of the four — the embed carries Substack chrome, ignores the site’s tokens, and an
<iframe>can’t inherit dark mode or fonts. Reads as “a Substack bolted onto a site,” the exact opposite of the goal. - Useful sub-part: the subscribe embed (just the email-capture form) is worth keeping as a small element on a
/writingpage or footer — it’s the one piece that genuinely belongs inline. The post-list embed is not. - Effort: ~30 min. Risk: low technically, high to the “one cohesive presence” goal for the post list.
4. Comparison
| Branding parity | Posts on site | No-backend? | Freshness | Effort | Maintenance / risk | |
|---|---|---|---|---|---|---|
| 1. Subdomain | Approx (Substack-controlled) | No (link only) | ✅ static | n/a | ~1h + DNS | Very low |
| 2. RSS @ build (Actions) | Exact (our templates) | Yes, full control | ✅ static output | Per build / nightly cron | ~0.5–1 day | Medium (own the pipeline) |
| 3. Client fetch | Exact markup, but needs proxy | Yes | ✅ but needs proxy | Real-time | ~2–3h | High (CORS proxy, runtime JS) |
| 4. Embed | Poor (Substack chrome) | Yes (in widget) | ✅ static | Real-time | ~30m | Low tech / high to the goal |
5. Recommendation
Adopt Option 1 now; layer Option 2 later. Skip 3 and 4 (keep only Option 4’s subscribe form).
A phased pick, because the two halves of “integrated presence” have very different cost/value:
5a. Now (cheap, high-leverage) — Option 1 + branding pass
writing.libearden.dev→ Substack (custom subdomain). One $50 fee, one CNAME, done. This buys the single biggest perceptual win: the writing lives at your domain, notsomeone.substack.com.- Branding parity pass inside Substack settings: wordmark, amber accent, and a “← libearden.dev” link home.
- Site → writing: add a “Writing” nav item pointing at
writing.libearden.dev, and upgrade the existing homepage writing stub (research-section.html/ the prototype’s#writing) from a dead#link to the real subdomain.
This is ~1–2 hours total and delivers ~80% of the felt integration. It also has zero ongoing maintenance and is fully reversible.
5b. Later (when there are ≥3–5 posts) — Option 2, native /writing index
Once the publication has enough posts that a list looks alive (empty/near-empty lists advertise emptiness — the same reason the IA-heavy sections are deferred until the arXiv preprint), add the build-time RSS pull via GitHub Actions to render a native /writing/ index in the site’s own type system. This upgrades the nav target from “off to Substack” to “a first-class section of the site that happens to deep-link out for the full read + comments + subscribe.”
Why this order, explicitly:
- Option 1 is reversible, near-zero-maintenance, and solves identity/nav — the parts that matter most — immediately.
- Option 2 is the only option giving exact branding for on-site post surfacing, but it costs a build pipeline and only pays off once posts exist. Building it against an empty feed is wasted effort that would also display an empty section.
- Option 3 violates the site’s own minimal-JS / no-layout-shift principles and depends on a fragile proxy.
- Option 4’s post-list embed actively fights the cohesion goal; only its subscribe form is worth keeping.
Rough effort: Phase-now ≈ 1–2h (mostly DNS propagation wait). Phase-later ≈ 0.5–1 day for the Action + parser + the /writing template + fail-soft caching.
Tradeoffs to accept:
- Substack still owns the actual reading experience, comments, and subscribe — by design. The site surfaces and frames; it doesn’t reproduce. Every on-site post deep-links back.
- Option 1’s branding is “close,” not pixel-perfect; you don’t control Substack’s layout.
- Option 2 means migrating off native GH Pages build onto Actions. That’s a deliberate, mostly one-way setup choice — worth it for the control, but flag it.
Decisions that need the user (not researchable here)
- The Substack publication’s actual URL / handle (the
<pub>in<pub>.substack.com/feed) — needed for both the subdomain target and the RSS pull. The homepage stub still has adata-todo="substack-url"placeholder and an openhref="#". - Whether to spend the $50 custom-domain fee now vs. defer the subdomain until the first posts ship.
6. Branding / nav parity checklist (applies regardless of option)
To make both surfaces read as one presence:
- Wordmark: same “Li Bearden” treatment on both. In the chosen prototype variant the name uses the serif display face — Substack can’t match the face, but it can match the text + amber.
- Accent: amber
#E8B339as Substack’s theme accent. - Round trip: site has a “Writing” nav entry → Substack; Substack has a “← Back to libearden.dev” link + the site in its About/links. No dead ends.
- Voice: the eyebrow/label system (Geist Mono, uppercase) is a site-only motif; don’t try to force it into Substack. Let Substack be Substack with your colors, not a failed clone of the site.
- Subscribe: one subscribe affordance, sourced from Substack (its embed form), placed once on the site (footer or
/writing). Don’t reintroduce Mailchimp —_config.ymlalready retired it for this reason.
7. Proposed “Writing” index design (deferred build)
For when Option 2 lands and there are enough posts. Built in the site’s own tokens/type system (matches whichever prototype variant wins — examples below assume Variant A, Editorial scholar).
IA & layout
- Route:
/writing/. Nav label: Writing. Add to top nav (note: this is the slot freed by the Phase-B consulting demotion — see the build prompt §4). - Single narrow column at the
--maxwbook measure (~640px), consistent with the editorial direction. No card grid — a card grid of 2 posts looks emptier than a list. - Reverse-chronological list of entries, each:
- serif post title (links to the full post on Substack,
target="_blank" rel="noopener"), - Geist Mono date eyebrow (
YYYY-MM-DD), - 1–2 line excerpt/dek (from the RSS
<description>), - a quiet “Read on Substack →” deep link.
- serif post title (links to the full post on Substack,
- Optional lightweight topic tags (eval-validity / sycophancy / loyalty-audit / methodology) if the feed carries categories — but only once there’s enough volume to make filtering meaningful. Defer until then.
States (design all of them, per the site’s principles)
- Populated: the list above.
- Empty / pre-launch: a single honest line — “Writing on evaluation methodology — first posts landing soon. Subscribe ↓” + the subscribe form. (This is also the correct current state of the homepage stub.)
- Feed-unavailable (build fell back to cache): render last-good
_data; never show a broken/empty section due to a transient Substack outage.
On the homepage
- Replace the current dead writing stub with a “Latest writing” teaser: the top 1–3 entries (title + date) pulled from the same
_data/substack.*, then “All writing →” to/writing/. Keeps the homepage alive without duplicating the full index.
Accessibility & perf (carry the prototype’s non-negotiables)
- Semantic
<article>per entry, logical heading order, visible focus rings, ≥44px tap targets on links. - Because content is baked at build time (Option 2), no runtime fetch, no layout shift, no spinner — which is exactly why Option 2 beats Option 3 here.
8. One-paragraph summary for the busy reader
Identity and navigation — the parts that actually make two surfaces feel like one — are nearly free: put the blog on writing.libearden.dev (one $50 fee + a CNAME), theme Substack with the amber accent + wordmark, add a Writing nav item, and make both ends link to each other. Do that now. The harder part — showing posts on the site in your own type system — is worth doing only once posts exist, and only via a GitHub Action that fetches Substack’s RSS at build time (native GH Pages can’t fetch during build; client-side fetch needs a fragile CORS proxy; embeds carry Substack’s chrome). Until then, the homepage writing stub should say “coming soon” honestly rather than render an empty list.
Sources: Substack custom domain setup · Substack RSS feed · GitHub Pages plugin/safe-mode versions · jekyll-feed