Skip to content

Commit bb47549

Browse files
gemivnetclaude
andcommitted
rv12is section: nav tab + reusable post sections in build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 23fa9fc commit bb47549

11 files changed

Lines changed: 140 additions & 96 deletions

File tree

content/rv12is/build/empennage/.gitkeep

Whitespace-only changes.

content/rv12is/build/misc/.gitkeep

Whitespace-only changes.

content/rv12is/build/wings/.gitkeep

Whitespace-only changes.

copy.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ nav:
1818
tabs:
1919
- { id: musings, label: "musings", href: "/musings" }
2020
- { id: media, label: "media", href: "/media" }
21+
- { id: rv12is, label: "RV-12iS", href: "/rv12is" }
2122

2223
# ── homepage ────────────────────────────────────────────────
2324
home:
@@ -65,9 +66,11 @@ footer:
6566

6667
# ── musings index / category / tag ──────────────────────────
6768
musings:
69+
heading: "Musings."
6870
index:
6971
breadcrumb: "~/musings"
7072
title: "musings"
73+
seo_description: "All musings by {author}. {count} posts on travel, code, aviation, home projects."
7174
sidebar_categories: "categories"
7275
sidebar_tags: "tags"
7376
category:
@@ -89,6 +92,28 @@ musings:
8992
next_label: "next →"
9093
oldest_note: "— (this is the oldest)"
9194
newest_note: "— (this is the newest)"
95+
back_label: "↑ back to all musings"
96+
97+
# ── rv-12is build log ───────────────────────────────────────
98+
rv12is:
99+
heading: "RV-12iS."
100+
index:
101+
breadcrumb: "~/rv12is"
102+
title: "rv12is"
103+
seo_description: "Van's RV-12iS build log — and notes once it's flying."
104+
category:
105+
breadcrumb_prefix: "~/rv12is/"
106+
no_direct_posts: "no entries directly here — see subfolders"
107+
tag:
108+
breadcrumb_prefix: "~/rv12is/tag/"
109+
intro: "Cross-cutting tag. Entries here can live in different folders."
110+
post:
111+
crumbs_root: "rv12is"
112+
prev_label: "← previous"
113+
next_label: "next →"
114+
oldest_note: "— (this is the oldest)"
115+
newest_note: "— (this is the newest)"
116+
back_label: "↑ back to the build log"
92117

93118
# ── media ───────────────────────────────────────────────────
94119
media:

scripts/build.mjs

