Content Rewrite

This commit is contained in:
2026-05-07 21:47:42 +12:00
parent 0d86f450ec
commit a90dfb7c66
47 changed files with 1352 additions and 527 deletions
+133
View File
@@ -0,0 +1,133 @@
# Marketing Principles for Goodwalk
A working reference for the Goodwalk site rebuild and ongoing marketing decisions. Drawn from Chris Do (The Futur) and Debbie Millman (Design Matters), applied to the goal of acquiring 10 new clients.
---
## Chris Do's Principles
### 1. Sell the transformation, not the service
People don't buy "dog walking" — they buy peace of mind at work, a tired happy dog, not feeling guilty.
The headline shouldn't be "Professional Dog Walking in Wellington." It should speak to the outcome:
- "Come home to a happy, exercised dog"
- "Your dog's best part of the day, while you're at work"
### 2. Niche down to stand out
"Dog walker" competes with everyone. "Dog walker for working professionals in [suburb] with anxious or reactive dogs" competes with almost no one — and can charge more.
Pick a wedge.
### 3. Price on value, not time
Don't lead with "$25 per walk." Lead with packages and outcomes:
> **The Working Professional Plan** — 3 walks/week, GPS updates, photo reports
Hide the hourly rate. Make it about what they get, not what you do.
### 4. Show, don't tell
Testimonials and proof crush adjectives. "Reliable and caring" is meaningless.
A photo of a muddy grinning dog with a one-line quote from the owner sells:
> "Bowie pulls me to the door when he sees Sarah's car."
### 5. Free is a magnet
Most dog walking sites just have a contact form — that's a closed door. Open one with:
- A free first walk
- A free meet-and-greet
- A downloadable "Is your dog getting enough exercise?" checklist
Get people into the funnel.
---
## Debbie Millman's Principles
### 1. Brand is a story people tell themselves about you
Branding is deliberate differentiation through storytelling.
What's the Goodwalk story? Why do you do this? Are you the ex-vet-nurse who only walks small dogs? The runner who takes high-energy breeds on actual trail runs?
That story belongs on the homepage, not buried on About.
### 2. Consistency builds trust
One voice, one visual identity, everywhere:
- Website
- Instagram
- Car magnet
- The message sent when running 5 minutes late
Owners are handing you keys to their house and the life of their dog. Visual and verbal consistency signals "I am organised and reliable" before you've said a word.
### 3. Design is a tool for clarity, not decoration
Debbie often quotes Massimo Vignelli — design should make the message clearer.
In 3 seconds, can a stranger answer:
- What do you do?
- Who is it for?
- How do I book?
If they have to scroll or think, you're losing them.
---
## Applied: A Plan for 10 New Clients
A site rewrite with these principles in mind.
### 1. Homepage hero
- Outcome-focused headline
- One strong photo of a happy dog mid-walk
- One button: **"Book a free meet-and-greet"**
### 2. Pick a niche and say it out loud
Even just "for [your suburb] working professionals" narrows the field and helps you rank.
### 3. Three packages, not an hourly rate
Make the middle one the obvious choice (the "decoy effect" — Chris talks about this).
### 4. Three testimonials with photos and dog names
Real names, real dogs. Not "J.S. — Customer."
### 5. One story section
Who you are, why you do this, why someone should trust you with their dog and their house key.
### 6. Lead magnet
A free PDF like "How much exercise does your dog actually need?" in exchange for an email. Then you have a list to follow up with.
### 7. Kill booking friction
One-click to a calendar or a WhatsApp link. Not a 7-field form.
---
## Quick Reference Checklist
- [ ] Headline sells the outcome, not the service
- [ ] Niche is named explicitly on the homepage
- [ ] Pricing presented as packages, not hourly
- [ ] At least 3 testimonials with real names, dog names, and photos
- [ ] Founder story visible on homepage
- [ ] Lead magnet (PDF or free meet-and-greet) above the fold
- [ ] Booking is one click — calendar link or WhatsApp
- [ ] Visual and verbal identity consistent across site, Instagram, and comms
- [ ] In 3 seconds: what / who / how-to-book is obvious
+3 -3
View File
@@ -52,7 +52,7 @@
box-shadow: 0 24px 60px rgba(17, 20, 24, 0.2);
text-align: left;
font-family: 'Readex Pro', system-ui, sans-serif;
color: #213021;
color: var(--gw-green);
}
.no-js-kicker {
@@ -65,7 +65,7 @@
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #213021;
color: var(--gw-green);
}
.no-js-card h1 {
@@ -74,7 +74,7 @@
font-size: clamp(28px, 4vw, 38px);
line-height: 1.05;
letter-spacing: -0.04em;
color: #213021;
color: var(--gw-green);
}
.no-js-card p {
+3 -3
View File
@@ -18,9 +18,9 @@
<h1>Contact Us</h1>
<p class="booking-page-sub">
{#if allowGeneralEnquiry}
Fill in the form below to book a Meet &amp; Greet or send a general enquiry.
Book a Meet &amp; Greet or send a general enquiry. Well come back within 24 hours.
{:else}
Fill in the form below and we'll be in touch to arrange a free introduction.
Tell us a little about your dog and well be in touch within 24 hours to arrange a free Meet &amp; Greet.
{/if}
</p>
<div class="booking-page-contact">
@@ -46,7 +46,7 @@
}
.booking-page-hero {
background: var(--green);
background: var(--gw-green);
color: #fff;
padding: 64px 0 72px;
}
+216 -93
View File
@@ -11,7 +11,38 @@
type EnquiryType = 'booking' | 'general';
const visitStartedStorageKey = 'goodwalk_visit_started_at';
const journeyStorageKey = 'goodwalk_journey';
const requestedServiceStorageKey = 'goodwalk_requested_service';
const maxJourneyEntries = 8;
const servicePrompts: Record<
string,
{
intro: string;
messageLabel: string;
messagePlaceholder: string;
}
> = {
'Pack Walks': {
intro:
'Tell us about your dog, your area, and how they feel around other dogs so we can see if Pack Walks are the right fit.',
messageLabel: 'Pack Walks fit',
messagePlaceholder:
'How old is your dog, how do they feel in groups, and is there anything about confidence, recall, or social behaviour we should know?'
},
'1:1 Walks': {
intro:
'Tell us about your dog, your area, and what you want from one-on-one walks so we can plan the right routine.',
messageLabel: '1:1 walk needs',
messagePlaceholder:
'Tell us about your dogs size, pace, leash manners, confidence, and anything else that would help us tailor a one-on-one walk.'
},
'Puppy Visits': {
intro:
'Tell us about your puppy, your area, and the kind of support you need at home so we can plan the right visit.',
messageLabel: 'Puppy visit details',
messagePlaceholder:
'Tell us your puppys age, routine, toilet needs, feeding schedule, and anything important we should know before visiting.'
}
};
let step = 1;
$: headingParts = splitBookingTitle(booking.title);
@@ -76,7 +107,9 @@
const defaultGeneralSubtitle =
'Almost there — just your contact details so we can reply properly to your message.';
$: dogIntro = booking.dogIntro?.trim() || defaultDogIntro;
$: primarySelectedService = selectedServices[0] ?? '';
$: activeServicePrompt = servicePrompts[primarySelectedService];
$: dogIntro = activeServicePrompt?.intro || booking.dogIntro?.trim() || defaultDogIntro;
$: generalIntro = booking.generalIntro?.trim() || defaultGeneralIntro;
$: hasServices = booking.serviceOptions.length > 0;
$: if (!allowGeneralEnquiry && enquiryType === 'general') {
@@ -90,6 +123,16 @@
$: dogStepLabel = booking.dogStepLabel?.trim() || 'Your dog';
$: detailsStepLabel = isGeneralEnquiry ? 'Your enquiry' : dogStepLabel;
$: detailsStepIntro = isGeneralEnquiry ? generalIntro : dogIntro;
$: bookingEyebrow = isGeneralEnquiry ? 'Friendly contact' : primarySelectedService || 'Free Meet & Greet';
$: bookingIntro = isGeneralEnquiry
? 'Send us the details and well point you in the right direction.'
: 'Tell us a little about your dog first. Well come back within 24 hours with the right next step.';
$: detailsMessageLabel = isGeneralEnquiry
? 'Your Message'
: activeServicePrompt?.messageLabel || 'About Your Dog';
$: detailsMessagePlaceholder = isGeneralEnquiry
? 'Tell us if this is feedback, a complaint, a business enquiry, or anything else we should know.'
: activeServicePrompt?.messagePlaceholder || 'Describe your pet, any special needs, or anything we should know.';
$: successPetName = petName.trim() || 'your dog';
onMount(() => {
@@ -98,6 +141,19 @@
pageEnteredAt = now;
visitStartedAt = readOrCreateVisitStartedAt(now);
journey = updateJourneySnapshot(window.location.pathname, window.location.search);
applyRequestedService();
const handleRequestedService = (event: Event) => {
const customEvent = event as CustomEvent<{ service?: string }>;
applyRequestedService(customEvent.detail?.service?.trim());
};
window.addEventListener('goodwalk:service-selected', handleRequestedService as EventListener);
return () => {
window.removeEventListener('goodwalk:service-selected', handleRequestedService as EventListener);
};
});
function splitBookingTitle(title: string) {
@@ -169,11 +225,35 @@
errors = {};
}
function applyRequestedService(service?: string) {
const requestedService =
service ||
(() => {
try {
return window.sessionStorage.getItem(requestedServiceStorageKey)?.trim() || '';
} catch {
return '';
}
})();
if (!requestedService || !booking.serviceOptions.includes(requestedService)) {
return;
}
selectedServices = [requestedService];
try {
window.sessionStorage.removeItem(requestedServiceStorageKey);
} catch {
// Ignore storage cleanup failures.
}
}
function toggleService(service: string, checked: boolean) {
noteInteraction();
if (checked) {
selectedServices = [...selectedServices, service];
selectedServices = [service, ...selectedServices.filter((item) => item !== service)];
return;
}
@@ -342,10 +422,22 @@
{/if}
<div class="booking-header">
<span class="booking-eyebrow">{bookingEyebrow}</span>
<h2 class="booking-title">
<span class="booking-title-plain">{headingParts.plain}</span>{' '}
<span class="booking-title-highlight">{headingParts.highlight}</span>
</h2>
<p class="booking-intro">{bookingIntro}</p>
<div class="booking-trust-row" aria-label="Booking highlights">
<span class="booking-trust-chip">
<Icon name="fas fa-comment-dots" />
Reply within 24 hours
</span>
<span class="booking-trust-chip">
<Icon name="fas fa-paw" />
Free, no-obligation Meet &amp; Greet
</span>
</div>
<div class="booking-stepper" aria-label="Booking form steps">
<button
@@ -438,113 +530,138 @@
{/if}
{#if !isGeneralEnquiry}
<div class="booking-field-card" class:booking-field-card-invalid={errors.petName}>
<label for="petName">
<Icon name="fas fa-dog" />&nbsp;Dog's Name <span class="booking-required">*</span>
</label>
<input
bind:this={petNameInput}
bind:value={petName}
type="text"
id="petName"
name="petName"
required
placeholder="Your dog's name"
class:input-invalid={errors.petName}
on:input={() => clearError('petName')}
/>
{#if errors.petName}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.petName}
</p>
{/if}
</div>
<div class="booking-field-card booking-field-card-group booking-field-card-full">
<div class="booking-field-group booking-field-group-dog">
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.petName}>
<label for="petName">
<Icon name="fas fa-dog" />&nbsp;Dog's Name <span class="booking-required">*</span>
</label>
<input
bind:this={petNameInput}
bind:value={petName}
type="text"
id="petName"
name="petName"
required
placeholder="Your dog's name"
class:input-invalid={errors.petName}
on:input={() => clearError('petName')}
/>
{#if errors.petName}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.petName}
</p>
{/if}
</div>
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.location}>
<label for="location">
<Icon name="fas fa-location-dot" />&nbsp;Location <span class="booking-required">*</span>
</label>
<input
bind:this={locationInput}
bind:value={location}
type="text"
id="location"
name="location"
required
placeholder="Suburb, street..."
class:input-invalid={errors.location}
on:input={() => clearError('location')}
/>
{#if errors.location}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.location}
</p>
{/if}
</div>
<div
class="booking-field-stack booking-field-stack-full"
class:booking-field-stack-invalid={errors.message}
>
<label for="message">
<Icon name="fas fa-comment" />&nbsp;{detailsMessageLabel}
</label>
<textarea
bind:value={message}
id="message"
name="message"
rows="4"
placeholder={detailsMessagePlaceholder}
class:input-invalid={errors.message}
on:input={() => clearError('message')}
></textarea>
{#if errors.message}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.message}
</p>
{/if}
</div>
{#if hasServices}
<div class="booking-field-stack booking-field-stack-full">
<span class="booking-service-label"><Icon name="fas fa-paw" />&nbsp;Services</span>
<div class="booking-service-options">
{#each booking.serviceOptions as service}
<label class="booking-check-option">
<input
type="checkbox"
name="services"
value={service}
checked={selectedServices.includes(service)}
on:change={(event) =>
toggleService(service, (event.currentTarget as HTMLInputElement).checked)}
/>
<span class="booking-check-box" aria-hidden="true"></span>
<span>{service}</span>
</label>
{/each}
</div>
</div>
{/if}
</div>
</div>
{/if}
{#if isGeneralEnquiry}
<div
class="booking-field-card booking-field-card-wide"
class:booking-field-card-invalid={errors.location}
class="booking-field-card booking-field-card-full"
class:booking-field-card-invalid={errors.message}
>
<label for="location">
<Icon name="fas fa-location-dot" />&nbsp;Location <span class="booking-required">*</span>
<label for="message">
<Icon name="fas fa-comment" />&nbsp;{detailsMessageLabel}
<span class="booking-required">*</span>
</label>
<input
bind:this={locationInput}
bind:value={location}
type="text"
id="location"
name="location"
required
placeholder="Suburb, street..."
class:input-invalid={errors.location}
on:input={() => clearError('location')}
/>
{#if errors.location}
<textarea
bind:value={message}
id="message"
name="message"
rows="4"
placeholder={detailsMessagePlaceholder}
class:input-invalid={errors.message}
on:input={() => clearError('message')}
></textarea>
{#if errors.message}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.location}
{errors.message}
</p>
{/if}
</div>
{/if}
<div
class="booking-field-card booking-field-card-full"
class:booking-field-card-invalid={errors.message}
>
<label for="message">
<Icon name="fas fa-comment" />&nbsp;{isGeneralEnquiry ? 'Your Message' : 'About Your Dog'}
{#if isGeneralEnquiry}<span class="booking-required">*</span>{/if}
</label>
<textarea
bind:value={message}
id="message"
name="message"
rows="4"
placeholder={isGeneralEnquiry
? 'Tell us if this is feedback, a complaint, a business enquiry, or anything else we should know.'
: 'Describe your pet, any special needs, or anything we should know.'}
class:input-invalid={errors.message}
on:input={() => clearError('message')}
></textarea>
{#if errors.message}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.message}
</p>
{/if}
</div>
</div>
{#if hasServices && !isGeneralEnquiry}
<div class="booking-service-row">
<span class="booking-service-label"><Icon name="fas fa-paw" />&nbsp;Services</span>
<div class="booking-service-options">
{#each booking.serviceOptions as service}
<label class="booking-check-option">
<input
type="checkbox"
name="services"
value={service}
checked={selectedServices.includes(service)}
on:change={(event) =>
toggleService(service, (event.currentTarget as HTMLInputElement).checked)}
/>
<span class="booking-check-box" aria-hidden="true"></span>
<span>{service}</span>
</label>
{/each}
</div>
</div>
{/if}
</div>
<div class="booking-actions booking-actions-next">
<button type="button" class="btn btn-yellow booking-next-button" on:click={goToOwnerStep}>
{ownerStepLabel}
Next: {ownerStepLabel.toLowerCase()}
<Icon name="fas fa-arrow-right" />
</button>
<p class="booking-next-note">Response from us within 24 hours</p>
<p class="booking-next-note">No payment, no pressure, just the right starting point for your dog.</p>
</div>
{:else}
<input type="hidden" name="fullName" value={fullName} />
@@ -652,7 +769,13 @@
Back
</button>
<button type="submit" class="btn btn-yellow booking-submit-button" disabled={submitting}>
{#if submitting}Sending…{:else}Send <Icon name="fas fa-arrow-right" />{/if}
{#if submitting}
Sending…
{:else if isGeneralEnquiry}
Send enquiry <Icon name="fas fa-arrow-right" />
{:else}
Request Meet &amp; Greet <Icon name="fas fa-arrow-right" />
{/if}
</button>
</div>
{/if}
+6 -6
View File
@@ -141,7 +141,7 @@
margin: 0 0 12px;
font-size: 24px;
font-weight: 700;
color: #213021;
color: var(--gw-green);
line-height: 1.25;
}
@@ -162,7 +162,7 @@
border-radius: 14px;
background: #f7f6f1;
border: 1px solid #ebe9df;
color: #213021;
color: var(--gw-green);
text-decoration: none;
transition:
background 0.15s ease,
@@ -187,7 +187,7 @@
.modal-email-address {
font-size: 17px;
font-weight: 600;
color: #213021;
color: var(--gw-green);
word-break: break-all;
}
@@ -226,7 +226,7 @@
}
.modal-btn-primary {
background: #213021;
background: var(--gw-green);
color: #ffd100;
}
@@ -237,13 +237,13 @@
.modal-btn-secondary {
background: transparent;
color: #213021;
color: var(--gw-green);
border: 1px solid #d4d2c6;
}
.modal-btn-secondary:hover {
background: #f2f2f0;
border-color: #213021;
border-color: var(--gw-green);
}
@keyframes backdrop-in {
+24 -2
View File
@@ -25,6 +25,14 @@
connector: ''
};
}
function linkTarget(external?: boolean) {
return external ? '_blank' : undefined;
}
function linkRel(external?: boolean) {
return external ? 'noopener' : undefined;
}
</script>
<section id="hero">
@@ -71,8 +79,22 @@
{/if}
<div class="hero-buttons">
<a href={hero.primaryCta.href} class="btn btn-yellow">{hero.primaryCta.label}</a>
<a href={hero.secondaryCta.href} class="btn btn-outline">{hero.secondaryCta.label}</a>
<a
href={hero.primaryCta.href}
target={linkTarget(hero.primaryCta.external)}
rel={linkRel(hero.primaryCta.external)}
class="btn btn-yellow"
>
{hero.primaryCta.label}
</a>
<a
href={hero.secondaryCta.href}
target={linkTarget(hero.secondaryCta.external)}
rel={linkRel(hero.secondaryCta.external)}
class="btn btn-outline"
>
{hero.secondaryCta.label}
</a>
</div>
</div>
+135 -129
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { reveal } from '$lib/actions/reveal';
import Icon from '$lib/components/Icon.svelte';
import type { HowItWorksContent } from '$lib/types';
export let content: HowItWorksContent;
@@ -17,28 +16,22 @@
<div class="how-it-works-flow" aria-label="How it works">
{#each content.steps as step, index}
<article class="how-it-works-step">
<div class="how-it-works-badge" aria-hidden="true">
<span class="how-it-works-count">0{index + 1}</span>
<article class:how-it-works-step-payoff={index === content.steps.length - 1} class="how-it-works-step">
<div class="how-it-works-rail-node" aria-hidden="true">
<span class="how-it-works-rail-dot"></span>
</div>
{#if step.icon}
<div class="how-it-works-icon-bubble">
<Icon name={step.icon} className="how-it-works-icon" />
</div>
{/if}
<h3>{step.title}</h3>
<p>{step.body}</p>
<div class="how-it-works-step-top">
<span class="how-it-works-count">{`0${index + 1}`}</span>
<span class="how-it-works-phase">{step.phase || `Step ${index + 1}`}</span>
</div>
<div class="how-it-works-copy">
<h3>{step.title}</h3>
{#if step.benefit}
<p class="how-it-works-benefit">{step.benefit}</p>
{/if}
</div>
<p class="how-it-works-body">{step.body}</p>
</article>
{#if index < content.steps.length - 1}
<div class="how-it-works-connector" aria-hidden="true">
<span class="how-it-works-connector-line"></span>
<div class="how-it-works-connector-bubble">
<Icon name="fas fa-paw" className="how-it-works-connector-icon" />
</div>
<span class="how-it-works-connector-line"></span>
</div>
{/if}
{/each}
</div>
</div>
@@ -84,108 +77,113 @@
.how-it-works-flow {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
gap: 18px;
position: relative;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 28px;
align-items: stretch;
margin-top: 34px;
margin-top: 40px;
}
.how-it-works-flow::before {
content: '';
position: absolute;
left: 18px;
right: 18px;
top: 22px;
height: 1px;
background: linear-gradient(
90deg,
rgba(33, 48, 33, 0.12) 0%,
rgba(33, 48, 33, 0.28) 50%,
rgba(33, 48, 33, 0.12) 100%
);
}
.how-it-works-step {
position: relative;
padding: 26px 24px;
border-radius: 28px;
background:
radial-gradient(circle at top center, rgba(255, 209, 0, 0.18), transparent 36%),
linear-gradient(180deg, #fffaf0 0%, #f8f4ea 100%);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 14px 28px rgba(17, 20, 24, 0.04);
text-align: center;
display: flex;
flex-direction: column;
padding: 0 18px 0 0;
text-align: left;
}
.how-it-works-badge {
display: inline-flex;
.how-it-works-step-payoff {
transform: translateY(-4px);
}
.how-it-works-rail-node {
position: relative;
display: flex;
align-items: center;
margin-bottom: 14px;
padding: 8px 12px;
border-radius: 999px;
background: #fff;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
min-height: 44px;
margin-bottom: 24px;
}
.how-it-works-rail-dot {
display: inline-flex;
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--gw-green);
box-shadow:
0 0 0 7px #fff,
0 0 0 8px rgba(33, 48, 33, 0.12);
}
.how-it-works-step-top {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 12px;
}
.how-it-works-count {
color: var(--green);
color: rgba(33, 48, 33, 0.3);
font-family: var(--font-head);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.04em;
font-size: 36px;
font-weight: 800;
line-height: 0.9;
}
.how-it-works-icon-bubble {
display: inline-flex;
align-items: center;
justify-content: center;
width: 72px;
height: 72px;
margin: 0 auto 16px;
border-radius: 50%;
background: var(--green);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 16px 28px rgba(33, 48, 33, 0.16);
.how-it-works-phase {
color: var(--gw-green);
font-family: var(--font-head);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
:global(.how-it-works-icon.icon) {
color: #fff;
font-size: 24px;
.how-it-works-copy {
padding: 22px 22px 20px;
border-radius: 24px;
background:
linear-gradient(180deg, rgba(255, 250, 240, 0.92) 0%, rgba(248, 244, 234, 0.92) 100%);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
}
.how-it-works-step h3 {
margin: 0 0 10px;
margin: 0;
font-size: 20px;
line-height: 1.18;
}
.how-it-works-step p {
margin: 0;
.how-it-works-benefit {
margin: 10px 0 0;
color: #6b5830;
font-size: 14px;
font-weight: 700;
line-height: 1.35;
}
.how-it-works-body {
margin: 14px 0 0;
padding: 0 4px 0 22px;
color: #4c5056;
font-size: 15px;
line-height: 1.6;
}
.how-it-works-connector {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
min-width: 90px;
}
.how-it-works-connector-line {
width: 24px;
height: 2px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.22);
}
.how-it-works-connector-bubble {
display: inline-flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(180deg, #254129 0%, #213021 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 14px 24px rgba(33, 48, 33, 0.14);
}
:global(.how-it-works-connector-icon.icon) {
color: var(--green);
color: #ffd54a;
font-size: 16px;
}
@media (max-width: 768px) {
#how-it-works {
padding-top: 6px;
@@ -202,58 +200,66 @@
.how-it-works-flow {
grid-template-columns: 1fr;
gap: 14px;
gap: 24px;
margin-top: 26px;
}
.how-it-works-flow::before {
left: 5px;
right: auto;
top: 22px;
bottom: 22px;
width: 1px;
height: auto;
background: linear-gradient(
180deg,
rgba(33, 48, 33, 0.12) 0%,
rgba(33, 48, 33, 0.28) 50%,
rgba(33, 48, 33, 0.12) 100%
);
}
.how-it-works-step {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-areas:
'icon title badge'
'body body body';
column-gap: 14px;
row-gap: 10px;
padding: 20px 18px;
text-align: left;
padding: 0 0 0 28px;
}
.how-it-works-badge {
grid-area: badge;
justify-self: end;
align-self: start;
margin-bottom: 0;
padding: 7px 10px;
.how-it-works-step-payoff {
transform: none;
}
.how-it-works-icon-bubble {
grid-area: icon;
width: 64px;
height: 64px;
margin: 0;
align-self: start;
.how-it-works-rail-node {
position: absolute;
left: 0;
top: 0;
min-height: 0;
margin: 18px 0 0;
}
.how-it-works-rail-dot {
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 12px 22px rgba(33, 48, 33, 0.14);
0 0 0 5px #fff,
0 0 0 6px rgba(33, 48, 33, 0.12);
}
.how-it-works-step-top {
margin-bottom: 10px;
}
.how-it-works-step h3 {
grid-area: title;
align-self: center;
margin: 0;
font-size: 18px;
line-height: 1.2;
}
.how-it-works-step p {
grid-area: body;
.how-it-works-benefit {
margin-top: 8px;
font-size: 13px;
}
.how-it-works-body {
margin-top: 2px;
padding: 14px 2px 0 18px;
font-size: 14px;
line-height: 1.55;
}
.how-it-works-connector {
display: none;
}
}
</style>
+3 -3
View File
@@ -78,7 +78,7 @@
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 10px 24px rgba(17, 20, 24, 0.04);
color: #213021;
color: var(--gw-green);
font-size: 14px;
font-weight: 600;
line-height: 1.2;
@@ -103,7 +103,7 @@
.info-nearby-kicker {
display: inline-block;
color: var(--green);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
@@ -119,7 +119,7 @@
min-height: 48px;
padding: 12px 18px;
border-radius: 999px;
background: var(--green);
background: var(--gw-green);
color: #fff;
font-family: var(--font-head);
font-size: 14px;
+7 -7
View File
@@ -4,7 +4,7 @@
export let instagram: HomePageContent['instagram'];
const dogCutoutSrc = '/images/smiling-dogs-instagram-cta.png';
const dogCutoutSrc = '/images/dog-cutout.png';
</script>
<aside id="instagram" aria-label="Follow Goodwalk on Instagram">
@@ -29,7 +29,7 @@
<style>
#instagram {
overflow: visible;
overflow: hidden;
padding-bottom: 40px;
}
@@ -89,12 +89,12 @@
.instagram-dog-wrap {
position: absolute;
right: 24px;
bottom: 0;
right: -56px;
bottom: -14px;
display: flex;
align-items: flex-end;
justify-content: center;
width: 280px;
width: 380px;
pointer-events: none;
z-index: 0;
}
@@ -149,8 +149,8 @@
.instagram-dog-wrap {
left: 50%;
right: auto;
bottom: -80px;
width: min(260px, calc(100% - 40px));
bottom: -96px;
width: min(300px, calc(100% - 32px));
transform: translateX(-50%);
}
+2 -2
View File
@@ -101,7 +101,7 @@
.legal-section h2 {
margin: 0 0 16px;
padding-left: 14px;
border-left: 3px solid var(--green);
border-left: 3px solid var(--gw-green);
font-family: var(--font-head);
font-size: clamp(14px, 1.4vw, 17px);
line-height: 1.3;
@@ -140,7 +140,7 @@
content: '';
position: absolute;
left: 0;
color: var(--green);
color: var(--gw-green);
font-size: 14px;
line-height: 1.9;
}
+32 -8
View File
@@ -12,6 +12,26 @@
const scrollDepthThreshold = 0.65;
const desktopPromptMediaQuery = '(min-width: 769px)';
function numericPrice(price: string) {
const value = Number(price.replace(/[^0-9.]/g, ''));
return Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
}
function decoratePlans<T extends { price: string }>(plans: T[]) {
const sorted = [...plans]
.map((plan, index) => ({ plan, index, value: numericPrice(plan.price) }))
.sort((a, b) => a.value - b.value || a.index - b.index);
const cheapestIndex = sorted[0]?.index ?? -1;
const mobileOrder = new Map(sorted.map((entry, order) => [entry.index, order]));
return plans.map((plan, index) => ({
...plan,
isPopular: index === cheapestIndex,
mobileOrder: mobileOrder.get(index) ?? index
}));
}
let showMeetGreetPrompt = false;
let dismissMeetGreetPrompt = false;
let bookingInView = false;
@@ -147,9 +167,13 @@
</div>
<div class:pricing-plan-grid-three={section.plans.length === 3} class="pricing-plan-grid">
{#each section.plans as plan}
<article class:pricing-plan-popular={plan.popular} class="pricing-plan-card">
{#if plan.popular}
{#each decoratePlans(section.plans) as plan}
<article
class:pricing-plan-popular={plan.isPopular}
class="pricing-plan-card"
style={`--mobile-order:${plan.mobileOrder};`}
>
{#if plan.isPopular}
<span class="pricing-plan-ribbon">Popular</span>
{/if}
@@ -226,7 +250,7 @@
}
.pricing-page-hero {
background: var(--green);
background: var(--gw-green);
padding: 56px 0 64px;
text-align: center;
}
@@ -320,7 +344,7 @@
height: 56px;
margin-bottom: 16px;
border-radius: 16px;
background: var(--green);
background: var(--gw-green);
color: #fff;
font-size: 22px;
}
@@ -469,7 +493,7 @@
padding: 6px 10px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
color: var(--green);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 12px;
letter-spacing: 0.04em;
@@ -635,7 +659,7 @@
}
.pricing-plan-popular {
order: -1;
order: var(--mobile-order, 0);
}
.pricing-plan-card {
@@ -672,7 +696,7 @@
align-items: center;
gap: 8px;
margin-bottom: 10px;
color: var(--green);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
+10 -8
View File
@@ -28,14 +28,16 @@
</div>
<div class="promise-img">
<img
src={promise.imageUrl}
alt={promise.imageAlt}
width={promiseImage?.width}
height={promiseImage?.height}
loading="lazy"
decoding="async"
/>
<div class="promise-img-frame">
<img
src={promise.imageUrl}
alt={promise.imageAlt}
width={promiseImage?.width}
height={promiseImage?.height}
loading="lazy"
decoding="async"
/>
</div>
</div>
</div>
</section>
+160 -35
View File
@@ -10,9 +10,35 @@
export let pageContent: ServicePageContent;
export let currentPath = '';
function numericPrice(price: string) {
const value = Number(price.replace(/[^0-9.]/g, ''));
return Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
}
function decoratePlans<T extends { price: string }>(plans: T[]) {
const sorted = [...plans]
.map((plan, index) => ({ plan, index, value: numericPrice(plan.price) }))
.sort((a, b) => a.value - b.value || a.index - b.index);
const cheapestIndex = sorted[0]?.index ?? -1;
const mobileOrder = new Map(sorted.map((entry, order) => [entry.index, order]));
return plans.map((plan, index) => ({
...plan,
isPopular: index === cheapestIndex,
mobileOrder: mobileOrder.get(index) ?? index
}));
}
$: heroImage = getImageMetadata(pageContent.hero.imageUrl);
$: highlightImage = pageContent.highlight ? getImageMetadata(pageContent.highlight.imageUrl) : null;
$: highlightCollageImages =
pageContent.highlight?.collageImages?.map((image) => ({
...image,
meta: getImageMetadata(image.imageUrl)
})) ?? [];
$: relatedServices = content.services.filter((s) => s.href && s.href !== currentPath);
$: pricingPlans = decoratePlans(pageContent.pricing.plans);
$: relatedCards = [
...relatedServices.map((s) => ({
@@ -68,16 +94,33 @@
</div>
<div class="service-inner">
<div class="service-highlight-image">
<img
src={pageContent.highlight.imageUrl}
alt={pageContent.highlight.imageAlt}
width={highlightImage?.width}
height={highlightImage?.height}
loading="lazy"
decoding="async"
/>
</div>
{#if highlightCollageImages.length}
<div class="service-highlight-collage" aria-label={pageContent.highlight.title}>
{#each highlightCollageImages as image, index}
<figure class={`service-collage-card service-collage-card-${index + 1}`}>
<img
src={image.imageUrl}
alt={image.imageAlt}
width={image.meta?.width}
height={image.meta?.height}
loading="lazy"
decoding="async"
/>
</figure>
{/each}
</div>
{:else}
<div class="service-highlight-image">
<img
src={pageContent.highlight.imageUrl}
alt={pageContent.highlight.imageAlt}
width={highlightImage?.width}
height={highlightImage?.height}
loading="lazy"
decoding="async"
/>
</div>
{/if}
</div>
</section>
{/if}
@@ -92,9 +135,13 @@
</div>
<div class:service-plan-grid-three={pageContent.pricing.plans.length === 3} class="service-plan-grid">
{#each pageContent.pricing.plans as plan}
<article class:service-plan-popular={plan.popular} class="service-plan-card">
{#if plan.popular}
{#each pricingPlans as plan}
<article
class:service-plan-popular={plan.isPopular}
class="service-plan-card"
style={`--mobile-order:${plan.mobileOrder};`}
>
{#if plan.isPopular}
<span class="service-plan-ribbon">Popular</span>
{/if}
@@ -263,11 +310,11 @@
}
.service-related-tint-1 {
--card-accent: var(--green);
--card-accent: var(--gw-green);
}
.service-related-tint-1 .service-related-icon {
background: #dce6dc;
color: var(--green);
color: var(--gw-green);
}
.service-related-tint-2 {
@@ -275,7 +322,7 @@
}
.service-related-tint-2 .service-related-icon {
background: #efe4d1;
color: var(--green);
color: var(--gw-green);
}
.service-related-tint-3 {
@@ -305,7 +352,7 @@
height: 52px;
border-radius: 50%;
background: #efe4d1;
color: var(--green);
color: var(--gw-green);
font-size: 18px;
margin-bottom: 8px;
}
@@ -334,7 +381,7 @@
.service-related-price {
font-family: var(--font-head);
font-weight: 700;
color: var(--green);
color: var(--gw-green);
font-size: 14px;
}
@@ -352,7 +399,7 @@
.service-related-link {
margin-top: auto;
padding-top: 6px;
color: var(--green);
color: var(--gw-green);
font-family: var(--font-head);
font-weight: 700;
font-size: 14px;
@@ -386,18 +433,69 @@
line-height: 1.7;
}
.service-hero-media,
.service-highlight-image {
position: relative;
overflow: hidden;
border-radius: 28px;
background: #f4efe7;
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
}
.service-hero-media {
aspect-ratio: 4 / 3;
}
.service-highlight-image {
aspect-ratio: 4 / 3;
max-width: 900px;
margin: 0 auto;
}
.service-highlight-collage {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
max-width: 780px;
margin: 0 auto;
align-items: stretch;
}
.service-collage-card {
position: relative;
overflow: hidden;
border-radius: 28px;
background: #f4efe7;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 16px 40px rgba(17, 20, 24, 0.08);
}
.service-collage-card img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.service-collage-card-1 {
min-height: 250px;
}
.service-collage-card-2 {
min-height: 250px;
}
.service-collage-card-3 {
min-height: 250px;
}
.service-hero-media img,
.service-highlight-image img {
display: block;
width: 100%;
border-radius: 28px;
height: 100%;
object-fit: cover;
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
}
.service-hero-media img {
aspect-ratio: 4 / 3;
height: auto;
}
.service-highlight {
@@ -418,10 +516,6 @@
margin: 0 0 16px;
}
.service-highlight-image img {
max-height: 620px;
}
.service-pricing,
.service-benefits {
padding: 0 0 96px;
@@ -460,6 +554,13 @@
border-color 0.22s ease;
}
.service-plan-card {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
}
.service-plan-popular {
border: 2px solid var(--yellow);
}
@@ -521,7 +622,7 @@
font-family: var(--font-head);
font-size: 44px;
line-height: 1;
color: var(--green);
color: var(--gw-green);
}
.service-plan-period {
@@ -537,6 +638,7 @@
margin: 24px 0 0;
padding: 0;
list-style: none;
flex: 1 1 auto;
}
.service-plan-features li {
@@ -583,7 +685,7 @@
padding: 8px 16px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
color: var(--green);
color: var(--gw-green);
font-size: 14px;
font-weight: 600;
}
@@ -662,7 +764,7 @@
.service-extra-price {
font-family: var(--font-head);
font-weight: 700;
color: var(--green);
color: var(--gw-green);
white-space: nowrap;
}
@@ -680,7 +782,7 @@
height: 52px;
border-radius: 50%;
background: #efe4d1;
color: var(--green);
color: var(--gw-green);
font-size: 18px;
margin-bottom: 18px;
}
@@ -702,6 +804,16 @@
.service-hero-grid {
align-items: start;
}
.service-highlight-collage {
max-width: 720px;
}
.service-collage-card-2,
.service-collage-card-3,
.service-collage-card-1 {
min-height: 220px;
}
}
@media (max-width: 768px) {
@@ -723,7 +835,7 @@
}
.service-plan-popular {
order: -1;
order: var(--mobile-order, 0);
}
.service-highlight,
@@ -748,5 +860,18 @@
margin: 18px auto 0;
font-family: var(--font-head);
}
.service-highlight-collage {
grid-template-columns: 1fr;
gap: 12px;
max-width: 420px;
}
.service-collage-card-1,
.service-collage-card-2,
.service-collage-card-3 {
min-height: 220px;
transform: none;
}
}
</style>
+71 -4
View File
@@ -5,12 +5,35 @@
export let services: IconCard[];
export let heading = 'What we do';
export let intro =
'Choose the walk style that fits your dog best, then book a free Meet & Greet when you are ready.';
const requestedServiceStorageKey = 'goodwalk_requested_service';
function bookingHref() {
return '#newlead';
}
function primeBookingService(serviceTitle: string) {
try {
window.sessionStorage.setItem(requestedServiceStorageKey, serviceTitle);
} catch {
// Ignore storage failures and continue with the link target.
}
window.dispatchEvent(
new CustomEvent('goodwalk:service-selected', {
detail: { service: serviceTitle }
})
);
}
</script>
<section id="services" use:reveal={{ delay: 20 }} class="reveal-block">
<div class="services-inner">
<h2 class="section-heading">{heading}</h2>
<p class="services-intro">{intro}</p>
<div class="services-grid">
{#each services as service}
@@ -26,10 +49,16 @@
{/if}
{#if service.href}
<a href={service.href} class="btn btn-green">
<span>See {service.title} pricing</span>
<Icon name="fas fa-arrow-right" />
</a>
<div class="service-card-actions">
<a href={bookingHref()} class="btn btn-green" on:click={() => primeBookingService(service.title)}>
<span>Book {service.title}</span>
<Icon name="fas fa-arrow-right" />
</a>
<a href={service.href} class="service-card-link">
View details &amp; pricing
</a>
</div>
{/if}
</div>
{/each}
@@ -81,4 +110,42 @@
transform: translateY(0);
}
}
.services-intro {
max-width: 700px;
margin: 18px auto 0;
text-align: center;
color: #4c5056;
font-size: 17px;
line-height: 1.65;
}
.service-card-actions {
display: grid;
gap: 12px;
margin-top: 18px;
}
.service-card-actions :global(.btn) {
width: 100%;
justify-content: center;
}
.service-card-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 28px;
color: var(--gw-green);
font-size: 14px;
font-weight: 700;
text-decoration: none;
}
@media (hover: hover) {
.service-card-link:hover {
text-decoration: underline;
text-underline-offset: 0.18em;
}
}
</style>
+5 -4
View File
@@ -7,6 +7,7 @@
export let email: string;
export let enquiryType: 'booking' | 'general' = 'booking';
export let onClose: () => void;
const gwGreenHex = '#213021';
$: isGeneralEnquiry = enquiryType === 'general';
@@ -20,7 +21,7 @@
angle: 60,
spread: 65,
origin: { x: 0, y: 0.75 },
colors: ['#FFD100', '#213021', '#ffffff', '#7aaa7a', '#ffeaa0'],
colors: ['#FFD100', gwGreenHex, '#ffffff', '#7aaa7a', '#ffeaa0'],
gravity: 0.9,
scalar: 1.1,
});
@@ -29,7 +30,7 @@
angle: 120,
spread: 65,
origin: { x: 1, y: 0.75 },
colors: ['#FFD100', '#213021', '#ffffff', '#7aaa7a', '#ffeaa0'],
colors: ['#FFD100', gwGreenHex, '#ffffff', '#7aaa7a', '#ffeaa0'],
gravity: 0.9,
scalar: 1.1,
});
@@ -157,7 +158,7 @@
margin: 0 0 14px;
font-size: 26px;
font-weight: 700;
color: #213021;
color: var(--gw-green);
line-height: 1.2;
}
@@ -186,7 +187,7 @@
.modal-btn {
display: inline-block;
padding: 14px 36px;
background: #213021;
background: var(--gw-green);
color: #FFD100;
font-size: 15px;
font-weight: 600;
+31 -6
View File
@@ -6,8 +6,9 @@
import type { TestimonialContent } from '$lib/types';
export let testimonials: TestimonialContent[];
export let heading = 'Why people choose us!';
export let blurb = 'Busy parents get peace of mind. Dogs come home tired and happy. See why 30+ Auckland families trust the Tiny Gang — follow along on Instagram for daily adventures, wagging tails and the odd zoomie.';
export let eyebrow = '30+ five-star reviews';
export let heading = 'Proof your dog is in good hands';
export let blurb = 'Peace of mind for busy Auckland dog owners. Happier dogs, smoother routines, and a team owners trust with the important stuff.';
export let instagramHref = 'https://www.instagram.com/goodwalk.nz/';
export let instagramLabel = 'goodwalk.nz';
@@ -30,7 +31,7 @@
},
Ross: {
reviewer: 'Ross',
detail: "Otis's Dad",
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, theres a reason she has 5 stars!',
imageUrl: '/images/otis-auckland-dog-walking-review.png'
@@ -123,6 +124,7 @@
<section id="testimonials" use:reveal={{ delay: 40 }} class="reveal-block">
<div class="testimonials-inner">
<span class="testimonials-eyebrow">{eyebrow}</span>
<h2 class="section-heading">{heading}</h2>
<div class="testimonials-intro">
<p>{blurb}</p>
@@ -244,6 +246,21 @@
</section>
<style>
.testimonials-eyebrow {
display: block;
width: fit-content;
margin: 0 auto 10px;
padding: 7px 12px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
}
.testimonials-intro {
max-width: 760px;
margin: 18px auto 0;
@@ -266,7 +283,7 @@
padding: 10px 16px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
color: var(--green);
color: var(--gw-green);
font-weight: 700;
text-decoration: none;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
@@ -297,9 +314,16 @@
.testimonials-carousel {
position: relative;
margin-top: 48px;
padding: 0 38px;
}
@media (max-width: 768px) {
.testimonials-eyebrow {
margin-bottom: 8px;
padding: 6px 10px;
font-size: 11px;
}
.testimonials-intro {
margin-top: 14px;
}
@@ -555,11 +579,11 @@
}
.testimonial-arrow-left {
left: -38px;
left: 0;
}
.testimonial-arrow-right {
right: -38px;
right: 0;
}
@media (max-width: 1024px) {
@@ -588,6 +612,7 @@
@media (max-width: 767px) {
.testimonials-carousel {
margin-top: 32px;
padding: 0;
}
.testimonial-stage {
+45 -1
View File
@@ -21,7 +21,11 @@
<section id="values">
<div class="values-inner">
<h2 class="section-heading">Where dogs come first</h2>
<span class="values-eyebrow">Why owners stay</span>
<h2 class="section-heading">Calmer dogs. Clearer routines. Less worry.</h2>
<p class="values-intro">
Everything is designed to make life easier for busy Auckland dog owners and safer, happier for the dogs in our care.
</p>
<div class="values-grid">
{#each orderedValues as value}
@@ -34,3 +38,43 @@
</div>
</div>
</section>
<style>
.values-eyebrow {
display: block;
width: fit-content;
margin: 0 auto 10px;
padding: 7px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: var(--yellow);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.values-intro {
max-width: 760px;
margin: 18px auto 0;
text-align: center;
color: rgba(255, 255, 255, 0.82);
font-size: 17px;
line-height: 1.65;
}
@media (max-width: 768px) {
.values-eyebrow {
margin-bottom: 8px;
padding: 6px 10px;
font-size: 11px;
}
.values-intro {
margin-top: 14px;
font-size: 15px;
line-height: 1.55;
}
}
</style>
+59 -41
View File
@@ -3,82 +3,100 @@ import type { ServicePageContent } from '$lib/types';
export const dogWalkingContent: ServicePageContent = {
hero: {
eyebrow: '1:1 Walks',
title: 'Walks for larger breeds, too!',
title: 'A calmer walk for dogs who need more attention',
paragraphs: [
'Looking for personal attention for your big pup? Our professional dog walking service is perfect for larger dogs who walk well on leash and have good recall. Safety is paramount, so we only accommodate dogs with no reactivity issues. At Goodwalk, we pride ourselves on building relationships with both you and your furry family member.',
"Give your dog his best life while joining our growing community of happy pet parents! For those seeking extra care, we offer specialised one-on-one walks tailored to your dog's individual needs and personality"
'Goodwalk 1:1 Walks are for dogs who do better with more individual attention, a quieter setup, and a walk tailored to their own pace, confidence, and routine.',
'They can be a great fit for larger dogs, dogs who are not suited to group walks, or owners who want a more personal approach with extra care and consistency.',
'If your dog needs space, structure, and a walker who can focus fully on them, our one-on-one walks are designed for exactly that.'
],
imageUrl:
'/images/auckland-large-dog-one-on-one-walk.jpg',
imageUrl: '/images/auckland-large-dog-one-on-one-walk.jpg',
imageAlt: 'Large breed dog enjoying a Goodwalk one on one dog walk'
},
highlight: {
eyebrow: '▼・ᴥ・▼',
title: 'Personalised adventures for your dog!',
eyebrow: 'One dog. Full attention.',
title: 'Built for dogs who need a more individual kind of walk',
imageUrl: '/images/auckland-dogs-outdoor-pack.jpg',
imageAlt: 'Goodwalk dogs gathered together outdoors'
},
pricing: {
title: '1:1 Large Dog Breed Prices',
plans: [
imageAlt: 'Goodwalk dogs gathered together outdoors',
collageImages: [
{
title: '30 Minutes',
price: '$45',
period: 'Per Walk',
features: ['Free pickup/dropoff', '30 minute walk', 'Social media updates', 'Basic training']
imageUrl: '/images/one-on-one-dog-portrait-1.png',
imageAlt: 'Happy black dog on a one-on-one Goodwalk walk in Auckland'
},
{
title: '45 Minutes',
price: '$55',
period: 'Per Walk',
popular: true,
features: ['Free pickup/dropoff', '45 minute walk', 'Social media updates', 'Basic training']
imageUrl: '/images/one-on-one-dog-portrait-2.png',
imageAlt: 'Older black dog enjoying a calm one-on-one Goodwalk walk in Auckland'
},
{
title: '60 Minutes',
price: '$65',
period: 'Per Walk',
features: ['Free pickup/dropoff', '60 minute walk', 'Social media updates', 'Basic training']
imageUrl: '/images/one-on-one-dog-portrait-3.png',
imageAlt: 'Brown curly dog resting during a one-on-one Goodwalk walk in Auckland'
}
]
},
pricing: {
title: 'Choose the walk length that suits your dog',
intro:
'Our 1:1 walks are shaped around your dog, not a group schedule. Ideal for dogs who need extra attention, a steadier pace, or a more personalised walking routine.',
plans: [
{
title: '30 Minute 1:1 Walk',
price: '$45',
period: 'Per Walk',
features: ['Free pickup and drop-off', 'Shorter one-on-one walk', 'Personal attention throughout', 'Good fit for lower-energy dogs']
},
{
title: '45 Minute 1:1 Walk',
price: '$55',
period: 'Per Walk',
popular: true,
features: ['Free pickup and drop-off', 'Balanced walk length for most dogs', 'Time for calm handling and structure', 'Best fit for many routines']
},
{
title: '60 Minute 1:1 Walk',
price: '$65',
period: 'Per Walk',
features: ['Free pickup and drop-off', 'Longer individual walk', 'More time for movement and engagement', 'Best for dogs needing a fuller outing']
}
],
scarcityNote: 'A limited number of 1:1 slots are available each week.'
},
benefits: {
title: 'Benefits of our 1:1 walks',
title: 'Why some dogs do better on 1:1 walks',
items: [
{
title: 'Individualised Attention',
body: 'Large breeds receive personalised care and undivided attention from the walker, addressing their unique needs and preferences without competition from other dogs.'
title: 'They get the walkers full attention',
body: 'One-on-one walks give your dog focused handling and a calmer experience without competing with the needs of a group.'
},
{
title: 'Tailored Exercise',
body: 'Walkers can customise the pace, duration, and route of the walk to accommodate the energy levels and physical abilities of large breeds, providing an appropriate level of exercise tailored to their specific needs.'
title: 'The walk matches their pace',
body: 'We can tailor the route, speed, and duration to suit your dogs energy, confidence, and physical needs.'
},
{
title: 'Bonding and Socialisation',
body: 'During one-on-one walks, large breeds bond closely with their walker and socialise with people and animals encountered, promoting confidence and social skills'
title: 'They have more space to relax',
body: 'Dogs who are not suited to pack walks often feel more comfortable when they can move through the world without the pressure of a group.'
},
{
title: 'Enhanced safety',
body: "With one-on-one walks, there's reduced risk of potential conflicts or incidents that may arise in group settings, ensuring a safer walking experience for large breeds."
title: 'You get a more tailored routine',
body: 'A 1:1 setup gives us more flexibility to build a walking routine around what works best for your dog and your week.'
},
{
title: 'Training Opportunities',
body: 'One-on-one walks offer dedicated time for training and reinforcement of good behaviours, such as loose leash walking or obedience commands, helping large breeds develop and maintain positive habits.'
title: 'There is room for better habits',
body: 'One-on-one walks create more opportunity to reinforce calm walking, better focus, and the practical behaviours that make daily life easier.'
},
{
title: 'Stress Reduction',
body: 'Large breeds may feel more relaxed and comfortable during one-on-one walks, as they can explore and enjoy their surroundings without the potential stressors of a group dynamic, leading to a more positive walking experience overall.'
title: 'You can feel more confident leaving them with us',
body: 'For owners of dogs who need a bit more care, 1:1 walks offer reassurance that your dog is getting a more considered, individual approach.'
}
]
},
testimonialsHeading: 'What our clients say',
booking: {
title: "Let's meet!",
subtitle: 'Fill out your details below, and we can arrange a Meet & Greet for a one on one walk!',
title: 'See if 1:1 walks are the right fit',
subtitle: 'Fill out your details below and well arrange a free Meet & Greet to learn more about your dog.',
formAction: '/contact-us',
serviceOptions: [],
ownerStepLabel: 'Your details',
dogStepLabel: 'Your dog',
dogIntro: 'Tell us about your dog, your area, and anything important we should know before arranging a one on one Meet & Greet.'
dogIntro:
'Tell us about your dog, your area, and anything important we should know so we can see whether a 1:1 walk is the right fit.'
}
};
+59 -33
View File
@@ -31,12 +31,18 @@ export const homepageContent: HomePageContent = {
megaMenuFooter: { label: 'View all pricing', href: '/our-pricing' }
},
hero: {
title: 'Unleashing Fun in',
highlight: "Your Dog's Day!",
mobileTitle: "Unleashing Fun in\nYour Dog's Day!",
subtitle: "Trusted Auckland Central dog walking — small packs, solo adventures, and puppy visits from a team that knows your dog by name",
title: 'Come home to a',
highlight: 'calm, happy dog',
mobileTitle: 'Come home to a\ncalm, happy dog',
subtitle:
'Dog walking for busy Auckland Central professionals who want a reliable, relationship-led team their dog knows by name.',
primaryCta: { label: 'Book a Meet & Greet', href: '#newlead', variant: 'yellow' },
secondaryCta: { label: 'Explore our services →', href: '#services', variant: 'outline' },
secondaryCta: {
label: 'Message us on Instagram',
href: 'https://www.instagram.com/goodwalk.nz/',
variant: 'outline',
external: true
},
imageUrl: '/images/auckland-dog-walking-happy-dog-hero.png',
imageAlt: 'Happy dog ready for a professional pack walk with Goodwalk Auckland dog walking service'
},
@@ -49,16 +55,16 @@ export const homepageContent: HomePageContent = {
}
},
promise: {
title: 'Happy pets,',
subtitle: 'happy humans',
title: 'Meet Aless,',
subtitle: 'the heart of Goodwalk',
body: [
'We specialise in the unique needs of small-to-medium breeds — easing stress and anxiety while keeping tails wagging.',
'Professional dog walking across Auckland for small, medium and large breeds, with tailored pack walks for smaller dogs and one-on-one walks for larger breeds — giving every dog the personalised attention they deserve. Ready to join our'
'Goodwalk was built for owners who want more than a basic walk. Alessandra leads the business with a calm, hands-on approach shaped by years of experience, a love of small dogs, and a real focus on trust, routine, and safety.',
"From house keys to nervous first walks, we take the responsibility seriously. You'll know who is walking your dog, your dog will know who is at the door, and you'll get a reliable team that treats your dog like family. Ready to join the"
],
emphasis: 'TINY GANG?',
cta: { label: 'Book a free Meet & Greet', href: '/contact-us', variant: 'green' },
imageUrl: '/images/auckland-dog-walking-happy-dogs-happy-humans.webp',
imageAlt: 'Woman cuddling a dog for Goodwalk Auckland dog walking services'
imageUrl: '/images/goodwalk-dog-walker-alessandra.png',
imageAlt: 'Alessandra from Goodwalk with a dog in Auckland'
},
services: [
{
@@ -85,21 +91,31 @@ export const homepageContent: HomePageContent = {
],
howItWorks: {
title: 'How it works',
//intro: 'A simple onboarding flow designed to make sure the fit is right for both you and your dog.',
intro:
'A calm, simple start designed to give you confidence quickly and help your dog settle into the right routine.',
steps: [
{
title: 'Meet & Greet',
body: 'We meet you and your dog first, talk through routine, temperament, and what support you need.',
phase: 'Meet',
benefit: 'No pressure, just clarity',
title: 'We get to know your dog properly',
body:
'Your free Meet & Greet gives you a chance to ask questions, talk through routine and temperament, and make sure the fit feels right before anything starts.',
icon: 'fas fa-handshake'
},
{
title: 'Two assessment walks',
body: 'We ease your dog in with two assessment walks so we can check confidence, handling, and group fit.',
phase: 'Settle',
benefit: 'A smoother start for nervous dogs',
title: 'Your dog eases in without overwhelm',
body:
'We start with assessment walks so your dog can build confidence, settle with the walker, and find the right pace before moving into a regular routine.',
icon: 'fas fa-clipboard-check'
},
{
title: 'Happy dogs, happy humans',
body: 'Once approved, your dog joins regular walks and comes home tired, settled, and ready for a nap.',
phase: 'Thrive',
benefit: 'The outcome you actually want',
title: 'You get a calmer, happier dog at home',
body:
'Once everything feels right, your dog joins their regular walks and comes home exercised, settled, and ready to relax while you get more peace of mind in your week.',
icon: 'fas fa-heart'
}
]
@@ -107,42 +123,42 @@ export const homepageContent: HomePageContent = {
values: [
{
icon: 'fas fa-heart',
title: 'Kindness',
title: 'Calm, kind handling',
body:
'With gentle care and genuine affection, we make every walk a calm, happy experience. We use positive reinforcement to encourage good behaviour because kindness is at the heart of everything we do.'
'We use positive reinforcement, gentle handling, and patient routines so dogs build confidence instead of stress.'
},
{
icon: 'fas fa-camera',
title: 'Daily Updates',
title: 'Daily updates you will actually want',
body:
"Catch your pup in action with daily social updates - showcasing their walks, playtime, and mischief with the Tiny Gang. It's your window into their happiest moments."
"You get to see your dog out enjoying the day, which means less wondering and more peace of mind while you're at work."
},
{
icon: 'fas fa-users',
title: 'Small Pack Sizes',
order: 2,
body:
'With just 4-8 dogs per group, our walks are calm, controlled, and respectful of public spaces - ensuring every dog gets the attention and care they deserve.'
'With just 4-8 dogs per group, walks stay calm, structured, and manageable, with enough attention for every dog.'
},
{
icon: 'fas fa-shield-heart',
title: 'Safety',
title: 'Safety-first by default',
order: 1,
body:
'Our team is fully pet first aid certified and trained to handle any situation calmly and confidently. With proactive safety protocols and constant situational awareness, we create a secure environment for every walk.'
'Pet first aid, careful screening, and proactive handling are built into every walk, not added on as a nice extra.'
},
{
icon: 'fas fa-calendar-check',
title: 'Flexibility',
title: 'Built for real schedules',
body:
"We know life gets busy - so while we specialise in regular, permanent walks, we're always happy to adapt. Just give us a little notice, and we'll do our best to accommodate your changing schedule."
"We specialise in regular walks, but we know life changes. Give us notice and we'll do our best to help keep things running smoothly."
},
{
icon: 'fas fa-clock',
title: 'Reliability',
title: 'Reliable pickup, clear communication',
order: 3,
body:
"We guarantee punctuality and consistency, so you can count on us. With clear communication, you'll always be in the loop - and your dog's needs will always be our top priority."
"You should not have to chase your dog walker. We keep things consistent, communicate clearly, and make the practical side feel easy."
}
],
testimonials: [
@@ -164,7 +180,7 @@ export const homepageContent: HomePageContent = {
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, theres a reason she has 5 stars!',
reviewer: 'Ross',
detail: "Otis's Dad",
detail: "Otis's dad",
imageUrl: '/images/otis-auckland-dog-walking-review.jpg'
},
{
@@ -178,15 +194,15 @@ export const homepageContent: HomePageContent = {
booking: {
title: "Let's meet!",
subtitle:
"Almost there — just your contact details. We'll reply within 24 hours to arrange your free, no-obligation Meet & Greet.",
"A few contact details and well be in touch within 24 hours to arrange your free, no-obligation Meet & Greet.",
generalSubtitle:
"Almost there — just your contact details. We'll reply within 24 hours.",
"A few contact details and well reply properly within 24 hours.",
formAction: '/contact-us',
serviceOptions: ['Pack Walks', '1:1 Walks', 'Puppy Visits', 'Other Services'],
ownerStepLabel: 'Your details',
dogStepLabel: 'Your dog',
generalIntro:
'Got feedback, a complaint, or a business enquiry? Choose general enquiry and send us the details without filling in dog or service information.'
'Need to send feedback, make a complaint, or ask a business question? Choose general enquiry and send us the details without filling in dog or service information.'
},
info: {
title: 'Locations & Hours',
@@ -208,6 +224,11 @@ export const homepageContent: HomePageContent = {
question: 'How does payment work?',
answer: 'All walks are paid for a week in advance, via invoice.'
},
{
question: 'Do you provide a casual service?',
answer:
'Yes, we do offer casual rates, but they are priced higher. The best value for money is regular walks.'
},
{
question: 'What requirements does my dog need?',
answer:
@@ -218,6 +239,11 @@ export const homepageContent: HomePageContent = {
answer:
'All walkers are covered by public liability insurance, and all walkers hold a current First Aid training certificate.'
},
{
question: 'Do I need to leave keys with you?',
answer:
'Usually, yes, if no one will be home when we collect or return your dog. We can go over the best option for access during your Meet & Greet.'
},
{
question: 'What happens if the weather is bad?',
answer:
+7 -7
View File
@@ -5,13 +5,13 @@ import { puppyVisitsContent } from './puppy-visits';
export const ourPricingContent: PricingPageContent = {
title: 'Our Pricing',
subtitle: 'Simple, transparent pricing — no lock-in contracts.',
subtitle: 'Choose the Goodwalk routine that fits your dog, your week, and the kind of support you need.',
sections: [
{
title: 'Pack Walks',
icon: 'fas fa-paw',
blurb:
'Small group adventures for calm, social dogs who thrive with structure, play, and regular weekly outings.',
'Our specialty for sociable small and medium dogs who thrive with calm structure, regular weekly outings, and the right dog company.',
detailCta: {
label: 'View Pack Walks',
href: '/pack-walks',
@@ -23,7 +23,7 @@ export const ourPricingContent: PricingPageContent = {
title: '1:1 Walks',
icon: 'fas fa-person-walking',
blurb:
'One-on-one walks tailored to your dogs pace, confidence, and personality for a more focused outing.',
'A more individual option for dogs who need extra attention, more space, or a walk shaped around their own pace and confidence.',
detailCta: {
label: 'View 1:1 Walks',
href: '/dog-walking',
@@ -35,7 +35,7 @@ export const ourPricingContent: PricingPageContent = {
title: 'Puppy Visits',
icon: 'fas fa-dog',
blurb:
'Short home visits for young pups who need company, enrichment, toilet breaks, and gentle routine support.',
'Home visits for young puppies who need company, toilet breaks, routine support, and a calmer start before they are ready for bigger adventures.',
detailCta: {
label: 'View Puppy Visits',
href: '/puppy-visits',
@@ -46,13 +46,13 @@ export const ourPricingContent: PricingPageContent = {
],
testimonialsHeading: 'What our clients say',
booking: {
title: 'Ready to join the Tiny Gang?',
title: 'Tell us about your dog',
subtitle: '',
formAction: '/contact-us',
serviceOptions: [],
ownerStepLabel: 'Your details',
dogStepLabel: 'Dog details',
dogStepLabel: 'Your dog',
dogIntro:
'Tell us about your dog, where you are based, and anything important we should know before we arrange a Meet & Greet.'
'Tell us about your dog, where you are based, and what kind of support you are looking for so we can help point you to the right Goodwalk service.'
}
};
+36 -35
View File
@@ -3,50 +3,50 @@ import type { ServicePageContent } from '$lib/types';
export const packWalksContent: ServicePageContent = {
hero: {
eyebrow: 'Pack Walks',
title: 'Join our Tiny Gang!',
title: 'Come home to a calm, happy dog',
paragraphs: [
'Fun, safe, and specially designed for little paws, these adventures help your dog build friendships and confidence in a calm, friendly group.',
'We only welcome sociable dogs, so every outing feels secure and stress-free. As small dog owners ourselves, we know just what it takes to help your pup feel relaxed, happy, and right at home.',
'Join the Tiny Gang today—because your dog deserves more than just a walk. They deserve a tail-wagging good time!'
'Goodwalk Pack Walks are built for Auckland Central owners of small and medium dogs who want a reliable weekly routine, a well-exercised dog, and more peace of mind during the workday.',
'Our Tiny Gang packs stay small, calm, and carefully matched, so sociable dogs can build confidence, enjoy safe group outings, and come home settled instead of overstimulated.',
'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.'
],
imageUrl: '/images/auckland-small-dog-pack-walk.jpg',
imageAlt: 'Small dogs together on a Goodwalk Tiny Gang pack walk'
imageUrl: '/images/auckland-pack-walk-small-dogs-group.png',
imageAlt: "Small dogs from Goodwalk's Tiny Gang pack walk sitting together in an Auckland park"
},
highlight: {
eyebrow: '▼・ᴥ・▼',
title: 'Goodwalk is the best choice for small and medium size dogs!',
imageUrl: '/images/tiny-gang-auckland-dog-pack.jpg',
imageAlt: 'Goodwalk Tiny Gang dogs gathered together in Auckland'
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',
imageAlt: 'Small and medium dogs together on a Goodwalk pack walk in Auckland'
},
pricing: {
title: 'Tiny Gang Prices',
title: 'Choose the weekly routine that suits your dog',
intro:
'Small packs of 4-8 dogs, 2-hour outings at Aucklands scenic dog parks and beaches, with free pick-up and drop-off included. We reinforce recall, car manners, and leash etiquette while your dog plays. Booked as a permanent weekly slot — gift your dog the best life!',
'Tiny Gang Pack Walks are our specialty: small packs of 4-8 dogs, structured outings, and free pick-up and drop-off across Auckland Central. Best suited to small and medium sociable dogs who thrive with routine, good company, and calm handling.',
plans: [
{
title: '1 Walk',
title: '1 Walk Per Week',
price: '$58',
period: 'Per Walk',
features: ['Free pickup/dropoff', '1 hour adventure', 'Social media updates', 'Basic training']
features: ['One regular walk each week', 'Free pickup and drop-off', 'Calm small-group outing', 'Best for dogs starting out']
},
{
title: '2-3 Walks',
title: '2-3 Walks Per Week',
price: '$55',
period: 'Per Walk',
popular: true,
features: ['Free pickup/dropoff', '1 hour adventure', 'Social media updates', 'Basic training']
features: ['Two to three regular walks each week', 'Free pickup and drop-off', 'Consistent exercise and social time', 'Best fit for busy owners']
},
{
title: '4-5 Walks',
title: '4-5 Walks Per Week',
price: '$49.50',
period: 'Per Walk',
features: ['Free pickup/dropoff', '1 hour adventure', 'Social media updates', 'Basic training']
features: ['Four to five regular walks each week', 'Free pickup and drop-off', 'Maximum consistency and structure', 'Best for high-energy social dogs']
},
{
title: 'Casual Walk',
title: 'Casual Pack Walk',
price: '$65',
period: 'Per Walk',
features: ['Free pickup/dropoff', '1 hour adventure', 'Social media updates', 'Basic training']
features: ['Casual availability only', 'Free pickup and drop-off', 'For dogs already suited to pack walks', 'Higher rate than weekly routines']
}
],
extras: [
@@ -57,42 +57,43 @@ export const packWalksContent: ServicePageContent = {
scarcityNote: 'We keep packs small (4-8 dogs) — popular days fill up fast.'
},
benefits: {
title: 'Tiny Gang membership benefits',
title: 'Why small and medium dogs do so well in Tiny Gang',
items: [
{
title: 'Socialisation with other dogs',
body: 'Tiny Gang pack walks help small and medium-sized dogs mingle and learn social skills from each other, boosting their confidence and positive behaviour.'
title: 'They come home more settled',
body: 'Regular structured outings help dogs burn energy properly, so they are more likely to come home relaxed, content, and ready to rest.'
},
{
title: 'Tailored pace',
body: 'Our handlers can adjust the pace and intensity of the walk to suit the energy levels and abilities of small and medium-sized dogs, ensuring a pleasant and enjoyable experience for all participants.'
title: 'They build confidence around the right dogs',
body: 'Carefully matched small-group walks help sociable dogs enjoy company without the chaos or pressure that can come with bigger mixed packs.'
},
{
title: 'Comfort',
body: 'Smaller groups create a more relaxed and comfortable atmosphere for dogs, allowing them to explore and enjoy the walk without feeling overwhelmed by larger dogs.'
title: 'They are not overwhelmed by size mismatch',
body: 'Because we specialise in small and medium dogs, the pace, play, and group dynamic are designed around what helps them feel safe and comfortable.'
},
{
title: 'Increased bonding',
body: 'Tiny Gang pack walks foster stronger bonds between dogs and their walker, as well as between the dogs themselves, enhancing trust and companionship among the group.'
title: 'You get a routine you can rely on',
body: 'Regular weekly slots make life easier for busy owners who want dependable exercise and less guilt while they are at work.'
},
{
title: 'Individualised attention',
body: 'Small pack sizes allow for more personalised care and attention from the walker, addressing the unique needs and preferences of small and medium-sized breeds.'
title: 'They still get individual attention',
body: 'Keeping packs to 4-8 dogs means we can pay attention to confidence, handling, and the little things that make a big difference.'
},
{
title: 'Safety',
body: "With a smaller group composed of dogs of similar sizes, there's reduced risk of accidental injury or intimidation, ensuring a safer walking environment."
title: 'Safety stays built in',
body: 'Unlike overloaded pack walks, our small, compatible groups reduce intimidation and help create a safer, calmer environment than a one-size-fits-all approach.'
}
]
},
testimonialsHeading: 'What our clients say',
booking: {
title: 'Join the Tiny Gang!',
title: 'See if your dog fits our Tiny Gang',
subtitle: '',
formAction: '/contact-us',
serviceOptions: [],
ownerStepLabel: 'Your details',
dogStepLabel: 'Dog details',
dogIntro: 'Tell us about your dog and where you are based so we can plan the right Tiny Gang Meet & Greet.'
dogIntro:
'Tell us about your small or medium dog, where you are based, and anything important we should know so we can see if Tiny Gang is the right fit.'
}
};
+43 -23
View File
@@ -3,65 +3,85 @@ import type { ServicePageContent } from '$lib/types';
export const puppyVisitsContent: ServicePageContent = {
hero: {
eyebrow: 'Puppy Visits',
title: 'Introducing Puppy Visits: Building strong foundations for our pack walks!',
title: 'Give your puppy a calmer start while you are out',
paragraphs: [
"We love puppies! Our puppy home visits are perfect for young pups not quite ready to join the pack and busy owners with hectic schedules. We lay the groundwork for future pack walks, including fun games, potty breaks, and even feeding if required. Let us help your furry friend thrive while you're away!"
'Goodwalk Puppy Visits are designed for busy owners who want their puppy cared for properly during the day, with toilet breaks, play, feeding, and calm one-on-one attention at home.',
'They are also the first stage of the Goodwalk journey. For puppies who may later join our Pack Walks, these visits help build familiarity, confidence, and the early routines that make that transition much smoother.',
'Instead of just getting through the day, your puppy gets a more thoughtful start, and you get more peace of mind while you are away.'
],
imageUrl: '/images/auckland-puppy-home-visit.jpg',
imageAlt: 'Puppy Visits page splash image'
imageAlt: 'Puppy receiving a calm Goodwalk home visit in Auckland'
},
highlight: {
eyebrow: 'Start well. Grow well.',
title: 'A home visit now can help set your puppy up for calmer routines and future Pack Walks later on',
imageUrl: '/images/auckland-puppy-visits-cavalier-king-charles-spaniel.jpg',
imageAlt: 'Young Cavalier King Charles Spaniel puppy resting at home before future Goodwalk Pack Walk training in Auckland'
},
pricing: {
title: 'Puppy Visits',
title: 'Choose the visit length that suits your puppy',
intro:
'Puppy Visits are built around your puppys age, routine, and energy levels, with practical support now and foundations for later social walking if they are a good fit for our Tiny Gang.',
plans: [
{
title: '20 Minutes',
title: '20 Minute Visit',
price: '$39',
period: 'Per Visit',
features: ['Bathroom break', 'Pet feed', 'Basic training', 'Enrichment games']
features: ['Toilet break and check-in', 'Feeding if needed', 'Gentle one-on-one attention', 'Good for shorter midday support']
},
{
title: '45 Minutes',
title: '45 Minute Visit',
price: '$49',
period: 'Per Visit',
features: ['Bathroom break', 'Pet feed', 'Basic training', 'Enrichment games']
features: ['Toilet break and feeding if needed', 'Play and enrichment time', 'Early routine-building support', 'Best fit for many puppies']
},
{
title: '1 Hour',
title: '60 Minute Visit',
price: '$55',
period: 'Per Visit',
features: ['Bathroom break', 'Pet feed', 'Basic training', 'Enrichment games']
features: ['Longer home visit', 'More play, settling, and engagement', 'Extra support for younger puppies', 'Best for pups needing more time']
}
]
],
scarcityNote: 'Puppy Visit spaces are limited so we can keep care consistent.'
},
benefits: {
title: 'Puppy Visits benefits',
title: 'Why Puppy Visits matter early',
items: [
{
title: 'Enrichment',
body: 'From stimulating games to sensory toys, we keep those curious minds engaged and little tails wagging.'
title: 'Fewer long stretches alone',
body: 'Regular visits break up the day, help with toilet timing, and give your puppy company, care, and comfort while you are out.'
},
{
title: 'Setting up the basics for pack walks',
body: "Lay the groundwork for your pup's adult life. We'll guide you through setting the right tone, offering basic training tips and tricks along the way."
title: 'Better foundations for future Pack Walks',
body: 'For puppies who may later join our Tiny Gang, early visits help build confidence, familiarity, and the routines that support a smoother next step.'
},
{
title: 'Reduce anxiety',
body: "With time your pup will know when to expect a visit, reducing the chances of accidents while you're away. With regular visits, your pup will feel loved and secure, minimising any time spent at home alone."
title: 'A calmer puppy at home',
body: 'Play, enrichment, and routine help use up some puppy energy in the right way, which can mean a more settled puppy through the rest of the day.'
},
{
title: 'Expert advice',
body: "As experienced dog pawrents, we've been through it all with many adorable puppies. Consider us your go-to for any questions or concerns as your furry friend grows up."
title: 'Support for busy owners too',
body: 'You get practical help during a demanding stage, plus guidance from a team that understands how much consistency matters when puppies are learning fast.'
},
{
title: 'Early habits start taking shape',
body: 'Visits give us time to reinforce the basics around handling, routine, and calm engagement before those small habits become bigger problems.'
},
{
title: 'A more personal start with Goodwalk',
body: 'Puppy Visits help your puppy get to know us early, which builds trust and makes any future transition into other Goodwalk services feel more natural.'
}
]
},
testimonialsHeading: 'What our clients say',
booking: {
title: 'Ready to join the Tiny Gang?',
title: 'See if Puppy Visits are the right start',
subtitle: '',
formAction: '/contact-us',
serviceOptions: [],
ownerStepLabel: 'Your details',
dogStepLabel: 'Dog details',
dogIntro: 'Tell us about your puppy, your area, and any special needs so we can plan the right visit.'
dogStepLabel: 'Puppy details',
dogIntro:
'Tell us about your puppy, your area, routine, and any special needs so we can plan the right visit and see what support fits best.'
}
};
+6 -6
View File
@@ -1,20 +1,20 @@
export const staticPages = {
'pack-walks': {
title: 'Pack Walks | Join Our Tiny Gang',
title: 'Pack Walks for Small & Medium Dogs | Auckland Central',
description:
'Join our Tiny Gang pack walks. We take our dogs to beautiful parks and beaches around the Auckland region.',
'Pack walks for sociable small and medium dogs in Auckland Central. Calm group outings, regular weekly routines, and free Meet & Greet with Goodwalk.',
canonicalPath: '/pack-walks'
},
'dog-walking': {
title: '1 on 1 Walks | Professional Dog Walking | Auckland Wide',
title: '1:1 Dog Walks for Dogs Who Need More Attention | Auckland Central',
description:
'Our 1:1 (one on one) are perfect for dogs with great recall and leash manners, our walks guarantee a stress-free experience!',
'One-on-one dog walks in Auckland Central for dogs who need more attention, more space, or a calmer routine. Free Meet & Greet with Goodwalk.',
canonicalPath: '/dog-walking'
},
'puppy-visits': {
title: 'Puppy Visits | Auckland In-Home Puppy Care | Goodwalk',
title: 'Puppy Visits & In-Home Puppy Care | Auckland Central',
description:
'In-home puppy visits across Auckland Central toilet breaks, feeding, play and gentle early training for pups not yet ready for pack walks.',
'In-home puppy visits across Auckland Central with toilet breaks, feeding, play, and early routine support. A calm start before future pack walks.',
canonicalPath: '/puppy-visits'
},
'our-pricing': {
+6
View File
@@ -13,10 +13,16 @@ const imageMetadata: Record<string, ImageMetadata> = {
'/images/otis-auckland-dog-walking-review.png': { width: 1254, height: 1254 },
'/images/wallace-auckland-dog-walking-review.png': { 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/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 },
'/images/auckland-puppy-home-visit.jpg': { width: 640, height: 427 },
'/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/goodwalk-dog-walker-alessandra.png': { width: 640, height: 640 }
+4
View File
@@ -3,3 +3,7 @@ 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);
}
+2
View File
@@ -8,6 +8,7 @@
html {
scroll-behavior: smooth;
overflow-x: clip;
}
body {
@@ -16,6 +17,7 @@ body {
line-height: 1.6;
color: var(--text);
background: var(--off-white);
overflow-x: clip;
}
img {
+5 -5
View File
@@ -34,7 +34,7 @@
}
.btn-green {
background: var(--green);
background: var(--gw-green);
color: #fff;
}
@@ -59,15 +59,15 @@
.btn-outline:hover {
background: #fff;
color: var(--green);
color: var(--gw-green);
}
.btn-outline-green {
color: var(--green);
border-color: var(--green);
color: var(--gw-green);
border-color: var(--gw-green);
}
.btn-outline-green:hover {
background: var(--green);
background: var(--gw-green);
color: #fff;
}
+116 -24
View File
@@ -7,6 +7,20 @@
margin-bottom: 44px;
}
.booking-eyebrow {
display: inline-block;
margin-bottom: 14px;
padding: 7px 12px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
}
.booking-title {
display: block;
text-align: center;
@@ -28,6 +42,39 @@
color: var(--yellow);
}
.booking-intro {
max-width: 720px;
margin: -8px auto 0;
color: #4c5056;
font-size: 17px;
line-height: 1.65;
}
.booking-trust-row {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
margin-top: 18px;
}
.booking-trust-chip {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 40px;
padding: 8px 14px;
border-radius: 999px;
background: #fff;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 10px 24px rgba(17, 20, 24, 0.04);
color: var(--gw-green);
font-size: 14px;
font-weight: 600;
line-height: 1.2;
}
.booking-title-highlight::after {
content: '';
position: absolute;
@@ -37,6 +84,31 @@
height: 28px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 34' fill='none'%3E%3Cpath d='M4 24C67 10 131 4 198 5c43 1 82 6 118 18' stroke='%23192419' stroke-width='8' stroke-linecap='round'/%3E%3C/svg%3E")
center/contain no-repeat;
transform-origin: left center;
animation: booking-underline-draw 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0.2s both;
}
@keyframes booking-underline-draw {
0% {
opacity: 0;
transform: scaleX(0.2) translateY(6px) rotate(-1.5deg);
}
65% {
opacity: 1;
transform: scaleX(1.04) translateY(0) rotate(0deg);
}
100% {
opacity: 1;
transform: scaleX(1) translateY(0) rotate(0deg);
}
}
@media (prefers-reduced-motion: reduce) {
.booking-title-highlight::after {
animation: none;
}
}
.booking-stepper {
@@ -44,6 +116,7 @@
align-items: center;
justify-content: center;
gap: 28px;
margin-top: 24px;
}
.booking-step {
@@ -63,7 +136,7 @@
.booking-step-number {
width: 56px;
height: 56px;
border: 3px solid #111;
border: 2px solid rgba(33, 48, 33, 0.18);
border-radius: 50%;
display: inline-flex;
align-items: center;
@@ -80,7 +153,8 @@
}
.booking-step.active .booking-step-number {
background: #eadbbf;
background: rgba(33, 48, 33, 0.1);
border-color: var(--gw-green);
}
@media (hover: hover) {
@@ -119,14 +193,15 @@
}
.booking-panel-banner {
background: #eadbbf;
background: linear-gradient(180deg, #f6f2ea 0%, #f1ece3 100%);
color: #34363a;
border-radius: 30px 30px 0 0;
padding: 28px 32px 42px;
border-radius: 30px;
padding: 24px 28px 34px;
text-align: center;
font-family: var(--font-body);
font-size: 15px;
line-height: 1.35;
line-height: 1.55;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
}
.booking-card-grid {
@@ -135,7 +210,7 @@
}
.booking-card-grid-with-banner {
margin-top: -30px;
margin-top: -18px;
}
.booking-card-grid-owner {
@@ -150,7 +225,9 @@
background: #fff;
border-radius: 28px;
padding: 32px 38px 30px;
box-shadow: 0 10px 30px rgba(17, 20, 24, 0.04);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 10px 30px rgba(17, 20, 24, 0.04);
}
.booking-field-card-group {
@@ -175,10 +252,18 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.booking-field-group-dog {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.booking-field-stack {
min-width: 0;
}
.booking-field-stack-full {
grid-column: 1 / -1;
}
.booking-field-card label,
.booking-service-label {
display: inline-flex;
@@ -198,6 +283,10 @@
color: #666;
}
.booking-field-stack .booking-service-label {
margin-bottom: 14px;
}
.booking-required {
color: var(--yellow);
}
@@ -205,7 +294,7 @@
.booking-field-card input,
.booking-field-card textarea {
width: 100%;
border: 3px solid #111;
border: 2px solid rgba(33, 48, 33, 0.18);
border-radius: 20px;
background: #fff;
padding: 16px 28px;
@@ -219,14 +308,14 @@
.booking-field-card input:hover,
.booking-field-card textarea:hover {
background: #f6f0e5;
background: #f8f5ef;
}
.booking-field-card input:focus,
.booking-field-card textarea:focus {
outline: none;
border-color: var(--green);
background: #f6f0e5;
border-color: var(--gw-green);
background: #f8f5ef;
}
.booking-field-card textarea {
@@ -243,6 +332,9 @@
border-radius: 28px;
padding: 22px 28px;
box-shadow: 0 10px 30px rgba(17, 20, 24, 0.04);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 10px 30px rgba(17, 20, 24, 0.04);
}
.booking-toggle-group {
@@ -257,7 +349,7 @@
gap: 12px;
min-height: 56px;
padding: 14px 18px;
border: 3px solid #111;
border: 2px solid rgba(33, 48, 33, 0.18);
border-radius: 18px;
font-family: var(--font-head);
font-size: 15px;
@@ -271,7 +363,7 @@
}
.booking-toggle-option:hover {
background: #f6f0e5;
background: #f8f5ef;
}
.booking-toggle-option input {
@@ -283,7 +375,7 @@
.booking-toggle-indicator {
width: 24px;
height: 24px;
border: 3px solid #111;
border: 2px solid rgba(33, 48, 33, 0.28);
border-radius: 50%;
display: inline-flex;
align-items: center;
@@ -295,8 +387,8 @@
}
.booking-toggle-option input:checked + .booking-toggle-indicator {
border-color: #111;
background: var(--yellow);
border-color: var(--gw-green);
background: rgba(33, 48, 33, 0.12);
}
.booking-toggle-option input:checked + .booking-toggle-indicator::after {
@@ -304,7 +396,7 @@
width: 10px;
height: 10px;
border-radius: 50%;
background: #111;
background: var(--gw-green);
}
@@ -334,7 +426,7 @@
.booking-check-box {
width: 30px;
height: 30px;
border: 3px solid #111;
border: 2px solid rgba(33, 48, 33, 0.28);
border-radius: 8px;
display: inline-flex;
align-items: center;
@@ -346,16 +438,16 @@
}
.booking-check-option input:checked + .booking-check-box {
background: var(--yellow);
border-color: #111;
background: rgba(33, 48, 33, 0.12);
border-color: var(--gw-green);
}
.booking-check-option input:checked + .booking-check-box::after {
content: '';
width: 10px;
height: 16px;
border-right: 3px solid #111;
border-bottom: 3px solid #111;
border-right: 3px solid var(--gw-green);
border-bottom: 3px solid var(--gw-green);
transform: rotate(40deg) translate(-1px, -2px);
}
@@ -379,7 +471,7 @@
text-align: center;
font-size: 13px;
line-height: 1.45;
color: #666;
color: #6a6d72;
}
.booking-next-button,
+5 -5
View File
@@ -3,7 +3,7 @@ header {
z-index: 100;
isolation: isolate;
overflow: visible;
background: var(--green);
background: var(--gw-green);
}
nav,
@@ -54,12 +54,12 @@ nav {
.nav-links > li > a:hover {
background: #fff;
color: var(--green);
color: var(--gw-green);
}
.nav-links > li > a.nav-link-active {
background: #fff;
color: var(--green);
color: var(--gw-green);
}
.mega-chevron {
@@ -113,7 +113,7 @@ nav {
padding: 12px 14px;
border-radius: 12px;
background: rgba(33, 48, 33, 0.04);
color: var(--green);
color: var(--gw-green);
font-size: 13px;
font-weight: 700;
text-decoration: none;
@@ -158,7 +158,7 @@ nav {
.mega-icon {
width: 64px;
height: 64px;
background: var(--green);
background: var(--gw-green);
border-radius: 16px;
display: flex;
align-items: center;
+32 -6
View File
@@ -44,6 +44,9 @@
*/
body {
font-size: 16px;
}
body.mobile-cta-enabled {
padding-bottom: 64px;
}
@@ -104,7 +107,7 @@
min-height: 44px;
padding: 11px 14px;
background: rgba(33, 48, 33, 0.1);
color: var(--green);
color: var(--gw-green);
font-size: 13px;
font-weight: 600;
}
@@ -400,12 +403,34 @@
margin-bottom: 34px;
}
.booking-eyebrow {
margin-bottom: 12px;
padding: 6px 10px;
font-size: 11px;
}
.booking-title {
font-size: 34px;
line-height: 1.02;
margin-bottom: 24px;
}
.booking-intro {
margin-top: -4px;
font-size: 15px;
line-height: 1.55;
}
.booking-trust-row {
gap: 10px;
margin-top: 16px;
}
.booking-trust-chip {
padding: 8px 12px;
font-size: 13px;
}
.booking-title-highlight::after {
left: 12px;
right: 12px;
@@ -453,7 +478,8 @@
grid-template-columns: 1fr;
}
.booking-field-group-owner {
.booking-field-group-owner,
.booking-field-group-dog {
grid-template-columns: 1fr;
gap: 18px;
}
@@ -566,10 +592,6 @@
border-radius: 24px;
}
.footer-nav {
columns: 1;
}
.footer-action-title {
font-size: 22px;
}
@@ -602,6 +624,10 @@
}
@media (max-width: 480px) {
.footer-nav {
gap: 2px 16px;
}
.mobile-phone {
gap: 6px;
padding: 10px 12px;
+38 -11
View File
@@ -3,7 +3,7 @@ section {
}
#hero {
background: var(--green);
background: var(--gw-green);
color: #fff;
padding: 44px 50px 0;
display: flex;
@@ -207,7 +207,7 @@ section {
padding: 9px 16px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: var(--green);
color: var(--gw-green);
font-size: 16px;
font-weight: 700;
text-decoration: none;
@@ -282,9 +282,35 @@ section {
align-items: flex-end;
}
.promise-img-frame {
position: relative;
width: min(100%, 520px);
padding: 18px 18px 32px 32px;
border-radius: 32px;
background: linear-gradient(180deg, #faf8f3 0%, #f2eee6 100%);
box-shadow: 0 18px 34px rgba(17, 20, 24, 0.08);
}
.promise-img-frame::before {
content: '';
position: absolute;
left: 0;
right: 46px;
bottom: 0;
height: 18px;
border-radius: 999px;
background: var(--gw-green);
opacity: 0.14;
pointer-events: none;
}
.promise-img img {
width: 100%;
max-width: 560px;
display: block;
border-radius: 28px 28px 88px 28px;
box-shadow: 0 10px 24px rgba(17, 20, 24, 0.1);
object-fit: cover;
}
.service-card {
@@ -344,7 +370,7 @@ section {
.service-card .service-card-icon {
font-size: 34px;
color: var(--green);
color: var(--gw-green);
margin-bottom: 0;
}
@@ -355,7 +381,7 @@ section {
padding: 6px 14px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
color: var(--green);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
@@ -372,7 +398,7 @@ section {
#values,
footer {
background: var(--green);
background: var(--gw-green);
color: #fff;
}
@@ -470,7 +496,7 @@ footer {
}
.info-copy a {
color: var(--green);
color: var(--gw-green);
font-weight: 600;
}
@@ -495,7 +521,7 @@ footer {
.faq summary::after {
content: '+';
font-size: 20px;
color: var(--green);
color: var(--gw-green);
}
.faq details[open] summary::after {
@@ -520,7 +546,7 @@ footer {
}
#instagram .btn {
background: var(--green);
background: var(--gw-green);
color: #fff;
}
@@ -594,13 +620,14 @@ footer {
.footer-nav {
list-style: none;
columns: 2;
column-gap: 24px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 2px 24px;
}
.footer-nav li {
margin: 0;
break-inside: avoid;
min-width: 0;
}
.footer-nav a {
+1 -1
View File
@@ -53,7 +53,7 @@
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--green);
color: var(--gw-green);
margin: 0 0 14px;
}
+1 -4
View File
@@ -1,5 +1,5 @@
:root {
--green: #213021;
--gw-green: #213021;
--yellow: #ffd100;
--gray: #59606d;
--beige: #e5d6c2;
@@ -8,9 +8,6 @@
--max-w: 1280px;
--font-body: 'Readex Pro', sans-serif;
--font-head: 'Unbounded', sans-serif;
/* Legacy "navy" tokens now intentionally render as Goodwalk green. */
--navy: var(--green);
}
@media (min-width: 1800px) {
+8
View File
@@ -75,6 +75,8 @@ export interface TestimonialContent {
}
export interface ProcessStep {
phase?: string;
benefit?: string;
title: string;
body: string;
icon?: string;
@@ -117,6 +119,11 @@ export interface ServiceBenefit {
body: string;
}
export interface ServiceHighlightImage {
imageUrl: string;
imageAlt: string;
}
export interface ServicePageContent {
hero: {
eyebrow: string;
@@ -130,6 +137,7 @@ export interface ServicePageContent {
title: string;
imageUrl: string;
imageAlt: string;
collageImages?: ServiceHighlightImage[];
};
pricing: {
title: string;
+1 -1
View File
@@ -48,7 +48,7 @@
background:
radial-gradient(circle at 20% 15%, #2a3e2a 0%, transparent 55%),
radial-gradient(circle at 80% 85%, #1a261a 0%, transparent 55%),
var(--green);
var(--gw-green);
color: #fff;
overflow: hidden;
padding: 48px 24px;
+5
View File
@@ -5,6 +5,7 @@
import { initClickTracking, trackPageView } from '$lib/analytics';
import MobileBookBar from '$lib/components/MobileBookBar.svelte';
import RouteSkeleton from '$lib/components/RouteSkeleton.svelte';
import { isMobileCtaButtonEnabled } from '$lib/feature-flags';
import '$lib/styles/variables.css';
import '$lib/styles/base.css';
import '$lib/styles/layout.css';
@@ -14,6 +15,8 @@
import '$lib/styles/sections.css';
import '$lib/styles/responsive.css';
const mobileCtaButtonEnabled = isMobileCtaButtonEnabled();
onMount(() => initClickTracking());
function shouldShowSkeleton() {
@@ -61,6 +64,8 @@
});
</script>
<svelte:body class:mobile-cta-enabled={mobileCtaButtonEnabled} />
<div class="layout-shell">
<div class:layout-content-loading={showRouteSkeleton} class="layout-content" aria-hidden={showRouteSkeleton}>
<slot />
+3 -1
View File
@@ -1,7 +1,9 @@
import { getHomepageContent } from '$lib/server/content';
import { isHomepageHowItWorksEnabled } from '$lib/server/feature-flags';
export async function load() {
return {
content: await getHomepageContent()
content: await getHomepageContent(),
howItWorksEnabled: isHomepageHowItWorksEnabled()
};
}
+3 -1
View File
@@ -140,7 +140,9 @@
<HeroSection hero={data.content.hero} reviewCta={data.content.intro.reviewCta} />
<PromiseSection promise={data.content.promise} />
<ServicesSection services={data.content.services} />
<HowItWorksSection content={data.content.howItWorks} />
{#if data.howItWorksEnabled}
<HowItWorksSection content={data.content.howItWorks} />
{/if}
<TestimonialsSection testimonials={data.content.testimonials} />
<ValuesSection values={data.content.values} />
<BookingSection booking={data.content.booking} />
+6 -6
View File
@@ -101,9 +101,9 @@
{
'@context': 'https://schema.org',
'@type': 'Service',
name: packWalksContent.hero.title,
name: 'Goodwalk Pack Walks',
description: data.page.description,
serviceType: 'Pack Walks',
serviceType: 'Pack walks for small and medium dogs',
provider: {
'@type': 'LocalBusiness',
name: 'Goodwalk',
@@ -124,9 +124,9 @@
{
'@context': 'https://schema.org',
'@type': 'Service',
name: dogWalkingContent.hero.title,
name: 'Goodwalk 1:1 Dog Walks',
description: data.page.description,
serviceType: '1:1 Dog Walking',
serviceType: 'One-on-one dog walking',
provider: {
'@type': 'LocalBusiness',
name: 'Goodwalk',
@@ -147,9 +147,9 @@
{
'@context': 'https://schema.org',
'@type': 'Service',
name: puppyVisitsContent.hero.title,
name: 'Goodwalk Puppy Visits',
description: data.page.description,
serviceType: 'Puppy Visits',
serviceType: 'In-home puppy visits',
provider: {
'@type': 'LocalBusiness',
name: 'Goodwalk',
+20 -3
View File
@@ -1,22 +1,39 @@
import { describe, expect, it, vi } from 'vitest';
import { homepageContent } from '$lib/content/homepage';
const { getHomepageContent } = vi.hoisted(() => ({
getHomepageContent: vi.fn()
const { getHomepageContent, isHomepageHowItWorksEnabled } = vi.hoisted(() => ({
getHomepageContent: vi.fn(),
isHomepageHowItWorksEnabled: vi.fn()
}));
vi.mock('$lib/server/content', () => ({
getHomepageContent
}));
vi.mock('$lib/server/feature-flags', () => ({
isHomepageHowItWorksEnabled
}));
import { load } from './+page.server';
describe('home page server load', () => {
it('returns homepage content', async () => {
getHomepageContent.mockResolvedValue(homepageContent);
isHomepageHowItWorksEnabled.mockReturnValue(false);
await expect(load()).resolves.toEqual({
content: homepageContent
content: homepageContent,
howItWorksEnabled: false
});
});
it('returns the how it works flag when enabled', async () => {
getHomepageContent.mockResolvedValue(homepageContent);
isHomepageHowItWorksEnabled.mockReturnValue(true);
await expect(load()).resolves.toEqual({
content: homepageContent,
howItWorksEnabled: true
});
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB