From 6cd50965e510ec4ba1ed4667f2381d93ad1a9705 Mon Sep 17 00:00:00 2001 From: ponzischeme89 Date: Sun, 3 May 2026 11:49:59 +1200 Subject: [PATCH] SEO tweaks, design tweaks --- src/lib/components/BookingSection.svelte | 242 +++++++++--------- src/lib/components/BookingSection.test.ts | 65 +++-- src/lib/components/Header.svelte | 39 ++- src/lib/components/HeroSection.svelte | 4 + src/lib/components/PricingPage.svelte | 52 ++-- src/lib/components/PromiseSection.svelte | 12 +- src/lib/components/ServiceLandingPage.svelte | 2 +- src/lib/components/ServicesSection.svelte | 4 + src/lib/components/TestimonialsSection.svelte | 44 +++- .../components/TestimonialsSection.test.ts | 2 +- src/lib/content/about.ts | 2 +- src/lib/content/homepage.ts | 51 ++-- src/lib/styles/layout.css | 33 ++- src/lib/styles/responsive.css | 7 + src/lib/styles/sections.css | 25 ++ src/lib/types.ts | 5 +- vitest.setup.ts | 14 +- 17 files changed, 359 insertions(+), 244 deletions(-) diff --git a/src/lib/components/BookingSection.svelte b/src/lib/components/BookingSection.svelte index 578b645..f8bbf37 100644 --- a/src/lib/components/BookingSection.svelte +++ b/src/lib/components/BookingSection.svelte @@ -99,27 +99,22 @@ selectedServices = selectedServices.filter((item) => item !== service); } - function validateStepOne(): boolean { + function validateDogStep(): boolean { const next: Record = {}; - 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 = {}; - 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)} > 1 - {ownerStepLabel} + {dogStepLabel} @@ -246,6 +246,107 @@ {#if step === 1} +
+ {#if dogIntro} +
{dogIntro}
+ {/if} + +
+
+ + clearError('petName')} + /> + {#if errors.petName} +

+ + {errors.petName} +

+ {/if} +
+ +
+ + clearError('location')} + /> + {#if errors.location} +

+ + {errors.location} +

+ {/if} +
+ +
+ + +
+
+ + {#if hasServices} +
+  Services +
+ {#each booking.serviceOptions as service} + + {/each} +
+
+ {/if} +
+ +
+ +
+ {:else} + + + + {#each selectedServices as service} + + {/each} +
{#if hasBanner}
{booking.subtitle}
@@ -330,107 +431,6 @@
- - {#if hasServices} -
-  Services -
- {#each booking.serviceOptions as service} - - {/each} -
-
- {/if} - - -
- -
- {:else} - - - - {#each selectedServices as service} - - {/each} - -
- {#if dogIntro} -
{dogIntro}
- {/if} - -
-
- - clearError('petName')} - /> - {#if errors.petName} -

- - {errors.petName} -

- {/if} -
- -
- - clearError('location')} - /> - {#if errors.location} -

- - {errors.location} -

- {/if} -
- -
- - -
-
diff --git a/src/lib/components/BookingSection.test.ts b/src/lib/components/BookingSection.test.ts index 2745094..c45038b 100644 --- a/src/lib/components/BookingSection.test.ts +++ b/src/lib/components/BookingSection.test.ts @@ -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')!); diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 3cb1067..3dd60c6 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -90,22 +90,35 @@ {#if i === 0 && navigation.megaMenuServices?.length}
- {#each navigation.megaMenuServices as service} +
+ {#each navigation.megaMenuServices as service} + +
+ +
+ {service.label} + {#if service.description} + {service.description} + {/if} +
+ {/each} +
+ {#if navigation.megaMenuFooter} -
- -
- {service.label} - {#if service.description} - {service.description} - {/if} + {navigation.megaMenuFooter.label} +
- {/each} + {/if}
{/if} diff --git a/src/lib/components/HeroSection.svelte b/src/lib/components/HeroSection.svelte index 41ded22..60bdf94 100644 --- a/src/lib/components/HeroSection.svelte +++ b/src/lib/components/HeroSection.svelte @@ -40,6 +40,10 @@ {mobileTitle} + {#if hero.subtitle} +

{hero.subtitle}

+ {/if} +
{hero.primaryCta.label} {hero.secondaryCta.label} diff --git a/src/lib/components/PricingPage.svelte b/src/lib/components/PricingPage.svelte index 03d5de9..4ff7b23 100644 --- a/src/lib/components/PricingPage.svelte +++ b/src/lib/components/PricingPage.svelte @@ -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 = [ - '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} - Book Now + Book a Meet & Greet {/each}
@@ -188,7 +166,7 @@

- Let's chat + Book a Meet & Greet {/if} diff --git a/src/lib/components/PromiseSection.svelte b/src/lib/components/PromiseSection.svelte index 6e874a1..9e98a05 100644 --- a/src/lib/components/PromiseSection.svelte +++ b/src/lib/components/PromiseSection.svelte @@ -15,10 +15,14 @@ {promise.subtitle} -

- {promise.body} - {promise.emphasis} -

+ {#each promise.body as paragraph, idx} +

+ {paragraph} + {#if idx === promise.body.length - 1} + {promise.emphasis} + {/if} +

+ {/each} {promise.cta.label} diff --git a/src/lib/components/ServiceLandingPage.svelte b/src/lib/components/ServiceLandingPage.svelte index d7a483c..8665418 100644 --- a/src/lib/components/ServiceLandingPage.svelte +++ b/src/lib/components/ServiceLandingPage.svelte @@ -87,7 +87,7 @@ {/each} - Book Now + Book a Meet & Greet {/each} diff --git a/src/lib/components/ServicesSection.svelte b/src/lib/components/ServicesSection.svelte index a23486e..f320203 100644 --- a/src/lib/components/ServicesSection.svelte +++ b/src/lib/components/ServicesSection.svelte @@ -20,6 +20,10 @@

{service.title}

{service.body}

+ {#if service.priceFrom} +

{service.priceFrom}

+ {/if} + {#if service.href} Learn more {/if} diff --git a/src/lib/components/TestimonialsSection.svelte b/src/lib/components/TestimonialsSection.svelte index 7b5a4d4..887f5e5 100644 --- a/src/lib/components/TestimonialsSection.svelte +++ b/src/lib/components/TestimonialsSection.svelte @@ -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(); + }; }); @@ -96,11 +123,14 @@ {#if slides.length}