Astro garden skeleton: Quartz-compatible URLs, Obsidian markdown, odometer homepage

- glob content loader over the vault's Digital_Garden with verbatim-path ids
- remark plugin: wikilinks, image embeds (wiki + relative md), callouts
- URL parity with live Quartz sitemap verified 102/102 (built set is a
  strict superset: +30 synthetic folder listings)
- KaTeX, lazy mermaid, shiki dual themes, quoted-string draft handling
- copy-assets resolves embeds vault-wide (fixes live-site 404s for
  Blog Scratch/assets images) into flat /assets/
- homepage odometer widget: fetches api.c0smere.net baseline, ticks at
  lifetime average rate, fail-closed hidden on error
- deploy/: nginx try_files config + compose (port 18100) + containerized
  build script for cyrion

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wes
2026-07-15 14:18:59 -04:00
co-authored by Claude Opus 4.8
commit 04c9aa2fb2
15 changed files with 7527 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
---
import { getCollection, render } from 'astro:content';
import Base from '../layouts/Base.astro';
import { slugForId, urlForId } from '../lib/quartz-compat.mjs';
export async function getStaticPaths() {
const entries = await getCollection('garden', (e) => !e.data.draft);
const ids = new Set(entries.map((e) => e.id));
const paths = entries
.filter((e) => e.id !== 'index')
.map((e) => ({
params: { slug: slugForId(e.id) },
props: { entry: e, listing: null },
}));
// synthetic listing pages for folders that have no index.md
const folders = new Set<string>();
for (const id of ids) {
let d = id.includes('/') ? id.slice(0, id.lastIndexOf('/')) : '';
while (d) {
folders.add(d);
d = d.includes('/') ? d.slice(0, d.lastIndexOf('/')) : '';
}
}
for (const folder of folders) {
if (ids.has(`${folder}/index`)) continue;
const children = entries
.filter((e) => {
const dir = e.id.includes('/') ? e.id.slice(0, e.id.lastIndexOf('/')) : '';
return dir === folder && !e.id.endsWith('/index');
})
.map((e) => ({
title: e.data.title ?? e.id.split('/').pop(),
url: urlForId(e.id),
}));
const subfolders = [...folders]
.filter((f) => {
const dir = f.includes('/') ? f.slice(0, f.lastIndexOf('/')) : '';
return dir === folder;
})
.map((f) => ({
title: f.split('/').pop(),
url: '/' + slugForId(f) + '/',
}));
paths.push({
params: { slug: slugForId(folder) + '/index' },
props: {
entry: null,
listing: { name: folder.split('/').pop(), items: [...subfolders, ...children] },
},
});
}
return paths;
}
const { entry, listing } = Astro.props;
const title = entry
? (entry.data.title ?? entry.id.split('/').pop())
: listing.name;
const Content = entry ? (await render(entry)).Content : null;
---
<Base title={title} description={entry?.data.description}>
<h1>{title}</h1>
{Content && <Content />}
{
listing && (
<ul>
{listing.items.map((item) => (
<li>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)
}
</Base>