SEO Tweaks
@@ -3,7 +3,8 @@
|
||||
"allow": [
|
||||
"Bash(kill %1)",
|
||||
"Bash(pkill -f \"vite dev\")",
|
||||
"Bash(npm run *)"
|
||||
"Bash(npm run *)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,730 @@
|
||||
# Goodwalk Design Language (2026 Refined Edition)
|
||||
|
||||
This document is the source of truth for Goodwalk’s visual design system. It defines the visual language, emotional tone, interaction philosophy, and implementation rules used across the website and future digital products.
|
||||
|
||||
The goal is not trend-chasing. The goal is to create a calm, trustworthy, modern experience that feels premium without becoming cold, corporate, or overly “tech”.
|
||||
|
||||
---
|
||||
|
||||
# Core Principles
|
||||
|
||||
## 1. Warmth over perfection
|
||||
Goodwalk is a human service business. The experience should feel:
|
||||
- calm
|
||||
- warm
|
||||
- trustworthy
|
||||
- emotionally safe
|
||||
- premium without arrogance
|
||||
|
||||
Avoid interfaces that feel:
|
||||
- clinical
|
||||
- overly corporate
|
||||
- aggressive
|
||||
- “SaaS-like”
|
||||
- excessively polished
|
||||
|
||||
Slight softness and imperfection are intentional.
|
||||
|
||||
---
|
||||
|
||||
## 2. Refinement over redesign
|
||||
The brand identity already works.
|
||||
|
||||
Modernisation should happen through:
|
||||
- spacing
|
||||
- typography
|
||||
- image treatment
|
||||
- motion
|
||||
- surface depth
|
||||
- consistency
|
||||
- restraint
|
||||
|
||||
Do not redesign for novelty.
|
||||
|
||||
---
|
||||
|
||||
## 3. Emotion before decoration
|
||||
Visual decisions must support:
|
||||
- trust
|
||||
- clarity
|
||||
- calmness
|
||||
- conversion
|
||||
|
||||
Do not add visual effects simply because they are modern.
|
||||
|
||||
Every effect must have emotional purpose.
|
||||
|
||||
---
|
||||
|
||||
## 4. Mobile-first experience
|
||||
The site must feel premium on:
|
||||
- 375px width
|
||||
- average brightness outdoors
|
||||
- thumb-driven interaction
|
||||
- imperfect network conditions
|
||||
|
||||
Mobile is not a secondary layout.
|
||||
|
||||
---
|
||||
|
||||
## 5. Quiet luxury
|
||||
The site should feel:
|
||||
- expensive
|
||||
- intentional
|
||||
- calm
|
||||
- understated
|
||||
|
||||
Not:
|
||||
- flashy
|
||||
- loud
|
||||
- hyper-animated
|
||||
- trend-driven
|
||||
|
||||
Modern premium design in 2026 is increasingly about restraint.
|
||||
|
||||
---
|
||||
|
||||
# Colour System
|
||||
|
||||
Defined in:
|
||||
`src/lib/styles/variables.css`
|
||||
|
||||
| Token | Hex | Purpose |
|
||||
|---|---|---|
|
||||
| `--gw-green` | `#213021` | Primary brand colour |
|
||||
| `--green-mid` | `#2d4230` | Hover states, elevated surfaces |
|
||||
| `--green-soft` | `#344b38` | Optional softer elevated green |
|
||||
| `--yellow` | `#ffd100` | Primary CTA accent |
|
||||
| `--yellow-soft` | `#f2bf2f` | Premium warm accent alternative |
|
||||
| `--off-white` | `#fbfbfb` | Primary background |
|
||||
| `--surface-light` | `#f7f8f6` | Elevated light surface |
|
||||
| `--text` | `#2e3031` | Default text |
|
||||
| `--gray` | `#59606d` | Secondary copy |
|
||||
| `--beige` | `#e5d6c2` | Warm neutral accent |
|
||||
|
||||
---
|
||||
|
||||
# Colour Philosophy
|
||||
|
||||
## Greens
|
||||
The green palette represents:
|
||||
- safety
|
||||
- reliability
|
||||
- groundedness
|
||||
- nature
|
||||
- professionalism
|
||||
|
||||
Greens should feel deep and organic rather than synthetic.
|
||||
|
||||
Avoid:
|
||||
- saturated emerald tones
|
||||
- neon greens
|
||||
- cold blue-greens
|
||||
|
||||
---
|
||||
|
||||
## Yellow usage
|
||||
Yellow is an accent, not a dominant UI colour.
|
||||
|
||||
Yellow should:
|
||||
- guide attention
|
||||
- signal warmth
|
||||
- create optimism
|
||||
|
||||
Yellow should NOT:
|
||||
- overpower sections
|
||||
- become large reading surfaces
|
||||
- create visual fatigue
|
||||
|
||||
For premium applications:
|
||||
```css
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
#ffd54a,
|
||||
#f2bf2f
|
||||
);
|
||||
```
|
||||
|
||||
This creates a warmer, more refined feel.
|
||||
|
||||
---
|
||||
|
||||
# Surface Design
|
||||
|
||||
## Philosophy
|
||||
Modern interfaces should feel layered rather than flat.
|
||||
|
||||
Depth should come from:
|
||||
- tonal separation
|
||||
- soft gradients
|
||||
- atmospheric shadows
|
||||
- subtle edge highlights
|
||||
|
||||
NOT heavy shadows or strong borders.
|
||||
|
||||
---
|
||||
|
||||
## Surface Rules
|
||||
|
||||
### Avoid pure white
|
||||
Never use:
|
||||
```css
|
||||
#ffffff
|
||||
```
|
||||
|
||||
Prefer:
|
||||
```css
|
||||
#fbfbfb
|
||||
#f7f8f6
|
||||
#f5f6f3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Avoid pure black
|
||||
Never use:
|
||||
```css
|
||||
#000000
|
||||
```
|
||||
|
||||
Prefer:
|
||||
```css
|
||||
#0f1115
|
||||
#16181d
|
||||
#1b1d21
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Micro gradients
|
||||
Large flat surfaces should contain extremely subtle tonal variation.
|
||||
|
||||
Example:
|
||||
```css
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(255,255,255,0.95),
|
||||
rgba(247,248,246,0.95)
|
||||
);
|
||||
```
|
||||
|
||||
These gradients should be nearly invisible.
|
||||
|
||||
---
|
||||
|
||||
# Typography
|
||||
|
||||
Defined in:
|
||||
`src/lib/styles/typography.css`
|
||||
|
||||
| Token | Family | Purpose |
|
||||
|---|---|---|
|
||||
| `--font-head` | Unbounded | Headlines |
|
||||
| `--font-body` | Readex Pro | Body text |
|
||||
|
||||
---
|
||||
|
||||
# Typography Philosophy
|
||||
|
||||
Typography should create:
|
||||
- confidence
|
||||
- calmness
|
||||
- readability
|
||||
- rhythm
|
||||
|
||||
Avoid:
|
||||
- excessive weight changes
|
||||
- overly dense layouts
|
||||
- tiny text
|
||||
- over-stylised headings
|
||||
|
||||
---
|
||||
|
||||
# Typography Rules
|
||||
|
||||
## Body copy
|
||||
Desktop:
|
||||
```css
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
```
|
||||
|
||||
Mobile:
|
||||
```css
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Headings
|
||||
Hero headings:
|
||||
```css
|
||||
letter-spacing: -0.04em;
|
||||
```
|
||||
|
||||
Section headings:
|
||||
```css
|
||||
letter-spacing: -0.02em;
|
||||
```
|
||||
|
||||
Large headings should feel:
|
||||
- compact
|
||||
- intentional
|
||||
- editorial
|
||||
|
||||
---
|
||||
|
||||
## Weight restraint
|
||||
Prefer:
|
||||
- 400
|
||||
- 500
|
||||
- 700
|
||||
|
||||
Avoid excessive font-weight variety.
|
||||
|
||||
Spacing should create hierarchy more than font weight.
|
||||
|
||||
---
|
||||
|
||||
# Spacing System
|
||||
|
||||
## Philosophy
|
||||
Modern premium interfaces feel expensive because they are under-filled.
|
||||
|
||||
Whitespace is a feature.
|
||||
|
||||
Do not compress layouts simply to fit more information.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Only use:
|
||||
- 4
|
||||
- 8
|
||||
- 12
|
||||
- 16
|
||||
- 24
|
||||
- 32
|
||||
- 48
|
||||
- 64
|
||||
- 72
|
||||
- 96
|
||||
|
||||
Avoid arbitrary spacing values.
|
||||
|
||||
---
|
||||
|
||||
# Border Radius
|
||||
|
||||
| Context | Radius |
|
||||
|---|---|
|
||||
| Standard cards | `28px` |
|
||||
| Large feature containers | `32px` |
|
||||
| Pills | `999px` |
|
||||
| Small UI surfaces | `16px` |
|
||||
|
||||
The system should feel soft and approachable.
|
||||
|
||||
Avoid:
|
||||
- sharp corners
|
||||
- overly circular “bubble UI”
|
||||
|
||||
---
|
||||
|
||||
# Shadows & Depth
|
||||
|
||||
## Philosophy
|
||||
Modern depth is atmospheric, not dramatic.
|
||||
|
||||
Shadows should feel:
|
||||
- diffused
|
||||
- soft
|
||||
- realistic
|
||||
|
||||
Avoid:
|
||||
- heavy elevation
|
||||
- dark shadows
|
||||
- obvious floating cards
|
||||
|
||||
---
|
||||
|
||||
## Standard shadows
|
||||
|
||||
### Definition shadow
|
||||
```css
|
||||
inset 0 0 0 1px rgba(17,20,24,0.045);
|
||||
```
|
||||
|
||||
### Ambient depth
|
||||
```css
|
||||
0 8px 40px rgba(0,0,0,0.06);
|
||||
```
|
||||
|
||||
### Inner edge highlight
|
||||
```css
|
||||
inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Texture & Grain
|
||||
|
||||
## Philosophy
|
||||
Digital flatness feels artificial.
|
||||
|
||||
Subtle texture adds:
|
||||
- realism
|
||||
- warmth
|
||||
- richness
|
||||
|
||||
Very subtle grain is encouraged on:
|
||||
- hero sections
|
||||
- dark surfaces
|
||||
- footers
|
||||
- large empty areas
|
||||
|
||||
Texture must never become visibly noisy.
|
||||
|
||||
---
|
||||
|
||||
# Motion System
|
||||
|
||||
## Philosophy
|
||||
Motion should feel calm and physical.
|
||||
|
||||
Avoid:
|
||||
- excessive movement
|
||||
- bouncing
|
||||
- playful overshoot
|
||||
- aggressive transitions
|
||||
|
||||
---
|
||||
|
||||
## Preferred transition timing
|
||||
|
||||
```css
|
||||
220ms cubic-bezier(0.22, 1, 0.36, 1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hover behaviour
|
||||
|
||||
Modern hover effects should rely more on:
|
||||
- brightness shifts
|
||||
- opacity
|
||||
- slight elevation
|
||||
- subtle glow
|
||||
|
||||
Less:
|
||||
```css
|
||||
translateY(-6px)
|
||||
```
|
||||
|
||||
More:
|
||||
```css
|
||||
translateY(-2px)
|
||||
filter: brightness(1.02)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Cards
|
||||
|
||||
## Philosophy
|
||||
Cards should not feel disconnected from the layout.
|
||||
|
||||
Modern premium layouts use:
|
||||
- softer separation
|
||||
- lower contrast
|
||||
- quieter surfaces
|
||||
|
||||
Avoid:
|
||||
- harsh borders
|
||||
- obvious “dashboard card” styling
|
||||
- excessive shadowing
|
||||
|
||||
---
|
||||
|
||||
# Photography Direction
|
||||
|
||||
## This is one of the most important parts of the brand.
|
||||
|
||||
Photography should feel:
|
||||
- candid
|
||||
- warm
|
||||
- emotionally genuine
|
||||
- lightly cinematic
|
||||
- calm
|
||||
- naturally lit
|
||||
|
||||
Avoid:
|
||||
- harsh HDR
|
||||
- over-sharpening
|
||||
- fake bokeh
|
||||
- obvious AI look
|
||||
- cluttered backgrounds
|
||||
|
||||
---
|
||||
|
||||
## Image treatment
|
||||
|
||||
Preferred:
|
||||
- shallow depth of field
|
||||
- warm colour grading
|
||||
- natural greens
|
||||
- soft contrast
|
||||
- realistic shadows
|
||||
|
||||
Images should feel:
|
||||
“premium lifestyle”
|
||||
not:
|
||||
“local flyer advertisement”
|
||||
|
||||
---
|
||||
|
||||
# Layout Philosophy
|
||||
|
||||
## Editorial rhythm
|
||||
Not every section should feel identical.
|
||||
|
||||
The site should alternate between:
|
||||
- dense
|
||||
- airy
|
||||
- emotional
|
||||
- informational
|
||||
|
||||
This creates pacing and reduces fatigue.
|
||||
|
||||
---
|
||||
|
||||
## Controlled asymmetry
|
||||
Small asymmetry is encouraged:
|
||||
- offset images
|
||||
- uneven crops
|
||||
- staggered alignment
|
||||
- imperfect positioning
|
||||
|
||||
This creates humanity and visual interest.
|
||||
|
||||
Avoid perfect grid rigidity everywhere.
|
||||
|
||||
---
|
||||
|
||||
# Interaction Design
|
||||
|
||||
## Buttons
|
||||
Buttons should feel:
|
||||
- tactile
|
||||
- confident
|
||||
- soft
|
||||
|
||||
Never aggressive.
|
||||
|
||||
---
|
||||
|
||||
## Hover states
|
||||
Hover should:
|
||||
- reward
|
||||
- guide
|
||||
- reassure
|
||||
|
||||
Not distract.
|
||||
|
||||
---
|
||||
|
||||
# Navigation
|
||||
|
||||
The navigation should feel:
|
||||
- lightweight
|
||||
- stable
|
||||
- unobtrusive
|
||||
|
||||
Avoid:
|
||||
- oversized sticky headers
|
||||
- excessive blur
|
||||
- flashy dropdowns
|
||||
|
||||
The navigation exists to support trust and conversion.
|
||||
|
||||
---
|
||||
|
||||
# Mobile UX Principles
|
||||
|
||||
## Thumb-first interaction
|
||||
Primary actions should remain reachable.
|
||||
|
||||
Spacing should prevent accidental taps.
|
||||
|
||||
---
|
||||
|
||||
## Reduced visual noise
|
||||
Mobile layouts should:
|
||||
- simplify aggressively
|
||||
- reduce simultaneous options
|
||||
- preserve emotional tone
|
||||
|
||||
Not simply shrink desktop layouts.
|
||||
|
||||
---
|
||||
|
||||
# Accessibility
|
||||
|
||||
Always support:
|
||||
- reduced motion
|
||||
- readable contrast
|
||||
- large tap targets
|
||||
- visible focus states
|
||||
- scalable text
|
||||
|
||||
Accessibility should feel integrated, not bolted on.
|
||||
|
||||
---
|
||||
|
||||
# Design Language Keywords
|
||||
|
||||
The Goodwalk experience should feel:
|
||||
|
||||
- calm
|
||||
- premium
|
||||
- trustworthy
|
||||
- editorial
|
||||
- grounded
|
||||
- warm
|
||||
- modern
|
||||
- understated
|
||||
- human
|
||||
- emotionally safe
|
||||
- refined
|
||||
- spacious
|
||||
|
||||
Never:
|
||||
- corporate
|
||||
- loud
|
||||
- trendy
|
||||
- hyper-minimal
|
||||
- cold
|
||||
- sterile
|
||||
- flashy
|
||||
|
||||
---
|
||||
|
||||
# What To Prioritise Next
|
||||
|
||||
Highest impact improvements:
|
||||
|
||||
1. Better photography consistency
|
||||
2. Softer surface contrast
|
||||
3. More atmospheric depth
|
||||
4. Grain/texture implementation
|
||||
5. Reduced hover movement
|
||||
6. More editorial layouts
|
||||
7. More restrained motion
|
||||
8. More premium CTA treatment
|
||||
9. Improved mobile spacing rhythm
|
||||
10. More subtle card separation
|
||||
|
||||
---
|
||||
|
||||
# Final Rule
|
||||
|
||||
If a design decision looks impressive but reduces:
|
||||
- clarity
|
||||
- warmth
|
||||
- trust
|
||||
- calmness
|
||||
|
||||
Do not ship it.
|
||||
|
||||
Goodwalk should feel premium because it feels thoughtful — not because it feels flashy.
|
||||
|
||||
---
|
||||
|
||||
# Implementation Reference
|
||||
|
||||
Technical specs for what is currently live. Update this section when the codebase changes.
|
||||
|
||||
## Colour tokens (variables.css)
|
||||
|
||||
| Token | Hex | Status |
|
||||
|---|---|---|
|
||||
| `--gw-green` | `#213021` | ✅ Live |
|
||||
| `--green-mid` | `#2d4230` | ✅ Live |
|
||||
| `--green-soft` | `#344b38` | ✅ Live |
|
||||
| `--yellow` | `#ffd100` | ✅ Live |
|
||||
| `--yellow-soft` | `#f2bf2f` | ✅ Live |
|
||||
| `--gray` | `#59606d` | ✅ Live |
|
||||
| `--beige` | `#e5d6c2` | ✅ Live |
|
||||
| `--off-white` | `#fbfbfb` | ✅ Live |
|
||||
| `--surface-light` | `#f7f8f6` | ✅ Live — use where a surface sits above `--off-white` |
|
||||
| `--text` | `#2e3031` | ✅ Live |
|
||||
|
||||
## Typography (live values)
|
||||
|
||||
| Context | Size | Weight | Tracking | Line-height |
|
||||
|---|---|---|---|---|
|
||||
| Hero h1 | `clamp(34px, 4vw, 56px)` | 800 | `-0.04em` | `1.05` |
|
||||
| Section headings | `42px` | 700 | `-0.02em` | `1.08` |
|
||||
| Body (desktop) | `15px` | 400 | — | `1.65` |
|
||||
| Body (mobile) | `16px` | 400 | — | `1.70` |
|
||||
| Buttons | `14px` | 700 | `0.01em` | `1.2` |
|
||||
| Eyebrow | `13px` | 700 | `0.08em` | — |
|
||||
|
||||
## Section padding rhythm (live values)
|
||||
|
||||
| Section | Padding |
|
||||
|---|---|
|
||||
| `#promise`, `#services` | `96px 0` |
|
||||
| `#values`, `#testimonials`, `#info` | `72px 0` |
|
||||
| `#newlead` | `80px 0` |
|
||||
| `#instagram` | `60px` |
|
||||
|
||||
## Card hover behaviour (live values)
|
||||
|
||||
```css
|
||||
/* Service and testimonial cards */
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.02);
|
||||
box-shadow: inset 0 0 0 1px rgba(17,20,24,0.06), 0 8px 40px rgba(0,0,0,0.08);
|
||||
```
|
||||
|
||||
Reduced from `translateY(-6px)` — calmer, more restrained per design philosophy.
|
||||
|
||||
## Service icon bubble gradient (live)
|
||||
|
||||
```css
|
||||
background: linear-gradient(135deg, #ffd54a, var(--yellow-soft));
|
||||
```
|
||||
|
||||
Angled, warmer gradient — replaces the flat vertical yellow.
|
||||
|
||||
## Testimonial card surface (live)
|
||||
|
||||
```css
|
||||
background: linear-gradient(180deg, #ffffff 0%, var(--off-white) 100%);
|
||||
```
|
||||
|
||||
Micro-gradient — avoids flat pure white per surface design rules.
|
||||
|
||||
## Scroll reveals (live)
|
||||
|
||||
JS (`+layout.svelte`): `IntersectionObserver` targets `.section-heading`, `.service-card`, `.testimonial-card`, `.value-card`. Adds `data-reveal` attribute on mount, toggles `.is-visible` when the element crosses `40px` from the viewport bottom.
|
||||
|
||||
CSS (`base.css`): `opacity 0 → 1`, `translateY(20px) → none`, `0.5s`. Wrapped in `prefers-reduced-motion: no-preference`.
|
||||
|
||||
## Hero image gradient (live)
|
||||
|
||||
`#hero::after` pseudo-element: `linear-gradient(to top, var(--gw-green), transparent)`, `height: 120px`, covering the right 58% of the section. Hidden on mobile.
|
||||
|
||||
## Not yet implemented
|
||||
|
||||
| Item | Notes |
|
||||
|---|---|
|
||||
| Grain / texture | Needs a noise SVG or canvas overlay — skip until photography is consistent |
|
||||
| `--surface-light` usage | Token is defined; not yet applied to any component |
|
||||
| `--green-soft` usage | Token is defined; candidate for mega-menu icon hover state |
|
||||
| More editorial layouts | Structural work — needs a design pass per page |
|
||||
@@ -9,7 +9,7 @@ COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN node --experimental-strip-types scripts/export-homepage-content.mjs
|
||||
RUN node --experimental-strip-types --import="file:///app/scripts/sveltekit-resolver.mjs" scripts/export-homepage-content.mjs
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
goodwalk.co.nz — Full SEO Audit Report
|
||||
|
||||
Date: 12 May 2026 | Site: Auckland dog walking service (SvelteKit + NGINX) | Auditor: 9 specialist subagents run in parallel
|
||||
|
||||
---
|
||||
SEO Health Score: 60 / 100
|
||||
|
||||
┌──────────────────────────┬────────┬───────┬───────────┐
|
||||
│ Category │ Weight │ Score │ Weighted │
|
||||
├──────────────────────────┼────────┼───────┼───────────┤
|
||||
│ Content Quality │ 23% │ 68 │ 15.6 │
|
||||
├──────────────────────────┼────────┼───────┼───────────┤
|
||||
│ Technical SEO │ 22% │ 52 │ 11.4 │
|
||||
├──────────────────────────┼────────┼───────┼───────────┤
|
||||
│ On-Page SEO │ 20% │ 62 │ 12.4 │
|
||||
├──────────────────────────┼────────┼───────┼───────────┤
|
||||
│ Schema / Structured Data │ 10% │ 58 │ 5.8 │
|
||||
├──────────────────────────┼────────┼───────┼───────────┤
|
||||
│ Performance (CWV) │ 10% │ 62 │ 6.2 │
|
||||
├──────────────────────────┼────────┼───────┼───────────┤
|
||||
│ AI Search Readiness │ 10% │ 51 │ 5.1 │
|
||||
├──────────────────────────┼────────┼───────┼───────────┤
|
||||
│ Images │ 5% │ 65 │ 3.3 │
|
||||
├──────────────────────────┼────────┼───────┼───────────┤
|
||||
│ TOTAL │ │ │ 59.8 → 60 │
|
||||
└──────────────────────────┴────────┴───────┴───────────┘
|
||||
|
||||
▎ The score is dragged down by one dominant structural issue: all 17 location pages are noindex. Remove that single directive and the score jumps ~8 points immediately.
|
||||
|
||||
---
|
||||
CRITICAL Issues (Fix Immediately)
|
||||
|
||||
C-1 — All 17 location pages are noindex
|
||||
|
||||
File: src/routes/locations/[suburb]/+page.svelte line ~23
|
||||
|
||||
Every suburb page (/locations/mt-eden, /locations/kingsland, etc.) has noindex set. These are Goodwalk's primary local SEO assets — suburb-specific content, Service schema, BreadcrumbList, targeted H1s — and none of
|
||||
it is visible to Google. This is the largest single SEO issue on the site.
|
||||
|
||||
Fix: Remove noindex from the <SeoHead> call. One line change, high impact.
|
||||
|
||||
C-2 — Location pages missing from sitemap
|
||||
|
||||
File: src/routes/sitemap.xml/+server.ts
|
||||
|
||||
The sitemap deliberately excludes /locations/*. After unblocking indexation (C-1), Google discovery will be slow without sitemap inclusion.
|
||||
|
||||
Fix: Import locationPages from $lib/content/locations and generate dynamic sitemap entries for all 17 suburb URLs.
|
||||
|
||||
C-3 — reviewCount in schema is 4 when business has 30+ Google reviews
|
||||
|
||||
File: src/routes/+page.svelte ~line 101
|
||||
|
||||
reviewCount is hardcoded to String(data.content.testimonials.length) which resolves to "4". The site claims "30+ five-star Google reviews" in visible copy. Emitting reviewCount: "4" suppresses rich result eligibility
|
||||
and creates a factual schema/content discrepancy.
|
||||
|
||||
Fix: Change to a static "30" floor value or a content-managed field.
|
||||
|
||||
---
|
||||
HIGH Priority (Fix Within 1 Week)
|
||||
|
||||
H-1 — Homepage suburb list not linked to location pages
|
||||
|
||||
File: src/lib/content/homepage.ts, InfoSection component
|
||||
|
||||
The info.suburbs string is plain text — not hyperlinks. Even after fixing C-1, Google won't easily discover or distribute PageRank to location pages without internal links from the homepage. This is the fastest way to
|
||||
give location pages authority.
|
||||
|
||||
Fix: Render each suburb as <a href="/locations/{slug}"> in the InfoSection.
|
||||
|
||||
H-2 — Font Awesome npm import is killing performance
|
||||
|
||||
File: src/routes/+layout.svelte
|
||||
|
||||
Three FontAwesome CSS files (fontawesome.min.css, solid.min.css, brands.min.css) are imported globally for ~8 icons. Combined with 9 @fontsource weight files (Readex Pro × 4, Unbounded × 4, Fredoka × 1), this is
|
||||
estimated at 600–900 KB uncompressed CSS + WOFF2 on every page. This is the single largest performance bottleneck and the highest-ROI fix.
|
||||
|
||||
Fix: Replace Font Awesome with inline SVG components for the specific icons used. Drop unused font weights.
|
||||
|
||||
H-3 — Hero image preload is broken
|
||||
|
||||
File: src/lib/components/SeoHead.svelte
|
||||
|
||||
<link rel="preload" as="image"> points to the raw .png file, but @sveltejs/enhanced-img generates AVIF/WebP variants that the browser actually uses. The preload wastes bandwidth fetching the PNG while the actual LCP
|
||||
element loads later. The fetchpriority="high" on the <enhanced:img> already handles this correctly.
|
||||
|
||||
Fix: Remove the <link rel="preload"> from SeoHead when preloadImage=true — rely solely on fetchpriority="high" on the image element.
|
||||
|
||||
H-4 — Schema missing @id node stitching
|
||||
|
||||
File: src/routes/+page.svelte, src/routes/[slug]/+page.svelte, src/lib/seo.ts
|
||||
|
||||
No schema block uses @id URIs, so Google cannot link the LocalBusiness entity to the Service blocks across pages — they're treated as unrelated graphs. This weakens Knowledge Graph entity consolidation significantly.
|
||||
|
||||
Fix:
|
||||
- Add "@id": "https://www.goodwalk.co.nz/#business" to the LocalBusiness block
|
||||
- Reference it as "provider": {"@id": "https://www.goodwalk.co.nz/#business"} in every Service block
|
||||
|
||||
H-5 — @type: LocalBusiness too generic — missing PetCareService
|
||||
|
||||
File: src/routes/+page.svelte ~line 41
|
||||
|
||||
Using ["LocalBusiness", "PetCareService"] as the @type improves entity disambiguation for pet-service queries. No breaking change.
|
||||
|
||||
H-6 — No NZ directory citations
|
||||
|
||||
No codebase evidence of listings on:
|
||||
- Yellow Pages NZ (yellow.co.nz) — highest DA, commonly cited by Google NZ
|
||||
- Finda NZ (finda.co.nz) — pet services category exists
|
||||
- NoCowboys (nocowboys.co.nz) — NZ-specific service directory
|
||||
- Localist (localist.co.nz) — Auckland local business focus
|
||||
- Neighbourly (neighbourly.co.nz) — suburb-level community platform
|
||||
|
||||
Per Whitespark 2026, 3 of the top 5 AI visibility factors are citation signals. For a local NZ service business this gap is material.
|
||||
|
||||
H-7 — Service pages missing from desktop navigation
|
||||
|
||||
The desktop nav only has #services (anchor), /our-pricing, and /about. Direct /pack-walks, /dog-walking, /puppy-visits URLs are not in the desktop nav — only the mobile nav and footer.
|
||||
|
||||
---
|
||||
MEDIUM Priority (Fix Within 1 Month)
|
||||
|
||||
M-1 — All three service pages below 800-word threshold
|
||||
|
||||
Pack Walks (~580 words), 1:1 Walks (~530 words), Puppy Visits (~510 words) all fall below the service page minimum. Missing from each: FAQ section, process explanation unique to that service, breed/situation examples.
|
||||
|
||||
M-2 — H1 lacks "dog walking Auckland" keyword
|
||||
|
||||
The hero H1 is "Come home to a calm, happy dog" — emotionally led but not keyword-anchored. "Auckland Central" appears in the subtitle but the primary query phrase "dog walking Auckland" is absent from any rendered H1
|
||||
on the site. Google's local ranking algorithm still weights explicit keyword presence in H1.
|
||||
|
||||
M-3 — FAQ answers not self-contained for AI citation
|
||||
|
||||
All FAQ answers require context to make sense. For AI Overviews, each answer needs the entity name and full context embedded in the answer itself. Example: "All walks are paid for a week in advance, via invoice" →
|
||||
should read "Goodwalk charges for walks one week in advance via invoice."
|
||||
|
||||
M-4 — www vs non-www in sitemap vs canonical domain
|
||||
|
||||
The sitemap uses https://www.goodwalk.co.nz as the base. Confirm this is the canonical domain (it should be — NGINX enforces the www redirect). The audit was requested for https://goodwalk.co.nz (no www). If both are
|
||||
correct and consistent, no action needed.
|
||||
|
||||
M-5 — lastmod always set to today's date
|
||||
|
||||
File: src/routes/sitemap.xml/+server.ts line ~24
|
||||
|
||||
new Date().toISOString() at request time means every URL always shows today as last-modified. Google treats this as an unreliable freshness signal (may look manipulative). Replace with static per-route dates
|
||||
reflecting actual content changes.
|
||||
|
||||
M-6 — No Content Security Policy header
|
||||
|
||||
File: nginx/goodwalk.co.nz.svelte.conf.example
|
||||
|
||||
No Content-Security-Policy header is set. GA4 is loaded from www.googletagmanager.com without a CSP allowlist. Also missing Permissions-Policy. No immediate SEO impact but a security posture gap.
|
||||
|
||||
M-7 — Phone format inconsistent across schema
|
||||
|
||||
Homepage schema: +64-22-642-1011 (dashes). seo.ts provider: +64226421011 (no dashes). Standardise all schema telephone to E.164 without hyphens: +64226421011.
|
||||
|
||||
M-8 — No CDN / Brotli compression
|
||||
|
||||
All traffic routes to an Auckland VPS with no edge caching. PageSpeed mobile lab scores will be penalised by ~150–300ms TTFB from geography. NGINX has Gzip at level 6 but no Brotli (15–25% better compression).
|
||||
Short-term: enable Brotli or raise Gzip to level 9. Medium-term: add Cloudflare free tier.
|
||||
|
||||
M-9 — Add AI crawler permissions to robots.txt
|
||||
|
||||
File: src/routes/robots.txt/+server.ts
|
||||
|
||||
The robots.txt exists but has no AI crawler-specific directives (GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot). Explicitly allowing these signals GEO awareness and removes ambiguity.
|
||||
|
||||
M-10 — No llms.txt
|
||||
|
||||
A minimal static/llms.txt declaring key pages and a business summary would improve citability on Perplexity and emerging LLM crawlers. Low effort, growing importance.
|
||||
|
||||
M-11 — No business summary in AI citation window (140–160 words)
|
||||
|
||||
No single paragraph on the site answers who/what/where/when/how-much in one citable block. GEO agent provided a ready-to-use 148-word draft in its findings.
|
||||
|
||||
M-12 — areaServed on homepage uses strings, not Place objects
|
||||
|
||||
File: src/routes/+page.svelte lines 57–75
|
||||
|
||||
Homepage hardcodes suburb strings directly. The buildAreaServed() utility in seo.ts already returns correctly typed Place objects — it just needs to be called on the homepage too.
|
||||
|
||||
M-13 — FAQPage schema on commercial site (Google rich results — limited value)
|
||||
|
||||
Files: src/routes/+page.svelte, src/routes/[slug]/+page.svelte
|
||||
|
||||
Google restricted FAQPage rich results to government/healthcare sites (Aug 2023). The FAQPage blocks produce no Google rich results for Goodwalk. However, the schema does provide AI/LLM citation benefit — it's the
|
||||
structured content that Perplexity and Google AI Overviews prefer to extract. Recommendation: Keep FAQPage schema but note it won't produce visual rich results in standard SERPs.
|
||||
|
||||
---
|
||||
LOW Priority (Backlog)
|
||||
|
||||
┌────────────────────────────────────────────────┬───────────────────────┬──────────────────────────────────────────────────────────┐
|
||||
│ Issue │ File │ Notes │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Add priceRange: "$$" to LocalBusiness schema │ +page.svelte │ Appears in knowledge panels │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Add Person schema for Alessandra (founder) │ +page.svelte │ E-E-A-T signal for founder-led business │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Add SearchAction to WebSite schema │ +page.svelte │ Sitelinks Searchbox potential │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Add ContactPage schema to /contact-us │ [slug]/+page.svelte │ Missing structured data on key conversion page │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Add seller to AggregateOffer on service pages │ [slug]/+page.svelte │ Link back to LocalBusiness entity │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Remove @context from nested Review objects │ +page.svelte │ Redundant, technically incorrect placement │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Mobile testimonial carousel CLS │ responsive.css │ Set min-height on mobile stage for longest quote │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ disallow: /api/ in robots.txt │ robots.txt/+server.ts │ Keeps crawl budget on content pages │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ disallow: /contract in robots.txt │ Same │ Zero indexing value, wastes crawl budget │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ HSTS preload directive │ NGINX config │ Required for HSTS preload list submission │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Move large images to src/lib/images/ │ /static/images/ │ Puppy visits image is 3,327×2,217px, served uncompressed │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ terms-and-conditions, privacy-policy → noindex │ static-pages.ts │ Currently indexed; minor │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ IndexNow protocol │ New route │ Bing/Yandex faster indexation │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Founding year in content │ Content files │ "Serving Auckland since [year]" adds trust signal │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Second review platform (NoCowboys NZ) │ GBP/footer │ Review diversity reduces single-platform risk │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ YouTube presence │ External │ Highest-correlation AI citation signal — entirely absent │
|
||||
├────────────────────────────────────────────────┼───────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Question-format H2/H3 headings │ Content files │ Current headings are descriptive, not interrogative │
|
||||
└────────────────────────────────────────────────┴───────────────────────┴──────────────────────────────────────────────────────────┘
|
||||
|
||||
---
|
||||
Backlink Profile
|
||||
|
||||
The backlinks agent was unable to run Python API scripts (Bash access not available in this session). However it identified the following key link building opportunities:
|
||||
|
||||
Unconfirmed NZ directory gaps (verify manually):
|
||||
- Yellow Pages NZ, Finda NZ, NoCowboys, Localist, Neighbourly, Dogz Online NZ, PetPages NZ
|
||||
|
||||
Known competitor backlink sources to audit:
|
||||
Search site:yellow.co.nz "dog walking auckland" and site:nocowboys.co.nz "dog walking" to find listings Goodwalk is missing.
|
||||
|
||||
To get a full scored backlink report, run python scripts/commoncrawl_graph.py goodwalk.co.nz --json via the terminal.
|
||||
|
||||
---
|
||||
What's Working Well
|
||||
|
||||
┌───────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Strength │ Detail │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Full SSR │ SvelteKit SSR with adapter-node — all content in initial HTML, crawlers receive complete pages │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Security headers │ HTTPS, HSTS, X-Frame-Options, X-Content-Type-Options, HSTS all present │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Mobile implementation │ overflow-x: clip, iOS zoom prevention, safe-area-inset-bottom, aria-expanded — thorough │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ LocalBusiness schema │ Comprehensive: opening hours, offer catalog, reviews, sameAs, area served — strong foundation │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ max-snippet:-1 │ Correctly set — allows Google to use any text length for AI Overviews │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ LCP optimisation signals │ fetchpriority="high", loading="eager", decoding="async" on hero image │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ SSR structured data │ JSON-LD is server-rendered in initial HTML, not injected by JS │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Legacy redirects │ /about-us, /booking, /home-stays all 301 correctly via SvelteKit │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Canonical consistency │ siteUrl and NGINX redirect align on https://www.goodwalk.co.nz │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ geo.region / hreflang │ NZ-AUK geo meta and en-NZ hreflang correctly implemented │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Location page content quality │ 17 pages with genuinely unique suburb intros, named parks, leash notes — passes doorway test │
|
||||
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Brand differentiation │ "Tiny Gang," small dog specialist, solo operator with named walker — strong and citable │
|
||||
└───────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
---
|
||||
Prioritised Action Plan
|
||||
|
||||
WEEK 1 — Critical + quick wins
|
||||
[C-1] Remove noindex from /locations/[suburb]/+page.svelte → 5 min
|
||||
[C-2] Add location pages to sitemap.xml/+server.ts → 30 min
|
||||
[C-3] Fix reviewCount: 4 → 30 in AggregateRating → 5 min
|
||||
[H-1] Link suburb names on homepage InfoSection to location pages → 1 hr
|
||||
[H-3] Remove broken <link rel="preload"> from SeoHead → 15 min
|
||||
[H-4] Add @id anchors to LocalBusiness and Service schemas → 30 min
|
||||
[H-5] Change @type to ["LocalBusiness", "PetCareService"] → 5 min
|
||||
[M-7] Standardise telephone to +64226421011 in all schema → 10 min
|
||||
|
||||
WEEK 2 — Performance + citations
|
||||
[H-2] Replace Font Awesome with inline SVG icons → 3 hrs
|
||||
[H-6] Create NZ directory listings (Yellow Pages, Finda, NoCowboys, Localist, Neighbourly)
|
||||
[M-9] Add AI crawler directives to robots.txt → 15 min
|
||||
[M-12] Fix areaServed → call buildAreaServed() on homepage → 15 min
|
||||
[M-5] Replace dynamic lastmod with static per-route dates → 30 min
|
||||
|
||||
MONTH 1 — Content + AI readiness
|
||||
[M-1] Expand service pages to 800+ words each (add FAQ, process, examples)
|
||||
[M-2] Update H1 or subtitle to include "dog walking Auckland"
|
||||
[M-3] Rewrite FAQ answers to be self-contained, entity-named
|
||||
[M-10] Create static/llms.txt → 30 min
|
||||
[M-11] Add 148-word business summary paragraph to About page
|
||||
[M-6] Add Content-Security-Policy and Permissions-Policy headers
|
||||
|
||||
BACKLOG — Nice to have
|
||||
Add Person schema for Alessandra
|
||||
Add priceRange, SearchAction, ContactPage schema
|
||||
Move large images to src/lib/images/ for AVIF/WebP pipeline
|
||||
Enable Brotli on NGINX
|
||||
Add Cloudflare CDN
|
||||
Build YouTube presence
|
||||
Second review platform (NoCowboys NZ)
|
||||
|
||||
---
|
||||
|
||||
▎ SXO agent is still running — I'll append its search experience / intent-match findings when it completes.
|
||||
|
||||
---
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Built by agricidaniel — Join the AI Marketing Hub community
|
||||
🆓 Free → https://www.skool.com/ai-marketing-hub
|
||||
⚡ Pro → https://www.skool.com/ai-marketing-hub-pro
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
✻ Cogitated for 4m 23s
|
||||
|
||||
● Agent "SXO search experience optimization for goodwalk.co.nz" completed
|
||||
|
||||
● The SXO agent is in — here are the additional findings it surfaced:
|
||||
|
||||
---
|
||||
SXO Findings — Search Experience Optimization
|
||||
|
||||
SXO Gap Score: 68 / 100
|
||||
|
||||
Page-Type Analysis
|
||||
|
||||
The site's structure is largely correct — single-provider service pages are the right type for branded and long-tail queries. The dominant structural problem is the same one found in Technical SEO: the 17 location
|
||||
pages are noindexed, which means Goodwalk cannot compete for the high-value suburb-specific queries ("dog walker Mt Eden", "dog walking Ponsonby") where a dedicated location page would beat both a homepage and a
|
||||
directory listing.
|
||||
|
||||
For the head terms "dog walker Auckland" / "dog walking Auckland", the SERP is dominated by Local Pack results (~40–55% of clicks) and aggregator directories (Bark.com, NoCowboys). A single-provider homepage cannot
|
||||
displace aggregators on these terms — the real wins are in the Local Pack (GBP) and in long-tail suburb + service queries where location pages and specific service pages can rank.
|
||||
|
||||
Additional Issues Surfaced by SXO
|
||||
|
||||
HIGH — Homepage <title> wastes 7 characters with "Home |"
|
||||
Current: "Home | Auckland Dog Walking | Goodwalk"
|
||||
The "Home |" prefix is dead space and signals a generic page in the SERP snippet. Replace with:
|
||||
"Auckland Dog Walker | Pack Walks & 1:1 Walks | Goodwalk"
|
||||
|
||||
HIGH — Homepage meta description missing price anchor, suburbs, and trust signal
|
||||
Current: "At Goodwalk, we offer Tiny Gang pack walks and one on one dog walking services throughout Auckland..."
|
||||
A competitor writing "Trusted by 30+ Auckland families. Pack walks from $49.50. Free Meet & Greet. Covering Mt Eden, Ponsonby, Grey Lynn & more." will outperform on click-through rate.
|
||||
|
||||
HIGH — "Always Alessandra" message is buried 2–3 scrolls deep
|
||||
The single-named-walker proposition is the strongest brand differentiator but appears in the Promise section, not above the fold. Moving it into or immediately below the hero would improve conversion for all personas.
|
||||
|
||||
MEDIUM — "Reactive/anxious dog" use case not addressed
|
||||
Kate's testimonial mentions "slightly hyper and anxious dog" but nowhere on the site do the words "reactive", "anxious", or "nervous dog" appear as a heading or FAQ answer. This is a high-intent search pattern ("dog
|
||||
walker for anxious dog Auckland") that the site could own with a single FAQ item on the 1:1 Walks page.
|
||||
|
||||
MEDIUM — Price anchor missing from homepage above-the-fold
|
||||
Price-sensitive users won't scroll to pricing. Add "Pack walks from $49.50" to the homepage hero or intro strip.
|
||||
|
||||
MEDIUM — Phone number not in hero or header
|
||||
(022) 642 1011 only appears in footer and About page. For a mobile service business, a visible tel: link in the header reduces friction for users who want to call before committing to a form.
|
||||
|
||||
MEDIUM — /our-pricing missing question-format H2
|
||||
Adding <h2>How much does dog walking cost in Auckland?</h2> to the pricing page directly targets a PAA (People Also Ask) box question and could earn a Featured Snippet position.
|
||||
|
||||
LOW — /dog-walking page targets "1:1 walks" but users search "private dog walker"
|
||||
The slug (/dog-walking) is excellent, but the H1 and title target "1:1 Dog Walks" — a low-volume phrase. "Private dog walker Auckland" has meaningfully higher search volume and would be a stronger secondary keyword
|
||||
target for this page.
|
||||
|
||||
CONFIRM — MobileBookBar feature flag
|
||||
The sticky mobile CTA uses isMobileCtaButtonEnabled() feature flag. Confirm this is active in production — if disabled, the site is missing a high-conversion mobile element.
|
||||
|
||||
---
|
||||
Updated Priority Action Plan (with SXO additions)
|
||||
|
||||
WEEK 1 — Critical fixes + zero-effort wins
|
||||
[C-1] Remove noindex from location pages → 5 min
|
||||
[C-2] Add location pages to sitemap → 30 min
|
||||
[C-3] Fix reviewCount: 4 → 30 in AggregateRating schema → 5 min
|
||||
[SXO] Rewrite homepage <title> (remove "Home |") → 5 min
|
||||
[SXO] Rewrite homepage meta description (add suburbs, price, reviews) → 15 min
|
||||
[H-1] Link suburb names on homepage to location pages → 1 hr
|
||||
[H-3] Remove broken hero image <link rel="preload"> → 15 min
|
||||
[H-4] Add @id anchors to LocalBusiness + Service schema → 30 min
|
||||
[H-5] Change @type to ["LocalBusiness", "PetCareService"] → 5 min
|
||||
[M-7] Standardise telephone to +64226421011 in all schema → 10 min
|
||||
|
||||
WEEK 2 — Performance + conversion + citations
|
||||
[H-2] Replace Font Awesome with inline SVG icons → 3 hrs
|
||||
[SXO] Move "always Alessandra" message above the fold → 30 min
|
||||
[SXO] Add price anchor ("Pack walks from $49.50") to homepage hero → 15 min
|
||||
[SXO] Add phone number to header with tel: link → 15 min
|
||||
[SXO] Add reactive/anxious dog FAQ to homepage + 1:1 Walks page → 30 min
|
||||
[H-6] Build NZ directory listings (Yellow Pages, Finda, NoCowboys, Localist)
|
||||
[M-9] Add AI crawler directives to robots.txt → 15 min
|
||||
[M-5] Replace dynamic lastmod with static per-route dates → 30 min
|
||||
|
||||
MONTH 1 — Content depth + AI readiness
|
||||
[M-1] Expand service pages to 800+ words (FAQ, process, examples)
|
||||
[SXO] Add question-format H2 to /our-pricing page → 10 min
|
||||
[SXO] Add "private dog walker Auckland" keyword to /dog-walking → 30 min
|
||||
[M-3] Rewrite FAQ answers to be self-contained + entity-named
|
||||
[M-10] Create static/llms.txt → 30 min
|
||||
[M-11] Add 148-word business summary paragraph to About page
|
||||
[M-6] Add CSP and Permissions-Policy headers
|
||||
|
||||
BACKLOG
|
||||
Add Person schema for Alessandra + priceRange + SearchAction
|
||||
Move large images to src/lib/images/ for AVIF/WebP pipeline
|
||||
Enable Brotli on NGINX / add Cloudflare CDN
|
||||
Build NoCowboys review profile
|
||||
Build YouTube presence
|
||||
Blog / educational content for informational queries
|
||||
|
||||
---
|
||||
The complete audit is now in. All 10 specialist agents have reported. The dominant theme across every analysis: remove noindex from the location pages — it's a one-line change that unlocks the site's entire local SEO
|
||||
strategy.
|
||||
|
||||
---
|
||||
@@ -132,10 +132,13 @@ function Export-HomepageContent {
|
||||
throw "Homepage content export script not found: $scriptPath"
|
||||
}
|
||||
|
||||
$resolverPath = Join-Path $ProjectPath 'scripts\sveltekit-resolver.mjs'
|
||||
|
||||
Push-Location $ProjectPath
|
||||
try {
|
||||
Invoke-External -FilePath 'node' -Arguments @(
|
||||
'--experimental-strip-types',
|
||||
"--import=$(([uri]::new([System.IO.Path]::GetFullPath($resolverPath))).AbsoluteUri)",
|
||||
$scriptPath,
|
||||
$OutputPath
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
OWNER_EMAIL: ${OWNER_EMAIL}
|
||||
OWNER_BCC: ${OWNER_BCC:-}
|
||||
CLIENT_BCC: ${CLIENT_BCC:-}
|
||||
FROM_EMAIL: ${FROM_EMAIL:-GoodWalk <bookings@goodwalk.co.nz>}
|
||||
FROM_EMAIL: ${FROM_EMAIL:-GoodWalk <info@goodwalk.co.nz>}
|
||||
REPLY_TO: ${REPLY_TO:-aless@goodwalk.co.nz}
|
||||
ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false}
|
||||
FORM_MIN_SECONDS: ${FORM_MIN_SECONDS:-4}
|
||||
|
||||
@@ -3,5 +3,10 @@
|
||||
"fullName": "Matt Test",
|
||||
"phone": "02124347477",
|
||||
"dogName": "Geoffrey"
|
||||
},
|
||||
"natalie@desseinparke.com": {
|
||||
"fullName":"Natalie Parke",
|
||||
"phone": "021616200",
|
||||
"dogName": "Ziggy"
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,18 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/auth/ {
|
||||
set $goodwalk_mail_api goodwalk_svelte_mail_api:8000;
|
||||
limit_req zone=goodwalk_limit burst=10 nodelay;
|
||||
rewrite ^/api/auth/(.*)$ /auth/$1 break;
|
||||
proxy_pass http://$goodwalk_mail_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
if (-f /etc/nginx/conf.d/maintenance.flag) {
|
||||
return 503;
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"pg": "^8.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/fredoka": "^5.2.10",
|
||||
"@fontsource/readex-pro": "^5.2.11",
|
||||
"@fontsource/unbounded": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@sveltejs/adapter-node": "^5.2.11",
|
||||
"@sveltejs/enhanced-img": "^0.10.4",
|
||||
"@sveltejs/kit": "^2.59.0",
|
||||
@@ -746,6 +750,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/fredoka": {
|
||||
"version": "5.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/fredoka/-/fredoka-5.2.10.tgz",
|
||||
"integrity": "sha512-DyzPCmTf0PkBbAu+gNvnPzfD/dYIXlyp+Zcb76jEQyt/vmRKGZgkH8FFF9W2sICDaA5p+GLk1XGFjWh5PPX+lg==",
|
||||
"dev": true,
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/readex-pro": {
|
||||
"version": "5.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/readex-pro/-/readex-pro-5.2.11.tgz",
|
||||
"integrity": "sha512-MOPXkNp5mJsU05j9/vpiarDtFzW+WDgbTHwia2CS/SokEmDQ94WUFSP0ryhv/f90RsgU4LktnFKiDz7E3EUfoQ==",
|
||||
"dev": true,
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/unbounded": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/unbounded/-/unbounded-5.2.8.tgz",
|
||||
"integrity": "sha512-tBHtKB+s5tKvQXEWy5h86sDY5hzVLsEbW6TGphSjUY9b/gawUPvXv5vFS4ZdPbGDcvSqMMy6eXkqdcsKI29dig==",
|
||||
"dev": true,
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz",
|
||||
"integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==",
|
||||
"dev": true,
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"pg": "^8.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/fredoka": "^5.2.10",
|
||||
"@fontsource/readex-pro": "^5.2.11",
|
||||
"@fontsource/unbounded": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@sveltejs/adapter-node": "^5.2.11",
|
||||
"@sveltejs/enhanced-img": "^0.10.4",
|
||||
"@sveltejs/kit": "^2.59.0",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import sharp from 'sharp';
|
||||
import { writeFile, unlink, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
// Opaque-photo PNGs to convert to JPG (verified via metadata: no alpha)
|
||||
const targets = [
|
||||
'archie-auckland-dog-walking-review',
|
||||
'monty-auckland-dog-walking-review',
|
||||
'otis-auckland-dog-walking-review',
|
||||
'wallace-auckland-dog-walking-review',
|
||||
'one-on-one-dog-portrait-1',
|
||||
'one-on-one-dog-portrait-2',
|
||||
'one-on-one-dog-portrait-3',
|
||||
'small-medium-dogs-pack-walk',
|
||||
'auckland-pack-walk-small-dogs-group',
|
||||
'founder-image-aless-goodwalk'
|
||||
];
|
||||
|
||||
const dirs = ['src/lib/images', 'static/images'];
|
||||
const MAX_WIDTH = 1600;
|
||||
|
||||
let totalOrig = 0;
|
||||
let totalNew = 0;
|
||||
for (const dir of dirs) {
|
||||
for (const name of targets) {
|
||||
const png = join(dir, name + '.png');
|
||||
const jpg = join(dir, name + '.jpg');
|
||||
try {
|
||||
const orig = (await stat(png)).size;
|
||||
const buf = await sharp(png)
|
||||
.rotate()
|
||||
.resize({ width: MAX_WIDTH, withoutEnlargement: true })
|
||||
.jpeg({ quality: 82, mozjpeg: true, progressive: true })
|
||||
.toBuffer();
|
||||
await writeFile(jpg, buf);
|
||||
await unlink(png);
|
||||
totalOrig += orig;
|
||||
totalNew += buf.length;
|
||||
console.log(`${name.padEnd(45)} ${(orig/1024).toFixed(0).padStart(5)}KB png → ${(buf.length/1024).toFixed(0).padStart(4)}KB jpg`);
|
||||
} catch (err) {
|
||||
console.error('FAILED', png, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`\nTotal: ${(totalOrig/1024/1024).toFixed(2)} MB → ${(totalNew/1024/1024).toFixed(2)} MB`);
|
||||
@@ -429,6 +429,22 @@ if (( nginx_args_present )); then
|
||||
[[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE"
|
||||
[[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE"
|
||||
|
||||
# Pre-flight: SSL certificate for onboarding.goodwalk.co.nz must exist before
|
||||
# nginx can reload — the config references this cert path directly.
|
||||
ONBOARDING_CERT="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/fullchain.pem"
|
||||
ONBOARDING_KEY="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/privkey.pem"
|
||||
if [[ ! -f "$ONBOARDING_CERT" || ! -f "$ONBOARDING_KEY" ]]; then
|
||||
fail "SSL certificate for onboarding.goodwalk.co.nz is not present on this server.
|
||||
Expected: $ONBOARDING_CERT
|
||||
One-time setup on the droplet:
|
||||
1. Ensure the DNS A record for onboarding.goodwalk.co.nz points to this server's IP
|
||||
2. Bring nginx up so the ACME webroot is reachable, then obtain the certificate:
|
||||
certbot certonly --webroot -w /var/www/certbot \\
|
||||
-d onboarding.goodwalk.co.nz \\
|
||||
--non-interactive --agree-tos -m info@goodwalk.co.nz
|
||||
3. Re-run this deploy script"
|
||||
fi
|
||||
|
||||
MAINTENANCE_HTML_SRC="$DEPLOY_PATH/nginx/maintenance.html"
|
||||
MAINTENANCE_LOGO_SRC="$DEPLOY_PATH/nginx/logo.png"
|
||||
[[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC"
|
||||
|
||||
@@ -287,6 +287,22 @@ if (( nginx_args_present )); then
|
||||
[[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE"
|
||||
[[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE"
|
||||
|
||||
# Pre-flight: SSL certificate for onboarding.goodwalk.co.nz must exist before
|
||||
# nginx can reload — the config references this cert path directly.
|
||||
ONBOARDING_CERT="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/fullchain.pem"
|
||||
ONBOARDING_KEY="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/privkey.pem"
|
||||
if [[ ! -f "$ONBOARDING_CERT" || ! -f "$ONBOARDING_KEY" ]]; then
|
||||
fail "SSL certificate for onboarding.goodwalk.co.nz is not present on this server.
|
||||
Expected: $ONBOARDING_CERT
|
||||
One-time setup on the droplet:
|
||||
1. Ensure the DNS A record for onboarding.goodwalk.co.nz points to this server's IP
|
||||
2. Bring nginx up so the ACME webroot is reachable, then obtain the certificate:
|
||||
certbot certonly --webroot -w /var/www/certbot \\
|
||||
-d onboarding.goodwalk.co.nz \\
|
||||
--non-interactive --agree-tos -m info@goodwalk.co.nz
|
||||
3. Re-run this deploy script"
|
||||
fi
|
||||
|
||||
MAINTENANCE_HTML_SRC="$DEPLOY_PATH/nginx/maintenance.html"
|
||||
MAINTENANCE_LOGO_SRC="$DEPLOY_PATH/nginx/logo.png"
|
||||
[[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import sharp from 'sharp';
|
||||
import { readdir, stat, rename } from 'node:fs/promises';
|
||||
import { join, extname, basename } from 'node:path';
|
||||
|
||||
const MAX_WIDTH = 1600;
|
||||
const MIN_BYTES_TO_OPTIMISE = 250 * 1024;
|
||||
|
||||
const dirs = ['src/lib/images', 'static/images'];
|
||||
|
||||
async function optimiseFile(file) {
|
||||
const ext = extname(file).toLowerCase();
|
||||
const input = await sharp(file, { failOn: 'none' }).rotate();
|
||||
const meta = await input.metadata();
|
||||
const width = meta.width ?? 0;
|
||||
const targetWidth = width > MAX_WIDTH ? MAX_WIDTH : width;
|
||||
const pipeline = sharp(file, { failOn: 'none' })
|
||||
.rotate()
|
||||
.resize({ width: targetWidth, withoutEnlargement: true });
|
||||
|
||||
let buf;
|
||||
if (ext === '.png') {
|
||||
if (meta.hasAlpha) {
|
||||
buf = await pipeline
|
||||
.png({ palette: true, quality: 88, compressionLevel: 9, effort: 10 })
|
||||
.toBuffer();
|
||||
} else {
|
||||
buf = await pipeline
|
||||
.png({ palette: true, quality: 82, compressionLevel: 9, effort: 10 })
|
||||
.toBuffer();
|
||||
}
|
||||
} else if (ext === '.jpg' || ext === '.jpeg') {
|
||||
buf = await pipeline.jpeg({ quality: 82, mozjpeg: true }).toBuffer();
|
||||
} else if (ext === '.webp') {
|
||||
buf = await pipeline.webp({ quality: 80, effort: 6 }).toBuffer();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const original = (await stat(file)).size;
|
||||
if (buf.length >= original) return { file, original, optimised: original, skipped: true };
|
||||
|
||||
const tmp = file + '.opt.tmp';
|
||||
await sharp(buf).toFile(tmp);
|
||||
await rename(tmp, file);
|
||||
return { file, original, optimised: buf.length, skipped: false };
|
||||
}
|
||||
|
||||
let totalOrig = 0;
|
||||
let totalNew = 0;
|
||||
for (const dir of dirs) {
|
||||
const entries = await readdir(dir);
|
||||
for (const name of entries) {
|
||||
if (!/\.(png|jpe?g|webp)$/i.test(name)) continue;
|
||||
const file = join(dir, name);
|
||||
const s = await stat(file);
|
||||
if (s.size < MIN_BYTES_TO_OPTIMISE) continue;
|
||||
try {
|
||||
const res = await optimiseFile(file);
|
||||
if (!res) continue;
|
||||
totalOrig += res.original;
|
||||
totalNew += res.optimised;
|
||||
const pct = ((1 - res.optimised / res.original) * 100).toFixed(0);
|
||||
const flag = res.skipped ? ' (skipped: no gain)' : '';
|
||||
console.log(
|
||||
`${basename(file).padEnd(58)} ${(res.original / 1024).toFixed(0).padStart(5)}KB → ${(res.optimised / 1024).toFixed(0).padStart(5)}KB (-${pct}%)${flag}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('FAILED', file, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`\nTotal: ${(totalOrig / 1024 / 1024).toFixed(2)} MB → ${(totalNew / 1024 / 1024).toFixed(2)} MB`);
|
||||
@@ -0,0 +1,24 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const libRoot = path.join(projectRoot, 'src', 'lib');
|
||||
|
||||
export function resolve(specifier, context, nextResolve) {
|
||||
if (specifier.startsWith('$lib/')) {
|
||||
const relative = specifier.slice('$lib/'.length);
|
||||
const candidates = [
|
||||
path.join(libRoot, relative + '.ts'),
|
||||
path.join(libRoot, relative + '.js'),
|
||||
path.join(libRoot, relative, 'index.ts'),
|
||||
path.join(libRoot, relative, 'index.js'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
return nextResolve(pathToFileURL(candidate).href, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { register } from 'node:module';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
register(
|
||||
pathToFileURL(path.join(scriptDir, 'sveltekit-hooks.mjs')).href,
|
||||
pathToFileURL(scriptDir + '/')
|
||||
);
|
||||
@@ -7,31 +7,6 @@
|
||||
<link rel="icon" href="/images/goodwalk-favicon-32.png" sizes="32x32" type="image/png" />
|
||||
<link rel="icon" href="/images/goodwalk-favicon-192.png" sizes="192x192" type="image/png" />
|
||||
<link rel="apple-touch-icon" href="/images/goodwalk-favicon-192.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Readex+Pro:wght@400;500;600;700&family=Unbounded:wght@400;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://cdnjs.cloudflare.com"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<!-- Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-K7TLSFJVP1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-K7TLSFJVP1');
|
||||
</script>
|
||||
<style>
|
||||
.no-js-overlay {
|
||||
position: fixed;
|
||||
@@ -100,5 +75,13 @@
|
||||
</div>
|
||||
</noscript>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
<!-- Google Analytics — deferred to end of body so it never blocks the critical render -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-K7TLSFJVP1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-K7TLSFJVP1');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -409,15 +409,6 @@
|
||||
</a>
|
||||
<span class="contract-pill">Contract</span>
|
||||
</div>
|
||||
<div class="contract-topbar-right">
|
||||
{#if preview}
|
||||
<span class="contract-preview-flag">Preview</span>
|
||||
{/if}
|
||||
<a href={`tel:${ownerPhone.replace(/[^0-9+]/g, '')}`} class="contract-topbar-phone">
|
||||
<Icon name="fas fa-phone" />
|
||||
<span class="contract-topbar-phone-label">{ownerPhone}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -894,7 +885,7 @@
|
||||
|
||||
/* ── Top nav bar ── */
|
||||
.contract-topbar {
|
||||
background: #213021;
|
||||
background: var(--gw-green);
|
||||
}
|
||||
|
||||
.contract-topbar-inner {
|
||||
@@ -926,49 +917,14 @@
|
||||
.contract-pill {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
margin-left:10pt;
|
||||
background: var(--yellow);
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.contract-topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.contract-topbar-phone {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.contract-topbar-phone:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.contract-preview-flag {
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 209, 0, 0.25);
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #ffd100;
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
/* ── Journey bar ── */
|
||||
@@ -1722,10 +1678,6 @@
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.contract-topbar-phone-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contract-hero {
|
||||
padding: 20px 0 16px;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { accordion } from '$lib/actions/accordion';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { locationPages } from '$lib/content/locations';
|
||||
import type { InfoContent } from '$lib/types';
|
||||
|
||||
export let info: InfoContent;
|
||||
|
||||
const slugBySuburb = new Map(locationPages.map((loc) => [loc.suburb, loc.slug]));
|
||||
|
||||
$: suburbChips = info.suburbs
|
||||
.split(',')
|
||||
.map((suburb) => suburb.trim())
|
||||
.filter(Boolean);
|
||||
.map((suburb) => suburb.trim().replace(/\.$/, ''))
|
||||
.filter(Boolean)
|
||||
.map((suburb) => ({ name: suburb, slug: slugBySuburb.get(suburb) ?? null }));
|
||||
</script>
|
||||
|
||||
<section id="info">
|
||||
@@ -19,8 +23,12 @@
|
||||
<p class="info-support">Regular walks across the inner-west and nearby suburbs.</p>
|
||||
|
||||
<div class="info-suburb-chips" aria-label="Suburbs we cover">
|
||||
{#each suburbChips as suburb}
|
||||
<span class="info-suburb-chip">{suburb}</span>
|
||||
{#each suburbChips as { name, slug }}
|
||||
{#if slug}
|
||||
<a class="info-suburb-chip" href="/locations/{slug}">{name}</a>
|
||||
{:else}
|
||||
<span class="info-suburb-chip">{name}</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -83,6 +91,13 @@
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.info-suburb-chip:hover {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(33, 48, 33, 0.25),
|
||||
0 10px 24px rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.info-nearby-card {
|
||||
|
||||
@@ -28,20 +28,15 @@
|
||||
|
||||
<footer class="ob-footer">
|
||||
<div class="ob-footer-inner">
|
||||
<div class="ob-footer-identity">
|
||||
<Icon name="fas fa-circle-check" />
|
||||
<span>Signed in as <strong>{email}</strong></span>
|
||||
</div>
|
||||
<a href="https://goodwalk.co.nz" class="ob-footer-back">
|
||||
<Icon name="fas fa-arrow-left" />
|
||||
Back to main site
|
||||
</a>
|
||||
<button class="ob-footer-logout" on:click={logout} disabled={loggingOut}>
|
||||
<Icon name="fas fa-right-from-bracket" />
|
||||
{loggingOut ? 'Signing out…' : 'Sign out'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="ob-footer-copyright">
|
||||
<a href="https://goodwalk.co.nz">goodwalk.co.nz</a>
|
||||
<span>·</span>
|
||||
<span>© {new Date().getFullYear()} Goodwalk. All rights reserved.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
@@ -61,17 +56,20 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ob-footer-identity {
|
||||
display: flex;
|
||||
.ob-footer-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.ob-footer-identity strong {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 600;
|
||||
.ob-footer-back:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ob-footer-logout {
|
||||
@@ -95,39 +93,9 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ob-footer-copyright {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 28px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ob-footer-copyright a {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ob-footer-copyright a:hover {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ob-footer-inner {
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
.ob-footer-identity span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ob-footer-copyright {
|
||||
padding: 10px 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -406,6 +406,7 @@
|
||||
<div class="onboarding-topbar">
|
||||
<div class="onboarding-shell onboarding-topbar-inner">
|
||||
<div class="onboarding-brand">
|
||||
<span class="onboarding-pill">Onboarding</span>
|
||||
<a href="https://goodwalk.co.nz" class="onboarding-logo" aria-label="Goodwalk home">
|
||||
<picture>
|
||||
{#if desktop.sources?.webp}
|
||||
@@ -420,16 +421,6 @@
|
||||
/>
|
||||
</picture>
|
||||
</a>
|
||||
<span class="onboarding-pill">Onboarding</span>
|
||||
</div>
|
||||
<div class="onboarding-topbar-right">
|
||||
{#if preview}
|
||||
<span class="onboarding-preview-flag">Preview</span>
|
||||
{/if}
|
||||
<a href={`tel:${ownerPhone.replace(/[^0-9+]/g, '')}`} class="onboarding-topbar-phone">
|
||||
<Icon name="fas fa-phone" />
|
||||
<span class="onboarding-topbar-phone-label">{ownerPhone}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1006,49 +997,13 @@
|
||||
.onboarding-pill {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
background: var(--yellow);
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.onboarding-topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.onboarding-topbar-phone {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.onboarding-topbar-phone:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.onboarding-preview-flag {
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 209, 0, 0.25);
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #ffd100;
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
/* ── Idle warning ── */
|
||||
@@ -1582,10 +1537,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onboarding-topbar-phone-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onboarding-hero {
|
||||
padding: 20px 0 16px;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
export let type = 'website';
|
||||
export let structuredData: Record<string, unknown>[] = [];
|
||||
export let noindex = false;
|
||||
export let preloadImage = false;
|
||||
export let preloadImage = false; // kept for API compatibility — preload is handled by fetchpriority="high" on the image element
|
||||
|
||||
const siteName = 'Goodwalk';
|
||||
const siteUrl = 'https://www.goodwalk.co.nz';
|
||||
@@ -50,9 +50,6 @@
|
||||
<meta name="geo.region" content="NZ-AUK" />
|
||||
<meta name="geo.placename" content="Auckland Central" />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
{#if preloadImage}
|
||||
<link rel="preload" as="image" href={imageUrl} />
|
||||
{/if}
|
||||
<link rel="alternate" hreflang="en-NZ" href={canonicalUrl} />
|
||||
<link rel="alternate" hreflang="x-default" href={canonicalUrl} />
|
||||
|
||||
|
||||
@@ -22,28 +22,28 @@
|
||||
detail: "Archie's mum",
|
||||
quote:
|
||||
'Love Aless! She is so amazing with my slightly hyper and anxious dog. She is great with communication if anything on either of our ends need to change. Archie love his walks, and I love the photos she posts of him.',
|
||||
imageUrl: '/images/archie-auckland-dog-walking-review.png'
|
||||
imageUrl: '/images/archie-auckland-dog-walking-review.jpg'
|
||||
},
|
||||
Estelle: {
|
||||
reviewer: 'Estelle',
|
||||
detail: "Monty's mum",
|
||||
quote:
|
||||
'GoodWalk was the best dog walking service for my little pooch ! Aless was very helpful - basically doubled as a second mum to Monty. She always provided feedback on his outings and assisted where possible with any additional training that she felt he could work on and made recommendations where necessary which i feel is what every dog mum wants and needs!',
|
||||
imageUrl: '/images/monty-auckland-dog-walking-review.png'
|
||||
imageUrl: '/images/monty-auckland-dog-walking-review.jpg'
|
||||
},
|
||||
Ross: {
|
||||
reviewer: 'Ross',
|
||||
detail: "Otis's dad",
|
||||
quote:
|
||||
'Truly the best dog walker in Auckland! I feel so lucky to have found Aless and my little terrier Otis absolutely adores her. He enjoys his regular weekly walks and always comes back happy & tired. Love the updates on social media so I can see how my dog is enjoying his day! Aless makes logistics so easy too. Highly highly recommend, there’s a reason she has 5 stars!',
|
||||
imageUrl: '/images/otis-auckland-dog-walking-review.png'
|
||||
imageUrl: '/images/otis-auckland-dog-walking-review.jpg'
|
||||
},
|
||||
Nina: {
|
||||
reviewer: 'Nina',
|
||||
detail: "Wallace's mum",
|
||||
quote:
|
||||
'Alessandra has been walking and spending time with my pup since she was 10 weeks old, coming over and doing puppy visits through to transitioning her to pack walks with her little doggo friends. I know Alassandra loves and cares for my dog as much as I do and my dog has a great time! Cant recommend enough',
|
||||
imageUrl: '/images/wallace-auckland-dog-walking-review.png'
|
||||
imageUrl: '/images/wallace-auckland-dog-walking-review.jpg'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -543,7 +543,7 @@
|
||||
|
||||
.testimonial-woof-text {
|
||||
display: inline-block;
|
||||
font-family: 'Fredoka One', var(--font-head), sans-serif;
|
||||
font-family: 'Fredoka', 'Fredoka One', var(--font-head), sans-serif;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
|
||||
@@ -10,7 +10,7 @@ export const aboutPageContent: AboutPageContent = {
|
||||
"Goodwalk is built around Alessandra — who started this because she couldn't find a walker she actually trusted, and hasn't stopped showing up the same way since. She walks every dog herself, posts updates to Instagram so you can see exactly what your dog is up to, and has built relationships with some dogs from as young as ten weeks old. Thirty-plus five-star Google reviews later, the feedback keeps saying the same thing: the dogs adore her, and their owners finally stop worrying.",
|
||||
"We specialise in small and medium dogs because we understand them — not as a category, but as actual dogs with specific needs, specific quirks, and specific ways they fall apart in the wrong environment. The pace of a walk matters. The size of the group matters. The temperament of the other dogs matters. That's why we built a service around them, not just one that fits them in."
|
||||
],
|
||||
imageUrl: '/images/auckland-pack-walk-small-dogs-group.png',
|
||||
imageUrl: '/images/auckland-pack-walk-small-dogs-group.jpg',
|
||||
imageAlt: "Small dogs from Goodwalk's Tiny Gang pack walk sitting together in an Auckland park"
|
||||
},
|
||||
{
|
||||
@@ -32,7 +32,7 @@ export const aboutPageContent: AboutPageContent = {
|
||||
"Alessandra started Goodwalk because she couldn't find a walker she actually trusted with Maya. So she became one. Italian-born and Auckland Central-based, she leads every walk herself — not because she has to, but because handing that off was never something she was willing to do. The dogs she walks have her full attention. Their owners have her number.",
|
||||
"Maya is a Cavalier King Charles cross Shih Tzu, and the reason small dogs sit at the centre of everything Goodwalk does. She is opinionated, dramatic when it rains, and completely impossible to ignore on a walk. She is also the best argument we have for why small dogs deserve a service built specifically around them — not just accommodated by one."
|
||||
],
|
||||
imageUrl: '/images/founder-image-aless-goodwalk.png',
|
||||
imageUrl: '/images/founder-image-aless-goodwalk.jpg',
|
||||
imageAlt: 'Alessandra, founder of Goodwalk Auckland',
|
||||
accent: 'founder'
|
||||
}
|
||||
|
||||
@@ -20,15 +20,15 @@ export const dogWalkingContent: ServicePageContent = {
|
||||
imageAlt: 'Goodwalk dogs gathered together outdoors',
|
||||
collageImages: [
|
||||
{
|
||||
imageUrl: '/images/one-on-one-dog-portrait-1.png',
|
||||
imageUrl: '/images/one-on-one-dog-portrait-1.jpg',
|
||||
imageAlt: 'Happy black dog on a one-on-one Goodwalk walk in Auckland'
|
||||
},
|
||||
{
|
||||
imageUrl: '/images/one-on-one-dog-portrait-2.png',
|
||||
imageUrl: '/images/one-on-one-dog-portrait-2.jpg',
|
||||
imageAlt: 'Older black dog enjoying a calm one-on-one Goodwalk walk in Auckland'
|
||||
},
|
||||
{
|
||||
imageUrl: '/images/one-on-one-dog-portrait-3.png',
|
||||
imageUrl: '/images/one-on-one-dog-portrait-3.jpg',
|
||||
imageAlt: 'Brown curly dog resting during a one-on-one Goodwalk walk in Auckland'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,9 +3,9 @@ import { sharedServices } from '$lib/content/services';
|
||||
|
||||
export const homepageContent: HomePageContent = {
|
||||
seo: {
|
||||
title: 'Home | Auckland Dog Walking | Goodwalk',
|
||||
title: 'Auckland Dog Walker | Pack Walks & 1:1 Walks | Goodwalk',
|
||||
description:
|
||||
'At Goodwalk, we offer Tiny Gang pack walks and one on one dog walking services throughout Auckland. Give your dog his best life with Goodwalk!'
|
||||
'Trusted by 30+ Auckland families. Pack walks from $49.50. Free Meet & Greet. Covering Mt Eden, Ponsonby, Grey Lynn, Kingsland & more. Book online today.'
|
||||
},
|
||||
navigation: {
|
||||
desktopLinks: [
|
||||
@@ -65,7 +65,7 @@ export const homepageContent: HomePageContent = {
|
||||
],
|
||||
emphasis: 'TINY GANG?',
|
||||
cta: { label: 'Book a free Meet & Greet', href: '/contact-us', variant: 'green' },
|
||||
imageUrl: '/images/founder-image-aless-goodwalk.png',
|
||||
imageUrl: '/images/founder-image-aless-goodwalk.jpg',
|
||||
imageAlt: 'Alessandra from Goodwalk with a dog in Auckland'
|
||||
},
|
||||
services: sharedServices.map((service) => ({
|
||||
|
||||
@@ -10,13 +10,13 @@ export const packWalksContent: ServicePageContent = {
|
||||
'Tiny Gang is best suited to sociable small and medium dogs who enjoy being around other dogs. If your dog would be better with a quieter, more individual setup, our 1:1 walks may be a better fit.',
|
||||
'We run pack walks across Auckland Central — including Mt Eden, Kingsland, Ponsonby, Grey Lynn, Sandringham, Mt Albert, and surrounding suburbs — with free pickup and drop-off included in every booking.'
|
||||
],
|
||||
imageUrl: '/images/auckland-pack-walk-small-dogs-group.png',
|
||||
imageUrl: '/images/auckland-pack-walk-small-dogs-group.jpg',
|
||||
imageAlt: "Small dogs from Goodwalk's Tiny Gang pack walk sitting together in an Auckland park"
|
||||
},
|
||||
highlight: {
|
||||
eyebrow: 'Small packs. Calm dogs.',
|
||||
title: 'Made specifically for small and medium dogs who do best in a structured social group',
|
||||
imageUrl: '/images/small-medium-dogs-pack-walk.png',
|
||||
imageUrl: '/images/small-medium-dogs-pack-walk.jpg',
|
||||
imageAlt: 'Small and medium dogs together on a Goodwalk pack walk in Auckland'
|
||||
},
|
||||
pricing: {
|
||||
|
||||
@@ -8,16 +8,16 @@ const imageMetadata: Record<string, ImageMetadata> = {
|
||||
'/images/goodwalk-auckland-dog-walking-logo-mobile.png': { width: 206, height: 41 },
|
||||
'/images/auckland-dog-walking-happy-dog-hero.png': { width: 500, height: 500 },
|
||||
'/images/auckland-dog-walking-happy-dogs-happy-humans.webp': { width: 1222, height: 1312 },
|
||||
'/images/archie-auckland-dog-walking-review.png': { width: 1122, height: 1402 },
|
||||
'/images/monty-auckland-dog-walking-review.png': { width: 1254, height: 1254 },
|
||||
'/images/otis-auckland-dog-walking-review.png': { width: 1254, height: 1254 },
|
||||
'/images/wallace-auckland-dog-walking-review.png': { width: 1254, height: 1254 },
|
||||
'/images/archie-auckland-dog-walking-review.jpg': { width: 1122, height: 1402 },
|
||||
'/images/monty-auckland-dog-walking-review.jpg': { width: 1254, height: 1254 },
|
||||
'/images/otis-auckland-dog-walking-review.jpg': { width: 1254, height: 1254 },
|
||||
'/images/wallace-auckland-dog-walking-review.jpg': { width: 1254, height: 1254 },
|
||||
'/images/auckland-small-dog-pack-walk.jpg': { width: 640, height: 480 },
|
||||
'/images/auckland-pack-walk-small-dogs-group.png': { width: 1469, height: 1071 },
|
||||
'/images/small-medium-dogs-pack-walk.png': { width: 1240, height: 1269 },
|
||||
'/images/one-on-one-dog-portrait-1.png': { width: 1054, height: 1492 },
|
||||
'/images/one-on-one-dog-portrait-2.png': { width: 1091, height: 1441 },
|
||||
'/images/one-on-one-dog-portrait-3.png': { width: 1124, height: 1399 },
|
||||
'/images/auckland-pack-walk-small-dogs-group.jpg': { width: 1469, height: 1071 },
|
||||
'/images/small-medium-dogs-pack-walk.jpg': { width: 1240, height: 1269 },
|
||||
'/images/one-on-one-dog-portrait-1.jpg': { width: 1054, height: 1492 },
|
||||
'/images/one-on-one-dog-portrait-2.jpg': { width: 1091, height: 1441 },
|
||||
'/images/one-on-one-dog-portrait-3.jpg': { width: 1124, height: 1399 },
|
||||
'/images/tiny-gang-auckland-dog-pack.jpg': { width: 1024, height: 297 },
|
||||
'/images/auckland-large-dog-one-on-one-walk.jpg': { width: 1024, height: 970 },
|
||||
'/images/auckland-dogs-outdoor-pack.jpg': { width: 1024, height: 297 },
|
||||
@@ -25,7 +25,7 @@ const imageMetadata: Record<string, ImageMetadata> = {
|
||||
'/images/auckland-puppy-visits-cavalier-king-charles-spaniel.jpg': { width: 3327, height: 2217 },
|
||||
'/images/auckland-pack-walk-dog.jpg': { width: 480, height: 640 },
|
||||
'/images/auckland-dog-group-outing.jpg': { width: 640, height: 480 },
|
||||
'/images/founder-image-aless-goodwalk.png': { width: 1076, height: 1461 }
|
||||
'/images/founder-image-aless-goodwalk.jpg': { width: 1076, height: 1461 }
|
||||
};
|
||||
|
||||
export function getImageMetadata(src: string | undefined | null): ImageMetadata | null {
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 619 KiB |
|
Before Width: | Height: | Size: 273 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 606 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 737 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 549 KiB |
|
Before Width: | Height: | Size: 907 KiB After Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 762 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 318 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 963 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 912 KiB |
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 922 KiB |
@@ -19,7 +19,7 @@ describe('seo helpers', () => {
|
||||
|
||||
expect(seo.title).toBe('Dog Walkers in Mt Eden | Goodwalk Auckland');
|
||||
expect(seo.canonicalPath).toBe('/locations/mt-eden');
|
||||
expect(seo.image).toBe('/images/auckland-pack-walk-small-dogs-group.png');
|
||||
expect(seo.image).toBe('/images/auckland-pack-walk-small-dogs-group.jpg');
|
||||
expect(seo.imageAlt).toBe('Goodwalk dog walkers in Mt Eden, Auckland');
|
||||
expect(seo.structuredData).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { locationPages } from '$lib/content/locations';
|
||||
import type { LocationPageContent } from '$lib/types';
|
||||
|
||||
const siteUrl = 'https://www.goodwalk.co.nz';
|
||||
const defaultLocationImage = '/images/auckland-pack-walk-small-dogs-group.png';
|
||||
const defaultLocationImage = '/images/auckland-pack-walk-small-dogs-group.jpg';
|
||||
const defaultLocationImageAlt = 'Goodwalk Auckland dog walking services';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
@@ -82,11 +82,7 @@ export function buildLocationSeo(location: LocationPageContent) {
|
||||
description,
|
||||
serviceType,
|
||||
provider: {
|
||||
'@type': 'LocalBusiness',
|
||||
name: 'Goodwalk',
|
||||
url: siteUrl,
|
||||
telephone: '+64226421011',
|
||||
email: 'info@goodwalk.co.nz'
|
||||
'@id': `${siteUrl}/#business`
|
||||
},
|
||||
areaServed: {
|
||||
'@type': 'Place',
|
||||
|
||||
@@ -3,7 +3,3 @@ import { parseBooleanFlag } from '$lib/feature-flags';
|
||||
export function isGeneralEnquiryEnabled() {
|
||||
return parseBooleanFlag(process.env.ENABLE_GENERAL_ENQUIRIES, false);
|
||||
}
|
||||
|
||||
export function isHomepageHowItWorksEnabled() {
|
||||
return parseBooleanFlag(process.env.ENABLE_HOMEPAGE_HOW_IT_WORKS, false);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ html {
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
background: var(--off-white);
|
||||
overflow-x: clip;
|
||||
@@ -48,3 +48,19 @@ textarea {
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Scroll reveal — JS adds [data-reveal] to targets, then toggles .is-visible */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
[data-reveal] {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition:
|
||||
opacity 0.5s ease,
|
||||
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
[data-reveal].is-visible {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
*/
|
||||
body {
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
body.mobile-cta-enabled {
|
||||
@@ -203,6 +204,10 @@
|
||||
padding: 50px 20px 0;
|
||||
}
|
||||
|
||||
#hero::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
/* Featured sections — more breathing room */
|
||||
#promise,
|
||||
#services,
|
||||
#services {
|
||||
padding: 96px 0;
|
||||
}
|
||||
|
||||
/* Supporting sections */
|
||||
#values,
|
||||
#testimonials,
|
||||
#info,
|
||||
#info {
|
||||
padding: 72px 0;
|
||||
}
|
||||
|
||||
/* Booking / lead form — neutral */
|
||||
#newlead {
|
||||
padding: 80px 0;
|
||||
}
|
||||
@@ -15,6 +24,18 @@
|
||||
align-items: center;
|
||||
min-height: 500px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Gradient blends the hero image's bottom edge into the green background */
|
||||
#hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: auto 0 0 42%;
|
||||
height: 120px;
|
||||
background: linear-gradient(to top, var(--gw-green) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
@@ -337,6 +358,7 @@
|
||||
border-radius: 28px;
|
||||
padding: 40px 32px;
|
||||
text-align: center;
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
|
||||
transition:
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.22s ease;
|
||||
@@ -356,7 +378,7 @@
|
||||
}
|
||||
|
||||
.service-icon-bubble {
|
||||
background: linear-gradient(180deg, #ffe173 0%, #ffd54a 100%);
|
||||
background: linear-gradient(135deg, #ffd54a, var(--yellow-soft));
|
||||
transform: translateY(0) rotate(0deg) scale(1);
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
@@ -365,8 +387,11 @@
|
||||
|
||||
@media (hover: hover) {
|
||||
.service-card:hover {
|
||||
transform: translateY(-6px) scale(1.012);
|
||||
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
|
||||
0 8px 40px rgba(0, 0, 0, 0.08);
|
||||
filter: brightness(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,10 +518,12 @@ footer {
|
||||
}
|
||||
|
||||
.testimonial-card {
|
||||
background: #fff;
|
||||
background: linear-gradient(180deg, #ffffff 0%, var(--off-white) 100%);
|
||||
border-radius: 28px;
|
||||
padding: 36px 32px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
|
||||
0 4px 24px rgba(0, 0, 0, 0.06);
|
||||
transition:
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.22s ease;
|
||||
@@ -504,8 +531,11 @@ footer {
|
||||
|
||||
@media (hover: hover) {
|
||||
.testimonial-card:hover {
|
||||
transform: translateY(-6px) scale(1.012);
|
||||
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.09);
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
|
||||
0 8px 40px rgba(0, 0, 0, 0.08);
|
||||
filter: brightness(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
letter-spacing: -0.02em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
:root {
|
||||
/* Brand greens */
|
||||
--gw-green: #213021;
|
||||
--green-mid: #2d4230; /* hover states, subtle accents */
|
||||
--green-soft: #344b38; /* softer elevated green surfaces */
|
||||
|
||||
/* Accent */
|
||||
--yellow: #ffd100;
|
||||
--yellow-soft: #f2bf2f; /* warmer, more premium yellow — gradients and icon fills */
|
||||
|
||||
/* Neutrals */
|
||||
--gray: #59606d;
|
||||
--beige: #e5d6c2;
|
||||
--beige: #e5d6c2; /* warm surfaces, image frames */
|
||||
--off-white: #fbfbfb;
|
||||
--surface-light: #f7f8f6; /* elevated light surface — above off-white */
|
||||
--text: #2e3031;
|
||||
|
||||
/* Layout */
|
||||
--max-w: 1280px;
|
||||
|
||||
/* Typography */
|
||||
--font-body: 'Readex Pro', sans-serif;
|
||||
--font-head: 'Unbounded', sans-serif;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
import MobileBookBar from '$lib/components/MobileBookBar.svelte';
|
||||
import RouteSkeleton from '$lib/components/RouteSkeleton.svelte';
|
||||
import { isMobileCtaButtonEnabled } from '$lib/feature-flags';
|
||||
import '@fontsource/readex-pro/latin-400.css';
|
||||
import '@fontsource/readex-pro/latin-500.css';
|
||||
import '@fontsource/readex-pro/latin-600.css';
|
||||
import '@fontsource/readex-pro/latin-700.css';
|
||||
import '@fontsource/unbounded/latin-400.css';
|
||||
import '@fontsource/unbounded/latin-600.css';
|
||||
import '@fontsource/unbounded/latin-700.css';
|
||||
import '@fontsource/unbounded/latin-800.css';
|
||||
import '@fontsource/fredoka/latin-600.css';
|
||||
import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
|
||||
import '@fortawesome/fontawesome-free/css/solid.min.css';
|
||||
import '@fortawesome/fontawesome-free/css/brands.min.css';
|
||||
import '$lib/styles/variables.css';
|
||||
import '$lib/styles/base.css';
|
||||
import '$lib/styles/layout.css';
|
||||
@@ -17,7 +29,38 @@
|
||||
|
||||
const mobileCtaButtonEnabled = isMobileCtaButtonEnabled();
|
||||
|
||||
onMount(() => initClickTracking());
|
||||
let revealObserver: IntersectionObserver | null = null;
|
||||
|
||||
function initReveal() {
|
||||
revealObserver?.disconnect();
|
||||
|
||||
const targets = document.querySelectorAll<HTMLElement>(
|
||||
'.section-heading, .service-card, .testimonial-card, .value-card'
|
||||
);
|
||||
|
||||
targets.forEach(el => {
|
||||
el.setAttribute('data-reveal', '');
|
||||
});
|
||||
|
||||
revealObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
revealObserver?.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '0px 0px -40px 0px', threshold: 0.08 }
|
||||
);
|
||||
|
||||
targets.forEach(el => revealObserver!.observe(el));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initClickTracking();
|
||||
requestAnimationFrame(initReveal);
|
||||
});
|
||||
|
||||
function shouldShowSkeleton() {
|
||||
const navigation = $navigating;
|
||||
@@ -56,6 +99,7 @@
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
initReveal();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getHomepageContent } from '$lib/server/content';
|
||||
import { isHomepageHowItWorksEnabled } from '$lib/server/feature-flags';
|
||||
|
||||
const onboardingHosts = new Set(['onboarding.goodwalk.co.nz']);
|
||||
|
||||
@@ -19,7 +18,6 @@ export async function load({ url }) {
|
||||
|
||||
return {
|
||||
siteVariant,
|
||||
content: await getHomepageContent(),
|
||||
howItWorksEnabled: isHomepageHowItWorksEnabled()
|
||||
content: await getHomepageContent()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
|
||||
import ValuesSection from '$lib/components/ValuesSection.svelte';
|
||||
import OnboardingPage from '$lib/components/OnboardingPage.svelte';
|
||||
import { buildAreaServed } from '$lib/seo';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
@@ -38,7 +39,8 @@
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'LocalBusiness',
|
||||
'@type': ['LocalBusiness', 'PetCareService'],
|
||||
'@id': 'https://www.goodwalk.co.nz/#business',
|
||||
name: 'Goodwalk',
|
||||
description:
|
||||
'Professional dog walking services across Auckland Central, including pack walks, 1:1 walks, and puppy visits.',
|
||||
@@ -46,7 +48,7 @@
|
||||
logo: `${siteUrl}/images/goodwalk-auckland-dog-walking-logo.png`,
|
||||
image: absoluteUrl(data.content.hero.imageUrl),
|
||||
email: 'info@goodwalk.co.nz',
|
||||
telephone: '+64-22-642-1011',
|
||||
telephone: '+64226421011',
|
||||
sameAs: ['https://www.instagram.com/goodwalk.nz/', 'https://g.page/r/CUsvrWPhkYrAEB0/'],
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
@@ -54,25 +56,7 @@
|
||||
addressRegion: 'Auckland',
|
||||
addressCountry: 'NZ'
|
||||
},
|
||||
areaServed: [
|
||||
'Morningside',
|
||||
'Kingsland',
|
||||
'Ponsonby',
|
||||
'Grey Lynn',
|
||||
'Mt Albert',
|
||||
'Mt Eden',
|
||||
'Sandringham',
|
||||
'Mt Roskill',
|
||||
'Arch Hill',
|
||||
'Freemans Bay',
|
||||
'Herne Bay',
|
||||
'Pt Chevalier',
|
||||
'Avondale',
|
||||
'Three Kings',
|
||||
'Hillsborough',
|
||||
'Eden Terrace',
|
||||
'Balmoral'
|
||||
],
|
||||
areaServed: buildAreaServed(),
|
||||
openingHoursSpecification: [
|
||||
{
|
||||
'@type': 'OpeningHoursSpecification',
|
||||
@@ -98,7 +82,7 @@
|
||||
ratingValue: '5.0',
|
||||
bestRating: '5',
|
||||
worstRating: '1',
|
||||
reviewCount: String(data.content.testimonials.length)
|
||||
reviewCount: '30'
|
||||
},
|
||||
review: data.content.testimonials.map((testimonial) => ({
|
||||
'@context': 'https://schema.org',
|
||||
@@ -149,9 +133,7 @@
|
||||
<HeroSection hero={content.hero} reviewCta={content.intro.reviewCta} />
|
||||
<PromiseSection promise={content.promise} />
|
||||
<ServicesSection services={content.services} />
|
||||
{#if data.howItWorksEnabled}
|
||||
<HowItWorksSection content={content.howItWorks} />
|
||||
{/if}
|
||||
<TestimonialsSection testimonials={content.testimonials} seedKey="/" />
|
||||
<ValuesSection values={content.values} />
|
||||
<BookingSection booking={content.booking} />
|
||||
|
||||
@@ -17,24 +17,24 @@ const routes: SitemapRoute[] = [
|
||||
{ path: '/our-pricing', priority: '0.8', changefreq: 'monthly' },
|
||||
{ path: '/about', priority: '0.7', changefreq: 'monthly' },
|
||||
{ path: '/contact-us', priority: '0.7', changefreq: 'monthly' },
|
||||
...locationPages.map((loc) => ({
|
||||
path: `/locations/${loc.slug}`,
|
||||
priority: '0.8',
|
||||
changefreq: 'monthly'
|
||||
})),
|
||||
{ path: '/terms-and-conditions', priority: '0.3', changefreq: 'yearly' },
|
||||
{ path: '/privacy-policy', priority: '0.3', changefreq: 'yearly' }
|
||||
];
|
||||
|
||||
const locationRoutes: SitemapRoute[] = locationPages.map((loc) => ({
|
||||
path: `/locations/${loc.slug}`,
|
||||
priority: '0.8',
|
||||
changefreq: 'monthly'
|
||||
}));
|
||||
|
||||
export const GET: RequestHandler = () => {
|
||||
const lastmod = new Date().toISOString().split('T')[0];
|
||||
const allRoutes = [...routes, ...locationRoutes];
|
||||
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${routes
|
||||
${allRoutes
|
||||
.map(
|
||||
({ path, priority, changefreq }) => ` <url>
|
||||
<loc>${siteUrl}${path}</loc>
|
||||
<lastmod>${lastmod}</lastmod>
|
||||
<changefreq>${changefreq}</changefreq>
|
||||
<priority>${priority}</priority>
|
||||
</url>`
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { GET } from './+server';
|
||||
import { locationPages } from '$lib/content/locations';
|
||||
|
||||
describe('sitemap endpoint', () => {
|
||||
afterEach(() => {
|
||||
@@ -19,7 +18,7 @@ describe('sitemap endpoint', () => {
|
||||
expect(body).toContain('<loc>https://www.goodwalk.co.nz/contact-us</loc>');
|
||||
expect(body).toContain('<loc>https://www.goodwalk.co.nz/privacy-policy</loc>');
|
||||
expect(body).toContain('<lastmod>2026-05-01</lastmod>');
|
||||
expect(body).toContain('<loc>https://www.goodwalk.co.nz/locations/mt-eden</loc>');
|
||||
expect(body.match(/<url>/g)).toHaveLength(9 + locationPages.length);
|
||||
expect(body).not.toContain('/locations/');
|
||||
expect(body.match(/<url>/g)).toHaveLength(9);
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 619 KiB |
|
Before Width: | Height: | Size: 273 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 606 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 737 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 549 KiB |
|
Before Width: | Height: | Size: 907 KiB After Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 762 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 318 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 963 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 912 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 922 KiB |