Skip to content

dame-is/dame.is

Repository files navigation

dame.is

An atmospheric personal website. Every page is a record on the AT Protocol; the site is a view layer over those records.

Stack

  • Vite + React SPA, plain CSS, no state library.
  • AT Protocol as the database. Records live under is.dame.* lexicons on the user's PDS; build-time JSON snapshots come from app.bsky.feed.getAuthorFeed, com.atproto.repo.listRecords, and app.bsky.actor.getProfile.
  • Vercel for hosting. A 6-hour cron pings /api/rebuild, which fires the Vercel deploy hook so new records publish without a manual redeploy.

Lexicons

Schema-only documentation lives under lexicons/:

  • is.dame.now — short status updates ("dame.is hiking").
  • is.dame.page — page-content records keyed by literal page name.
  • is.dame.profile — extended long-form bio at at://{me}/is.dame.profile/self.
  • is.dame.creating.work — portfolio works (art, software, writing, music…).
  • is.dame.resume / is.dame.resume.job / is.dame.resume.education — a backlinked resume model: many resume versions curate shared job/education records and pick which highlight bullets to show. See lexicons/RESUME.md; renders at /resume. Bulk-import from a LinkedIn/resume PDF with scripts/import-resume.mjs.
  • is.dame.annotating — stretch lexicon for book-style margin notes.
  • is.dame.mothing / is.dame.mothing.observation — a mirror of moth observations from iNaturalist, rendered at /mothing. The summary singleton lives at at://{me}/is.dame.mothing/self; one observation record per iNaturalist observation is keyed by its iNat id. No location data is ever mirrored — coordinates, place names, and timezone are stripped in src/lib/inaturalist.js. Sync with scripts/mirror-inaturalist.mjs. Once mirrored, moth observations also appear in the home feed under the mothing verb, ordered by observation date.
  • is.dame.observing / is.dame.observing.observation — the same mirror for every non-moth iNaturalist observation (birds, plants, fungi, butterflies, other insects…), rendered in the home feed under the observing verb. The same sync writes both: it pulls every observation once and splits each by taxonomy — moths (Lepidoptera minus butterflies) become is.dame.mothing.* records, everything else is.dame.observing.* — so one iNaturalist log shows up as mothing if it's a moth and observing otherwise, with nothing dropped or double-counted. Same location-stripping guarantee. The is.dame.observing/self summary doubles as the incremental mirror's freshness marker across all observations.
  • is.dame.guestbook / is.dame.guestbook.entry — a decentralized guestbook, rendered at /guestbook. The book is a singleton on my PDS (at://{me}/is.dame.guestbook/self); each signature is an is.dame.guestbook.entry record on the visitor's own PDS whose subject backlinks to the book. Reads are assembled live from Constellation backlinks hydrated through Slingshot; writes happen in the browser via atproto OAuth. See lexicons/GUESTBOOK.md. Create the book once with scripts/create-guestbook.mjs.
  • is.dame.state — live iPhone-sourced physical + ambient state (heart rate, activity, battery, ambient sound, calories). An append-only log keyed by TID: the latest record is the current "right now" rendered by the atmosphere-bar vitals panel (src/components/VitalsPanel.jsx, read via src/hooks/useDameState.js), and the full series is the history. Each record is immutable, so an is.dame.now status can strong-ref the one captured alongside it via its optional stateRef — coupling a note to the body-state behind it, which renders as a small chip (src/components/VitalsChip.jsx) under the status in the feed and on its record page. Posted straight from an iPhone Apple Shortcut (app-password createSessioncreateRecord, no server), and also charts over time in the /logging dashboard (src/components/StateStats.jsx). See lexicons/STATE.md.
  • is.dame.nav — optional PDS override for the nav-menu (dock sheet) route list. When its enabled flag is set, its items select / reorder / relabel / hide the entries; otherwise the site uses the hardcoded routes in src/lib/navRoutes.js. Edited in the admin "Nav menu" panel.

Plus fm.teal.alpha.feed.play (teal.fm) for the now-playing signal.

Mothing / iNaturalist

/mothing reads moth observations for the iNaturalist user configured in src/config.js (INATURALIST_USER), scoped to Lepidoptera minus butterflies. The page paints from the build-time public/data/mothing.json snapshot and refreshes live from the iNaturalist API in the browser.

The home feed goes further: every iNaturalist observation is mirrored onto the PDS and surfaced there — moths under the mothing verb, everything else (birds, plants, fungi, butterflies, other insects…) under observing. The mirror pulls all observations once and classifies each from its taxon ancestry (isMoth = Lepidoptera, minus butterflies) so the two feed verbs partition the set with nothing dropped or double-counted.

To keep that owned copy on the PDS, run the mirror:

BSKY_IDENTIFIER=dame.is BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
  node scripts/mirror-inaturalist.mjs           # --full to force a complete resync

The mirror is incremental: a cheap freshness check (one small request, over all observations) skips the run entirely when nothing changed; otherwise it re-pulls only observations changed since the last sync (via iNaturalist's updated_since), writes just those to the right collection plus the two refreshed summaries, and deletes records for observations removed upstream (or moved across the moth boundary by a re-identification). --dry-run plans without writing; --full ignores the freshness marker. A Vercel cron runs the same sync every 6 hours (/api/mirror-mothing, see vercel.json), just before each /api/rebuild, so new observations reach the PDS — and the home feed — within hours instead of a day. It uses BSKY_APP_PASSWORD (+ optional BSKY_IDENTIFIER); set CRON_SECRET to lock the endpoint down. The freshness check makes the extra runs cheap: they no-op when nothing changed upstream.

The /mothing page applies the same freshness check in the browser: it paints from the snapshot and only re-pulls the full observation set from iNaturalist when the cheap signature says something actually changed.

Local development

npm install
npm run prefetch          # writes public/data/*.json from your PDS + AppView
npm run dev               # Vite dev server on http://localhost:5173

npm run build runs the prefetch script, then vite build. npm run build:offline skips the prefetch (useful when a snapshot already exists).

Architecture

Concept Lives in
AT Protocol client (Node + browser) src/lib/atproto.js
Build-time data fetcher scripts/prefetch.mjs
Hybrid snapshot/live data hook src/hooks/useLiveFeed.js
Per-route AT URI lookup src/hooks/useAtUri.js
<head> discoverability tags src/components/AtUriHead.jsx
Atmosphere debug overlay src/components/DebugPane.jsx
Day-of-life math src/lib/dayOfLife.js

Deploy hook setup

After deploying, in Vercel:

  1. Settings → Git → Deploy Hooks → create one named pds-rebuild and copy the URL.
  2. Settings → Environment Variables → set DEPLOY_HOOK_URL to that URL.
  3. (Optional) point your publishing flow (Apple Shortcuts, etc.) at the same URL so a curl -X POST after writing a record kicks an instant rebuild.

Credits

Releases

Packages

Used by

Contributors

Languages