The frame rendered at 300x150 -- the HTML default for an unstyled iframe --
instead of the applet's 770x540. Astro's <style> is scoped: it compiles
.cn-frame to `.cn-frame[data-astro-cid-kr4ipkmt]` and stamps that attribute on
elements in the template. The iframe was built with document.createElement()
in the script, carried no such attribute, and therefore matched none of its
own rules.
So the iframe lives in the template now and the click only sets its src. An
iframe with no src loads nothing, so the payload still waits for the click,
which was the only reason it was being created dynamically.
It is hidden with a class rather than the `hidden` attribute, because the
`display: block` in the sizing rule outranks the user agent's
`[hidden] { display: none }` and it would have shown regardless.
Verified by rendering the staged branch locally against a stub manifest --
the page only builds its Play button when an export is present, so the
previous change shipped without its markup ever being looked at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4wdbGSVzvUDHBWRjSVxfr
garden-astro
Astro rebuild of the digital garden at garden.c0smere.net, replacing
Quartz v4. Content stays in the Obsidian vault (Digital_Garden/ folder);
this repo is only the rendering pipeline.
URL compatibility
Built to be a drop-in replacement — every URL on the live Quartz site resolves identically here (verified against the live sitemap, 102/102):
- case preserved, spaces become
-, hyphens/em-dashes kept - leaf pages:
/Folder/Note-Name(build.format: 'file'+ nginxtry_files $uri $uri.html) - folder indexes:
/Folder/(fromFolder/index.md, or a synthetic listing page when no index exists)
Obsidian compatibility (src/lib/remark-obsidian.mjs)
[[wikilinks]]with aliases/anchors, resolved vault-path-first then by basename; unresolved links degrade to plain text![[image embeds]]and relativeboth rewrite to flat/assets/<name>URLs;scripts/copy-assets.mjsresolves the files (Digital_Garden first, then vault-wide) and copies them in at build time> [!type]callouts, KaTeX math, mermaid (lazy-loaded client-side only on pages that use it), shiki dual light/dark themesdraft:frontmatter excluded, including Quartz-style quoted"true"
Build
GARDEN_CONTENT=/path/to/vault/Digital_Garden npm run build
Defaults to the kotov vault copy. On cyrion use deploy/build.sh
(containerized node:24, vault mounted read-only), then
docker compose -f deploy/docker-compose.yml up -d serves dist/ on
port 18100, proxied as garden.c0smere.net (NPM public + Caddy internal).
marimo notebooks (/notebooks)
Notebooks from the marimo server (/home/nox/docker/marimo/notebooks/) can
be published into the garden. A notebook publishes only if it opts in, by
carrying this HTML comment in one of its markdown cells — invisible in the
rendered page, greppable in the .py source:
<!-- garden:publish
title: Dream of Spotification
description: A decade of Spotify history, worked two ways.
order: 30
-->
Only the garden:publish line is required. Optional keys: title,
description, order (sort weight, default 100), slug (URL override,
default derived from the filename), timeout (export seconds, default 600).
Delete the marker to unpublish — the next export run removes the page.
This is deliberately default-deny: garden.c0smere.net is public and the
export bakes each notebook's executed output into the page, not just its
code. deploy/export-notebooks.py also carries a NEVER_PUBLISH list
(genome_*, coursework) as a second latch, so a marker pasted into one of
those refuses loudly instead of publishing.
Pipeline:
deploy/export-notebooks.py(own timer,garden-notebooks-export.timer, every 30 min; own lock) scans for markers and runs each opted-in notebook in a one-shotmarimo:latestcontainer — same image and.envas the live server, so it hits the real databases. Output lands innotebooks-export/<slug>.htmlplus anindex.json.- Cached on notebook content: a run after no edits does no work.
- Per-notebook timeout, and a failure keeps the previous export and continues. A broken notebook can't take the garden down.
- It runs against a throwaway copy of the notebook dir, so notebooks that write scratch files don't dirty the notebooks git repo.
scripts/copy-notebooks.mjsstages those intopublic/nb/<slug>.html.- Astro reads
index.json(src/lib/notebooks.mjs) to build/notebooksand/notebooks/<slug>, which wrap the raw export in an iframe with the garden chrome, plus the sidebar section.
The raw export is at /nb/<slug>.html, deliberately not under
/notebooks — with build.format: 'file' the page and the raw file would
collide on the same URL.
URL shapes here are worth knowing, since build.format: 'file' makes the
listing a file (dist/notebooks.html) that sits beside a directory of
detail pages (dist/notebooks/<slug>.html). Verified against the running
nginx: /notebooks and /notebooks/<slug> resolve through the generic
try_files, but /notebooks/ does not — a bare directory has no
index.html. deploy/nginx.conf carries an explicit location = /notebooks/
so the trailing-slash form works like every other folder URL on the site.
Both notebooks-export/ and public/nb/ are gitignored: tracking them would
make every export dirty the tree, which flips auto-build.sh's change
signature and breaks the git pull at the top of each build tick. The
signature hashes notebooks-export/ separately instead.
Force a full re-export with deploy/export-notebooks.py --force.
Layout extras
Every page carries a left sidebar tree of the whole garden (folders
collapsible, current page highlighted; flows after the footer on narrow
screens) and the bandwidth odometer in the footer, fed by
https://api.c0smere.net/bandwidth/odometer. The widget animates at the
lifetime average rate from a fetched baseline — it never reflects live
throughput, and hides itself if the API is down.