SEO tweaks, design tweaks

This commit is contained in:
2026-05-03 11:49:59 +12:00
parent f27e0fed07
commit 6cd50965e5
17 changed files with 359 additions and 244 deletions
+121 -121
View File
@@ -99,27 +99,22 @@
selectedServices = selectedServices.filter((item) => item !== service);
}
function validateStepOne(): boolean {
function validateDogStep(): boolean {
const next: Record<string, string> = {};
if (!fullName.trim()) next.fullName = 'Please enter your full name';
const emailError = validateEmail(email);
if (emailError) next.email = emailError;
if (!phone.trim()) next.phone = 'Please enter your contact number';
if (!petName.trim()) next.petName = "Please enter your dog's name";
if (!location.trim()) next.location = 'Please enter your location';
errors = next;
if (next.fullName) { fullNameInput?.focus(); return false; }
if (next.email) { emailInput?.focus(); return false; }
if (next.phone) { phoneInput?.focus(); return false; }
if (next.petName) { petNameInput?.focus(); return false; }
if (next.location) { locationInput?.focus(); return false; }
return true;
}
function goToDogStep() {
if (!validateStepOne()) return;
function goToOwnerStep() {
if (!validateDogStep()) return;
errors = {};
step = 2;
}
@@ -128,18 +123,23 @@
event.preventDefault();
if (step === 1) {
goToDogStep();
goToOwnerStep();
return;
}
const next: Record<string, string> = {};
if (!petName.trim()) next.petName = "Please enter your dog's name";
if (!location.trim()) next.location = 'Please enter your location';
if (!fullName.trim()) next.fullName = 'Please enter your full name';
const emailError = validateEmail(email);
if (emailError) next.email = emailError;
if (!phone.trim()) next.phone = 'Please enter your contact number';
if (Object.keys(next).length > 0) {
errors = next;
if (next.petName) petNameInput?.focus();
else if (next.location) locationInput?.focus();
if (next.fullName) fullNameInput?.focus();
else if (next.email) emailInput?.focus();
else if (next.phone) phoneInput?.focus();
return;
}
@@ -212,17 +212,17 @@
on:click={() => (step = 1)}
>
<span class="booking-step-number">1</span>
<span class="booking-step-label">{ownerStepLabel}</span>
<span class="booking-step-label">{dogStepLabel}</span>
</button>
<span class="booking-step-divider" aria-hidden="true"></span>
<button
type="button"
class:active={step === 2}
class="booking-step"
on:click={goToDogStep}
on:click={goToOwnerStep}
>
<span class="booking-step-number">2</span>
<span class="booking-step-label">{dogStepLabel}</span>
<span class="booking-step-label">{ownerStepLabel}</span>
</button>
</div>
</div>
@@ -246,6 +246,107 @@
</div>
{#if step === 1}
<div class="booking-panel">
{#if dogIntro}
<div class="booking-panel-banner">{dogIntro}</div>
{/if}
<div class:booking-card-grid-with-banner={Boolean(dogIntro)} class="booking-card-grid booking-card-grid-dog">
<div class="booking-field-card" class:booking-field-card-invalid={errors.petName}>
<label for="petName">
<Icon name="fas fa-dog" />&nbsp;Pet'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-wide" class:booking-field-card-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="Neighborhood, 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-card booking-field-card-full">
<label for="message"><Icon name="fas fa-comment" />&nbsp;About Your Dog</label>
<textarea
bind:value={message}
id="message"
name="message"
rows="4"
placeholder="Describe your pet, any special needs, or anything we should know."
></textarea>
</div>
</div>
{#if hasServices}
<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}
<Icon name="fas fa-arrow-right" />
</button>
</div>
{:else}
<input type="hidden" name="petName" value={petName} />
<input type="hidden" name="location" value={location} />
<input type="hidden" name="message" value={message} />
{#each selectedServices as service}
<input type="hidden" name="services" value={service} />
{/each}
<div class="booking-panel">
{#if hasBanner}
<div class="booking-panel-banner">{booking.subtitle}</div>
@@ -330,107 +431,6 @@
</div>
</div>
</div>
{#if hasServices}
<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={goToDogStep}>
{dogStepLabel}
<Icon name="fas fa-arrow-right" />
</button>
</div>
{:else}
<input type="hidden" name="fullName" value={fullName} />
<input type="hidden" name="email" value={email} />
<input type="hidden" name="phone" value={phone} />
{#each selectedServices as service}
<input type="hidden" name="services" value={service} />
{/each}
<div class="booking-panel">
{#if dogIntro}
<div class="booking-panel-banner">{dogIntro}</div>
{/if}
<div class:booking-card-grid-with-banner={Boolean(dogIntro)} class="booking-card-grid booking-card-grid-dog">
<div class="booking-field-card" class:booking-field-card-invalid={errors.petName}>
<label for="petName">
<Icon name="fas fa-dog" />&nbsp;Pet'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-wide" class:booking-field-card-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="Neighborhood, 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-card booking-field-card-full">
<label for="message"><Icon name="fas fa-comment" />&nbsp;About Your Dog</label>
<textarea
bind:value={message}
id="message"
name="message"
rows="4"
placeholder="Describe your pet, any special needs, or anything we should know."
></textarea>
</div>
</div>
</div>
<div class="booking-actions booking-actions-final">
+31 -34
View File
@@ -11,38 +11,35 @@ describe('BookingSection', () => {
});
});
it('validates the owner details step before progressing', async () => {
it('validates the dog details step before progressing', async () => {
const { container } = render(BookingSection, {
booking: homepageContent.booking
});
await fireEvent.click(container.querySelector('.booking-next-button')!);
expect(screen.getByText('Please enter your full name')).toBeInTheDocument();
expect(screen.getByText('Please enter a valid email address')).toBeInTheDocument();
expect(screen.getByText('Please enter your contact number')).toBeInTheDocument();
expect(screen.getByText("Please enter your dog's name")).toBeInTheDocument();
expect(screen.getByText('Please enter your location')).toBeInTheDocument();
});
it('validates the dog details step before submitting', async () => {
it('validates the owner details step before submitting', async () => {
const { container } = render(BookingSection, {
booking: homepageContent.booking
});
await fireEvent.input(screen.getByLabelText(/Full Name/i), {
target: { value: 'Alex Walker' }
await fireEvent.input(screen.getByLabelText(/Pet's Name/i), {
target: { value: 'Maya' }
});
await fireEvent.input(screen.getByLabelText(/^Email/i), {
target: { value: 'alex@example.com' }
});
await fireEvent.input(screen.getByLabelText(/Contact #/i), {
target: { value: '021 123 4567' }
await fireEvent.input(screen.getByLabelText(/Location/i), {
target: { value: 'Kingsland' }
});
await fireEvent.click(container.querySelector('.booking-next-button')!);
await fireEvent.click(container.querySelector('.booking-submit-button')!);
expect(screen.getByText("Please enter your dog's name")).toBeInTheDocument();
expect(screen.getByText('Please enter your location')).toBeInTheDocument();
expect(screen.getByText('Please enter your full name')).toBeInTheDocument();
expect(screen.getByText('Please enter your email address')).toBeInTheDocument();
expect(screen.getByText('Please enter your contact number')).toBeInTheDocument();
});
it('submits the completed booking flow and shows the success modal', async () => {
@@ -59,18 +56,6 @@ describe('BookingSection', () => {
await fireEvent.click(screen.getByLabelText('Pack Walks'));
await fireEvent.click(screen.getByLabelText('Other Services'));
await fireEvent.input(screen.getByLabelText(/Full Name/i), {
target: { value: 'Alex Walker' }
});
await fireEvent.input(screen.getByLabelText(/^Email/i), {
target: { value: 'alex@example.com' }
});
await fireEvent.input(screen.getByLabelText(/Contact #/i), {
target: { value: '021 123 4567' }
});
await fireEvent.click(container.querySelector('.booking-next-button')!);
await fireEvent.input(screen.getByLabelText(/Pet's Name/i), {
target: { value: 'Maya' }
});
@@ -81,6 +66,18 @@ describe('BookingSection', () => {
target: { value: 'Loves small group walks.' }
});
await fireEvent.click(container.querySelector('.booking-next-button')!);
await fireEvent.input(screen.getByLabelText(/Full Name/i), {
target: { value: 'Alex Walker' }
});
await fireEvent.input(screen.getByLabelText(/^Email/i), {
target: { value: 'alex@example.com' }
});
await fireEvent.input(screen.getByLabelText(/Contact #/i), {
target: { value: '021 123 4567' }
});
await fireEvent.click(container.querySelector('.booking-submit-button')!);
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
@@ -129,6 +126,14 @@ describe('BookingSection', () => {
booking: homepageContent.booking
});
await fireEvent.input(screen.getByLabelText(/Pet's Name/i), {
target: { value: 'Maya' }
});
await fireEvent.input(screen.getByLabelText(/Location/i), {
target: { value: 'Kingsland' }
});
await fireEvent.click(container.querySelector('.booking-next-button')!);
await fireEvent.input(screen.getByLabelText(/Full Name/i), {
target: { value: 'Alex Walker' }
});
@@ -138,14 +143,6 @@ describe('BookingSection', () => {
await fireEvent.input(screen.getByLabelText(/Contact #/i), {
target: { value: '021 123 4567' }
});
await fireEvent.click(container.querySelector('.booking-next-button')!);
await fireEvent.input(screen.getByLabelText(/Pet's Name/i), {
target: { value: 'Maya' }
});
await fireEvent.input(screen.getByLabelText(/Location/i), {
target: { value: 'Kingsland' }
});
await fireEvent.click(container.querySelector('.booking-submit-button')!);
+26 -13
View File
@@ -90,22 +90,35 @@
{#if i === 0 && navigation.megaMenuServices?.length}
<div class="mega-menu">
<div class="mega-menu-inner">
{#each navigation.megaMenuServices as service}
<div class="mega-menu-services">
{#each navigation.megaMenuServices as service}
<a
href={service.href}
target={linkTarget(service.href.startsWith('http'))}
rel={linkRel(service.href.startsWith('http'))}
class="mega-service"
>
<div class="mega-icon">
<Icon name={service.icon} />
</div>
<span class="mega-service-label">{service.label}</span>
{#if service.description}
<span class="mega-service-desc">{service.description}</span>
{/if}
</a>
{/each}
</div>
{#if navigation.megaMenuFooter}
<a
href={service.href}
target={linkTarget(service.href.startsWith('http'))}
rel={linkRel(service.href.startsWith('http'))}
class="mega-service"
href={navigation.megaMenuFooter.href}
target={linkTarget(navigation.megaMenuFooter.external)}
rel={linkRel(navigation.megaMenuFooter.external)}
class="mega-menu-footer"
>
<div class="mega-icon">
<Icon name={service.icon} />
</div>
<span class="mega-service-label">{service.label}</span>
{#if service.description}
<span class="mega-service-desc">{service.description}</span>
{/if}
<span>{navigation.megaMenuFooter.label}</span>
<Icon name="fas fa-arrow-right" className="mega-menu-footer-arrow" />
</a>
{/each}
{/if}
</div>
</div>
{/if}
+4
View File
@@ -40,6 +40,10 @@
<span class="hero-heading-mobile">{mobileTitle}</span>
</h1>
{#if hero.subtitle}
<p class="hero-subtitle">{hero.subtitle}</p>
{/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>
+15 -37
View File
@@ -9,9 +9,7 @@
export let content: SiteSharedContent;
export let pageContent: PricingPageContent;
const promptDelayMs = 10000;
const promptIdleMs = 6000;
const minPromptDwellMs = 3500;
const scrollDepthThreshold = 0.65;
const desktopPromptMediaQuery = '(min-width: 769px)';
let showMeetGreetPrompt = false;
@@ -44,44 +42,28 @@
}
onMount(() => {
const startedAt = Date.now();
let lastInteractionAt = startedAt;
const desktopPromptQuery = window.matchMedia(desktopPromptMediaQuery);
canShowDesktopPrompt = desktopPromptQuery.matches;
const recordInteraction = () => {
lastInteractionAt = Date.now();
};
const handleDesktopPromptViewportChange = (event: MediaQueryListEvent) => {
canShowDesktopPrompt = event.matches;
};
const interactionEvents: Array<keyof WindowEventMap> = [
'pointerdown',
'mousemove',
'keydown',
'scroll',
'touchstart'
];
const handleScroll = () => {
if (promptShown || dismissMeetGreetPrompt || bookingInView || !canShowDesktopPrompt) {
return;
}
interactionEvents.forEach((eventName) => {
window.addEventListener(eventName, recordInteraction);
});
const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
if (scrollableHeight <= 0) return;
const delayedPrompt = window.setTimeout(() => {
revealMeetGreetPrompt();
}, promptDelayMs);
const idlePromptCheck = window.setInterval(() => {
const now = Date.now();
const hasWaitedLongEnough = now - startedAt >= minPromptDwellMs;
const looksIdle = now - lastInteractionAt >= promptIdleMs;
if (document.visibilityState === 'visible' && hasWaitedLongEnough && looksIdle) {
const scrollPercent = window.scrollY / scrollableHeight;
if (scrollPercent >= scrollDepthThreshold) {
revealMeetGreetPrompt();
}
}, 1000);
};
window.addEventListener('scroll', handleScroll, { passive: true });
const bookingSection = document.getElementById('newlead');
const bookingObserver = bookingSection
@@ -100,11 +82,7 @@
desktopPromptQuery.addEventListener('change', handleDesktopPromptViewportChange);
return () => {
window.clearTimeout(delayedPrompt);
window.clearInterval(idlePromptCheck);
interactionEvents.forEach((eventName) => {
window.removeEventListener(eventName, recordInteraction);
});
window.removeEventListener('scroll', handleScroll);
desktopPromptQuery.removeEventListener('change', handleDesktopPromptViewportChange);
bookingObserver?.disconnect();
};
@@ -161,7 +139,7 @@
{/each}
</ul>
<a class="btn btn-yellow pricing-plan-cta" href="#newlead">Book Now</a>
<a class="btn btn-yellow pricing-plan-cta" href="#newlead">Book a Meet &amp; Greet</a>
</article>
{/each}
</div>
@@ -188,7 +166,7 @@
</p>
</div>
<a class="meet-greet-cta" href="#newlead" on:click={handleMeetGreetCta}>Let's chat</a>
<a class="meet-greet-cta" href="#newlead" on:click={handleMeetGreetCta}>Book a Meet &amp; Greet</a>
</aside>
{/if}
</main>
+8 -4
View File
@@ -15,10 +15,14 @@
{promise.subtitle}
</h2>
<p>
{promise.body}
<strong>{promise.emphasis}</strong>
</p>
{#each promise.body as paragraph, idx}
<p>
{paragraph}
{#if idx === promise.body.length - 1}
<strong>{promise.emphasis}</strong>
{/if}
</p>
{/each}
<a href={promise.cta.href} class="btn btn-green">{promise.cta.label}</a>
</div>
+1 -1
View File
@@ -87,7 +87,7 @@
{/each}
</ul>
<a class="btn btn-yellow service-plan-cta" href="#newlead">Book Now</a>
<a class="btn btn-yellow service-plan-cta" href="#newlead">Book a Meet &amp; Greet</a>
</article>
{/each}
</div>
@@ -20,6 +20,10 @@
<h3>{service.title}</h3>
<p>{service.body}</p>
{#if service.priceFrom}
<p class="service-card-price">{service.priceFrom}</p>
{/if}
{#if service.href}
<a href={service.href} class="btn btn-green">Learn more</a>
{/if}
+37 -7
View File
@@ -47,6 +47,9 @@
let activeIndex = 0;
let paused = false;
let inView = false;
let prefersReducedMotion = false;
let carouselEl: HTMLDivElement | undefined;
$: slides = testimonials
.map((testimonial) => wordpressTestimonials[testimonial.reviewer] ?? testimonial)
@@ -73,13 +76,37 @@
}
onMount(() => {
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
prefersReducedMotion = motionQuery.matches;
const onMotionChange = (event: MediaQueryListEvent) => {
prefersReducedMotion = event.matches;
};
motionQuery.addEventListener('change', onMotionChange);
const observer = carouselEl
? new IntersectionObserver(
([entry]) => {
inView = entry.isIntersecting;
},
{ threshold: 0.25 }
)
: null;
if (observer && carouselEl) {
observer.observe(carouselEl);
}
const interval = window.setInterval(() => {
if (!paused && slides.length > 1) {
if (!paused && !prefersReducedMotion && inView && slides.length > 1) {
showNext();
}
}, 5000);
}, 9000);
return () => window.clearInterval(interval);
return () => {
window.clearInterval(interval);
motionQuery.removeEventListener('change', onMotionChange);
observer?.disconnect();
};
});
</script>
@@ -96,11 +123,14 @@
{#if slides.length}
<div
bind:this={carouselEl}
class="testimonials-carousel"
role="region"
aria-label="Customer testimonials"
on:mouseenter={() => (paused = true)}
on:mouseleave={() => (paused = false)}
on:focusin={() => (paused = true)}
on:focusout={() => (paused = false)}
>
<button
class="testimonial-arrow testimonial-arrow-left"
@@ -140,7 +170,7 @@
<div class="testimonial-copy">
<span class="testimonial-quote-mark">"</span>
<h5>{testimonial.quote}</h5>
<blockquote class="testimonial-quote">{testimonial.quote}</blockquote>
<div class="testimonial-author">
<span class="testimonial-author-name">{testimonial.reviewer}</span>
<span class="testimonial-author-detail">{testimonial.detail}</span>
@@ -344,7 +374,7 @@
user-select: none;
}
.testimonial-copy h5 {
.testimonial-copy .testimonial-quote {
max-width: 500px;
margin: 0;
font-size: 17px;
@@ -500,7 +530,7 @@
padding: 96px 72px 64px 8px;
}
.testimonial-copy h5 {
.testimonial-copy .testimonial-quote {
max-width: 460px;
font-size: 17px;
}
@@ -554,7 +584,7 @@
margin-bottom: 8px;
}
.testimonial-copy h5 {
.testimonial-copy .testimonial-quote {
font-size: 16px;
line-height: 1.55;
}
@@ -59,7 +59,7 @@ describe('TestimonialsSection', () => {
await fireEvent.click(nextButton);
expect(getActiveReviewer(container)).toBe('Estelle');
await vi.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(9000);
expect(getActiveReviewer(container)).toBe('Ross');
});
+1 -1
View File
@@ -39,7 +39,7 @@ export const aboutPageContent: AboutPageContent = {
email: 'info@goodwalk.co.nz',
phone: '(022) 642 1011',
cta: {
label: 'Contact us',
label: 'Book a Meet & Greet',
href: '/contact-us',
variant: 'yellow'
}
+30 -21
View File
@@ -21,25 +21,27 @@ export const homepageContent: HomePageContent = {
{ label: 'About Us', href: '/about' },
{ label: 'Contact Us', href: '/contact-us' }
],
cta: { label: 'Contact Us', href: '/contact-us', variant: 'yellow' },
cta: { label: 'Book a Meet & Greet', href: '/contact-us', variant: 'yellow' },
instagram: { href: 'https://www.instagram.com/goodwalk.nz/', external: true },
megaMenuServices: [
{ icon: 'fas fa-paw', label: 'Pack Walks', description: 'Tiny Gang outdoor adventures', href: '/pack-walks' },
{ icon: 'fas fa-person-walking', label: '1:1 Walks', description: 'Personalised solo walks', href: '/dog-walking' },
{ icon: 'fas fa-dog', label: 'Puppy Visits', description: 'Home visits for young pups', href: '/puppy-visits' }
]
],
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, professional dog walking across Auckland Central — pack walks, 1:1 walks, and puppy visits.',
primaryCta: { label: 'Learn more', href: '#services', variant: 'yellow' },
secondaryCta: { label: 'Enroll today', href: '#newlead', variant: 'outline' },
secondaryCta: { label: 'Book a Meet & Greet', href: '#newlead', variant: 'outline' },
imageUrl: '/images/auckland-dog-walking-happy-dog-hero.png',
imageAlt: 'Happy dog ready for a professional pack walk with Goodwalk Auckland dog walking service'
},
intro: {
text: 'Goodwalk delivers trusted, professional dog walking services across Auckland Central.',
text: 'Trusted by Auckland dog parents.',
reviewCta: {
label: 'All 5 star reviews on Google!',
href: 'https://g.page/r/CUsvrWPhkYrAEB0/',
@@ -49,10 +51,12 @@ export const homepageContent: HomePageContent = {
promise: {
title: 'Happy pets,',
subtitle: 'happy humans',
body:
'Professional dog walking services in Auckland for small, medium and large breeds. Our experienced walkers provide tailored pack walks for smaller dogs and one-on-one walks for larger breeds - giving every dog the personalised attention they deserve. We specialise in understanding the unique needs of small-to-medium breeds, helping to ease stress and anxiety while keeping tails wagging. Ready to join our',
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'
],
emphasis: 'TINY GANG?',
cta: { label: 'Book now', href: '#newlead', variant: 'green' },
cta: { label: 'See our services', href: '#services', variant: 'green' },
imageUrl: '/images/auckland-dog-walking-happy-dogs-happy-humans.webp',
imageAlt: 'Woman cuddling a dog for Goodwalk Auckland dog walking services'
},
@@ -61,18 +65,21 @@ export const homepageContent: HomePageContent = {
icon: 'fas fa-dog',
title: 'Pack Walks',
body: 'Small group Tiny Gang walks of 4-8 dogs - calm, social, and full of fun for your pup.',
priceFrom: 'From $49.50 / walk',
href: '/pack-walks'
},
{
icon: 'fas fa-person-walking',
title: '1:1 Walks',
body: "One-on-one walks tailored to your dog's individual pace, personality, and needs.",
priceFrom: 'From $45 / walk',
href: '/dog-walking'
},
{
icon: 'fas fa-house',
title: 'Puppy Visits',
body: 'In-home visits to check in on your puppy, play, and keep them company.',
priceFrom: 'From $39 / visit',
href: '/puppy-visits'
}
],
@@ -150,9 +157,11 @@ export const homepageContent: HomePageContent = {
booking: {
title: "Let's meet!",
subtitle:
'Ready to get started? Book your free, no-obligation Meet & Greet today — just enter your details below',
"Almost there — just your contact details so we can reach out to arrange your free, no-obligation Meet & Greet.",
formAction: '/contact-us',
serviceOptions: ['Pack Walks', '1:1 Walks', 'Puppy Visits', 'Other Services']
serviceOptions: ['Pack Walks', '1:1 Walks', 'Puppy Visits', 'Other Services'],
ownerStepLabel: 'Your details',
dogStepLabel: 'Your dog'
},
info: {
title: 'Locations & Hours',
@@ -160,21 +169,11 @@ export const homepageContent: HomePageContent = {
suburbs:
'Morningside, Kingsland, Ponsonby, Grey Lynn, Mt Albert, Mt Eden, Sandringham, Mt Roskill, Arch Hill, Freemans Bay, Herne Bay, Pt Chevalier, Avondale, Three Kings, Hillsborough, Eden Terrace, Balmoral.',
nearbyText: 'Live in a nearby suburb?',
nearbyCta: { label: 'Get in touch!', href: '#newlead' },
nearbyCta: { label: 'Book a Meet & Greet', href: '#newlead' },
hoursLabel: 'Opening Hours',
hours: 'Monday to Friday, 8am - 4pm.',
faqTitle: "FAQ's",
faqs: [
{
question: 'What happens if the weather is bad?',
answer:
"We operate in all weather conditions, except when there is a danger to the dog's health and safety."
},
{
question: 'What requirements does my dog need?',
answer:
'All dogs onboarding with Goodwalk need to have a current Auckland Council dog registration and be up to date with vaccinations to ensure the health and safety of other dogs.'
},
{
question: 'Can any dog use your service?',
answer:
@@ -184,10 +183,20 @@ export const homepageContent: HomePageContent = {
question: 'How does payment work?',
answer: 'All walks are paid for a week in advance, via invoice.'
},
{
question: 'What requirements does my dog need?',
answer:
'All dogs onboarding with Goodwalk need to have a current Auckland Council dog registration and be up to date with vaccinations to ensure the health and safety of other dogs.'
},
{
question: 'Do you have insurance or First Aid training?',
answer:
'All walkers are covered by public liability insurance, and all walkers hold a current First Aid training certificate.'
},
{
question: 'What happens if the weather is bad?',
answer:
"We operate in all weather conditions, except when there is a danger to the dog's health and safety."
}
]
},
@@ -209,7 +218,7 @@ export const homepageContent: HomePageContent = {
{ label: 'Contact Us', href: '/contact-us' }
],
contactLinks: [
{ label: 'Book a walk', href: '/contact-us' },
{ label: 'Book a Meet & Greet', href: '/contact-us' },
{ label: 'Instagram', href: 'https://www.instagram.com/goodwalk.nz/', external: true },
{ label: 'Google Reviews', href: 'https://g.page/r/CUsvrWPhkYrAEB0', external: true }
],
+31 -2
View File
@@ -91,14 +91,43 @@ nav {
border-radius: 20px;
padding: 16px;
display: flex;
flex-direction: row;
gap: 6px;
flex-direction: column;
gap: 0;
box-shadow:
0 4px 6px rgba(0, 0, 0, 0.04),
0 16px 40px rgba(0, 0, 0, 0.12);
min-width: 400px;
}
.mega-menu-services {
display: flex;
flex-direction: row;
gap: 6px;
}
.mega-menu-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
padding: 12px 14px;
border-radius: 12px;
background: rgba(33, 48, 33, 0.04);
color: var(--green);
font-size: 13px;
font-weight: 700;
text-decoration: none;
transition: background 0.16s ease;
}
.mega-menu-footer:hover {
background: rgba(33, 48, 33, 0.09);
}
:global(.mega-menu-footer-arrow) {
font-size: 12px;
}
.has-mega:hover .mega-menu {
opacity: 1;
visibility: visible;
+7
View File
@@ -201,6 +201,13 @@
line-height: 1.08;
}
.hero-subtitle {
margin: -10px 0 22px;
max-width: none;
font-size: 15.5px;
line-height: 1.5;
}
.hero-heading-desktop {
display: none;
}
+25
View File
@@ -25,6 +25,14 @@ section {
padding-bottom: 44px;
}
.hero-subtitle {
margin: -8px 0 26px;
max-width: 460px;
color: rgba(255, 255, 255, 0.86);
font-size: 18px;
line-height: 1.55;
}
.hero-buttons {
display: flex;
gap: 16px;
@@ -301,10 +309,27 @@ section {
}
.service-card-price {
display: inline-block;
margin: 14px 0 0;
padding: 6px 14px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
color: var(--green);
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
letter-spacing: 0.02em;
}
.service-card a.btn {
margin-top: 18px;
}
.service-card-price + a.btn {
margin-top: 14px;
}
#values,
footer {
background: var(--green);
+4 -1
View File
@@ -29,12 +29,14 @@ export interface NavigationContent {
cta: CallToAction;
instagram?: { href: string; external?: boolean };
megaMenuServices?: MegaMenuService[];
megaMenuFooter?: LinkItem;
}
export interface HeroContent {
title: string;
highlight: string;
mobileTitle?: string;
subtitle?: string;
primaryCta: CallToAction;
secondaryCta: CallToAction;
imageUrl: string;
@@ -49,7 +51,7 @@ export interface IntroContent {
export interface PromiseContent {
title: string;
subtitle: string;
body: string;
body: string[];
emphasis: string;
cta: CallToAction;
imageUrl: string;
@@ -62,6 +64,7 @@ export interface IconCard {
body: string;
href?: string;
order?: number;
priceFrom?: string;
}
export interface TestimonialContent {