diff --git a/package-lock.json b/package-lock.json index c2c830e..115a095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,12 @@ }, "devDependencies": { "@fontsource/fredoka": "^5.2.10", + "@fontsource/noto-sans": "^5.2.10", + "@fontsource/plus-jakarta-sans": "^5.2.8", + "@fontsource/poppins": "^5.2.7", "@fontsource/readex-pro": "^5.2.11", + "@fontsource/roboto": "^5.2.10", + "@fontsource/source-sans-3": "^5.2.9", "@fontsource/unbounded": "^5.2.8", "@fortawesome/fontawesome-free": "^7.2.0", "@sveltejs/adapter-node": "^5.2.11", @@ -760,6 +765,36 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@fontsource/noto-sans": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource/noto-sans/-/noto-sans-5.2.10.tgz", + "integrity": "sha512-J58RVfS/C0Z2VBF+PoU260Tx8cdRGYuS+e3yQe4hYaIYDl0sEVn5CzlLo5zVRvQD0HaIUTV8AZMfqR7rtdEpqQ==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/plus-jakarta-sans": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/plus-jakarta-sans/-/plus-jakarta-sans-5.2.8.tgz", + "integrity": "sha512-P5qE49fqdeD+7DXH1KBxmMPlB17LTz1zvBhFH0tFzfnYTKVJVyb0pR6plh0ZGXxcB+Oayb54FZZw3V42/DawTw==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/poppins": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/poppins/-/poppins-5.2.7.tgz", + "integrity": "sha512-6uQyPmseo4FgI97WIhA4yWRlNaoLk4vSDK/PyRwdqqZb5zAEuc+Kunt8JTMcsHYUEGYBtN15SNkMajMdqUSUmg==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@fontsource/readex-pro": { "version": "5.2.11", "resolved": "https://registry.npmjs.org/@fontsource/readex-pro/-/readex-pro-5.2.11.tgz", @@ -770,6 +805,26 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@fontsource/roboto": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.10.tgz", + "integrity": "sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/source-sans-3": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@fontsource/source-sans-3/-/source-sans-3-5.2.9.tgz", + "integrity": "sha512-u3ymIq4rfmCCyB9MEw/sFR5lPVJ1yTNXmIMbUz+9kVCFIHvNtfzXOEBuvkg3Tk0zhmioPeJ28ZK5smZ7TurezQ==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@fontsource/unbounded": { "version": "5.2.8", "resolved": "https://registry.npmjs.org/@fontsource/unbounded/-/unbounded-5.2.8.tgz", diff --git a/package.json b/package.json index 58cd843..386bfea 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,12 @@ }, "devDependencies": { "@fontsource/fredoka": "^5.2.10", + "@fontsource/noto-sans": "^5.2.10", + "@fontsource/plus-jakarta-sans": "^5.2.8", + "@fontsource/poppins": "^5.2.7", "@fontsource/readex-pro": "^5.2.11", + "@fontsource/roboto": "^5.2.10", + "@fontsource/source-sans-3": "^5.2.9", "@fontsource/unbounded": "^5.2.8", "@fortawesome/fontawesome-free": "^7.2.0", "@sveltejs/adapter-node": "^5.2.11", diff --git a/src/lib/components/AboutPage.svelte b/src/lib/components/AboutPage.svelte index e6b4bfc..0dce5fb 100644 --- a/src/lib/components/AboutPage.svelte +++ b/src/lib/components/AboutPage.svelte @@ -2,6 +2,7 @@ import { accordion } from '$lib/actions/accordion'; import { reveal } from '$lib/actions/reveal'; import Icon from '$lib/components/Icon.svelte'; + import PageHeader from '$lib/components/PageHeader.svelte'; import { getEnhancedImage } from '$lib/enhanced-images'; import type { AboutPageContent } from '$lib/types'; @@ -16,26 +17,26 @@
-
-
- About Goodwalk -

{pageContent.title}

-

Small dog specialists serving Auckland Central. A team your dog knows by name.

-
- - - 30+ five-star Google reviews - - Auckland Central - Small dog specialists -
+ +
+ + + 30+ five-star Google reviews + + Auckland Central + Small dog specialists
-
+ {#each standardSections as section} @@ -174,7 +175,6 @@ /* ── Shared eyebrow ── */ .about-eyebrow, - .about-hero-eyebrow, .about-contact-eyebrow { display: inline-block; margin-bottom: 14px; @@ -192,70 +192,11 @@ box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05); } - .about-hero-eyebrow, .about-contact-eyebrow { background: rgba(255, 255, 255, 0.14); color: #fff; } - /* ── Hero ── */ - .about-hero { - background: var(--gw-green); - color: #fff; - padding: 80px 0 72px; - text-align: center; - } - - .about-hero h1 { - margin: 0 0 16px; - font-family: var(--font-head); - font-size: clamp(40px, 5vw, 68px); - font-weight: 800; - line-height: 1.02; - letter-spacing: -0.04em; - color: #fff; - } - - .about-hero-desc { - max-width: 480px; - margin: 0 auto 28px; - color: rgba(255, 255, 255, 0.82); - font-size: 17px; - line-height: 1.55; - } - - .about-hero-chips { - display: flex; - justify-content: center; - gap: 10px; - flex-wrap: wrap; - } - - .about-hero-chip { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 9px 18px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.14); - color: #fff; - font-family: var(--font-head); - font-size: 14px; - font-weight: 700; - line-height: 1.2; - letter-spacing: 0.01em; - } - - .about-hero-chip-link { - text-decoration: none; - transition: background 0.18s ease; - } - - .about-hero-chip-link:hover { - background: rgba(255, 255, 255, 0.18); - } - .about-chip-stars { color: var(--yellow); letter-spacing: 1px; @@ -546,23 +487,6 @@ padding: 0 24px; } - .about-hero { - padding: 56px 0 48px; - } - - .about-hero h1 { - font-size: 38px; - } - - .about-hero-desc { - font-size: 15px; - } - - .about-hero-chip { - font-size: 13px; - padding: 8px 14px; - } - .about-section { padding: 60px 0; } diff --git a/src/lib/components/BookingPage.svelte b/src/lib/components/BookingPage.svelte index bfa4cd1..640d32f 100644 --- a/src/lib/components/BookingPage.svelte +++ b/src/lib/components/BookingPage.svelte @@ -2,6 +2,7 @@ import BookingSection from '$lib/components/BookingSection.svelte'; import Icon from '$lib/components/Icon.svelte'; import InfoSection from '$lib/components/InfoSection.svelte'; + import PageHeader from '$lib/components/PageHeader.svelte'; import type { BookingContent, InfoContent } from '$lib/types'; export let booking: BookingContent; @@ -10,31 +11,28 @@ const email = 'info@goodwalk.co.nz'; const phone = '(022) 642 1011'; + const phoneHref = `tel:${phone.replace(/[^0-9+]/g, '')}`;
-
-
-

Contact Us

-

- {#if allowGeneralEnquiry} - Book a Meet & Greet or send a general enquiry. We’ll come back within 24 hours. - {:else} - Tell us a little about your dog and we’ll be in touch within 24 hours to arrange a free Meet & Greet. - {/if} -

- + + -
+ @@ -45,42 +43,13 @@ background: var(--off-white); } - .booking-page-hero { - background: var(--gw-green); - color: #fff; - padding: 64px 0 72px; - } - - .booking-page-inner { - max-width: var(--max-w); - margin: 0 auto; - padding: 0 50px; - text-align: center; - } - - .booking-page-hero h1 { - margin: 0 0 14px; - font-family: var(--font-head); - font-size: clamp(34px, 4vw, 56px); - line-height: 1.05; - letter-spacing: -0.04em; - color: #fff; - } - - .booking-page-sub { - margin: 0 auto 32px; - max-width: 480px; - font-size: 16px; - line-height: 1.6; - opacity: 0.8; - } - .booking-page-contact { display: flex; align-items: center; justify-content: center; gap: 24px; flex-wrap: wrap; + margin-top: 28px; } .booking-contact-link { @@ -105,14 +74,6 @@ } @media (max-width: 768px) { - .booking-page-hero { - padding: 48px 0 56px; - } - - .booking-page-inner { - padding: 0 24px; - } - .booking-page-contact { gap: 12px; } diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index eb8554c..486ad41 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -131,5 +131,6 @@ Terms & Conditions Privacy Policy + ↑ Back to top diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 981aa16..36349a1 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -32,6 +32,23 @@ mobileMenuOpen = !mobileMenuOpen; } + function mobileLinkIcon(href: string) { + if (href === '/') return 'fas fa-house'; + if (href === '/pack-walks') return 'fas fa-paw'; + if (href === '/dog-walking') return 'fas fa-person-walking'; + if (href === '/puppy-visits') return 'fas fa-dog'; + if (href === '/our-pricing') return 'fas fa-tags'; + if (href === '/about') return 'fas fa-heart'; + if (href === '/contact-us') return 'fas fa-envelope'; + return 'fas fa-arrow-right'; + } + + function handleKeydown(event: KeyboardEvent) { + if (event.key === 'Escape' && mobileMenuOpen) { + closeMenu(); + } + } + function normalizePath(path: string) { if (!path || path === '/') { return '/'; @@ -79,11 +96,20 @@ } } + $: if (typeof document !== 'undefined') { + document.body.classList.toggle('mobile-menu-open', mobileMenuOpen); + } + onMount(() => { handleViewportChange(); window.addEventListener('resize', handleViewportChange); + window.addEventListener('keydown', handleKeydown); - return () => window.removeEventListener('resize', handleViewportChange); + return () => { + window.removeEventListener('resize', handleViewportChange); + window.removeEventListener('keydown', handleKeydown); + document.body.classList.remove('mobile-menu-open'); + }; }); @@ -148,17 +174,13 @@
- {link.label} - - {/each} + {#if $page.url.pathname === '/'} + + {/if} + +
+
+ +
diff --git a/src/lib/components/HeroSection.svelte b/src/lib/components/HeroSection.svelte index 6840e48..2e2fabd 100644 --- a/src/lib/components/HeroSection.svelte +++ b/src/lib/components/HeroSection.svelte @@ -8,6 +8,9 @@ $: titleParts = splitTitle(hero.title); $: mobileTitle = hero.mobileTitle?.trim() || `${hero.title} ${hero.highlight}`.trim(); + $: mobileLead = mobileTitle.includes(hero.highlight) + ? mobileTitle.slice(0, mobileTitle.lastIndexOf(hero.highlight)) + : mobileTitle; $: heroEnhanced = getEnhancedImage(hero.imageUrl); function splitTitle(title: string) { @@ -36,8 +39,33 @@
+ +
+ + {#if hero.desktopImageUrl} + + {/if} + {hero.imageAlt} + +
+ + {#if hero.floatingPill} +
{hero.floatingPill}
+ {/if} +
+ {#if hero.kicker} +

{hero.kicker}

+ {/if} +

{titleParts.lead} @@ -47,11 +75,24 @@
{hero.highlight}
- {mobileTitle} + + {mobileLead}{hero.highlight} +

{#if hero.subtitle} -

{hero.subtitle}

+

{hero.subtitle}

+ {/if} + + {#if hero.subtitleChips && hero.subtitleChips.length} +
+ {#each hero.subtitleChips as chip} + + + {chip.label} + + {/each} +
{/if} {#if reviewCta} @@ -94,28 +135,9 @@ class="btn btn-outline" > {hero.secondaryCta.label} +
- -
- {#if heroEnhanced} - - {:else} - {hero.imageAlt} - {/if} -
diff --git a/src/lib/components/PageHeader.svelte b/src/lib/components/PageHeader.svelte new file mode 100644 index 0000000..74c9d3e --- /dev/null +++ b/src/lib/components/PageHeader.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/PricingPage.svelte b/src/lib/components/PricingPage.svelte index 4cf79e2..19af29f 100644 --- a/src/lib/components/PricingPage.svelte +++ b/src/lib/components/PricingPage.svelte @@ -3,6 +3,7 @@ import { reveal } from '$lib/actions/reveal'; import BookingSection from '$lib/components/BookingSection.svelte'; import Icon from '$lib/components/Icon.svelte'; + import PageHeader from '$lib/components/PageHeader.svelte'; import TestimonialsSection from '$lib/components/TestimonialsSection.svelte'; import type { PricingPageContent, SiteSharedContent } from '$lib/types'; @@ -110,37 +111,30 @@
-
-
-

{pageContent.title}

- {#if pageContent.subtitle} -

{pageContent.subtitle}

- {/if} - - - - - 30+ 5-star Google reviews, trusted by Auckland dog owners - - -
-
+ + + + + 30+ 5-star Google reviews, trusted by Auckland dog owners + + + {#each pageContent.sections as section, index}
@@ -253,28 +247,6 @@ padding: 0 50px; } - .pricing-page-hero { - background: var(--gw-green); - padding: 56px 0 64px; - text-align: center; - } - - .pricing-page-hero h1 { - margin: 0 0 12px; - font-family: var(--font-head); - font-size: clamp(34px, 4vw, 56px); - line-height: 1.05; - letter-spacing: -0.04em; - color: #fff; - } - - .pricing-page-sub { - margin: 0; - font-size: 16px; - line-height: 1.5; - color: rgba(255, 255, 255, 0.7); - } - .pricing-trust { display: inline-flex; align-items: center; diff --git a/src/lib/components/SeoHead.svelte b/src/lib/components/SeoHead.svelte index a4e05ec..67ab066 100644 --- a/src/lib/components/SeoHead.svelte +++ b/src/lib/components/SeoHead.svelte @@ -9,7 +9,8 @@ export let type = 'website'; export let structuredData: Record[] = []; export let noindex = false; - export let preloadImage = false; // kept for API compatibility — preload is handled by fetchpriority="high" on the image element + export let preloadImage = false; + export let preloadImageUrl = ''; // explicit URL to preload (defaults to the og:image) const siteName = 'Goodwalk'; const siteUrl = 'https://www.goodwalk.co.nz'; @@ -34,6 +35,7 @@ $: canonicalUrl = absoluteUrl(canonicalPath); $: imageUrl = absoluteUrl(image); $: imageMeta = getImageMetadata(image); + $: resolvedPreloadUrl = preloadImageUrl || image; @@ -49,6 +51,9 @@ + {#if preloadImage && resolvedPreloadUrl} + + {/if} diff --git a/src/lib/components/ServiceHero.svelte b/src/lib/components/ServiceHero.svelte new file mode 100644 index 0000000..56230e5 --- /dev/null +++ b/src/lib/components/ServiceHero.svelte @@ -0,0 +1,293 @@ + + +
+ + +
+

{eyebrow}

+

{title}

+ {#if subtitle} +

{subtitle}

+ {/if} + + {#if chips.length} +
+ {#each chips as chip} + + + {chip.label} + + {/each} +
+ {/if} + + +
+ + +
+ {#if enhanced} + + {:else} + {imageAlt} + {/if} +
+ +
+ + diff --git a/src/lib/components/ServiceLandingPage.svelte b/src/lib/components/ServiceLandingPage.svelte index c4ed39d..dbd3585 100644 --- a/src/lib/components/ServiceLandingPage.svelte +++ b/src/lib/components/ServiceLandingPage.svelte @@ -2,6 +2,7 @@ import Icon from '$lib/components/Icon.svelte'; import { reveal } from '$lib/actions/reveal'; import BookingSection from '$lib/components/BookingSection.svelte'; + import ServiceHero from '$lib/components/ServiceHero.svelte'; import TestimonialsSection from '$lib/components/TestimonialsSection.svelte'; import { getEnhancedImage } from '$lib/enhanced-images'; import type { ServicePageContent, SiteSharedContent } from '$lib/types'; @@ -9,6 +10,7 @@ export let content: SiteSharedContent; export let pageContent: ServicePageContent; export let currentPath = ''; + let benefitScroller: HTMLDivElement | null = null; function numericPrice(price: string) { const value = Number(price.replace(/[^0-9.]/g, '')); @@ -30,7 +32,6 @@ })); } - $: heroEnhanced = getEnhancedImage(pageContent.hero.imageUrl); $: highlightEnhanced = pageContent.highlight ? getEnhancedImage(pageContent.highlight.imageUrl) : null; $: highlightCollageImages = pageContent.highlight?.collageImages?.map((image) => ({ @@ -39,6 +40,12 @@ })) ?? []; $: relatedServices = content.services.filter((s) => s.href && s.href !== currentPath); $: pricingPlans = decoratePlans(pageContent.pricing.plans); + $: benefitCards = pageContent.benefits.items.map((benefit, index) => ({ + ...benefit, + tintClass: `service-benefit-tint-${(index % 3) + 1}`, + featured: index === 0 + })); + $: showRelatedServices = relatedServices.length > 0 && currentPath !== '/pack-walks'; $: relatedCards = [ ...relatedServices.map((s) => ({ @@ -58,85 +65,123 @@ pill: 'All services' } ]; + + function scrollBenefits(direction: -1 | 1) { + if (!benefitScroller) return; + + benefitScroller.scrollBy({ + left: direction * Math.round(benefitScroller.clientWidth * 0.86), + behavior: 'smooth' + }); + }
-
-
-
-

{pageContent.hero.eyebrow}

-

{pageContent.hero.title}

- - {#each pageContent.hero.paragraphs as paragraph} -

{paragraph}

- {/each} -
- -
- {#if heroEnhanced} - - {:else} - {pageContent.hero.imageAlt} - {/if} -
-
-
+ {#if pageContent.highlight}
-
-

{pageContent.highlight.eyebrow}

-

{pageContent.highlight.title}

-
-
- {#if highlightCollageImages.length} -
- {#each highlightCollageImages as image, index} -
- {#if image.enhanced} - - {:else} - {image.imageAlt} - {/if} -
- {/each} -
- {:else} -
- {#if highlightEnhanced} - - {:else} - {pageContent.highlight.imageAlt} +
+
+

{pageContent.highlight.eyebrow}

+

{pageContent.highlight.title}

+ + {#if pageContent.highlight.points?.length} +
+ {#each pageContent.highlight.points as point} +
+

{point.title}

+

{point.body}

+
+ {/each} +
{/if}
- {/if} + + {#if highlightCollageImages.length} +
+ {#each highlightCollageImages as image, index} +
+ {#if image.enhanced} + + {:else} + {image.imageAlt} + {/if} +
+ {/each} +
+ {:else} +
+ {#if highlightEnhanced} + + {:else} + {pageContent.highlight.imageAlt} + {/if} +
+ {/if} +
{/if} +
+
+
+

{pageContent.benefits.title}

+ {#if pageContent.benefits.intro} +

{pageContent.benefits.intro}

+ {/if} +
+ +

Swipe through the reasons Tiny Gang works so well.

+ +
+
+ {#each benefitCards as benefit} +
+ + {#if benefit.badge} +

{benefit.badge}

+ {/if} +

{benefit.title}

+

{benefit.body}

+
+ {/each} +
+ +
+ + +
+
+
+
+
@@ -205,27 +250,7 @@
-
-
-
-

{pageContent.benefits.title}

-
- -
- {#each pageContent.benefits.items as benefit} -
- -

{benefit.title}

-

{benefit.body}

-
- {/each} -
-
-
- - {#if relatedServices.length} + {#if showRelatedServices}