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>
126 lines
4.8 KiB
Markdown
126 lines
4.8 KiB
Markdown
# 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
|
||
|
||
```sh
|
||
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`](https://github.com/fiatjaf/nak) 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
|
||
|
||
1. Visitor fills the form (name optional, contact method + value,
|
||
message). Validation runs client-side via `vee-validate` + `zod`.
|
||
2. `submitInquiry()` (`src/features/nostr/submitInquiry.ts`):
|
||
- generates a fresh ephemeral secp256k1 keypair
|
||
- decodes `VITE_OWNER_NPUB` to 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 `SimplePool` to the configured relays in parallel
|
||
3. 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:
|
||
```sh
|
||
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.ts` describes the room/feature, not
|
||
the location.
|
||
|
||
## Adding a project
|
||
|
||
1. Drop sequence-numbered images into `src/assets/projects/<slug>/`.
|
||
2. Add a new `Project` entry in `src/data/projects.ts` (typed) with
|
||
`slug`, `name`, `eyebrow`, `intro`, `cover`, `coverAlt`, and an
|
||
`images[]` array. Each image takes a `feature` tag
|
||
(`hero` | `wide` | `narrow` | `paired`) that drives the layout
|
||
slot in `ProjectDetail.vue`'s editorial scroll.
|
||
3. Add a 4-line view at `src/views/projects/<Slug>View.vue`:
|
||
```vue
|
||
<script setup lang="ts">
|
||
import ProjectDetail from '@/components/projects/ProjectDetail.vue'
|
||
import { mySlug } from '@/data/projects'
|
||
</script>
|
||
<template><ProjectDetail :project="mySlug" /></template>
|
||
```
|
||
4. Register the route in `src/router/index.ts`.
|
||
5. The `PortfolioView` already iterates `projects[]`, so the new
|
||
project's card appears automatically.
|
||
|
||
## Stack
|
||
|
||
Tracking the upstream boilerplate (`aiolabs/boilerplate-website`).
|
||
To pull dependency refreshes:
|
||
|
||
```sh
|
||
git remote add boilerplate forgejo@git.atitlan.io:aiolabs/boilerplate-website.git
|
||
git fetch boilerplate
|
||
git merge boilerplate/main
|
||
```
|