No description
  • Vue 66.6%
  • TypeScript 21.3%
  • CSS 11%
  • HTML 0.7%
  • JavaScript 0.4%
Find a file
Padreug 074ea23b1b docs: README image-discipline section reflects the assets/ pipeline
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>
2026-05-27 20:54:53 +02:00
src refactor: serve project images through Vite's asset pipeline 2026-05-27 20:54:16 +02:00
.editorconfig initial scaffold: vue 3 + vite 8 + shadcn-vue + tailwind 4 2026-05-27 10:05:39 +02:00
.env.example feat: VITE_DEV_TOOLS env-gate for preview tooling on deployed builds 2026-05-27 11:59:26 +02:00
.gitignore initial scaffold: vue 3 + vite 8 + shadcn-vue + tailwind 4 2026-05-27 10:05:39 +02:00
.mcp.json add shadcn-vue MCP server for Claude Code sessions 2026-05-27 10:44:48 +02:00
.prettierrc.json initial scaffold: vue 3 + vite 8 + shadcn-vue + tailwind 4 2026-05-27 10:05:39 +02:00
components.json chore: remove boilerplate demo content, fix components.json 2026-05-27 11:10:16 +02:00
env.d.ts feat: VITE_DEV_TOOLS env-gate for preview tooling on deployed builds 2026-05-27 11:59:26 +02:00
eslint.config.js initial scaffold: vue 3 + vite 8 + shadcn-vue + tailwind 4 2026-05-27 10:05:39 +02:00
index.html feat: stone-warm OKLCH palette + Fraunces/Inter typography 2026-05-27 11:11:01 +02:00
package.json feat: nostr-tools + ephemeral inquiry submission helper 2026-05-27 11:22:47 +02:00
pnpm-lock.yaml feat: nostr-tools + ephemeral inquiry submission helper 2026-05-27 11:22:47 +02:00
README.md docs: README image-discipline section reflects the assets/ pipeline 2026-05-27 20:54:53 +02:00
tsconfig.app.json fix: align shadcn-vue components with @lucide/vue + drop redundant baseUrl 2026-05-27 11:20:07 +02:00
tsconfig.json feat: install shadcn-vue components for the portfolio + form surface 2026-05-27 11:12:38 +02:00
tsconfig.node.json initial scaffold: vue 3 + vite 8 + shadcn-vue + tailwind 4 2026-05-27 10:05:39 +02:00
vite.config.ts initial scaffold: vue 3 + vite 8 + shadcn-vue + tailwind 4 2026-05-27 10:05:39 +02:00

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

  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, h1h3, 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.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:
    <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:

git remote add boilerplate forgejo@git.atitlan.io:aiolabs/boilerplate-website.git
git fetch boilerplate
git merge boilerplate/main