artifacts/incoming
Constellation Static Hub Machine v1
artifacts/incoming/constellation_static_hub_machine_v_1_cloudflare_pages_11_ty.mdRendered from markdown source. Open raw source on GitHub.
Constellation Static Hub Machine v1
Goal: many branded hubs (one domain per hub), shared theme, global/nested navigation across hubs, minimal friction for publishing essays, deployed on Cloudflare Pages. Designed so you can start with any root (e.g., ULiUA) and later promote/re-parent (e.g., Polememelop becomes the parent) by editing a single nav graph.
---
Core design principle
Navigation is data, not layout.
Each site declares only its identity key (e.g., uliua, howacceptanceworks). The global constellation graph defines hierarchy, nesting, labels, and URLs. The theme renders nav/breadcrumbs dynamically from that graph.
This makes restructuring (e.g., inserting Polememelop above ULiUA) a one-file change.
---
Stack
- 11ty (Eleventy) for static generation
- Cloudflare Pages for hosting + deploy previews
- GitHub for source control
- Shared theme as npm package (
@polememelop/themeor@ulilua/theme) - Shared constellation graph as npm package (
@polememelop/constellation) or bundled into theme for v1
Recommended: theme + nav graph as separate packages once you’re adding hubs frequently. For v1, bundling nav inside theme is acceptable.
---
Repo layout (recommended: multi-repo)
1) constellation-nav (npm package)
Purpose: single source of truth for the constellation graph.
Files:
package.jsonconstellation.json
Example constellation.json (node graph form):
{
"nodes": {
"uliua": {
"title": "ULiUA",
"url": "https://uliua.com",
"parent": null,
"order": 10
},
"howonething": {
"title": "How One Thing Works",
"url": "https://howonething.work",
"parent": "uliua",
"order": 20
},
"howacceptanceworks": {
"title": "How Acceptance Works",
"url": "https://howacceptanceworks.com",
"parent": "howonething",
"order": 10
},
"howegoworks": {
"title": "How Ego Works",
"url": "https://howegoworks.com",
"parent": "howonething",
"order": 20
}
}
}
Why node graph form?
- Easy to re-parent: change one
parentfield. - Theme can compute children, breadcrumbs, nesting.
Promotion example (later): Polememelop above ULiUA Add:
"polememelop": {
"title": "Polememelop",
"url": "https://polememelop.com",
"parent": null,
"order": 5
}
Change:
"uliua": { "parent": "polememelop", ... }
No hub repos change.
---
2) theme (npm package)
Purpose: brand + layouts + nav rendering + post listings + tags.
Files (illustrative):
package.jsonsrc/(theme implementation)_includes/layouts/base.njkpartials/nav.njkpartials/footer.njk_data/(theme defaults)theme.jsonassets/(css, fonts, icons)index.js(exports 11ty plugin)
Theme responsibilities:
- Render global header/nav from
constellation.json - Highlight current hub based on
constellationKey - Compute breadcrumbs by walking
parentpointers - Provide consistent typography + components
- Provide collections:
posts,tags
Theme consumes nav graph via dependency:
constellation-navadded tothemedependencies, ORconstellation-navadded to each hub and passed to theme.
---
3) Hub template repo (hub-template)
Purpose: minimal hub that installs theme + declares identity.
Files:
package.json.eleventy.jssite.json(hub identity)content/(markdown)index.mdabout.mdposts/(essays)
Example site.json:
{
"constellationKey": "howacceptanceworks",
"siteName": "How Acceptance Works",
"siteUrl": "https://howacceptanceworks.com",
"description": "Essays and tools for acceptance → unconditional love.",
"brand": {
"accent": "acceptance"
}
}
Hub responsibilities:
- Content only
- Small hub settings
- No nav duplication
- No layout duplication
---
How nav + nesting works
The theme builds an in-memory tree at build-time:
- Load
constellation.json - Compute children lists for each node
- Identify current node via
site.constellationKey - Render:
- Top nav: top-level children of the current root (or global root)
- Nested dropdowns: children-of-children recursively
- Breadcrumbs: current → parent → ... → root
Key: root is not hardcoded. You can choose:
- global root = the node with
parent=nulland lowestorder - OR a configured root for each build (e.g., show Polememelop-level nav everywhere)
v1 suggestion: theme chooses global root automatically.
---
Cloudflare Pages deployment contract
Each hub repo is a Cloudflare Pages project:
- Build command:
npm ci && npm run build - Output dir:
_site - Node version: set in Pages settings if needed
Pages provides:
- Automatic deploys on push
- Preview deploys on PRs
- Custom domain attachment per hub
---
“One command creates a hub” automation plan
Script: newhub
Inputs:
repo_name(e.g.,howacceptanceworks)domain(e.g.,howacceptanceworks.com)title(e.g.,How Acceptance Works)constellationKey(usually same as repo)
Actions:
- Create repo from
hub-templatevia GitHub CLI - Fill
site.jsonvalues - Initial commit + push
- Create Cloudflare Pages project (CLI/API)
- (Optional v2) Attach custom domain + create DNS record
What changes when you add Polememelop as parent?
Only constellation.json changes (in constellation-nav). Then:
- bump
constellation-navversion - hubs update dependency (can be batched later)
If you bundle nav in theme instead:
- bump
themeversion and update hubs.
---
Versioning strategy
- Theme version changes when:
- brand/layout/components change
- nav rendering logic changes
- Nav graph version changes when:
- you add/re-parent/rename nodes
- you change URLs
This keeps “structure edits” lightweight.
---
Minimal publishing workflow
For any hub:
- Create a new markdown file in
content/posts/with frontmatter:
---
title: "Why acceptance opens the gate"
date: 2026-02-19
tags: [acceptance, practice]
---
Essay body...
git push- Cloudflare Pages deploys; nav stays consistent
---
v1 scope (keep it shippable)
Ship v1 with:
- Theme + nav graph
- 1 hub deployed (start wherever you like)
- Hub template
- Manual custom-domain attach in CF Pages
Then v1.1:
newhubscript to automate repo creation + first deploy
Then v1.2:
- Full domain/DNS automation via Cloudflare API
---
Naming (suggestion)
@polememelop/constellation— the graph@polememelop/theme— the brand + rendering- Hub repos named after domain intent (
howacceptanceworks,uliua, etc.)
You can start with uliua as root and later insert polememelop above it by editing only @polememelop/constellation.
---
Executable v1 (Concrete Scaffolds)
This section makes v1 runnable: exact repo trees + minimal file contents + commands. The intent is: you can copy-paste these into repos (or generate them) and get a working constellation hub on Cloudflare Pages.
What “executable v1” guarantees
- You can build and serve any hub locally:
npm ci && npm run dev - You can build output to
_site:npm run build - Each hub has:
- Home page
- Posts collection (markdown essays)
- Tag pages
- RSS
- Global/nested nav (from constellation graph)
- Breadcrumbs
v1 does not require automation for Cloudflare domain attach; you can do that in UI. (We’ll add it in v1.2.)
---
Repo 1: @polememelop/constellation (npm package)
Tree
constellation-nav/
package.json
constellation.json
README.md
package.json
{
"name": "@polememelop/constellation",
"version": "0.1.0",
"private": false,
"type": "module",
"files": ["constellation.json"],
"exports": {
"./constellation.json": "./constellation.json"
}
}
constellation.json (start with ULiUA-root; promote later)
{
"nodes": {
"uliua": {
"title": "ULiUA",
"url": "https://uliua.com",
"parent": null,
"order": 10
},
"howonething": {
"title": "How One Thing Works",
"url": "https://howonething.work",
"parent": "uliua",
"order": 20
},
"howacceptanceworks": {
"title": "How Acceptance Works",
"url": "https://howacceptanceworks.com",
"parent": "howonething",
"order": 10
},
"howegoworks": {
"title": "How Ego Works",
"url": "https://howegoworks.com",
"parent": "howonething",
"order": 20
},
"howhappinessworks": {
"title": "How Happiness Works",
"url": "https://howhappinessworks.com",
"parent": "howonething",
"order": 30
}
}
}
Later “promotion” to Polememelop
To insert Polememelop above ULiUA:
- add node
polememelopwithparent: null - change
uliua.parenttopolememelop
No hub repos change.
---
Repo 2: @polememelop/theme (npm package)
Tree
theme/
package.json
index.js
src/
assets/
style.css
_includes/
layouts/
base.njk
post.njk
tag.njk
partials/
head.njk
nav.njk
footer.njk
breadcrumbs.njk
README.md
package.json
{
"name": "@polememelop/theme",
"version": "0.1.0",
"private": false,
"type": "module",
"main": "./index.js",
"files": ["index.js", "src"],
"dependencies": {
"@polememelop/constellation": "^0.1.0"
}
}
index.js (Eleventy plugin)
import constellation from "@polememelop/constellation/constellation.json" assert { type: "json" };
function buildIndex(nodes) {
const byKey = nodes;
const children = {};
for (const [k] of Object.entries(byKey)) children[k] = [];
for (const [k, n] of Object.entries(byKey)) {
if (n.parent) {
if (!children[n.parent]) children[n.parent] = [];
children[n.parent].push(k);
}
}
// sort children by order then title
for (const k of Object.keys(children)) {
children[k].sort((a, b) => {
const A = byKey[a], B = byKey[b];
const ao = (A?.order ?? 9999), bo = (B?.order ?? 9999);
if (ao !== bo) return ao - bo;
return (A?.title ?? a).localeCompare(B?.title ?? b);
});
}
// global root: node(s) with parent null; choose lowest order
const roots = Object.entries(byKey)
.filter(([, n]) => n.parent === null)
.sort(([, A], [, B]) => (A.order ?? 9999) - (B.order ?? 9999));
const rootKey = roots[0]?.[0] ?? null;
return { byKey, children, rootKey };
}
function breadcrumbTrail(index, currentKey) {
const trail = [];
let k = currentKey;
const seen = new Set();
while (k && index.byKey[k] && !seen.has(k)) {
seen.add(k);
trail.push({ key: k, ...index.byKey[k] });
k = index.byKey[k].parent;
}
return trail.reverse();
}
export default function polememelopTheme(eleventyConfig, opts = {}) {
const nodes = constellation.nodes ?? {};
const index = buildIndex(nodes);
// Expose constellation data to templates
eleventyConfig.addGlobalData("constellation", () => ({
index,
nodes: index.byKey,
rootKey: index.rootKey,
breadcrumbTrail
}));
// Pass-through CSS
eleventyConfig.addPassthroughCopy({
[new URL("./src/assets/style.css", import.meta.url).pathname]: "assets/style.css"
});
// Collections
eleventyConfig.addCollection("posts", (collectionApi) => {
return collectionApi
.getFilteredByGlob("content/posts/**/*.md")
.sort((a, b) => (b.date ?? 0) - (a.date ?? 0));
});
// Simple shortcode: current year
eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`);
// Defaults
eleventyConfig.addGlobalData("theme", () => ({
stylesheet: "/assets/style.css"
}));
return {
dir: {
// Theme does not impose dirs; hubs set their own
}
};
}
Theme templates
src/_includes/layouts/base.njk
<!doctype html>
<html lang="en">
{% include "partials/head.njk" %}
<body>
{% include "partials/nav.njk" %}
<main class="container">
{% include "partials/breadcrumbs.njk" %}
{{ content | safe }}
</main>
{% include "partials/footer.njk" %}
</body>
</html>
src/_includes/layouts/post.njk
---
layout: layouts/base.njk
---
<article class="post">
<h1>{{ title }}</h1>
<p class="meta">{{ page.date | date: "%Y-%m-%d" }}</p>
{{ content | safe }}
{% if tags %}
<p class="tags">
Tags:
{% for t in tags %}
{% if t != "posts" %}
<a href="/tags/{{ t | slug }}/">{{ t }}</a>{% if not loop.last %}, {% endif %}
{% endif %}
{% endfor %}
</p>
{% endif %}
</article>
src/_includes/layouts/tag.njk
---
layout: layouts/base.njk
---
<h1>Tag: {{ tag }}</h1>
<ul>
{% for post in posts %}
<li>
<a href="{{ post.url }}">{{ post.data.title }}</a>
<small class="meta">{{ post.date | date: "%Y-%m-%d" }}</small>
</li>
{% endfor %}
</ul>
src/_includes/partials/head.njk
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% if title %}{{ title }} · {% endif %}{{ site.siteName }}</title>
<meta name="description" content="{{ site.description }}" />
<link rel="stylesheet" href="{{ theme.stylesheet }}" />
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed.xml" />
</head>
src/_includes/partials/nav.njk
{% set ck = site.constellationKey %}
{% set idx = constellation.index %}
{% set root = constellation.rootKey %}
<header class="site-header">
<div class="container header-row">
<a class="brand" href="{{ site.siteUrl }}">{{ site.siteName }}</a>
<nav class="topnav" aria-label="Constellation">
{% set topChildren = idx.children[root] %}
{% if topChildren and topChildren.length %}
<ul class="nav-list">
{% for key in topChildren %}
{% set node = idx.byKey[key] %}
{% set kids = idx.children[key] %}
<li class="nav-item">
<a class="nav-link {% if key == ck %}active{% endif %}" href="{{ node.url }}">{{ node.title }}</a>
{% if kids and kids.length %}
<ul class="nav-sub">
{% for k2 in kids %}
{% set n2 = idx.byKey[k2] %}
<li><a class="nav-sublink {% if k2 == ck %}active{% endif %}" href="{{ n2.url }}">{{ n2.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</nav>
</div>
</header>
src/_includes/partials/breadcrumbs.njk
{% set trail = constellation.breadcrumbTrail(constellation.index, site.constellationKey) %}
{% if trail and trail.length > 1 %}
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
{% for n in trail %}
<li>
{% if loop.last %}
<span aria-current="page">{{ n.title }}</span>
{% else %}
<a href="{{ n.url }}">{{ n.title }}</a>
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
{% endif %}
src/_includes/partials/footer.njk
<footer class="site-footer">
<div class="container">
<small>© {% year %} {{ site.siteName }}</small>
</div>
</footer>
src/assets/style.css (minimal, readable)
:root { --max: 980px; }
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; line-height: 1.5; }
.container { max-width: var(--max); margin: 0 auto; padding: 20px; }
.site-header { border-bottom: 1px solid #eee; position: sticky; top: 0; background: white; }
.header-row { display: flex; gap: 16px; align-items: center; justify-content: space-between; }
.brand { font-weight: 700; text-decoration: none; color: inherit; }
.nav-list { list-style: none; margin: 0; padding: 0; display: flex; gap: 14px; }
.nav-item { position: relative; }
.nav-link { text-decoration: none; color: inherit; padding: 8px 10px; border-radius: 10px; display: inline-block; }
.nav-link.active { outline: 1px solid #ddd; }
.nav-sub { display: none; position: absolute; left: 0; top: 100%; background: white; border: 1px solid #eee; border-radius: 12px; padding: 8px; list-style: none; margin: 8px 0 0 0; min-width: 240px; }
.nav-item:hover .nav-sub { display: block; }
.nav-sublink { display: block; padding: 8px 10px; text-decoration: none; color: inherit; border-radius: 10px; }
.nav-sublink.active { outline: 1px solid #ddd; }
.breadcrumbs ol { list-style: none; padding: 0; margin: 12px 0 18px 0; display: flex; flex-wrap: wrap; gap: 8px; }
.breadcrumbs li::after { content: "›"; margin-left: 8px; opacity: 0.5; }
.breadcrumbs li:last-child::after { content: ""; }
.post .meta { opacity: 0.7; }
.tags a { text-decoration: none; }
.site-footer { border-top: 1px solid #eee; margin-top: 40px; }
---
Repo 3: hub-template (starter for each hub)
Tree
hub-template/
package.json
.eleventy.js
site.json
content/
index.md
about.md
posts/
2026-02-19-welcome.md
tags.njk
feed.njk
README.md
package.json
{
"name": "hub-template",
"private": true,
"type": "module",
"scripts": {
"dev": "npx @11ty/eleventy --serve",
"build": "npx @11ty/eleventy"
},
"dependencies": {
"@11ty/eleventy": "^2.0.1",
"@11ty/eleventy-plugin-rss": "^1.2.0",
"@polememelop/theme": "^0.1.0"
}
}
.eleventy.js
import pluginRss from "@11ty/eleventy-plugin-rss";
import polememelopTheme from "@polememelop/theme";
import fs from "node:fs";
export default function (eleventyConfig) {
eleventyConfig.addPlugin(pluginRss);
eleventyConfig.addPlugin(polememelopTheme);
// Load per-hub config from site.json
const site = JSON.parse(fs.readFileSync("./site.json", "utf8"));
eleventyConfig.addGlobalData("site", () => site);
// Layout aliases so content can just say `layout: base`
eleventyConfig.addLayoutAlias("base", "layouts/base.njk");
eleventyConfig.addLayoutAlias("post", "layouts/post.njk");
eleventyConfig.addLayoutAlias("tag", "layouts/tag.njk");
// Tags collection helpers
eleventyConfig.addCollection("tagList", (collectionApi) => {
const tags = new Set();
for (const item of collectionApi.getAll()) {
(item.data.tags || []).forEach((t) => {
if (t && t !== "posts") tags.add(t);
});
}
return [...tags].sort();
});
return {
dir: {
input: "content",
includes: "../node_modules/@polememelop/theme/src/_includes",
output: "_site"
},
markdownTemplateEngine: "njk",
htmlTemplateEngine: "njk"
};
}
site.json (edit per hub)
{
"constellationKey": "howacceptanceworks",
"siteName": "How Acceptance Works",
"siteUrl": "https://howacceptanceworks.com",
"description": "Essays and tools for acceptance → unconditional love."
}
content/index.md
---
layout: base
title: Home
---
# {{ site.siteName }}
{{ site.description }}
## Latest
<ul>
{% for post in collections.posts | slice(0, 10) %}
<li><a href="{{ post.url }}">{{ post.data.title }}</a> <small class="meta">{{ post.date | date: "%Y-%m-%d" }}</small></li>
{% endfor %}
</ul>
content/about.md
---
layout: base
title: About
---
Write the About page.
content/posts/2026-02-19-welcome.md
---
layout: post
title: Welcome
date: 2026-02-19
tags: [welcome]
---
This is the first post.
content/tags.njk (tag index)
---
layout: base
title: Tags
permalink: /tags/
---
<h1>Tags</h1>
<ul>
{% for tag in collections.tagList %}
<li><a href="/tags/{{ tag | slug }}/">{{ tag }}</a></li>
{% endfor %}
</ul>
content/feed.njk (RSS)
---
permalink: /feed.xml
eleventyExcludeFromCollections: true
---
{{ collections.posts | rssFeed({
title: site.siteName,
language: "en",
url: site.siteUrl,
subtitle: site.description,
feed_url: site.siteUrl + "/feed.xml",
author: "Polememelop"
}) }}
Tag pages (generated)
Add this file: content/tags/tag-pages.11ty.js
Tree add:
content/
tags/
tag-pages.11ty.js
content/tags/tag-pages.11ty.js:
export default class TagPages {
data() {
return {
pagination: {
data: "collections.tagList",
size: 1,
alias: "tag"
},
layout: "tag",
permalink: (data) => `/tags/${data.tag}/index.html`,
eleventyComputed: {
title: (data) => `Tag: ${data.tag}`
}
};
}
render(data) {
const posts = data.collections.posts.filter((p) => (p.data.tags || []).includes(data.tag));
// expose for tag layout
data.posts = posts;
return "";
}
}
(Notes: v1 keeps this simple; we can refine rendering to pass posts more elegantly if desired.)
---
Local run (any hub)
From a hub repo:
npm ci
npm run dev
Build:
npm run build
Output is _site/.
---
Cloudflare Pages setup (manual v1)
For each hub repo:
- Create a Cloudflare Pages project connected to the GitHub repo
- Build command:
npm run build - Output directory:
_site - Add the custom domain for the hub
---
Automation v1.1: newhub script (GitHub + scaffold)
This makes hub creation repeatable and eliminates “clip files from browser.”
newhub.sh
#!/usr/bin/env bash
set -euo pipefail
# Usage:
# ./newhub.sh howacceptanceworks howacceptanceworks.com "How Acceptance Works" howacceptanceworks
#
# Requires:
# - gh authenticated
# - a hub template repo available (see TEMPLATE)
REPO_NAME="${1:?repo required}"
DOMAIN="${2:?domain required}"
TITLE="${3:?title required}"
KEY="${4:-$REPO_NAME}"
TEMPLATE="YOUR_GH_ORG/hub-template" # <-- change
# 1) Create repo from template and clone
gh repo create "$REPO_NAME" --template "$TEMPLATE" --public --clone
cd "$REPO_NAME"
# 2) Write site.json
cat > site.json <<EOF
{
"constellationKey": "${KEY}",
"siteName": "${TITLE}",
"siteUrl": "https://${DOMAIN}",
"description": "${TITLE} (Polememelop constellation)."
}
EOF
# 3) Commit + push
git add site.json
git commit -m "Configure hub identity"
git push -u origin main
echo "Created hub repo: $REPO_NAME"
echo "Next (v1): create Cloudflare Pages project + attach domain in UI."
Automation v1.2 (future): Cloudflare Pages + DNS via API
We’ll extend newhub to:
- create Pages project
- connect to GitHub
- attach custom domain
- create DNS record
This is intentionally deferred until you’re happy with v1’s content + theme contract.
---
Practical workflow for constellation restructuring
When you want to introduce Polememelop above ULiUA:
- Edit
@polememelop/constellation/constellation.json(add node + re-parent) - Publish
@polememelop/constellationversion bump - Update hub dependencies (batch with one script later)
- Push — Cloudflare rebuilds each hub and nav updates everywhere
---
Sanity checklist (v1)
- [ ] Theme renders nav + breadcrumbs for current hub key
- [ ] A hub builds locally and produces
_site - [ ] Cloudflare Pages deploys
_site - [ ] Custom domain resolves
- [ ] Adding a new node to constellation updates nav everywhere after dependency bump
---
Optional simplifications (if you want fewer moving parts in v1)
If npm package publishing feels like overhead at the start:
- Bundle
constellation.jsoninside the theme repo (skip@polememelop/constellationinitially) - Or, keep
constellation.jsonin each hub (worst for restructuring, but simplest)
Recommended compromise:
- Keep separate
@polememelop/constellationfrom day one (lightweight, pays off immediately).