Lines changed: 89 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,16 @@ function imgFigure(href, alt, title, kls) {
234234

235235
// ── content loaders ───────────────────────────────────────────
236236

237-
async function loadMusings() {
238-
const indices = await fg('musings/**/index.md', { cwd: SRC('content') });
237+
// Post sections: each is a musings-shaped tree of markdown posts with its own
238+
// nav tab, index, category pages, tags, and year archives. Home page, feeds,
239+
// and gallery synthesis remain musings-only.
240+
const SECTIONS = {
241+
musings: { root: 'musings', base: '/musings', active: 'musings' },
242+
rv12is: { root: 'rv12is', base: '/rv12is', active: 'rv12is' },
243+
};
244+
245+
async function loadPosts(root) {
246+
const indices = await fg(root + '/**/index.md', { cwd: SRC('content') });
239247
const posts = [];
240248
for (const rel of indices) {
241249
const full = path.join(SRC('content'), rel);
@@ -246,7 +254,7 @@ async function loadMusings() {
246254
const fm = parsed.data;
247255
const folder = path.dirname(rel); // musings/travel/japan-tips
248256
const segs = folder.split('/');
249-
if (segs[0] !== 'musings') { fail(`Bad musings path: ${rel}`); continue; }
257+
if (segs[0] !== root) { fail(`Bad ${root} path: ${rel}`); continue; }
250258
const slug = segs[segs.length - 1];
251259
const categorySegs = segs.slice(1, -1); // [travel]
252260
if (!fm.title) { fail(`${rel}: frontmatter is missing required field 'title'`); continue; }
@@ -331,7 +339,7 @@ async function loadMusings() {
331339
url,
332340
folder,
333341
category: categorySegs,
334-
categoryPath: categorySegs.length ? '/musings/' + categorySegs.join('/') : '/musings',
342+
categoryPath: categorySegs.length ? '/' + root + '/' + categorySegs.join('/') : '/' + root,
335343
title: fm.title,
336344
subtitle: fm.subtitle || '',
337345
date: new Date(fm.date),
@@ -549,13 +557,13 @@ async function renderHome(musings, mediaNodes) {
549557

550558
// Build a folder/post tree for the sidebar.
551559
// Returns an array of nodes: { type:'folder'|'post', name, url, path, children, post? }
552-
function buildMusingsTree(musings) {
553-
const root = { type: 'folder', name: 'musings', url: '/musings', path: [], children: [] };
560+
function buildSectionTree(posts, section) {
561+
const root = { type: 'folder', name: section.root, url: section.base, path: [], children: [] };
554562
const folders = new Map(); // key -> node
555563
folders.set('', root);
556564

557565
// Ensure all folder ancestors exist for each post.
558-
for (const m of musings) {
566+
for (const m of posts) {
559567
let parentKey = '';
560568
let parentNode = root;
561569
for (let d = 0; d < m.category.length; d++) {
@@ -565,7 +573,7 @@ function buildMusingsTree(musings) {
565573
const node = {
566574
type: 'folder',
567575
name: segs[segs.length - 1],
568-
url: '/musings/' + key,
576+
url: section.base + '/' + key,
569577
path: segs,
570578
children: [],
571579
};
@@ -689,7 +697,7 @@ function synthesizeFromMusings(musings, existingNodes) {
689697
// Uses nested <ul> so long names wrap naturally and don't overflow.
690698
function renderTreeHtml(tree, activeUrl, activeCategory) {
691699
function walk(node, depth) {
692-
const name = node.type === 'folder' ? (depth === 0 ? 'musings/' : node.name + '/') : node.name;
700+
const name = node.type === 'folder' ? node.name + '/' : node.name;
693701
const isActive = node.url === activeUrl;
694702
const inActive = !isActive && activeCategory && node.type === 'folder' && depth > 0 && activeCategory.startsWith(node.path.join('/'));
695703
const cls = ['tree-item'];
@@ -706,34 +714,37 @@ function renderTreeHtml(tree, activeUrl, activeCategory) {
706714
return '<ul class="tree-list">' + walk(tree, 0) + '</ul>';
707715
}
708716

709-
// ── render: musings ───────────────────────────────────────────
717+
// ── render: post sections (musings, rv12is, …) ────────────────
710718

711-
async function renderMusings(musings) {
712-
// Folder tree, used by all musings pages (index + post + category).
713-
const tree = buildMusingsTree(musings);
714-
ALL_POST_URLS = musings.map(m => m.url);
719+
async function renderPostSection(posts, section) {
720+
const sc = COPY[section.root];
721+
// Folder tree, used by all section pages (index + post + category).
722+
const tree = buildSectionTree(posts, section);
723+
allRoutes.add(section.base);
715724

716725
// Index page.
717726
const indexHtml = await renderPage('musings-index', {
718727
page: {
719-
title: 'musings — ' + SITE.title,
720-
description: `All musings by ${SITE.author}. ${musings.length} posts on travel, code, aviation, home projects.`,
721-
url: '/musings',
722-
bodyClass: pageHelpers.bodyClass('musings'),
728+
title: section.root + ' — ' + SITE.title,
729+
description: (sc.index.seo_description || '')
730+
.replace('{count}', posts.length).replace('{author}', SITE.author),
731+
url: section.base,
732+
bodyClass: pageHelpers.bodyClass(section.active),
723733
type: 'website',
724734
},
725-
active: 'musings',
726-
posts: musings,
727-
treeHtml: renderTreeHtml(tree, '/musings', ''),
735+
active: section.active,
736+
section, sectionCopy: sc,
737+
posts,
738+
treeHtml: renderTreeHtml(tree, section.base, ''),
728739
});
729-
collectLinks('/musings', indexHtml);
730-
await writeFile('musings/index.html', indexHtml);
740+
collectLinks(section.base, indexHtml);
741+
await writeFile(path.join(section.root, 'index.html'), indexHtml);
731742

732743
// Helper: pick up to N related posts that share the most tags with a given post.
733744
function relatedFor(m, n = 3) {
734745
const mTags = new Set(m.tags || []);
735746
if (mTags.size === 0) return [];
736-
return musings
747+
return posts
737748
.filter(o => o.url !== m.url)
738749
.map(o => ({ post: o, score: (o.tags || []).filter(t => mTags.has(t)).length }))
739750
.filter(x => x.score > 0)
@@ -743,21 +754,22 @@ async function renderMusings(musings) {
743754
}
744755

745756
// Per-post — uses the same tree, marking the current item active.
746-
for (let i = 0; i < musings.length; i++) {
747-
const m = musings[i];
748-
const prev = musings[i + 1] || null;
749-
const next = musings[i - 1] || null;
757+
for (let i = 0; i < posts.length; i++) {
758+
const m = posts[i];
759+
const prev = posts[i + 1] || null;
760+
const next = posts[i - 1] || null;
750761
const html = await renderPage('musing', {
751762
page: {
752763
title: `${m.title}${SITE.title}`,
753764
description: m.seo.description || truncate(m.preview, 158),
754765
keywords: m.seo.keywords || m.tags,
755766
url: m.url,
756-
bodyClass: pageHelpers.bodyClass('musings'),
767+
bodyClass: pageHelpers.bodyClass(section.active),
757768
type: 'article',
758769
ogImage: m.featured,
759770
},
760-
active: 'musings',
771+
active: section.active,
772+
section, sectionCopy: sc,
761773
post: m,
762774
prev,
763775
next,
@@ -786,7 +798,7 @@ async function renderMusings(musings) {
786798

787799
// Categories.
788800
const cats = new Map(); // path -> { segs, posts, children }
789-
for (const m of musings) {
801+
for (const m of posts) {
790802
for (let d = 1; d <= m.category.length; d++) {
791803
const segs = m.category.slice(0, d);
792804
const key = segs.join('/');
@@ -803,24 +815,25 @@ async function renderMusings(musings) {
803815
}
804816

805817
for (const [key, cat] of cats) {
806-
const url = '/musings/' + key;
807-
const allDescendants = musings.filter(m => m.category.slice(0, cat.segs.length).join('/') === key);
818+
const url = section.base + '/' + key;
819+
const allDescendants = posts.filter(m => m.category.slice(0, cat.segs.length).join('/') === key);
808820
const directPosts = cat.posts.slice().sort((a, b) => b.date - a.date);
809821
const children = [...cat.childKeys].map(k => ({
810822
key: k,
811823
label: k.split('/').pop(),
812-
url: '/musings/' + k,
813-
count: musings.filter(m => m.category.slice(0, k.split('/').length).join('/') === k).length,
824+
url: section.base + '/' + k,
825+
count: posts.filter(m => m.category.slice(0, k.split('/').length).join('/') === k).length,
814826
}));
815827
const html = await renderPage('musings-category', {
816828
page: {
817-
title: `${key}musings${SITE.title}`,
829+
title: `${key}${section.root}${SITE.title}`,
818830
description: `Posts about ${cat.segs.join(' / ')} by ${SITE.author}.`,
819831
url,
820-
bodyClass: pageHelpers.bodyClass('musings'),
832+
bodyClass: pageHelpers.bodyClass(section.active),
821833
type: 'website',
822834
},
823-
active: 'musings',
835+
active: section.active,
836+
section, sectionCopy: sc,
824837
categoryKey: key,
825838
categorySegs: cat.segs,
826839
posts: directPosts,
@@ -830,82 +843,85 @@ async function renderMusings(musings) {
830843
randomPick: allDescendants[Math.floor((allDescendants.length || 1) / 2) % Math.max(allDescendants.length, 1)],
831844
});
832845
collectLinks(url, html);
833-
await writeFile(path.join('musings', key, 'index.html'), html);
846+
await writeFile(path.join(section.root, key, 'index.html'), html);
834847
allRoutes.add(url);
835848
}
836849

837850
// Tags.
838851
const tags = new Map();
839-
for (const m of musings) for (const t of m.tags) {
852+
for (const m of posts) for (const t of m.tags) {
840853
if (!tags.has(t)) tags.set(t, []);
841854
tags.get(t).push(m);
842855
}
843856

844-
// /musings/tags — alphabetical list of all tags + counts.
857+
// /<section>/tags — alphabetical list of all tags + counts.
845858
const tagList = [...tags.entries()]
846-
.map(([name, posts]) => ({ name, slug: slugify(name), count: posts.length }))
859+
.map(([name, tagged]) => ({ name, slug: slugify(name), count: tagged.length }))
847860
.sort((a, b) => a.name.localeCompare(b.name));
848861
const tagsIndexHtml = await renderPage('musings-tags-index', {
849862
page: {
850863
title: `tags — ${SITE.title}`,
851-
description: `All tags used across ${musings.length} musings.`,
852-
url: '/musings/tags',
853-
bodyClass: pageHelpers.bodyClass('musings'),
864+
description: `All tags used across ${posts.length} posts.`,
865+
url: `${section.base}/tags`,
866+
bodyClass: pageHelpers.bodyClass(section.active),
854867
type: 'website',
855868
},
856-
active: 'musings',
869+
active: section.active,
870+
section, sectionCopy: sc,
857871
tagList,
858872
});
859-
collectLinks('/musings/tags', tagsIndexHtml);
860-
await writeFile('musings/tags/index.html', tagsIndexHtml);
861-
allRoutes.add('/musings/tags');
873+
collectLinks(`${section.base}/tags`, tagsIndexHtml);
874+
await writeFile(path.join(section.root, 'tags', 'index.html'), tagsIndexHtml);
875+
allRoutes.add(`${section.base}/tags`);
862876

863-
// /musings/YYYY — year archives.
877+
// /<section>/YYYY — year archives.
864878
const byYear = new Map();
865-
for (const m of musings) {
879+
for (const m of posts) {
866880
const y = String(m.date.getUTCFullYear());
867881
if (!byYear.has(y)) byYear.set(y, []);
868882
byYear.get(y).push(m);
869883
}
870884
const allYears = [...byYear.keys()].sort().reverse();
871885
for (const year of allYears) {
872-
const posts = byYear.get(year);
886+
const yearPosts = byYear.get(year);
873887
const otherYears = allYears.filter(y => y !== year);
874888
const yearHtml = await renderPage('musings-year', {
875889
page: {
876-
title: `${year}musings${SITE.title}`,
890+
title: `${year}${section.root}${SITE.title}`,
877891
description: `Posts from ${year} by ${SITE.author}.`,
878-
url: `/musings/${year}`,
879-
bodyClass: pageHelpers.bodyClass('musings'),
892+
url: `${section.base}/${year}`,
893+
bodyClass: pageHelpers.bodyClass(section.active),
880894
type: 'website',
881895
},
882-
active: 'musings',
896+
active: section.active,
897+
section, sectionCopy: sc,
883898
year,
884-
posts,
899+
posts: yearPosts,
885900
otherYears,
886901
});
887-
collectLinks(`/musings/${year}`, yearHtml);
888-
await writeFile(`musings/${year}/index.html`, yearHtml);
889-
allRoutes.add(`/musings/${year}`);
902+
collectLinks(`${section.base}/${year}`, yearHtml);
903+
await writeFile(path.join(section.root, year, 'index.html'), yearHtml);
904+
allRoutes.add(`${section.base}/${year}`);
890905
}
891906

892-
for (const [tag, posts] of tags) {
893-
const url = `/musings/tag/${slugify(tag)}`;
907+
for (const [tag, tagged] of tags) {
908+
const url = `${section.base}/tag/${slugify(tag)}`;
894909
const html = await renderPage('musings-tag', {
895910
page: {
896911
title: `#${tag}${SITE.title}`,
897912
description: `Posts tagged "${tag}" by ${SITE.author}.`,
898-
keywords: [tag, ...posts.flatMap(p => p.tags)].slice(0, 10),
913+
keywords: [tag, ...tagged.flatMap(p => p.tags)].slice(0, 10),
899914
url,
900-
bodyClass: pageHelpers.bodyClass('musings'),
915+
bodyClass: pageHelpers.bodyClass(section.active),
901916
type: 'website',
902917
},
903-
active: 'musings',
918+
active: section.active,
919+
section, sectionCopy: sc,
904920
tag,
905-
posts,
921+
posts: tagged,
906922
});
907923
collectLinks(url, html);
908-
await writeFile(`musings/tag/${slugify(tag)}/index.html`, html);
924+
await writeFile(path.join(section.root, 'tag', slugify(tag), 'index.html'), html);
909925
allRoutes.add(url);
910926
}
911927
}
@@ -1227,8 +1243,9 @@ async function main() {
12271243
await ensureDir(OUT);
12281244

12291245
await copyDesign();
1230-
const musings = await loadMusings();
1231-
log(`${musings.length} musings`);
1246+
const musings = await loadPosts('musings');
1247+
const rvPosts = await loadPosts('rv12is');
1248+
log(`${musings.length} musings, ${rvPosts.length} rv12is posts`);
12321249
const { nodes: realMediaNodes } = await loadMedia();
12331250
const synthNodes = synthesizeFromMusings(musings, realMediaNodes);
12341251
const mediaNodes = realMediaNodes.concat(synthNodes);
@@ -1267,7 +1284,9 @@ async function main() {
12671284
log(`${realMediaNodes.length} real + ${synthNodes.length} synthesized gallery nodes`);
12681285

12691286
await renderHome(musings, mediaNodes);
1270-
await renderMusings(musings);
1287+
ALL_POST_URLS = musings.map(m => m.url);
1288+
await renderPostSection(musings, SECTIONS.musings);
1289+
await renderPostSection(rvPosts, SECTIONS.rv12is);
12711290
await renderMedia(mediaNodes);
12721291
await renderChangelog();
12731292
await renderLicense();

0 commit comments

Comments
 (0)