Updates the path (public/images/ → src/assets/projects/) and adds a sentence describing why the move was made: content-hashed filenames let the deploy's immutable cache header stay correct across image swaps without manual cache busts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.8 KiB
Earth Walker Design
Studio site for an interior designer. Vue 3 + Vite + shadcn-vue + Tailwind 4. Anonymous-by-default contact: inquiry submissions are encrypted in the visitor's browser and delivered to the designer's Nostr inbox.
Routes
| Path | Purpose |
|---|---|
/ |
Hero, studio voice, two project teasers, inquiry CTA |
/portfolio |
Two-up project grid (Boulder, Asheville) |
/portfolio/boulder |
Boulder project detail — editorial image scroll + lightbox |
/portfolio/asheville |
Asheville project detail |
/contact |
Inquiry form (Nostr-delivered) |
Local dev
pnpm install
cp .env.example .env # fill in VITE_OWNER_NPUB
pnpm dev # http://localhost:5173
pnpm build # type-check + production build to dist/
pnpm preview # serve dist/
pnpm lint
pnpm format
The contact form requires VITE_OWNER_NPUB to be set — without it,
submitInquiry throws so visitors see an explicit error rather
than silently dropping submissions. Generate one with
nak key generate or any Nostr client.
Env vars
| Var | Required | Default | Notes |
|---|---|---|---|
VITE_OWNER_NPUB |
yes | — | The designer's Nostr public key (npub1…). Inquiries are NIP-17 gift-wrapped to this key. |
VITE_NOSTR_RELAYS |
no | wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band |
Comma-separated wss:// list. Submission succeeds if any one relay accepts. |
Both are inlined at build time by Vite. Rotating either requires a rebuild + redeploy.
How the contact form delivers inquiries
- Visitor fills the form (name optional, contact method + value,
message). Validation runs client-side via
vee-validate+zod. submitInquiry()(src/features/nostr/submitInquiry.ts):- generates a fresh ephemeral secp256k1 keypair
- decodes
VITE_OWNER_NPUBto hex - calls
nip17.wrapEvent()to produce a kind:1059 gift-wrap with NIP-44 v2 encryption inside (the visitor's identity is the throwaway key, not anything they entered) - publishes via
SimplePoolto the configured relays in parallel
- The designer receives the inquiry as a DM in any NIP-17 capable Nostr client (Damus, Amethyst, 0xchat, etc.) signed in with the matching nsec.
No server in between stores the message. There is no inbox to leak.
Theming
Light + dark stone-warm palettes live in src/style.css as OKLCH
CSS variables. Toggle persists to localStorage under
ewd:theme (see src/composables/useTheme.ts). Typography pairs
Fraunces (display, h1–h3, brand wordmark) with Inter (UI/body) via
Bunny Fonts, declared in index.html and bound through Tailwind 4
@theme directives.
To recolor: edit the OKLCH triples in src/style.css under :root
and .dark. Keep all shadcn-vue token names intact.
Image discipline
All project images live under src/assets/projects/<slug>/ and flow
through Vite's asset pipeline: src/data/projects.ts resolves each
file via import.meta.glob, so every image lands in dist/assets/
with a content-hashed filename (08-abc123.jpg). Any swap changes the
hash, which busts the deploy's cache-control: immutable header
automatically — no manual cache-clear, no version query strings.
- Filenames are sequence-numbered (
01.jpg,02.jpg, …) — never leak addresses, neighborhood names, or owner identities in the filename. - EXIF / GPS / timestamp / maker metadata is stripped before commit.
Re-run on new assets:
nix-shell -p imagemagick --run \ 'for f in *.jpg; do magick "$f" -auto-orient -strip -resize "2400x2400>" \ -interlace Plane -sampling-factor 4:2:0 -quality 82 "${f}.tmp" \ && mv "${f}.tmp" "$f"; done' - Alt text in
src/data/projects.tsdescribes the room/feature, not the location.
Adding a project
- Drop sequence-numbered images into
src/assets/projects/<slug>/. - Add a new
Projectentry insrc/data/projects.ts(typed) withslug,name,eyebrow,intro,cover,coverAlt, and animages[]array. Each image takes afeaturetag (hero|wide|narrow|paired) that drives the layout slot inProjectDetail.vue's editorial scroll. - Add a 4-line view at
src/views/projects/<Slug>View.vue:<script setup lang="ts"> import ProjectDetail from '@/components/projects/ProjectDetail.vue' import { mySlug } from '@/data/projects' </script> <template><ProjectDetail :project="mySlug" /></template> - Register the route in
src/router/index.ts. - The
PortfolioViewalready iteratesprojects[], so the new project's card appears automatically.
Stack
Tracking the upstream boilerplate (aiolabs/boilerplate-website).
To pull dependency refreshes:
git remote add boilerplate forgejo@git.atitlan.io:aiolabs/boilerplate-website.git
git fetch boilerplate
git merge boilerplate/main