v4.0.0.2
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# Goodwalk docs
|
||||
|
||||
Reference, audit, and planning documents for the Goodwalk site. Project-level rules live in [`/CLAUDE.md`](../CLAUDE.md); everything else lives here.
|
||||
|
||||
## Product & brand
|
||||
|
||||
- [product.md](product.md) — users, purpose, brand personality, design principles
|
||||
- [marketing-principles.md](marketing-principles.md) — Chris Do / Debbie Millman principles, applied to the rebuild
|
||||
- [marketing-voice.md](marketing-voice.md) — copy/voice style guide (calm, certain, warm, specific)
|
||||
|
||||
## Design
|
||||
|
||||
- [design-language.md](design-language.md) — canonical visual system: colour, typography, spacing, motion, surface, photography direction
|
||||
- [design-audit.md](design-audit.md) — codebase-wide design drift audit + token/refactor changelog
|
||||
- [homepage-flow-audit.md](homepage-flow-audit.md) — section-by-section IA review of the homepage
|
||||
- [mobile-polish.md](mobile-polish.md) — mobile conversion & comfort tracker
|
||||
- [ux-polish.md](ux-polish.md) — conversion audit tracker
|
||||
|
||||
## SEO & growth
|
||||
|
||||
- [competitors.md](competitors.md) — Fetch / Sticks & Bones analysis + what to borrow
|
||||
- [comparison-page-plan.md](comparison-page-plan.md) — brief for the dog-walker-vs-daycare-vs-sitter page
|
||||
- [nz-citations.md](nz-citations.md) — canonical NAP block + directory submission list
|
||||
|
||||
## Ops
|
||||
|
||||
- [deployment.md](deployment.md) — production deploy flow, server layout, nginx cutover
|
||||
- [webp-conversion.md](webp-conversion.md) — one-time WebP setup for hero images
|
||||
- [onboarding.md](onboarding.md) — client onboarding flow, lifecycle status, legacy CSV migration, Postgres target schema
|
||||
|
||||
## Archive
|
||||
|
||||
- [archive/v3-design-audit.md](archive/v3-design-audit.md) — superseded by [design-audit.md](design-audit.md)
|
||||
- [archive/marketing-voice-v2-proposal.md](archive/marketing-voice-v2-proposal.md) — one-shot copy-change proposal against [marketing-voice.md](marketing-voice.md)
|
||||
@@ -0,0 +1,89 @@
|
||||
# Competitor analysis — Auckland dog walking
|
||||
|
||||
A look at what two of our closest competitors do well, and what's worth borrowing for Goodwalk. Captured 2026-05-20.
|
||||
|
||||
---
|
||||
|
||||
## Fetch Dog Walking — https://fetchdogwalking.co.nz
|
||||
|
||||
### What they do well
|
||||
|
||||
- **Founder-led narrative.** Andy Evans is named on the site and the philosophy ("freedom to explore in nature is an essential investment in our dog's health") is front and centre. Feels like a person, not a faceless service.
|
||||
- **Content depth.** They go beyond "book a walk" and publish:
|
||||
- Auckland **Dog Parks** guide
|
||||
- Basic **Training** tips
|
||||
- **FAQs** page
|
||||
- "Our Dogs" and "Our People" pages
|
||||
- **"Dog's Highlight of the Week"** — a recurring content hook that gives existing clients a reason to keep visiting the site and gives new visitors social proof.
|
||||
- **Diversified offering.** The 100-acre Waitakere Ranges farm-stay / boarding service is a real differentiator — they're not just selling walks, they're selling a destination.
|
||||
- **Positions on authority.** Tagline "Auckland's Leading Dog Walking Company" is bold and clear.
|
||||
- **Strong primary CTA.** Homepage contact form is short (company, dog name, phone, email, suburb, message) — low friction.
|
||||
|
||||
### What we could implement
|
||||
|
||||
| Idea | Effort | Why |
|
||||
|---|---|---|
|
||||
| **Founder bio / "About Matt" page** with photo and philosophy | S | Personality is a moat. Easy SEO win for branded searches. |
|
||||
| **"Dog of the Week" / Instagram-style spotlight on the homepage** | M | Gives the site an "alive" feel; reuses content we likely already post to social. Could pull from the existing client_profiles table. |
|
||||
| **Dog parks of Auckland** content page | M | Pure SEO play — high-intent local search ("best dog parks Auckland"). Drives top-of-funnel traffic. |
|
||||
| **Training tips / FAQ content hub** | M | Same SEO logic. Also reduces support load. |
|
||||
| **"Our pack" page showing all dogs we walk** (with owner consent) | S | Trust + community. We already collect dog photos in onboarding. |
|
||||
|
||||
---
|
||||
|
||||
## Sticks & Bones — https://sticksandbones.co.nz
|
||||
|
||||
### What they do well
|
||||
|
||||
- **Process is the product.** They sell their *enrollment process* as a trust signal:
|
||||
1. Free Meet & Greet at home
|
||||
2. **Minimum two Assessment Walks** before enrollment
|
||||
3. Handler matching
|
||||
4. App-based booking
|
||||
This makes the service feel professional and screened — they're not taking everyone.
|
||||
- **Real-time walk reports via app.** Clients get photos and a report after every walk. This is the single biggest differentiator and the most-mentioned feature in their testimonials.
|
||||
- **Concrete trust signals.**
|
||||
- Police-checked handlers
|
||||
- Secure key handling process documented
|
||||
- Handlers named individually ("Amanda walks our Cooper")
|
||||
- Maximum 4 dogs per Urban Adventure Walk (explicit cap = safety)
|
||||
- **Personalised client app** for scheduling, history, and reports — clients self-serve.
|
||||
- **In-home dog sitting**, but only for enrolled walking clients (creates retention + upsell).
|
||||
- **Tagline is benefit-led**, not feature-led: "dog walking made easy" + "active dogs behave better."
|
||||
|
||||
### What we could implement
|
||||
|
||||
| Idea | Effort | Why |
|
||||
|---|---|---|
|
||||
| **Post-walk reports** (photo + short note delivered by email or in the client portal) | L | This is the killer feature. Our admin dashboard already touches client_profiles — extending to walk records is a natural next step. Single biggest perceived-value lift available. |
|
||||
| **Document our enrollment process** as a 3-step visual on the homepage (Enquire → Meet & Greet → First Walk) | S | We already do a Meet & Greet — we're just not selling it. Pure copy/design change. |
|
||||
| **Cap group size publicly** (e.g. "max 4 dogs per pack") | S | Free trust signal if it matches reality. |
|
||||
| **Name the team** with photos on About page | S | Sticks & Bones gets specific mentions of handlers by name in reviews — that only happens when you make handlers visible. |
|
||||
| **Police-check / insurance / first-aid badges** in the footer or About page | S | Cheap credibility. |
|
||||
| **Client portal extensions** — walk history, upcoming bookings, ability to reschedule | L | We already have auth + onboarding + admin dashboard. The data model is most of the way there. |
|
||||
| **"Enrolled clients only" services** as a retention hook (e.g. boarding, sitting) | M | Mirrors S&B's sitting model. Existing clients get priority — gives a reason to stay engaged after onboarding. |
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting themes
|
||||
|
||||
Both competitors do these things; we should too:
|
||||
|
||||
1. **Named humans on the site.** Founder, team, dogs. The word "we" without faces is worth less.
|
||||
2. **Process as proof.** Both make their intake process visible — it doubles as marketing.
|
||||
3. **Service ladder, not a service.** Walks → assessments → boarding/sitting → premium options. Gives clients somewhere to grow into.
|
||||
4. **Locations as content.** Specific Auckland park / suburb mentions throughout — local SEO and "they know my area" trust.
|
||||
|
||||
## What Goodwalk has that they don't (lean into this)
|
||||
|
||||
- **Multi-step onboarding form** with signature, vet, emergency contacts, behavioural profile — already more thorough than what's visible on either competitor.
|
||||
- **Database-backed client profiles** — foundation for a portal richer than S&B's (their app is the obvious next ceiling; we have data they don't seem to capture).
|
||||
- **Brand-coloured design system** already in place (Goodwalk Green / Yellow) — distinct visual identity vs. the generic blue/green dog-walker templates.
|
||||
|
||||
## Suggested priority order
|
||||
|
||||
1. **Founder/team page + named handlers** (1 day, big trust lift)
|
||||
2. **Visualise the enrollment process on the homepage** (1 day, sells the Meet & Greet we already do)
|
||||
3. **Dog Parks of Auckland content page** (2-3 days, SEO compounding)
|
||||
4. **Post-walk reports** in the client portal (1-2 weeks, table-stakes vs. S&B)
|
||||
5. **Walk history / portal extensions** (2-3 weeks, retention moat)
|
||||
@@ -0,0 +1,252 @@
|
||||
# Onboarding
|
||||
|
||||
How a Goodwalk client comes into the system, how their record is shaped, and how
|
||||
that record evolves over time.
|
||||
|
||||
## Flow at a glance
|
||||
|
||||
1. **Enquiry / email claim** — the user signs in to `clients.goodwalk.co.nz`
|
||||
with a one-time code. A `client_profiles` entry is created keyed by the
|
||||
lower-cased email.
|
||||
2. **Onboarding form** (`src/lib/components/OnboardingPage.svelte`) — a 5-step
|
||||
form: Owner Details → Dog Details → Health → Behaviour → Sign. Every
|
||||
question must be answered; there is no skip / mark-complete shortcut.
|
||||
3. **Submit** — the form posts to `POST /api/onboarding-submit`. The backend
|
||||
validates, stores a `submissionSnapshot` on the profile, sends the owner an
|
||||
email + PDF, and flips `onboardingCompleted: true`.
|
||||
4. **Admin dashboard** (`src/lib/components/admin-dashboard/AdminDashboard.svelte`)
|
||||
surfaces pending and completed clients, and now lets the owner change a
|
||||
client's lifecycle status.
|
||||
|
||||
## Where data lives today
|
||||
|
||||
| Thing | Storage | Source of truth |
|
||||
| --- | --- | --- |
|
||||
| Client profiles | `mail-api/data/client_profiles.json` (mirrored into `admin_db` KV) | `_client_profiles` dict in `main.py` |
|
||||
| Onboarding drafts (in-progress, per email) | `mail-api/data/drafts.json` | `_drafts` dict |
|
||||
| Submitted onboarding snapshot | Inside the client profile as `onboardingSubmission` / `submissionSnapshot` | same |
|
||||
| Activity feed (admin actions) | SQLite via `admin_db.record_event` | same |
|
||||
| Legacy historical data (Gravity Forms exports) | `data/legacy-onboarding.json`, `data/legacy-contracts.json`, `data/legacy-clients.json` | regenerated from the CSVs in repo root |
|
||||
|
||||
The plan is to move the JSON file to Postgres later; the JSON shape is already
|
||||
relational-friendly (see *Postgres target* below).
|
||||
|
||||
## Client profile shape
|
||||
|
||||
Keyed by lower-cased email. Fields are merged over time as the client moves
|
||||
through the funnel.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"fullName": "Shelley McCabe",
|
||||
"phone": "+6421328907",
|
||||
"address": "...",
|
||||
"dogName": "Maisie",
|
||||
"dogBreed": "...",
|
||||
"dogAge": "2015-04-28",
|
||||
"services": ["Tiny Gang Pack Walks"],
|
||||
"onboardingCompleted": true,
|
||||
"onboardingSubmittedAt": "2026-03-14T16:14:20",
|
||||
"onboardingSubmission": { /* full 5-step snapshot */ },
|
||||
"welcomePackSentAt": "...",
|
||||
"welcomePackOffer": { "serviceType": "...", "priceDetails": "...", "startDate": "..." },
|
||||
"birthdayAutoSend": false,
|
||||
"birthdayEmailLastSentAt": "...",
|
||||
"lastEnquiryAt": "...",
|
||||
"lifecycle": {
|
||||
"status": "active", // active | paused | cancelled | archived
|
||||
"reason": "",
|
||||
"changedAt": "2026-05-20T10:11:00",
|
||||
"changedBy": "info@goodwalk.co.nz"
|
||||
},
|
||||
"lifecycleHistory": [
|
||||
{ "status": "active", "reason": "", "changedAt": "...", "changedBy": "..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle status
|
||||
|
||||
Soft-delete only. **No client is ever removed**, so we keep the full history
|
||||
for retention / newsletter work.
|
||||
|
||||
| Status | Meaning | Outreach included? |
|
||||
| --- | --- | --- |
|
||||
| `active` | Currently walking with us. | Yes — pending + completed + birthdays |
|
||||
| `paused` | Temporarily not walking (holiday, recovery, etc.). | Yes — still on outreach lists |
|
||||
| `cancelled` | No longer a client. Kept on file. | **No** — excluded from outreach |
|
||||
| `archived` | Long-term inactive. Hidden from default views. | **No** — excluded from outreach |
|
||||
|
||||
Defaults: every client starts as `active`. A missing `lifecycle` block is
|
||||
treated as `active` by the API and the dashboard.
|
||||
|
||||
### Filters that respect lifecycle
|
||||
|
||||
`_client_is_reachable(profile)` in `mail-api/main.py` is the single gate.
|
||||
`cancelled` and `archived` are filtered out of:
|
||||
|
||||
- `GET /owner/pending-onboarding`
|
||||
- `GET /owner/completed-onboarding`
|
||||
- `GET /owner/birthdays`
|
||||
|
||||
`GET /owner/all-clients` returns **everyone**, regardless of status, so the
|
||||
admin can always find a former client. The status pill on each row makes the
|
||||
state obvious.
|
||||
|
||||
### Changing a client's status
|
||||
|
||||
Admin dashboard → Clients tab → row action **"Change status"**. Opens a modal
|
||||
with the four status options and an optional 500-char reason. Saving calls:
|
||||
|
||||
```
|
||||
POST /api/owner/client-status
|
||||
Authorization: Bearer <admin session token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "owner@example.com",
|
||||
"status": "paused",
|
||||
"reason": "Moving house, back in June"
|
||||
}
|
||||
```
|
||||
|
||||
The backend writes the new `lifecycle` block, appends to `lifecycleHistory`
|
||||
(capped at the last 50 entries), and records an
|
||||
`owner_client_status_changed` event in the activity feed.
|
||||
|
||||
## Legacy data migration
|
||||
|
||||
Two CSV exports from the old Gravity Forms install:
|
||||
|
||||
- `dog-enrolment-form-2026-05-20.csv` — 72 onboarding submissions (2022–2026).
|
||||
- `goodwalk-contract-2026-05-20.csv` — 39 contract signings (2022-06 → 2024-02).
|
||||
|
||||
Two cleansing scripts produce relational-friendly JSON:
|
||||
|
||||
```
|
||||
node scripts/clean-legacy-onboarding.mjs # → data/legacy-onboarding.json
|
||||
node scripts/clean-legacy-contracts.mjs # → data/legacy-contracts.json
|
||||
# also enriches legacy-onboarding.json
|
||||
# and writes data/legacy-clients.json
|
||||
```
|
||||
|
||||
### Importing legacy clients into production
|
||||
|
||||
The deploy pipeline ships these 36 clients into the live system on every
|
||||
deploy. The flow:
|
||||
|
||||
1. `deploy.ps1` runs `scripts/build-legacy-seed.mjs`, which folds the three
|
||||
legacy JSON files into one email-keyed dict shaped like the live
|
||||
`_client_profiles` entries. Output: `mail-api/legacy-clients-seed.json`.
|
||||
2. The mail-api `Dockerfile` `COPY`s that file to `/app/legacy-clients-seed.json`
|
||||
so it lives **outside** the persistent `mail_api_data` volume — every
|
||||
deploy carries a fresh copy.
|
||||
3. On boot, `_merge_legacy_seed_if_present()` adds every email that is not
|
||||
already in `_client_profiles`, persists the result to JSON + admin_kv,
|
||||
and never overwrites a live entry. Re-running is idempotent.
|
||||
|
||||
Each imported client is created with `lifecycle: { status: 'archived' }` so
|
||||
they don't pollute outreach lists. The admin can promote any of them back to
|
||||
`active` from the dashboard. Provenance (entry IDs, signature URL, PDF URLs)
|
||||
is preserved on the `legacy` field of every profile.
|
||||
|
||||
To skip the rebuild on a hotfix deploy use `deploy.ps1 -SkipLegacySeed`; the
|
||||
previous seed file is reused as-is. To regenerate locally:
|
||||
|
||||
```
|
||||
node scripts/build-legacy-seed.mjs
|
||||
```
|
||||
|
||||
The contracts script backfills owner email + postal address onto onboarding
|
||||
records where a contract row matches (by owner name, then dog name, then a
|
||||
unique surname). At present: **38 of 72** onboarding rows now have an email
|
||||
attached; the rest were submitted after the contract form was retired in
|
||||
2024-02 and will need an email-claim flow before they can sign in.
|
||||
|
||||
### Known gaps in the legacy data
|
||||
|
||||
- 34 onboarding rows have no email (mostly post-2024-02).
|
||||
- Dog **breed** was never collected by the old forms — `null` everywhere.
|
||||
- Owner **address** was only collected on the contract form, not on
|
||||
onboarding directly.
|
||||
- One vet phone number (`6307168`) couldn't be normalised to E.164 — kept
|
||||
as `phoneRaw` only.
|
||||
|
||||
## Postgres target
|
||||
|
||||
The JSON shapes already map cleanly onto a relational schema. Suggested tables
|
||||
once we migrate off the JSON file:
|
||||
|
||||
```sql
|
||||
clients (
|
||||
email TEXT PRIMARY KEY,
|
||||
first_name, last_name, phone, phone_raw,
|
||||
address, address_street, address_line2, address_suburb,
|
||||
address_city, address_postal_code, address_country,
|
||||
-- lifecycle
|
||||
lifecycle_status TEXT NOT NULL DEFAULT 'active', -- enum: active|paused|cancelled|archived
|
||||
lifecycle_reason TEXT,
|
||||
lifecycle_changed_at TIMESTAMPTZ,
|
||||
lifecycle_changed_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
client_status_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
client_email TEXT REFERENCES clients(email),
|
||||
status TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
changed_at TIMESTAMPTZ NOT NULL,
|
||||
changed_by TEXT NOT NULL
|
||||
);
|
||||
|
||||
dogs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
client_email TEXT REFERENCES clients(email),
|
||||
name TEXT, surname TEXT, date_of_birth DATE, breed TEXT
|
||||
);
|
||||
|
||||
onboarding_submissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
client_email TEXT REFERENCES clients(email),
|
||||
dog_id INT REFERENCES dogs(id),
|
||||
legacy_entry_id TEXT,
|
||||
legacy_pdf_url TEXT,
|
||||
signature_url TEXT,
|
||||
vet_name TEXT, vet_address TEXT, vet_phone TEXT, vet_phone_raw TEXT,
|
||||
emergency_contact_name TEXT, emergency_contact_phone TEXT,
|
||||
health JSONB, -- vaccinated / allergies / diet / medication
|
||||
behaviour JSONB, -- recall / reactivity / car / commands
|
||||
misc JSONB, -- additional notes / referrals / socials
|
||||
signed_on DATE,
|
||||
submitted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
contracts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
client_email TEXT REFERENCES clients(email),
|
||||
legacy_entry_id TEXT,
|
||||
signature_url TEXT,
|
||||
pdf_url TEXT,
|
||||
consent_text TEXT,
|
||||
signed_on DATE
|
||||
);
|
||||
```
|
||||
|
||||
`health` / `behaviour` / `misc` stay as `JSONB` rather than exploding every
|
||||
nullable yes/no into a column — it matches the existing
|
||||
`submissionSnapshot: dict[str, Any]` shape in
|
||||
`mail-api/mail_api/models.py` and makes the form easier to evolve.
|
||||
|
||||
## Things that intentionally don't exist yet
|
||||
|
||||
- **Hard delete** of a client. Use `archived` instead. Hard delete would also
|
||||
break the activity feed and the legacy import provenance.
|
||||
- **A "former client" notes log.** If we need anything richer than a 500-char
|
||||
reason, the next step is to add a `client_notes` table keyed by email — but
|
||||
hold off until the JSON → Postgres migration is done.
|
||||
- **Re-engagement / win-back emails.** The data is there (lifecycle history +
|
||||
email), so this is unblocked whenever marketing wants to send a newsletter
|
||||
to past clients. `cancelled` and `archived` rows are still in
|
||||
`/owner/all-clients`.
|
||||
Reference in New Issue
Block a user