* Introduced wedding-themed CSS variables for styling. * Created layout component for the wedding site with navigation and countdown. * Added RSVP and photo upload pages with form handling. * Implemented QR code page for wedding site access. * Configured TypeScript and Vite for the project. * Added robots.txt for search engine crawling.
177 lines
12 KiB
Markdown
177 lines
12 KiB
Markdown
# Plan: Zoé & Shaldon Wedding Site
|
||
|
||
## Goal
|
||
QR-code-accessible site for save-the-date, RSVP, and guest photo uploads. Wedding date: **7 November 2026**.
|
||
|
||
## Stack
|
||
- **Frontend**: SvelteKit, `adapter-static`, prerendered. No SSR needed.
|
||
- **UI/theming**: Tailwind CSS + [Skeleton](https://skeleton.dev) (Svelte), custom theme for the blue/yellow palette — Skeleton handles light/dark switching itself, no hand-rolled CSS variables needed.
|
||
- **Backend**: Go, single binary, `embed.FS` serves the built static site + JSON API on one port.
|
||
- **Storage**: SQLite via `modernc.org/sqlite` (pure Go, no cgo — keeps the Docker build static/`CGO_ENABLED=0`) for RSVP/upload metadata. Photos on disk.
|
||
- **Email**: Go `net/smtp` (STARTTLS) to notify the couple on new RSVP / photo upload.
|
||
|
||
## Repo layout
|
||
```
|
||
zoe-shaldon/
|
||
├── ui/ SvelteKit frontend
|
||
│ ├── tailwind.config.ts
|
||
│ ├── assets/photos/ curated couple photos (checked in), source for the background slideshow
|
||
│ └── src/
|
||
│ ├── app.css Tailwind + Skeleton custom theme (wedding-theme)
|
||
│ ├── lib/components/PhotoBackground.svelte ambient slide-in/out photo background
|
||
│ └── routes/
|
||
│ ├── +page.svelte landing: hero, countdown, photo background, nav
|
||
│ ├── rsvp/+page.svelte RSVP form
|
||
│ └── photos/+page.svelte upload form (gated) + status message before open date
|
||
├── server/ Go backend
|
||
│ ├── main.go
|
||
│ ├── internal/api/ HTTP handlers
|
||
│ ├── internal/store/ SQLite access
|
||
│ ├── internal/mail/ SMTP sending
|
||
│ └── web/ embedded static build (go:embed)
|
||
├── data/ runtime data, gitignored
|
||
│ ├── rsvp.db
|
||
│ └── photos/<upload-id>/<file>
|
||
├── concept/ source assets (save-the-date image)
|
||
├── config.example.yaml template, checked in
|
||
├── config.yaml real config, gitignored
|
||
├── Dockerfile
|
||
└── docker-compose.yml
|
||
```
|
||
|
||
## Data model (SQLite)
|
||
```
|
||
rsvps(id, message, created_at)
|
||
rsvp_guests(id, rsvp_id FK, name)
|
||
photo_uploads(id, name, email, phone, message, created_at)
|
||
photo_files(id, upload_id FK, filename, path, url, created_at)
|
||
```
|
||
RSVP is a party: one or more guest names attached to a single RSVP + optional shared message — no email/phone captured for RSVP (contact for RSVP questions is the phone number on the invite instead). Photo upload keeps its own name/email/phone/message, unchanged.
|
||
|
||
## API
|
||
| Method | Path | Body | Notes |
|
||
|---|---|---|---|
|
||
| POST | `/api/rsvp` | `{names: string[], message?}` | at least one non-blank name required; insert party + guests, email couple |
|
||
| GET | `/api/config` | — | `{weddingDate, ceremonyTime, rsvpByDate, venueName, venueAddress, rsvpPhone}` (all RFC3339 where dates) — single source of truth for countdowns, gate, and displayed details |
|
||
| POST | `/api/photos/upload` | multipart: `name, message?, email?, phone?, files[]` | rejected with 403 if `now < weddingDate`, **enforced server-side** regardless of client clock |
|
||
| GET | `/healthz` | — | liveness |
|
||
|
||
## Countdowns
|
||
- Global: a compact countdown to `weddingDate` sits below the nav bar on every page (`+layout.svelte`). Once passed, it reads "Already married! 🎉" instead of digits.
|
||
- Landing page: hero shows the day/date (e.g. "Saturday, 7 November 2026"), `ceremonyTime` ("16:00 for 16:30"), and venue name/address — all from `GET /api/config`. RSVP button is hidden once the guest has RSVP'd (tracked client-side via `localStorage`, see below); replaced with a thank-you note.
|
||
- RSVP page: its own live countdown to `rsvpByDate` ("RSVP by <date>"). Once `rsvpByDate` passes, the countdown and form are replaced by an "RSVP missed" notice pointing to `rsvpPhone`.
|
||
- Photos page: reuses `weddingDate` — before that time shows "uploads open on the wedding day" instead of the upload form (no separate countdown here, the global one covers it).
|
||
- One date per purpose, no duplicated "opens at" settings to keep in sync.
|
||
|
||
## RSVP-submitted tracking
|
||
No accounts/auth — `localStorage` flag (`zoe-shaldon-rsvp-submitted`) set after a successful `POST /api/rsvp`, read by the landing page to decide whether to show the RSVP button or a confirmation. Per-browser only; a guest RSVPing from a different device won't see the flag, which is an acceptable prototype limitation.
|
||
|
||
## Photo background (landing page)
|
||
Curated couple photos already checked in at `ui/assets/photos` (33 images) — separate from `data/photos/` (runtime, guest-uploaded, gitignored). These are the source for an ambient background behind the hero content.
|
||
|
||
- `PhotoBackground.svelte`: full-bleed layer behind the hero text, cycling through a shuffled list of the photos on an interval (~6s). Each photo slides in from one edge and slides out the opposite edge (CSS `transform: translateX` + `opacity` transition, one photo at a time, next one queued behind).
|
||
- Photo list built at compile time via Vite's `import.meta.glob('/assets/photos/*.{jpg,jpeg,png}', { eager: true })` — drop a file in the folder, it's picked up on next build, no code change.
|
||
- A semi-transparent overlay in the theme's `primary` blue sits between the photos and the text so the hero copy (white/yellow) stays legible over any photo.
|
||
- Respect `prefers-reduced-motion: reduce` — fall back to a plain crossfade (opacity only, no slide) or a static single image.
|
||
- **`IMG_E8488.HEIC` in `ui/assets/photos` needs converting to JPG before build** — browsers don't render HEIC natively and Vite won't transform it; either convert now or add a build-step check that fails loudly if a `.heic` file is present.
|
||
|
||
## Photo upload gating
|
||
- Gate value is `wedding.date` from the config file (see Config below) — no separate open-date field.
|
||
- Client: countdown shown before that time; upload form hidden until then.
|
||
- Server: hard check on every upload request — client-side gating is UX only, not security.
|
||
|
||
## Upload validation
|
||
- Allowed types: jpg/png/webp/heic, sniffed via `http.DetectContentType` (not trusted from filename/extension).
|
||
- Max file size and max files per submission — values TBD, suggest 15MB/file, 10 files/submission.
|
||
- Stored at `data/photos/<upload-id>/<uuid>_<original-filename>`.
|
||
|
||
## Email
|
||
SMTP settings come from the config file (see below).
|
||
- On RSVP: send guest names + message.
|
||
- On photo upload: send name/message/contact + direct links to each stored photo.
|
||
|
||
## Config
|
||
Single YAML file, path resolved from `CONFIG_PATH` env var (default `./config.yaml`) — this is the *only* env var; everything else lives in the file so it can be edited/mounted without touching Docker env blocks.
|
||
|
||
```yaml
|
||
server:
|
||
port: 8080
|
||
|
||
wedding:
|
||
date: "2026-11-07T16:00:00+02:00" # RFC3339, guest arrival time; drives countdown + upload gate
|
||
ceremony_time: "16:00 for 16:30" # display text
|
||
rsvp_by: "2026-09-01T00:00:00+02:00" # RFC3339, RSVP deadline; drives the RSVP countdown
|
||
venue_name: "Rivier Plaas"
|
||
venue_address: "Langenhoven Rd, Sherman Park AH, Meyerton, 1961"
|
||
rsvp_phone: "076 925 1718"
|
||
|
||
storage:
|
||
data_dir: "./data" # SQLite db + uploaded photos
|
||
|
||
email:
|
||
smtp_host: ""
|
||
smtp_port: 587
|
||
smtp_user: ""
|
||
smtp_pass: ""
|
||
from: ""
|
||
to: "" # couple's inbox
|
||
|
||
upload:
|
||
max_size_mb: 15
|
||
max_files: 10
|
||
```
|
||
`config.example.yaml` is checked in as a template; `config.yaml` is gitignored (holds real SMTP credentials). Venue/RSVP details sourced from `concept/IMG-20260811-WA0020.jpg` (the formal invitation).
|
||
|
||
## Deployment
|
||
Host: **`zoeshaldon.warky.info`**, Docker Compose, behind an **existing Apache reverse proxy on the host** (Apache owns TLS/certs — no Caddy/in-container TLS needed). The container publishes only to `127.0.0.1:8080`, and Apache proxies to it.
|
||
|
||
### Docker
|
||
- `Dockerfile`: multi-stage — (1) `node:alpine` builds the SvelteKit static output, (2) `golang:alpine` compiles the Go binary with the built frontend copied in for `go:embed`, (3) minimal `alpine` runtime stage with just the binary.
|
||
- `docker-compose.yml`: one service, `wedding`, builds from the Dockerfile, mounts `./config.yaml:/app/config.yaml:ro` and `./data:/app/data`, published as `127.0.0.1:8080:8080` (loopback only — Apache is the only thing that should reach it).
|
||
|
||
### Apache config (host side, not part of this repo)
|
||
```
|
||
<VirtualHost *:443>
|
||
ServerName zoeshaldon.warky.info
|
||
|
||
ProxyPreserveHost On
|
||
ProxyPass / http://127.0.0.1:8080/
|
||
ProxyPassReverse / http://127.0.0.1:8080/
|
||
|
||
# existing SSL cert config for warky.info
|
||
</VirtualHost>
|
||
```
|
||
Needs `mod_proxy` and `mod_proxy_http` enabled. Multipart photo uploads: raise `LimitRequestBody` (or leave unset/0) on this vhost so Apache doesn't reject large uploads ahead of the app's own `upload.max_size_mb` check.
|
||
|
||
## QR code
|
||
Generate pointing at `https://zoeshaldon.warky.info` once the container is deployed behind Apache.
|
||
|
||
## Colour palette
|
||
Skeleton custom theme (`wedding-theme`), generated with the [Skeleton theme generator](https://skeleton.dev/docs/design/themes) from two seed colours and applied via `data-theme="wedding-theme"` in `app.html`. Light/dark mode is Skeleton's built-in mode switch (`data-mode`) — it derives both from the same theme, so there's no separate light/dark table to hand-maintain.
|
||
|
||
| Slot | Seed colour | Source |
|
||
|---|---|---|
|
||
| `primary` (blue) | `#1E4E79` | groom's shirt / dusk sky in `concept/IMG-20260811-WA0019.jpg` |
|
||
| `secondary` (yellow) | `#D9A441` | warm gold title text in the same image |
|
||
|
||
Skeleton generates the full 50–900 scale and light/dark contrast pairs from these two seeds. Tailwind utility classes (`bg-primary-500`, `text-secondary-600`, etc.) and Skeleton components consume the theme directly — no custom CSS variables needed on top.
|
||
|
||
## Design — invitation framing
|
||
Landing and RSVP pages borrow the framed-card look from the formal invitation (`concept/IMG-20260811-WA0020.jpg`): a thin border around the core content block, small-caps tracked micro-copy above the couple's names ("Save the date" / "Please join us to celebrate the union of"), day+date formatted as "Saturday, 7 November 2026" (`Intl.DateTimeFormat('en-GB', {weekday:'long', day:'numeric', month:'long', year:'numeric'})`), and the venue name/address displayed beneath.
|
||
|
||
## Open decisions (need input before/at implementation)
|
||
1. ~~Domain / hosting target~~ — resolved: `zoeshaldon.warky.info`, Docker Compose (see Deployment).
|
||
2. SMTP provider/credentials to send from (e.g. existing Gmail/SES/other).
|
||
3. Timezone for the 7 Nov 2026 cutoff — assumed SAST (+02:00).
|
||
4. Max photo size / count per submission — defaults proposed above, confirm.
|
||
5. ~~Colour palette~~ — resolved, see above. Fonts still open (image uses a script face for names/heading + serif for the date — confirm if we should source similar web fonts).
|
||
|
||
## Prototype status
|
||
Working end-to-end: `ui/` (landing with countdown + sliding photo background, invitation-framed RSVP form with party-of-guests entry + its own countdown, gated photo upload form) builds and is served by `server/` (Go, SQLite via `modernc.org/sqlite`, SMTP notifications, server-side upload gate). Verified locally: page rendering, multi-guest RSVP submit (add/remove guest rows, blank-name filtering), photo upload with content-type sniffing (rejects non-images, detects HEIC), and both the wedding-date and RSVP-deadline gates/countdowns.
|
||
|
||
Known gaps before this is real:
|
||
- SMTP not configured (open decision #2) — emails currently log "skipped" instead of sending.
|
||
- Photos in `ui/assets/photos` are unoptimized (several multi-MB originals) — fine for the background slideshow (one loads at a time), but worth compressing before going live given the site is reached over mobile data via QR code.
|
||
- No automated tests yet.
|
||
- `Dockerfile`/`docker-compose.yml` not yet run end-to-end (only `go build`/`npm run build` and the local binary were verified).
|