Internal Doc Space Styling & Navigation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Give the internal board.cloudbase.foundation site a branded baseline template (masthead + footer) and a real top-bar navigation with auto-listed section pages, while leaving the public tier byte-for-byte unchanged.
Architecture: base.liquid gains a tier-gated masthead + footer (rendered only when the internal build sets tier: "internal"). A _data/nav.json-driven site-nav component (reusing the nav.doc-nav classes already in theme.css) is included in the layout and hand-retrofitted onto the standalone showcase/agenda HTML pages. A _data/sections.js filesystem reader powers auto-listed landing pages at /agendas/, /it/, /research/. Theory of Change moves to the internal tier.
Tech Stack: Eleventy v3, Liquid layout/includes (Markdown templating stays OFF), Cloudflare Pages.
Global Constraints
- Repo:
Cloudbase-Foundation/documents, branchmain, on cbf-dev at~/cbf/documents. Docs/config here go direct tomainper org convention. - Internal tier only. The chrome must be gated on
tier == "internal"; the public build output must stay identical to its current state. Re-run the leak/parity checks each task. - Reuse existing brand tokens and classes —
theme.cssalready defines.masthead,footer.colophon, andnav.doc-nav(lines ~48–97, ~392–410, ~423–472). Do NOT restyle; reference these classes. markdownTemplateEnginestaysfalse(content is brace-safe Markdown). Only the Liquid layout/includes and the new.liquidlisting pages use templating.- Build commands:
npm run build→_site(internal),npm run build:public→_site-public(public). - Nav top-level items (fixed): Home
/· Agendas/agendas/· IT & Ops/it/· Research/research/· Reference/tech/.Referencepoints at the existingtech/index.html;resumes/,projects/,foundational/are not top-nav items.
Task 1: Tier flag + branded masthead/footer on the template
Files:
- Modify:
eleventy.config.js(add thetierglobal data) - Modify:
_includes/base.liquid(tier-gated masthead + footer)
Interfaces:
-
Produces: a global
tier === "internal"on the internal build only;base.liquidrendering.masthead+footer.colophonaround{{ content }}for internal pages. -
[ ] Step 1: Add the
tierglobal data to the internal build
In eleventy.config.js, inside the exported function (after shared(eleventyConfig);), add:
// Marks the internal tier so base.liquid renders the site chrome. The public
// build (eleventy.public.config.js) deliberately does NOT set this.
eleventyConfig.addGlobalData("tier", "internal");
- [ ] Step 2: Replace
_includes/base.liquidwith the chromed, tier-gated layout
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }} — CBF</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@300;400;500;600;700&family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<style>{% include "theme.css" %}</style>
</head>
<body>
{% if tier == "internal" %}
<header class="masthead">
<div class="masthead-inner">
<div>
<div class="id-block">The Cloudbase Foundation · Internal Documents</div>
<h1>{{ title }}</h1>
</div>
</div>
</header>
{% endif %}
<main class="doc-main">
{{ content }}
</main>
{% if tier == "internal" %}
<footer class="colophon">
<div class="col-inner">
<div>The Cloudbase Foundation · Internal Documents</div>
<div>board.cloudbase.foundation</div>
</div>
</footer>
{% endif %}
</body>
</html>
- [ ] Step 3: Build both tiers
Run: npm run build && npm run build:public
Expected: both exit 0.
- [ ] Step 4: Verify the internal pages gained the masthead + footer
Run: grep -c 'class="masthead"' _site/it/finances/index.html && grep -c 'footer class="colophon"' _site/it/finances/index.html
Expected: both non-zero.
- [ ] Step 5: Verify the PUBLIC pages did NOT (chrome stays internal)
Run: grep -c 'class="masthead"\|colophon' _site-public/index.html; echo "exit ok"
Expected: 0 then exit ok (no masthead/footer on public).
- [ ] Step 6: Commit
git add eleventy.config.js _includes/base.liquid
git commit -m "feat: tier-gated masthead + footer on the internal template"
Task 2: Site-nav component (_data/nav.json + _includes/site-nav.html)
Files:
- Create:
_data/nav.json(top-level links) - Create:
_includes/site-nav.html(nav markup, reusesnav.doc-navstyles) - Modify:
_includes/base.liquid(include the nav after the masthead, internal only)
Interfaces:
-
Consumes:
tier(Task 1). -
Produces: a
navglobal (array of{label, url}); asite-nav.htmlpartial renderingnav.doc-nav. -
[ ] Step 1: Create
_data/nav.json
[
{ "label": "Home", "url": "/" },
{ "label": "Agendas", "url": "/agendas/" },
{ "label": "IT & Ops", "url": "/it/" },
{ "label": "Research", "url": "/research/" },
{ "label": "Reference", "url": "/tech/" }
]
- [ ] Step 2: Create
_includes/site-nav.html
<nav class="doc-nav" aria-label="Site navigation">
<div class="nav-inner">
<span class="nav-label">CBF docs ›</span>
{% for link in nav %}
<a href="{{ link.url }}" class="nav-item">{{ link.label }}</a>
{% endfor %}
</div>
</nav>
- [ ] Step 3: Include the nav in
_includes/base.liquid
In _includes/base.liquid, change the masthead block so the nav follows the </header> (still inside the tier == "internal" guard):
{% if tier == "internal" %}
<header class="masthead">
<div class="masthead-inner">
<div>
<div class="id-block">The Cloudbase Foundation · Internal Documents</div>
<h1>{{ title }}</h1>
</div>
</div>
</header>
{% include "site-nav.html" %}
{% endif %}
- [ ] Step 4: Build and verify the nav renders on internal pages
Run: npm run build && grep -o 'class="nav-item"' _site/index.html | wc -l
Expected: 5 (one per nav link).
- [ ] Step 5: Verify the nav links are correct
Run: grep -oE 'href="(/|/agendas/|/it/|/research/|/tech/)" class="nav-item"' _site/index.html | wc -l
Expected: 5.
- [ ] Step 6: Verify the public build still has NO nav
Run: npm run build:public && grep -c 'nav-item' _site-public/index.html; echo done
Expected: 0 then done.
- [ ] Step 7: Commit
git add _data/nav.json _includes/site-nav.html _includes/base.liquid
git commit -m "feat: site-nav top bar (nav.json) on the internal template"
Task 3: Auto-listed section landing pages (/agendas/, /it/, /research/)
Files:
- Create:
_data/sections.js(filesystem reader → per-section doc lists) - Modify:
eleventy.config.js(addliquidtotemplateFormats) - Modify:
_includes/theme.css(append.doc-liststyles) - Create:
agendas/index.liquid,it/index.liquid,research/index.liquid
Interfaces:
-
Consumes:
tier,base.liquid(Tasks 1–2). -
Produces: a
sectionsglobal of shape{ agendas: [{title,url,sortKey}], it: [...], research: [...] }; three listing pages at/agendas/,/it/,/research/. -
[ ] Step 1: Create
_data/sections.js
const fs = require("fs");
const path = require("path");
// Sections to auto-list. Each lists *.md + *.html, excluding index/template/partials.
const SECTION_DIRS = ["agendas", "it", "research"];
function titleFromMd(raw) {
const fm = raw.match(/^---\n([\s\S]*?)\n---/);
if (fm) {
const t = fm[1].match(/^title:\s*(.+)$/m);
if (t) return t[1].trim().replace(/^["']|["']$/g, "");
}
return null;
}
function titleFromHtml(raw) {
const t = raw.match(/<title>([^<]*)<\/title>/i);
return t ? t[1].replace(/\s+—\s+CBF\s*$/, "").trim() : null;
}
function urlFor(dir, file) {
if (file.endsWith(".md")) return `/${dir}/${file.replace(/\.md$/, "")}/`;
return `/${dir}/${file}`;
}
module.exports = function () {
const out = {};
for (const dir of SECTION_DIRS) {
let files = [];
try { files = fs.readdirSync(dir); } catch (e) { files = []; }
const items = [];
for (const file of files) {
if (!/\.(md|html)$/i.test(file)) continue;
if (/^index\./i.test(file)) continue; // never list a section's own index
if (/^_/.test(file)) continue; // partials
if (file === "agenda-template.html") continue; // template, not a real agenda
const raw = fs.readFileSync(path.join(dir, file), "utf8");
const title = (file.endsWith(".md") ? titleFromMd(raw) : titleFromHtml(raw)) || file;
items.push({ title, url: urlFor(dir, file), sortKey: file });
}
// Date-prefixed filenames sort newest-first under reverse string order.
items.sort((a, b) => b.sortKey.localeCompare(a.sortKey));
out[dir] = items;
}
return out;
};
- [ ] Step 2: Allow
.liquidpages in the internal build
In eleventy.config.js, change the returned templateFormats so standalone .liquid listing pages render:
templateFormats: ["md", "liquid"],
(The public config is untouched — it stays ["md"], and its input is public/ only, so these section pages never reach the public tier.)
- [ ] Step 3: Append
.doc-liststyles to_includes/theme.css
Append to _includes/theme.css:
/* Section landing lists */
.doc-list { list-style: none; padding-left: 0; max-width: 60rem; }
.doc-list li {
border-top: 1px dotted var(--line-soft);
padding: 0.7rem 0;
}
.doc-list li:first-child { border-top: none; }
.doc-list a {
font-family: 'Barlow Condensed', sans-serif;
font-size: 1.15rem;
letter-spacing: 0.01em;
}
- [ ] Step 4: Create
agendas/index.liquid
---
title: Board Agendas
---
<ul class="doc-list">
{% for doc in sections.agendas %}
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
{% endfor %}
</ul>
- [ ] Step 5: Create
it/index.liquid
---
title: IT & Operations
---
<ul class="doc-list">
{% for doc in sections.it %}
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
{% endfor %}
</ul>
- [ ] Step 6: Create
research/index.liquid
---
title: Research
---
<ul class="doc-list">
{% for doc in sections.research %}
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
{% endfor %}
</ul>
- [ ] Step 7: Build and verify the landing pages list real docs
Run:
npm run build
test -f _site/agendas/index.html && test -f _site/it/index.html && test -f _site/research/index.html && echo "pages built"
grep -c 'doc-list' _site/agendas/index.html
grep -o 'href="/agendas/7-9-26-meeting.html"' _site/agendas/index.html
Expected: pages built; non-zero doc-list count; the 7-9-26-meeting.html link present (proves the HTML agenda was auto-listed). The agenda-template.html must NOT appear:
grep -c 'agenda-template' _site/agendas/index.html
Expected: 0.
- [ ] Step 8: Verify the public build is unaffected (no section pages, no leak)
Run:
npm run build:public
for d in it agendas research projects tech foundational resumes; do test -e _site-public/$d && echo "LEAK: $d"; done; echo "leak-check done"
Expected: leak-check done with NO LEAK: lines.
- [ ] Step 9: Commit
git add _data/sections.js eleventy.config.js _includes/theme.css agendas/index.liquid it/index.liquid research/index.liquid
git commit -m "feat: auto-listed section landing pages (agendas, it, research)"
Task 4: Retrofit the standalone HTML pages onto the site nav
The showcase pages (tech/domain-map.html, tech/roadmap.html) already carry a bespoke 3-item nav.doc-nav and the .doc-nav CSS in their embedded <style>. The agenda HTML pages have a masthead but no nav and no .doc-nav CSS. Standardize all of them to the site nav.
Files:
- Modify:
tech/domain-map.html,tech/roadmap.html(replace bespoke nav with the site nav) - Modify:
agendas/4-30-26-meeting.html,agendas/7-9-26-meeting.html,agendas/agenda-template.html(insert site nav + add.doc-navCSS)
Interfaces:
-
Consumes: the nav link set from Global Constraints (these are static pages — the nav is inlined, not a Liquid include, because they are passthrough HTML).
-
[ ] Step 1: Replace the bespoke nav in
tech/domain-map.html
Find the existing <nav class="doc-nav" ...>…</nav> block and replace the whole block with:
<nav class="doc-nav" aria-label="Site navigation">
<div class="nav-inner">
<span class="nav-label">CBF docs ›</span>
<a href="/" class="nav-item">Home</a>
<a href="/agendas/" class="nav-item">Agendas</a>
<a href="/it/" class="nav-item">IT & Ops</a>
<a href="/research/" class="nav-item">Research</a>
<a href="/tech/" class="nav-item current" aria-current="page">Reference</a>
</div>
</nav>
- [ ] Step 2: Replace the bespoke nav in
tech/roadmap.html
Same replacement as Step 1 (identical block — Reference is current since roadmap lives under /tech/).
- [ ] Step 3: Add
.doc-navCSS to the agenda pages' embedded styles
The agenda pages use the same :root tokens but lack the nav rules. In each of agendas/4-30-26-meeting.html, agendas/7-9-26-meeting.html, and agendas/agenda-template.html, insert this block immediately before the closing </style>:
/* ----------- SITE NAV ----------- */
nav.doc-nav { background: var(--paper-warm); border-bottom: 1px solid var(--line); padding: 0.65rem 0; position: relative; z-index: 3; }
nav.doc-nav .nav-inner { max-width: 820px; margin: 0 auto; padding: 0 2.5rem; display: flex; align-items: center; gap: 1.8rem; flex-wrap: wrap; }
nav.doc-nav .nav-label { font-family: 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: 0.16em; font-size: 0.66rem; color: var(--ink-mute); }
nav.doc-nav .nav-item { font-family: 'Barlow Condensed', sans-serif; text-transform: uppercase; letter-spacing: 0.14em; font-size: 0.82rem; font-weight: 500; color: var(--ink-soft); text-decoration: none; padding: 0.2rem 0; border-bottom: 2px solid transparent; transition: color 0.15s ease, border-color 0.15s ease; }
nav.doc-nav .nav-item:hover { color: var(--navy); border-bottom-color: var(--gold); }
nav.doc-nav .nav-item.current { color: var(--navy); border-bottom-color: var(--gold); cursor: default; pointer-events: none; }
- [ ] Step 4: Insert the site nav into each agenda page
In each of the three agenda files, insert this block immediately after the masthead's closing </header>:
<nav class="doc-nav" aria-label="Site navigation">
<div class="nav-inner">
<span class="nav-label">CBF docs ›</span>
<a href="/" class="nav-item">Home</a>
<a href="/agendas/" class="nav-item current" aria-current="page">Agendas</a>
<a href="/it/" class="nav-item">IT & Ops</a>
<a href="/research/" class="nav-item">Research</a>
<a href="/tech/" class="nav-item">Reference</a>
</div>
</nav>
- [ ] Step 5: Build and verify the site nav reached every standalone page
Run:
npm run build
for p in tech/domain-map.html tech/roadmap.html agendas/7-9-26-meeting.html agendas/4-30-26-meeting.html; do
echo "$p: $(grep -c 'href="/agendas/" class="nav-item"' _site/$p) site-nav"
done
Expected: each prints 1 (the site nav's Agendas link is present on every page).
- [ ] Step 6: Verify the old bespoke nav is gone from the showcase pages
Run: grep -c 'CBF strategy ›\|project-timeline.html' _site/tech/domain-map.html
Expected: 0 (the strategy-doc nav was replaced).
- [ ] Step 7: Commit
git add tech/domain-map.html tech/roadmap.html agendas/4-30-26-meeting.html agendas/7-9-26-meeting.html agendas/agenda-template.html
git commit -m "feat: standardize showcase + agenda pages onto the site nav"
Task 5: Move Theory of Change into the internal tier
Files:
- Move:
public/theory-of-change.md→foundational/theory-of-change.md - Modify:
public/index.md(mark ToC "coming soon") - Modify:
index.md(link the now-internal ToC from the dashboard)
Interfaces:
-
Consumes: nothing new.
-
Produces: ToC served on the internal tier with chrome; ToC absent from the public build.
-
[ ] Step 1: Move the file (preserve git history)
cd ~/cbf/documents
git mv public/theory-of-change.md foundational/theory-of-change.md
- [ ] Step 2: Update
public/index.mdso the public site has no dead ToC link
Replace the Theory of Change bullet with a "coming soon" line. The file should read:
---
title: Cloudbase Foundation — Public Documents
---
# Cloudbase Foundation
Public documents for The Cloudbase Foundation, a 501(c)(3) nonprofit (EIN 27-1359927).
- Theory of Change (coming soon)
- 501(c)(3) determination letter (coming soon)
- [ ] Step 3: Link the internal ToC from the dashboard
index.md
In index.md, under the ## Reference list (or a fitting spot), add:
- [Theory of Change](/foundational/theory-of-change/)
- [ ] Step 4: Build both tiers and verify placement
Run:
npm run build && npm run build:public
test -f _site/foundational/theory-of-change/index.html && echo "internal ToC OK"
test -e _site-public/theory-of-change && echo "STILL PUBLIC (bad)" || echo "ToC absent from public — good"
grep -c 'coming soon' _site-public/index.html
Expected: internal ToC OK; ToC absent from public — good; non-zero coming soon count.
- [ ] Step 5: Verify the internal ToC now renders with chrome
Run: grep -c 'class="masthead"\|nav-item' _site/foundational/theory-of-change/index.html
Expected: non-zero (the relocated ToC picks up the internal template).
- [ ] Step 6: Commit
git add public/theory-of-change.md foundational/theory-of-change.md public/index.md index.md
git commit -m "content: move Theory of Change to the internal tier (public: coming soon)"
Task 6: Final end-to-end verification + push
Files: none (verification only).
- [ ] Step 1: Clean rebuild of both tiers
Run: rm -rf _site _site-public && npm run build && npm run build:public
Expected: both exit 0.
- [ ] Step 2: Internal chrome present everywhere (template + standalone)
Run:
for p in index.html it/finances/index.html agendas/index.html foundational/theory-of-change/index.html \
tech/domain-map.html agendas/7-9-26-meeting.html; do
echo "$p: nav=$(grep -c 'class="nav-item"' _site/$p)"
done
Expected: every page reports a non-zero nav count.
- [ ] Step 3: THE leak/parity check — public tier unchanged
Run:
for d in it agendas research projects tech foundational resumes; do test -e _site-public/$d && echo "LEAK: $d"; done; echo "leak-check done"
grep -c 'masthead\|nav-item\|colophon' _site-public/index.html; echo "public-chrome-check done"
Expected: leak-check done with NO LEAK: lines; 0 then public-chrome-check done (public has no chrome).
- [ ] Step 4: Push
git push origin main
Expected: Cloudflare Pages redeploys both projects; board.* shows the new chrome/nav (verify in-browser as a board member), public.* unchanged.
Notes for the implementer
- The parity check (Task 6 Step 3) is the most important verification. The public site must stay byte-for-byte as it was — the chrome is internal-only by
tiergating, and the public build's input is scoped topublic/. Never mark it done without seeing a clean result. - Why the agenda pages get inlined CSS + nav (not a Liquid include): they're
addPassthroughCopyverbatim HTML with their own embedded<style>; they can't{% include %}. The nav markup +.doc-navrules are therefore inlined. Future agendas inherit it becauseagenda-template.htmlis edited too. Reference→/tech/uses the existingtech/index.html; we deliberately do not generate a/tech/listing (it would collide with that file).- ToC is temporarily off the public site by design; it returns to
public/when the public-site styling work begins.
Self-review result
- Spec coverage: §1 chrome → Task 1; §2 nav + auto-listed sections → Tasks 2–3; §3 showcase retrofit → Task 4; §4 ToC relocation → Task 5; tier-gating/leak-safety → every task + Task 6. All spec decisions map to a task.
- Type/name consistency:
tier == "internal",nav(fromnav.json),sections.{agendas,it,research}(fromsections.js),.doc-nav/.nav-item/.nav-inner/.nav-label/.masthead/.colophon/.doc-listare used consistently across tasks and match the existingtheme.cssclasses. - No placeholders: every code step contains the full content; verifications are concrete build+grep commands with expected output.