Design language

This commit is contained in:
2026-05-13 00:34:34 +12:00
parent ac6179e776
commit 6c943b14bd
34 changed files with 1986 additions and 697 deletions
+55
View File
@@ -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",
+5
View File
@@ -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",
+12 -88
View File
@@ -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 @@
<main class="about-page">
<!-- ── Hero ── -->
<section class="about-hero">
<div class="about-inner">
<span class="about-hero-eyebrow">About Goodwalk</span>
<h1>{pageContent.title}</h1>
<p class="about-hero-desc">Small dog specialists serving Auckland Central. A team your dog knows by name.</p>
<div class="about-hero-chips">
<PageHeader
variant="green"
eyebrow="About Goodwalk"
title={pageContent.title}
subtitle="Small dog specialists serving Auckland Central. A team your dog knows by name."
>
<div class="ph-chips">
<a
href="https://g.page/r/CUsvrWPhkYrAEB0/"
target="_blank"
rel="noopener"
class="about-hero-chip about-hero-chip-link"
class="ph-chip ph-chip--link"
>
<span class="about-chip-stars" aria-hidden="true">★★★★★</span>
30+ five-star Google reviews
</a>
<span class="about-hero-chip">Auckland Central</span>
<span class="about-hero-chip">Small dog specialists</span>
<span class="ph-chip">Auckland Central</span>
<span class="ph-chip">Small dog specialists</span>
</div>
</div>
</section>
</PageHeader>
<!-- ── Standard sections (Who we are, Our impact) ── -->
{#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;
}
+12 -51
View File
@@ -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, '')}`;
</script>
<main class="booking-page">
<section class="booking-page-hero">
<div class="booking-page-inner">
<h1>Contact Us</h1>
<p class="booking-page-sub">
{#if allowGeneralEnquiry}
Book a Meet &amp; Greet or send a general enquiry. Well come back within 24 hours.
{:else}
Tell us a little about your dog and well be in touch within 24 hours to arrange a free Meet &amp; Greet.
{/if}
</p>
<PageHeader
variant="green"
title="Contact Us"
subtitle={allowGeneralEnquiry
? "Book a Meet & Greet or send a general enquiry. We'll come back within 24 hours."
: "Tell us a little about your dog and we'll be in touch within 24 hours to arrange a free Meet & Greet."}
>
<div class="booking-page-contact">
<a href="mailto:{email}" class="booking-contact-link">
<Icon name="fas fa-envelope" />
{email}
</a>
<a href="tel:{phone.replace(/[^0-9+]/g, '')}" class="booking-contact-link">
<a href={phoneHref} class="booking-contact-link">
<Icon name="fas fa-phone" />
{phone}
</a>
</div>
</div>
</section>
</PageHeader>
<BookingSection {booking} {allowGeneralEnquiry} />
<InfoSection {info} />
@@ -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;
}
+1
View File
@@ -131,5 +131,6 @@
<a href="/terms-and-conditions">Terms &amp; Conditions</a>
<a href="/privacy-policy">Privacy Policy</a>
</nav>
<a href="#" class="footer-back-top" aria-label="Back to top">↑ Back to top</a>
</div>
</footer>
+51 -11
View File
@@ -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');
};
});
</script>
@@ -148,17 +174,13 @@
<a href="/" class="logo" aria-label="Goodwalk Auckland Dog Walking, home">
<picture>
{#if mobile.sources?.webp}
<source media="(max-width: 768px)" type="image/webp" srcset={mobile.sources.webp} />
{/if}
<source media="(max-width: 768px)" srcset={mobile.img.src} />
{#if desktop.sources?.webp}
<source type="image/webp" srcset={desktop.sources.webp} />
<source type="image/webp" srcset={mobile.sources.webp} />
{/if}
<img
src={desktop.img.src}
src={mobile.img.src}
alt="Goodwalk Auckland dog walking service logo"
width={desktop.img.w}
height={desktop.img.h}
width={mobile.img.w}
height={mobile.img.h}
decoding="async"
/>
</picture>
@@ -199,7 +221,19 @@
</button>
</nav>
<div class:open={mobileMenuOpen} class="mobile-menu" id="mobile-menu">
{#if $page.url.pathname === '/'}
<div class="nav-ribbon">
<span class="nav-ribbon-item"><Icon name="fas fa-paw" />Small &amp; Medium Dog Specialists</span>
<span class="nav-ribbon-divider"></span>
<span class="nav-ribbon-item"><Icon name="fas fa-handshake" />Free Meet &amp; Greet</span>
<span class="nav-ribbon-divider"></span>
<span class="nav-ribbon-item"><Icon name="fas fa-van-shuttle" />Free Pickup &amp; Drop-off</span>
</div>
{/if}
<div class:open={mobileMenuOpen} class="mobile-menu-shell">
<div class="mobile-menu" id="mobile-menu">
<div class="mobile-menu-links">
{#each navigation.mobileLinks as link}
<a
href={link.href}
@@ -209,8 +243,14 @@
class:mobile-link-active={isActiveLink(link.href)}
on:click={closeMenu}
>
{link.label}
<span class="mobile-menu-link-icon">
<Icon name={mobileLinkIcon(link.href)} />
</span>
<span class="mobile-menu-link-label">{link.label}</span>
<Icon name="fas fa-arrow-right" className="mobile-menu-link-arrow" />
</a>
{/each}
</div>
</div>
</div>
</header>
+44 -22
View File
@@ -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 @@
</script>
<section id="hero">
<!-- hero-img is a direct child of #hero so it can be absolutely
positioned relative to the section on mobile without being
constrained by hero-inner's stacking context -->
<div class="hero-img">
<picture>
{#if hero.desktopImageUrl}
<source media="(min-width: 769px)" srcset={hero.desktopImageUrl} />
{/if}
<img
src={hero.imageUrl}
alt={hero.imageAlt}
loading="eager"
fetchpriority="high"
/>
</picture>
</div>
{#if hero.floatingPill}
<div class="hero-floating-pill">{hero.floatingPill}</div>
{/if}
<div class="hero-inner">
<div class="hero-text">
{#if hero.kicker}
<p class="hero-kicker">{hero.kicker}</p>
{/if}
<h1 class="hero-heading">
<span class="hero-heading-desktop">
<span class="hero-title-main">{titleParts.lead}</span>
@@ -47,11 +75,24 @@
<br />
<span class="hero-title-highlight">{hero.highlight}</span>
</span>
<span class="hero-heading-mobile">{mobileTitle}</span>
<span class="hero-heading-mobile">
{mobileLead}<span class="hero-title-highlight">{hero.highlight}</span>
</span>
</h1>
{#if hero.subtitle}
<p class="hero-subtitle">{hero.subtitle}</p>
<p class="hero-subtitle hero-subtitle-desktop">{hero.subtitle}</p>
{/if}
{#if hero.subtitleChips && hero.subtitleChips.length}
<div class="hero-chips">
{#each hero.subtitleChips as chip}
<span class="hero-chip">
<Icon name={chip.icon} />
{chip.label}
</span>
{/each}
</div>
{/if}
{#if reviewCta}
@@ -94,28 +135,9 @@
class="btn btn-outline"
>
{hero.secondaryCta.label}
<Icon name="fas fa-arrow-down" className="hero-cta-arrow" />
</a>
</div>
</div>
<div class="hero-img">
{#if heroEnhanced}
<enhanced:img
src={heroEnhanced}
alt={hero.imageAlt}
loading="eager"
fetchpriority="high"
decoding="async"
/>
{:else}
<img
src={hero.imageUrl}
alt={hero.imageAlt}
loading="eager"
fetchpriority="high"
decoding="async"
/>
{/if}
</div>
</div>
</section>
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
export let variant: 'green' | 'white' = 'green';
export let eyebrow: string | undefined = undefined;
export let title: string;
export let subtitle: string | undefined = undefined;
</script>
<section class="page-header page-header--{variant}" class:page-header--has-media={$$slots.media}>
<div class="ph-inner" class:ph-inner--grid={$$slots.media}>
{#if $$slots.media}
<div class="ph-copy">
{#if eyebrow}<p class="eyebrow">{eyebrow}</p>{/if}
<h1 class="ph-title">{title}</h1>
{#if subtitle}<p class="ph-subtitle">{subtitle}</p>{/if}
<slot />
</div>
<div class="ph-media">
<slot name="media" />
</div>
{:else}
{#if eyebrow}<p class="eyebrow">{eyebrow}</p>{/if}
<h1 class="ph-title">{title}</h1>
{#if subtitle}<p class="ph-subtitle">{subtitle}</p>{/if}
<slot />
{/if}
</div>
</section>
+3 -31
View File
@@ -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,13 +111,7 @@
</script>
<main class="pricing-page">
<section class="pricing-page-hero">
<div class="pricing-inner">
<h1>{pageContent.title}</h1>
{#if pageContent.subtitle}
<p class="pricing-page-sub">{pageContent.subtitle}</p>
{/if}
<PageHeader variant="green" title={pageContent.title} subtitle={pageContent.subtitle}>
<a
class="pricing-trust"
href="https://g.page/r/CUsvrWPhkYrAEB0/"
@@ -139,8 +134,7 @@
<span class="pricing-trust-label">30+ 5-star Google reviews, trusted by Auckland dog owners</span>
<Icon name="fas fa-arrow-right" className="pricing-trust-arrow" />
</a>
</div>
</section>
</PageHeader>
{#each pageContent.sections as section, index}
<section use:reveal class="pricing-section reveal-block">
@@ -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;
+6 -1
View File
@@ -9,7 +9,8 @@
export let type = 'website';
export let structuredData: Record<string, unknown>[] = [];
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;
</script>
<svelte:head>
@@ -49,6 +51,9 @@
<meta name="publisher" content="Goodwalk" />
<meta name="geo.region" content="NZ-AUK" />
<meta name="geo.placename" content="Auckland Central" />
{#if preloadImage && resolvedPreloadUrl}
<link rel="preload" as="image" href={resolvedPreloadUrl} fetchpriority="high" />
{/if}
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" hreflang="en-NZ" href={canonicalUrl} />
<link rel="alternate" hreflang="x-default" href={canonicalUrl} />
+293
View File
@@ -0,0 +1,293 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
import { getEnhancedImage } from '$lib/enhanced-images';
import type { CallToAction, HeroChip } from '$lib/types';
export let eyebrow: string;
export let title: string;
export let subtitle: string | undefined = undefined;
export let imageUrl: string;
export let imageAlt: string;
export let chips: HeroChip[] = [];
export let cta: CallToAction | undefined = undefined;
const reviewHref = 'https://g.page/r/CUsvrWPhkYrAEB0/';
$: enhanced = getEnhancedImage(imageUrl);
</script>
<section class="sh">
<!-- Left: brand green copy column -->
<div class="sh-copy">
<p class="sh-eyebrow">{eyebrow}</p>
<h1 class="sh-title">{title}</h1>
{#if subtitle}
<p class="sh-subtitle">{subtitle}</p>
{/if}
{#if chips.length}
<div class="sh-chips">
{#each chips as chip}
<span class="sh-chip">
<Icon name={chip.icon} />
{chip.label}
</span>
{/each}
</div>
{/if}
<div class="sh-actions">
{#if cta}
<a href={cta.href} class="btn btn-yellow sh-cta">{cta.label}</a>
{/if}
<a
href={reviewHref}
class="sh-trust"
target="_blank"
rel="noopener"
aria-label="Read our Google reviews"
>
<span class="sh-stars" aria-hidden="true">★★★★★</span>
30+ five-star Google reviews
</a>
</div>
</div>
<!-- Right: full-height photo, no card, no shadow, bleeds to viewport edge -->
<div class="sh-media">
{#if enhanced}
<enhanced:img
src={enhanced}
alt={imageAlt}
loading="eager"
fetchpriority="high"
decoding="async"
/>
{:else}
<img
src={imageUrl}
alt={imageAlt}
loading="eager"
fetchpriority="high"
decoding="async"
/>
{/if}
</div>
</section>
<style>
/* ── Full-bleed split — green bleeds left, photo bleeds right, content stays centred ── */
.sh {
display: grid;
grid-template-columns: 1fr 1fr;
position: relative;
z-index: 1;
overflow: hidden;
}
/* ── Copy column ──
Left padding uses --sh-copy-left-pad so ultrawide overrides can live
entirely in responsive.css without touching this component. ── */
.sh-copy {
background: var(--gw-green);
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding: 80px 56px 80px var(--sh-copy-left-pad, max(40px, calc(50vw - 596px)));
}
/* Subtle yellow warmth on the copy side */
.sh-copy::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse 90% 80% at 5% 65%, rgba(255, 209, 0, 0.09) 0%, transparent 70%);
pointer-events: none;
}
/* Service name in Goodwalk Yellow */
.sh-eyebrow {
margin: 0 0 16px;
font-family: var(--font-head);
font-size: 13px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--yellow);
}
.sh-title {
margin: 0 0 14px;
font-family: var(--font-head);
font-size: clamp(30px, 3.2vw, 50px);
font-weight: 800;
line-height: 1.04;
letter-spacing: -0.04em;
color: #fff;
text-wrap: balance;
}
.sh-subtitle {
margin: 0 0 26px;
font-size: 16px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.68);
max-width: 38ch;
}
/* ── Chips ── */
.sh-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 30px;
}
.sh-chip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 7px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.09);
border: 1px solid rgba(255, 255, 255, 0.14);
color: #fff;
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.01em;
}
/* ── CTA row ── */
.sh-actions {
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
}
.sh-cta {
font-size: 15px;
padding: 12px 24px;
}
.sh-trust {
display: inline-flex;
align-items: center;
gap: 8px;
color: rgba(255, 255, 255, 0.62);
font-size: 13px;
font-weight: 600;
text-decoration: none;
line-height: 1.3;
transition: color 0.18s ease;
}
.sh-trust:hover {
color: rgba(255, 255, 255, 0.9);
}
.sh-stars {
color: var(--yellow);
letter-spacing: 2px;
font-size: 12px;
flex: 0 0 auto;
}
/* ── Photo column — fills full height, bleeds to right viewport edge ── */
.sh-media {
position: relative;
overflow: hidden;
/* Minimum height so the section never collapses on short copy */
min-height: 480px;
}
.sh-media::before {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: clamp(28px, 4.5vw, 76px);
background:
radial-gradient(circle at left center, rgba(255, 209, 0, 0.1) 0%, rgba(255, 209, 0, 0.04) 26%, transparent 62%),
linear-gradient(90deg, rgba(33, 48, 33, 0.76) 0%, rgba(33, 48, 33, 0.34) 46%, rgba(33, 48, 33, 0.08) 78%, transparent 100%);
pointer-events: none;
z-index: 1;
}
.sh-media :global(picture) {
position: absolute;
inset: 0;
display: block;
}
.sh-media :global(img) {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 25%;
transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.sh-media:hover :global(img) {
transform: scale(1.04);
}
/* ── Tablet — formula already floors at 40px here, just reduce vertical padding ── */
@media (max-width: 1024px) {
.sh-copy {
padding-top: 64px;
padding-bottom: 64px;
}
}
/* ── Mobile — stack vertically, photo above copy ── */
@media (max-width: 768px) {
.sh {
grid-template-columns: 1fr;
}
/* Photo goes first on mobile — visual hook before the pitch */
.sh-media {
order: 1;
min-height: 0;
aspect-ratio: 3 / 2;
position: relative;
}
.sh-media::before {
width: 0;
}
.sh-copy {
order: 2;
padding: 44px 24px 48px;
}
.sh-title {
font-size: clamp(28px, 7.5vw, 38px);
}
.sh-subtitle {
font-size: 15px;
}
.sh-actions {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.sh-cta {
width: 100%;
justify-content: center;
}
}
</style>
+456 -108
View File
@@ -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,50 +65,48 @@
pill: 'All services'
}
];
function scrollBenefits(direction: -1 | 1) {
if (!benefitScroller) return;
benefitScroller.scrollBy({
left: direction * Math.round(benefitScroller.clientWidth * 0.86),
behavior: 'smooth'
});
}
</script>
<main class="service-page">
<section class="service-hero">
<div class="service-inner service-hero-grid">
<div class="service-hero-copy">
<p class="eyebrow">{pageContent.hero.eyebrow}</p>
<h1>{pageContent.hero.title}</h1>
{#each pageContent.hero.paragraphs as paragraph}
<p>{paragraph}</p>
{/each}
</div>
<div class="service-hero-media">
{#if heroEnhanced}
<enhanced:img
src={heroEnhanced}
alt={pageContent.hero.imageAlt}
loading="eager"
fetchpriority="high"
decoding="async"
<ServiceHero
eyebrow={pageContent.hero.eyebrow}
title={pageContent.hero.title}
subtitle={pageContent.hero.subtitle}
imageUrl={pageContent.hero.imageUrl}
imageAlt={pageContent.hero.imageAlt}
chips={pageContent.hero.chips ?? []}
cta={pageContent.hero.cta}
/>
{:else}
<img
src={pageContent.hero.imageUrl}
alt={pageContent.hero.imageAlt}
loading="eager"
fetchpriority="high"
decoding="async"
/>
{/if}
</div>
</div>
</section>
{#if pageContent.highlight}
<section use:reveal class="service-highlight reveal-block">
<div class="service-inner service-highlight-copy">
<div class="service-inner">
<div class:service-highlight-layout-points={pageContent.highlight.points?.length} class="service-highlight-layout">
<div class="service-highlight-copy">
<p class="eyebrow service-highlight-eyebrow">{pageContent.highlight.eyebrow}</p>
<h2>{pageContent.highlight.title}</h2>
{#if pageContent.highlight.points?.length}
<div class="service-highlight-points">
{#each pageContent.highlight.points as point}
<article class="service-highlight-point">
<h3>{point.title}</h3>
<p>{point.body}</p>
</article>
{/each}
</div>
{/if}
</div>
<div class="service-inner">
{#if highlightCollageImages.length}
<div class="service-highlight-collage" aria-label={pageContent.highlight.title}>
{#each highlightCollageImages as image, index}
@@ -134,9 +139,49 @@
</div>
{/if}
</div>
</div>
</section>
{/if}
<section use:reveal class="service-benefits reveal-block">
<div class="service-inner">
<div class="service-section-heading">
<h2>{pageContent.benefits.title}</h2>
{#if pageContent.benefits.intro}
<p>{pageContent.benefits.intro}</p>
{/if}
</div>
<p class="service-benefit-mobile-hint">Swipe through the reasons Tiny Gang works so well.</p>
<div class="service-benefit-shell">
<div bind:this={benefitScroller} class="service-benefit-grid">
{#each benefitCards as benefit}
<article class:service-benefit-card-featured={benefit.featured} class={`service-benefit-card ${benefit.tintClass}`}>
<div class="service-benefit-icon" aria-hidden="true">
<Icon name={benefit.icon ?? 'fas fa-paw'} />
</div>
{#if benefit.badge}
<p class="service-benefit-kicker">{benefit.badge}</p>
{/if}
<h3>{benefit.title}</h3>
<p>{benefit.body}</p>
</article>
{/each}
</div>
<div class="service-benefit-mobile-controls" aria-label="Benefit cards navigation">
<button type="button" class="service-benefit-mobile-button" aria-label="Previous benefit" on:click={() => scrollBenefits(-1)}>
<Icon name="fas fa-chevron-left" />
</button>
<button type="button" class="service-benefit-mobile-button" aria-label="Next benefit" on:click={() => scrollBenefits(1)}>
<Icon name="fas fa-chevron-right" />
</button>
</div>
</div>
</div>
</section>
<section use:reveal class="service-pricing reveal-block">
<div class="service-inner">
<div class="service-section-heading">
@@ -205,27 +250,7 @@
</div>
</section>
<section use:reveal class="service-benefits reveal-block">
<div class="service-inner">
<div class="service-section-heading">
<h2>{pageContent.benefits.title}</h2>
</div>
<div class="service-benefit-grid">
{#each pageContent.benefits.items as benefit}
<article class="service-benefit-card">
<div class="service-benefit-icon" aria-hidden="true">
<Icon name="fas fa-paw" />
</div>
<h3>{benefit.title}</h3>
<p>{benefit.body}</p>
</article>
{/each}
</div>
</div>
</section>
{#if relatedServices.length}
{#if showRelatedServices}
<section use:reveal class="service-related reveal-block" aria-label="Other services">
<div class="service-inner">
<div class="service-section-heading">
@@ -277,10 +302,6 @@
padding: 0 50px;
}
.service-hero {
padding: 72px 0 96px;
}
.service-related {
padding: 0 0 96px;
}
@@ -390,14 +411,6 @@
font-size: 14px;
}
.service-hero-grid {
display: grid;
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
gap: 52px;
align-items: center;
}
.service-hero-copy h1,
.service-section-heading h2,
.service-highlight-copy h2 {
margin: 0;
@@ -406,11 +419,6 @@
color: #000;
}
.service-hero-copy h1 {
line-height: 1.05;
letter-spacing: -0.04em;
}
.service-section-heading h2,
.service-highlight-copy h2 {
font-weight: 700;
@@ -419,7 +427,7 @@
text-wrap: balance;
}
.service-hero-copy p,
.service-section-heading p,
.service-benefit-card p {
margin: 20px 0 0;
@@ -429,7 +437,6 @@
line-height: 1.7;
}
.service-hero-media,
.service-highlight-image {
position: relative;
overflow: hidden;
@@ -438,10 +445,6 @@
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;
@@ -486,7 +489,6 @@
min-height: 250px;
}
.service-hero-media img,
.service-highlight-image img {
display: block;
width: 100%;
@@ -495,12 +497,23 @@
}
.service-highlight {
padding: 0 0 96px;
padding: 80px 0 96px;
}
.service-highlight-layout {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
gap: 40px;
align-items: center;
}
.service-highlight-layout-points {
align-items: start;
}
.service-highlight-copy {
text-align: center;
margin-bottom: 34px;
text-align: left;
margin-bottom: 0;
}
/*
@@ -512,11 +525,55 @@
margin: 0 0 16px;
}
.service-highlight-points {
display: grid;
gap: 16px;
margin-top: 28px;
}
.service-highlight-point {
padding: 0 0 16px;
border-bottom: 1px solid rgba(33, 48, 33, 0.1);
}
.service-highlight-point:last-child {
padding-bottom: 0;
border-bottom: none;
}
.service-highlight-point h3 {
margin: 0 0 8px;
font-family: var(--font-head);
font-size: 18px;
line-height: 1.2;
color: #000;
}
.service-highlight-point p {
margin: 0;
color: #34363a;
font-size: 15px;
line-height: 1.65;
}
.service-pricing,
.service-benefits {
padding: 0 0 96px;
}
.service-benefit-shell {
position: relative;
overflow: visible;
}
.service-benefit-mobile-hint {
display: none;
}
.service-benefit-mobile-controls {
display: none;
}
.service-section-heading {
text-align: center;
margin-bottom: 38px;
@@ -540,10 +597,12 @@
.service-plan-card,
.service-benefit-card {
position: relative;
background: #fff;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(247, 248, 246, 0.98) 100%);
border-radius: 28px;
padding: 30px 26px;
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.045),
0 8px 40px rgba(0, 0, 0, 0.06);
transition:
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease,
@@ -578,10 +637,12 @@
}
@media (hover: hover) {
.service-plan-card:hover,
.service-benefit-card:hover {
transform: translateY(-6px) scale(1.012);
box-shadow: 0 22px 44px rgba(17, 20, 24, 0.1);
.service-plan-card:hover {
transform: translateY(-2px);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.055),
0 10px 40px rgba(0, 0, 0, 0.08);
filter: brightness(1.015);
}
}
@@ -767,38 +828,200 @@
.service-benefit-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
gap: 18px;
}
.service-benefit-card {
display: flex;
flex-direction: column;
align-items: flex-start;
min-height: 100%;
overflow: hidden;
border-radius: 28px;
padding: 28px 26px 26px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.97) 0%, rgba(247, 248, 246, 0.98) 100%);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.045),
0 8px 28px rgba(17, 20, 24, 0.05);
}
.service-benefit-card::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, transparent 100%);
pointer-events: none;
}
.service-benefit-card::after {
content: '';
position: absolute;
top: 24px;
left: 26px;
width: 42px;
height: 2px;
border-radius: 999px;
background: rgba(255, 209, 0, 0.42);
pointer-events: none;
}
.service-benefit-tint-1::before {
background:
radial-gradient(circle at top left, rgba(255, 209, 0, 0.08) 0%, rgba(255, 209, 0, 0.03) 24%, transparent 48%),
linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0%, transparent 100%);
}
.service-benefit-tint-2::before {
background:
radial-gradient(circle at top right, rgba(229, 214, 194, 0.12) 0%, rgba(229, 214, 194, 0.04) 28%, transparent 54%),
linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, transparent 100%);
}
.service-benefit-tint-3::before {
background:
radial-gradient(circle at top left, rgba(33, 48, 33, 0.04) 0%, rgba(33, 48, 33, 0.015) 28%, transparent 54%),
linear-gradient(180deg, rgba(255, 255, 255, 0.14) 0%, transparent 100%);
}
.service-benefit-card-featured {
background:
radial-gradient(circle at top right, rgba(255, 209, 0, 0.14) 0%, rgba(255, 209, 0, 0.04) 28%, transparent 52%),
linear-gradient(180deg, var(--green-mid) 0%, var(--gw-green) 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
0 14px 32px rgba(17, 20, 24, 0.12);
}
.service-benefit-card-featured::before {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, transparent 100%);
}
.service-benefit-card-featured::after {
background: linear-gradient(90deg, #ffd54a 0%, rgba(242, 191, 47, 0.72) 100%);
}
.service-benefit-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: #efe4d1;
width: 54px;
height: 54px;
border-radius: 16px;
background: linear-gradient(180deg, rgba(255, 250, 236, 0.96) 0%, rgba(242, 191, 47, 0.18) 100%);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.04),
0 8px 20px rgba(17, 20, 24, 0.08);
color: var(--gw-green);
font-size: 18px;
margin-bottom: 18px;
font-size: 17px;
margin-top: 4px;
margin-bottom: 14px;
position: relative;
z-index: 1;
}
.service-benefit-tint-1 .service-benefit-icon {
background: linear-gradient(180deg, rgba(255, 250, 236, 0.96) 0%, rgba(242, 191, 47, 0.16) 100%);
}
.service-benefit-tint-2 .service-benefit-icon {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(229, 214, 194, 0.42) 100%);
}
.service-benefit-tint-3 .service-benefit-icon {
background: linear-gradient(180deg, rgba(250, 252, 248, 0.96) 0%, rgba(33, 48, 33, 0.08) 100%);
}
.service-benefit-card-featured .service-benefit-icon {
background: linear-gradient(180deg, rgba(255, 248, 214, 0.96) 0%, rgba(255, 209, 0, 0.34) 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 10px 24px rgba(0, 0, 0, 0.16);
}
.service-benefit-card h3,
.service-benefit-card p,
.service-benefit-kicker {
position: relative;
z-index: 1;
}
.service-benefit-kicker {
margin: 0 0 12px;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.service-benefit-card h3 {
max-width: 16ch;
text-wrap: balance;
color: #000;
}
.service-benefit-card p {
margin-top: 14px;
font-size: 16px;
margin-top: 0;
color: #5b6067;
font-size: 15px;
line-height: 1.7;
}
.service-benefit-card-featured .service-benefit-kicker {
color: rgba(255, 245, 204, 0.92);
}
.service-benefit-card-featured h3 {
color: #fff;
}
.service-benefit-card-featured p {
color: rgba(255, 255, 255, 0.8);
}
@media (hover: hover) {
.service-benefit-card:hover {
transform: translateY(-2px);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 12px 30px rgba(17, 20, 24, 0.08);
filter: brightness(1.01);
}
.service-benefit-card-featured:hover {
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 16px 36px rgba(17, 20, 24, 0.14);
}
}
@media (max-width: 1024px) {
.service-hero-grid,
.service-plan-grid,
.service-plan-grid-three,
.service-benefit-grid,
.service-related-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.service-hero-grid {
align-items: start;
.service-benefit-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.service-highlight-layout {
grid-template-columns: 1fr;
gap: 28px;
}
.service-section-heading-split {
grid-template-columns: 1fr;
gap: 14px;
}
.service-section-heading-split p {
justify-self: start;
}
.service-highlight-collage {
@@ -817,30 +1040,155 @@
padding: 0 24px;
}
.service-hero {
padding: 48px 0 72px;
}
.service-hero-grid,
.service-plan-grid,
.service-plan-grid-three,
.service-benefit-grid,
.service-related-grid {
grid-template-columns: 1fr;
gap: 24px;
}
.service-benefit-grid {
grid-auto-flow: column;
grid-auto-columns: minmax(272px, 84vw);
grid-template-columns: none;
gap: 14px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x proximity;
scroll-padding-left: 24px;
padding: 0 24px 8px 0;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.service-benefit-grid::-webkit-scrollbar {
display: none;
}
.service-benefit-card {
padding: 24px 22px 22px;
border-radius: 24px;
scroll-snap-align: start;
}
.service-plan-card {
order: var(--mobile-order, 0);
}
.service-highlight,
.service-highlight {
padding-top: 56px;
padding-bottom: 72px;
}
.service-highlight-layout {
gap: 24px;
}
.service-highlight-copy {
text-align: left;
}
.service-highlight-points {
gap: 14px;
margin-top: 22px;
}
.service-highlight-point {
padding-bottom: 14px;
}
.service-highlight-point h3 {
font-size: 17px;
}
.service-highlight-point p {
font-size: 15px;
line-height: 1.62;
}
.service-pricing,
.service-benefits,
.service-related {
padding-bottom: 72px;
}
.service-section-heading h2 {
font-size: clamp(30px, 7vw, 38px);
}
.service-benefit-icon {
width: 50px;
height: 50px;
margin-bottom: 18px;
}
.service-benefit-card h3 {
max-width: none;
font-size: 20px;
}
.service-benefit-card p {
margin-top: 12px;
font-size: 15px;
line-height: 1.68;
}
.service-benefit-mobile-hint {
display: block;
margin: -10px 0 16px;
color: var(--gray);
font-size: 13px;
line-height: 1.5;
}
.service-benefit-mobile-controls {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
}
.service-benefit-mobile-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
border: none;
border-radius: 50%;
background: rgba(33, 48, 33, 0.08);
color: var(--gw-green);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
transition:
background 0.18s ease,
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
.service-benefit-mobile-button:active {
transform: scale(0.97);
}
.service-benefit-mobile-button :global(.icon) {
font-size: 14px;
}
.service-benefit-meta {
gap: 10px;
padding-top: 18px;
}
.service-benefit-badge {
min-height: 34px;
font-size: 11px;
padding: 7px 12px;
letter-spacing: 0.015em;
white-space: normal;
}
.service-benefit-meta::before {
background: linear-gradient(90deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.03) 100%);
}
.service-extra-row {
flex-direction: column;
align-items: flex-start;
+8 -1
View File
@@ -4,6 +4,7 @@ export const dogWalkingContent: ServicePageContent = {
hero: {
eyebrow: '1:1 Walks',
title: 'A calmer walk for dogs who need more attention',
subtitle: 'Full attention, your dog\'s pace. Free pickup and drop-off across Auckland Central.',
paragraphs: [
'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.',
@@ -11,7 +12,13 @@ export const dogWalkingContent: ServicePageContent = {
'We run 1:1 walks across Auckland Central — including Mt Eden, Ponsonby, Kingsland, Grey Lynn, Herne Bay, and surrounding suburbs — with free pickup and drop-off included.'
],
imageUrl: '/images/auckland-large-dog-one-on-one-walk.jpg',
imageAlt: 'Large breed dog enjoying a Goodwalk one on one dog walk'
imageAlt: 'Large breed dog enjoying a Goodwalk one on one dog walk',
chips: [
{ icon: 'fas fa-dog', label: 'One-on-one walk' },
{ icon: 'fas fa-tag', label: 'From $45 / walk' },
{ icon: 'fas fa-car', label: 'Free pickup & drop-off' }
],
cta: { label: 'Book a free Meet & Greet', href: '#newlead', variant: 'yellow' }
},
highlight: {
eyebrow: 'One dog. Full attention.',
+58 -30
View File
@@ -1,17 +1,16 @@
import type { HomePageContent } from '$lib/types';
import { sharedServices } from '$lib/content/services';
export const homepageContent: HomePageContent = {
seo: {
title: 'Auckland Dog Walker | Pack Walks & 1:1 Walks | Goodwalk',
title: 'Home | Auckland Dog Walking | Goodwalk',
description:
'Trusted by 30+ Auckland families. Pack walks from $49.50. Free Meet & Greet. Covering Mt Eden, Ponsonby, Grey Lynn, Kingsland & more. Book online today.'
'At Goodwalk, we offer Tiny Gang pack walks and one on one dog walking services throughout Auckland. Give your dog his best life with Goodwalk!'
},
navigation: {
desktopLinks: [
{ label: 'Our Services', href: '#services' },
{ label: 'Our Pricing', href: '/our-pricing' },
{ label: 'About Us', href: '/about' }
{ label: 'About', href: '/about' }
],
mobileLinks: [
{ label: 'Home', href: '/' },
@@ -19,17 +18,16 @@ export const homepageContent: HomePageContent = {
{ label: '1:1 Walks', href: '/dog-walking' },
{ label: 'Puppy Visits', href: '/puppy-visits' },
{ label: 'Our Pricing', href: '/our-pricing' },
{ label: 'About Us', href: '/about' },
{ label: 'Contact Us', href: '/contact-us' }
{ label: 'About', href: '/about' },
{ label: 'Contact', href: '/contact-us' }
],
cta: { label: 'Contact Us', href: '/contact-us', variant: 'yellow' },
instagram: { href: 'https://www.instagram.com/goodwalk.nz/', external: true },
megaMenuServices: sharedServices.map((service) => ({
icon: service.icon,
label: service.title,
description: service.megaMenuDescription,
href: service.href
})),
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: {
@@ -44,7 +42,7 @@ export const homepageContent: HomePageContent = {
href: '#how-it-works',
variant: 'outline'
},
imageUrl: '/images/auckland-dog-walking-happy-dog-hero.png',
imageUrl: '/images/maya-mascot.png',
imageAlt: 'Happy dog ready for a professional pack walk with Goodwalk Auckland dog walking service'
},
intro: {
@@ -59,22 +57,37 @@ export const homepageContent: HomePageContent = {
title: 'Meet Aless,',
subtitle: 'the heart of Goodwalk',
body: [
'Goodwalk is built around one thing: a dog who knows the routine, and an owner who stops worrying.',
'Alessandra runs every walk personally — calm, experienced, and trusted by Auckland Central families.',
"Your dog knows who's at the door. You know what happens on the walk. Ready to join the"
'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/founder-image-aless-goodwalk.jpg',
imageUrl: '/images/goodwalk-dog-walker-alessandra.png',
imageAlt: 'Alessandra from Goodwalk with a dog in Auckland'
},
services: sharedServices.map((service) => ({
icon: service.icon,
title: service.title,
body: service.cardBody,
priceFrom: service.priceFrom,
href: service.href
})),
services: [
{
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'
}
],
howItWorks: {
title: 'How it works',
intro:
@@ -111,25 +124,40 @@ export const homepageContent: HomePageContent = {
icon: 'fas fa-heart',
title: 'Calm, kind handling',
body:
'Positive reinforcement and patient routines so your dog builds confidence, not stress.'
'We use positive reinforcement, gentle handling, and patient routines so dogs build confidence instead of stress.'
},
{
icon: 'fas fa-camera',
title: 'Updates you will actually want',
title: 'Daily updates you will actually want',
body:
"See your dog out enjoying the day. Less wondering, more peace of mind while you're at work."
"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',
title: 'Small Pack Sizes',
order: 2,
body:
'48 dogs per group. Calm, structured walks with enough attention for every dog in the pack.'
'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-first by default',
order: 1,
body:
'Pet first aid, careful screening, and proactive handling built into every walk not added as an extra.'
'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: 'Built for real schedules',
body:
"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: 'Reliable pickup, clear communication',
order: 3,
body:
"You should not have to chase your dog walker. We keep things consistent, communicate clearly, and make the practical side feel easy."
}
],
testimonials: [
+57 -19
View File
@@ -3,7 +3,8 @@ import type { ServicePageContent } from '$lib/types';
export const packWalksContent: ServicePageContent = {
hero: {
eyebrow: 'Pack Walks',
title: 'Come home to a calm, happy dog',
title: 'Small-group pack walks for sociable small and medium dogs',
subtitle: 'Tiny Gang walks are social, active, and carefully matched for dogs who love the right company.',
paragraphs: [
'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.',
@@ -11,18 +12,41 @@ export const packWalksContent: ServicePageContent = {
'We run pack walks across Auckland Central — including Mt Eden, Kingsland, Ponsonby, Grey Lynn, Sandringham, Mt Albert, and surrounding suburbs — with free pickup and drop-off included in every booking.'
],
imageUrl: '/images/auckland-pack-walk-small-dogs-group.jpg',
imageAlt: "Small dogs from Goodwalk's Tiny Gang pack walk sitting together in an Auckland park"
imageAlt: "Small dogs from Goodwalk's Tiny Gang pack walk sitting together in an Auckland park",
chips: [
{ icon: 'fas fa-users', label: 'Small groups · 48 dogs' },
{ icon: 'fas fa-tag', label: 'From $49.50 / walk' },
{ icon: 'fas fa-car', label: 'Free pickup & drop-off' }
],
cta: { label: 'See if Tiny Gang fits your dog', href: '#newlead', variant: 'yellow' }
},
highlight: {
eyebrow: 'Small packs. Calm dogs.',
title: 'Made specifically for small and medium dogs who do best in a structured social group',
eyebrow: 'What Tiny Gang is',
title: 'A small-group walking routine for sociable dogs who love the right company',
imageUrl: '/images/small-medium-dogs-pack-walk.jpg',
imageAlt: 'Small and medium dogs together on a Goodwalk pack walk in Auckland'
imageAlt: 'Small and medium dogs together on a Goodwalk pack walk in Auckland',
points: [
{
title: 'Small, social groups',
body:
'Tiny Gang walks run in carefully matched groups of 4-8 dogs, with real play, movement, and social time without the chaos of oversized packs.'
},
{
title: 'Best for the right dogs',
body:
'These walks suit sociable small and medium dogs who enjoy company and tend to do well in a shared, active environment. If your dog needs more space, 1:1 walks may be a better fit.'
},
{
title: 'Exercise with a weekly rhythm',
body:
'Most owners use Tiny Gang as a regular weekday routine, with pickup and drop-off included across Auckland Central to make the whole thing easier to stick to.'
}
]
},
pricing: {
title: 'Choose the weekly routine that suits your dog',
intro:
'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.',
'Choose the routine that gives your dog the right amount of exercise, social time, and consistency each week.',
plans: [
{
title: '1 Walk Per Week',
@@ -58,31 +82,45 @@ export const packWalksContent: ServicePageContent = {
scarcityNote: 'We keep packs small (4-8 dogs) — popular days fill up fast.'
},
benefits: {
title: 'Why small and medium dogs do so well in Tiny Gang',
title: 'Why the right dogs thrive in Tiny Gang',
intro:
'Small, compatible groups give dogs the exercise, confidence, and routine they need without the chaos of oversized pack walks.',
items: [
{
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: 'Calmer evenings at home',
body: 'Small, structured outings help dogs burn energy without overstimulation, so they come home settled, content, and ready to rest.',
badge: 'Structured weekly walks',
icon: 'fas fa-house'
},
{
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: 'Confidence with the right dogs',
body: 'Carefully matched groups help sociable dogs enjoy company at the right pace, without the pressure of bigger mixed packs.',
badge: 'Carefully matched groups',
icon: 'fas fa-user-group'
},
{
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: 'No overwhelming pack dynamics',
body: 'Tiny Gang is designed for small and medium dogs, with group size, pace, and play style matched to help them feel safe.',
badge: 'Small & medium dogs',
icon: 'fas fa-shield-dog'
},
{
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: 'A routine owners can rely on',
body: 'Regular weekly slots give busy owners dependable exercise support and give dogs the comfort of a familiar rhythm.',
badge: 'Reliable weekly slots',
icon: 'fas fa-calendar-check'
},
{
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: 'Individual attention still matters',
body: 'Smaller groups mean our walkers can notice confidence, handling, behaviour, and the little details that make a difference.',
badge: '48 dogs per walk',
icon: 'fas fa-eye'
},
{
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.'
title: 'Safety is built into the group',
body: 'Calm, compatible packs reduce intimidation and create a safer walking environment than a one-size-fits-all approach.',
badge: 'Calm, compatible packs',
icon: 'fas fa-shield-heart'
}
]
},
+8 -1
View File
@@ -4,6 +4,7 @@ export const puppyVisitsContent: ServicePageContent = {
hero: {
eyebrow: 'Puppy Visits',
title: 'Give your puppy a calmer start while you are out',
subtitle: 'Toilet breaks, play, feeding, and calm one-on-one attention — at home, while you\'re out.',
paragraphs: [
'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.',
@@ -11,7 +12,13 @@ export const puppyVisitsContent: ServicePageContent = {
'We offer puppy visits across Auckland Central including Mt Eden, Ponsonby, Grey Lynn, Kingsland, Sandringham, Herne Bay, and surrounding suburbs.'
],
imageUrl: '/images/auckland-puppy-home-visit.jpg',
imageAlt: 'Puppy receiving a calm Goodwalk home visit in Auckland'
imageAlt: 'Puppy receiving a calm Goodwalk home visit in Auckland',
chips: [
{ icon: 'fas fa-house', label: 'In-home visit' },
{ icon: 'fas fa-tag', label: 'From $39 / visit' },
{ icon: 'fas fa-map-marker-alt', label: 'Auckland Central' }
],
cta: { label: 'Book a free Meet & Greet', href: '#newlead', variant: 'yellow' }
},
highlight: {
eyebrow: 'Start well. Grow well.',
+4 -2
View File
@@ -38,12 +38,14 @@ export const staticPages = {
'terms-and-conditions': {
title: 'Terms & Conditions',
description: 'Terms and conditions for Goodwalk Auckland dog walking services.',
canonicalPath: '/terms-and-conditions'
canonicalPath: '/terms-and-conditions',
noindex: true
},
'privacy-policy': {
title: 'Privacy Policy',
description: 'Privacy policy for Goodwalk Auckland dog walking services.',
canonicalPath: '/privacy-policy'
canonicalPath: '/privacy-policy',
noindex: true
}
} as const;
+25 -5
View File
@@ -13,8 +13,9 @@ html {
body {
font-family: var(--font-body);
font-size: 15px;
line-height: 1.65;
font-size: 16px;
line-height: 1.72;
letter-spacing: 0.002em;
color: var(--text);
background: var(--off-white);
overflow-x: clip;
@@ -49,14 +50,33 @@ textarea {
border: 0;
}
/* Anchor bounce — JS adds .anchor-bounce to the target section on hash navigation */
@media (prefers-reduced-motion: no-preference) {
@keyframes anchor-bounce {
0% { transform: translateY(0); }
18% { transform: translateY(-22px); }
38% { transform: translateY(0); }
54% { transform: translateY(-11px); }
70% { transform: translateY(0); }
82% { transform: translateY(-5px); }
92% { transform: translateY(0); }
97% { transform: translateY(-2px); }
100% { transform: translateY(0); }
}
.anchor-bounce {
animation: anchor-bounce 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}
}
/* Scroll reveal — JS adds [data-reveal] to targets, then toggles .is-visible */
@media (prefers-reduced-motion: no-preference) {
[data-reveal] {
opacity: 0;
transform: translateY(20px);
transform: translateY(44px);
transition:
opacity 0.5s ease,
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
opacity 0.55s ease,
transform 0.65s cubic-bezier(0.34, 1.56, 0.64, 1);
}
[data-reveal].is-visible {
+81 -30
View File
@@ -3,7 +3,75 @@ header {
z-index: 100;
isolation: isolate;
overflow: visible;
background: var(--gw-green);
background: linear-gradient(to bottom, #f7f7f5 0%, #ffffff 100%);
box-shadow: 0 2px 16px rgba(17, 20, 24, 0.08);
}
.nav-ribbon {
width: 100%;
background: var(--yellow);
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 11px 24px;
flex-wrap: nowrap;
overflow: hidden;
}
.nav-ribbon-item {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--gw-green);
white-space: nowrap;
padding: 0 28px;
}
.nav-ribbon-item .icon {
font-size: 14px;
}
.nav-ribbon-divider {
width: 1px;
height: 14px;
background: rgba(33, 48, 33, 0.2);
flex: none;
}
@media (max-width: 768px) {
.nav-ribbon {
padding: 10px 16px;
}
.nav-ribbon-item {
flex: 1;
justify-content: center;
padding: 0;
gap: 6px;
font-size: 9px;
letter-spacing: 0.04em;
}
.nav-ribbon-item .icon {
font-size: 13px;
}
/* Hide the third ribbon item and its preceding divider on mobile */
.nav-ribbon > :nth-child(4),
.nav-ribbon > :nth-child(5) {
display: none;
}
.nav-ribbon-divider {
height: 12px;
flex: none;
}
}
nav,
@@ -41,8 +109,12 @@ nav {
}
.nav-links > li > a {
color: #fff;
font-weight: 500;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 13px;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.2;
display: flex;
align-items: center;
gap: 6px;
@@ -53,12 +125,12 @@ nav {
}
.nav-links > li > a:hover {
background: #fff;
background: rgba(33, 48, 33, 0.09);
color: var(--gw-green);
}
.nav-links > li > a.nav-link-active {
background: #fff;
background: rgba(33, 48, 33, 0.11);
color: var(--gw-green);
}
@@ -264,7 +336,7 @@ nav {
}
.instagram-icon {
color: #fff;
color: var(--gw-green);
font-size: 26px;
line-height: 1;
display: flex;
@@ -273,34 +345,13 @@ nav {
}
.instagram-icon:hover {
color: var(--yellow);
color: var(--gw-green);
opacity: 0.7;
}
.mobile-menu-shell,
.mobile-menu {
display: none;
flex-direction: column;
gap: 0;
background: #fff;
padding: 0 0 18px;
}
.mobile-menu.open {
display: flex;
}
.mobile-menu a {
color: #0a304e;
font-weight: 700;
opacity: 0;
}
.mobile-menu a:hover {
color: #0a304e;
}
.mobile-menu a.mobile-link-active {
background: #eadbbf;
color: #0a304e;
}
.hamburger {
+239 -111
View File
@@ -5,11 +5,6 @@
padding-right: 30px;
}
#hero {
padding-left: 30px;
padding-right: 30px;
}
.promise-inner,
.services-inner,
.values-inner,
@@ -47,15 +42,10 @@
padding-bottom: 64px;
}
@keyframes mobileMenuBounceIn {
@keyframes mobileMenuSheetIn {
0% {
opacity: 0;
transform: translateY(-10px) scaleY(0.98);
}
70% {
opacity: 1;
transform: translateY(2px) scaleY(1.01);
transform: translateY(-16px) scale(0.985);
}
100% {
@@ -67,7 +57,7 @@
@keyframes mobileMenuItemIn {
0% {
opacity: 0;
transform: translateY(-6px);
transform: translateY(8px);
}
100% {
@@ -106,14 +96,14 @@
background: rgba(33, 48, 33, 0.1);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 13px;
font-size: 11px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.01em;
}
.mobile-phone .icon {
font-size: 14px;
font-size: 12px;
}
.nav-links {
@@ -144,103 +134,219 @@
}
.mobile-menu {
display: flex;
max-width: none;
padding-left: 0;
padding-right: 0;
transform-origin: top center;
flex-direction: column;
gap: 0;
padding: 10px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.985), rgba(248, 248, 245, 0.98));
border: 1px solid rgba(33, 48, 33, 0.08);
border-radius: 22px;
box-shadow:
0 16px 32px rgba(17, 20, 24, 0.12),
0 2px 10px rgba(17, 20, 24, 0.05);
opacity: 0;
transform: translateY(-10px) scale(0.992);
transition:
opacity 180ms ease,
transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.mobile-menu.open {
animation: mobileMenuBounceIn 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.mobile-menu a {
.mobile-menu-shell {
display: block;
padding: 14px 24px;
border-bottom: none;
font-size: 16px;
line-height: 1.35;
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
z-index: 120;
padding: 0 16px;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition:
opacity 160ms ease,
visibility 160ms ease;
}
.mobile-menu-shell.open {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.mobile-menu-shell.open .mobile-menu {
opacity: 1;
transform: translateY(0) scale(1);
animation: mobileMenuSheetIn 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.mobile-menu-links {
display: grid;
gap: 6px;
}
.mobile-menu-links a {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 9px 12px;
border-radius: 16px;
border: 1px solid rgba(33, 48, 33, 0.06);
background: rgba(255, 255, 255, 0.78);
color: var(--gw-green);
text-decoration: none;
opacity: 0;
animation: mobileMenuItemIn 220ms ease-out forwards;
box-shadow: 0 2px 10px rgba(17, 20, 24, 0.03);
-webkit-tap-highlight-color: transparent;
}
.mobile-menu.open a:nth-child(1) {
animation-delay: 30ms;
.mobile-menu-shell.open .mobile-menu-links a:nth-child(1) { animation-delay: 30ms; }
.mobile-menu-shell.open .mobile-menu-links a:nth-child(2) { animation-delay: 55ms; }
.mobile-menu-shell.open .mobile-menu-links a:nth-child(3) { animation-delay: 80ms; }
.mobile-menu-shell.open .mobile-menu-links a:nth-child(4) { animation-delay: 105ms; }
.mobile-menu-shell.open .mobile-menu-links a:nth-child(5) { animation-delay: 130ms; }
.mobile-menu-shell.open .mobile-menu-links a:nth-child(6) { animation-delay: 155ms; }
.mobile-menu-shell.open .mobile-menu-links a:nth-child(7) { animation-delay: 180ms; }
.mobile-menu-link-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 11px;
background: var(--gw-green);
color: var(--yellow);
font-size: 13px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.mobile-menu.open a:nth-child(2) {
animation-delay: 60ms;
.mobile-menu-link-label {
font-family: var(--font-head);
font-size: 13px;
font-weight: 700;
line-height: 1.2;
color: var(--gw-green);
min-width: 0;
letter-spacing: -0.02em;
}
.mobile-menu.open a:nth-child(3) {
animation-delay: 90ms;
:global(.mobile-menu-link-arrow) {
font-size: 12px;
color: rgba(33, 48, 33, 0.42);
}
.mobile-menu.open a:nth-child(4) {
animation-delay: 120ms;
.mobile-menu-links a.mobile-link-active {
background: linear-gradient(180deg, rgba(255, 209, 0, 0.2), rgba(255, 209, 0, 0.1));
border-color: rgba(255, 209, 0, 0.28);
box-shadow: 0 6px 16px rgba(255, 209, 0, 0.08);
}
.mobile-menu.open a:nth-child(5) {
animation-delay: 150ms;
body.mobile-menu-open {
overflow: visible;
}
.mobile-menu.open a:nth-child(6) {
animation-delay: 180ms;
.mobile-menu-shell:not(.open) .mobile-menu-links a {
animation: none;
}
.mobile-menu.open a:nth-child(7) {
animation-delay: 210ms;
.mobile-menu-shell.open .mobile-menu-links a {
opacity: 1;
}
.mobile-menu.open a:nth-child(8) {
animation-delay: 240ms;
}
.mobile-menu.open a:nth-child(9) {
animation-delay: 270ms;
}
/* ── Full-bleed mobile hero ── */
#hero {
min-height: auto;
padding: 50px 20px 0;
min-height: 90svh;
padding: 0;
gap: 0;
flex-direction: column;
justify-content: flex-end;
}
/* Full-section gradient: transparent top Goodwalk green bottom.
hero-img is now a sibling of hero-inner (direct child of #hero),
so this ::after correctly overlays the full section. */
#hero::after {
inset: 0;
height: auto;
background: linear-gradient(
to bottom,
transparent 22%,
rgba(33, 48, 33, 0.45) 48%,
rgba(33, 48, 33, 0.88) 65%,
#213021 78%
);
}
/* Dog photo fills the entire section as a background layer */
.hero-img {
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
margin: 0;
flex: none;
display: block;
}
.hero-img picture {
display: block;
width: 100%;
height: 100%;
}
.hero-img img {
width: 100%;
height: 100%;
max-width: none;
margin: 0;
object-fit: cover;
object-position: center 48%;
transform: none;
}
.hero-floating-pill {
display: none;
}
/* Text content sits above the gradient (z-index: 2) */
.hero-inner {
flex-direction: column;
gap: 18px;
align-items: stretch;
text-align: left;
padding: 0;
position: relative;
z-index: 2;
flex: none;
display: block;
width: 100%;
max-width: none;
margin: 0;
padding: 0 26px 36px;
}
.hero-text {
display: flex;
flex-direction: column;
align-items: flex-start;
align-items: center;
text-align: center;
width: 100%;
padding-bottom: 0;
}
.hero-kicker {
margin: 0 0 8px;
font-size: 10px;
letter-spacing: 0.13em;
color: rgba(255, 255, 255, 0.48);
}
.hero-text h1,
.hero-heading {
margin-bottom: 22px;
margin-bottom: 14px;
font-size: 38px;
line-height: 1.08;
}
.hero-subtitle {
margin: -10px 0 22px;
max-width: none;
font-size: 16px;
line-height: 1.5;
}
.hero-trust-chip {
margin-bottom: 18px;
padding: 10px 14px;
font-size: 14px;
line-height: 1.06;
}
.hero-heading-desktop {
@@ -251,48 +357,70 @@
display: block;
color: #fff;
font-family: var(--font-head);
font-size: 36px;
font-size: 40px;
font-weight: 800;
line-height: 1.08;
line-height: 1.06;
letter-spacing: -0.04em;
white-space: pre-line;
}
/* Subtitle and chips not needed — photo + kicker carry the context */
.hero-subtitle,
.hero-subtitle-desktop,
.hero-chips {
display: none;
}
.hero-trust-chip {
width: 100%;
justify-content: center;
margin-bottom: 14px;
padding: 9px 12px;
gap: 8px;
font-size: 11px;
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.18);
}
.hero-trust-stars {
font-size: 9px;
gap: 1px;
}
.hero-trust-logo {
width: 12px;
height: 13px;
}
.hero-buttons {
width: 100%;
justify-content: space-between;
flex-direction: column;
gap: 8px;
padding-right: 0;
}
.hero-buttons .btn {
flex: 1 1 0;
width: 0;
min-width: 0;
padding: 14px 10px;
font-size: 13px;
width: 100%;
padding: 13px 20px;
font-size: 14px;
font-weight: 700;
text-align: center;
border-radius: 999px;
line-height: 1.15;
line-height: 1.2;
letter-spacing: -0.01em;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.hero-buttons .btn:last-child {
margin-right: 0;
}
.hero-buttons .btn-yellow {
background: var(--yellow);
color: #000;
}
.hero-buttons .btn-outline {
background: transparent;
background: rgba(255, 255, 255, 0.08);
color: #fff;
border: 2px solid rgba(255, 255, 255, 0.84);
border: 1.5px solid rgba(255, 255, 255, 0.3);
}
.hero-buttons .btn:active {
@@ -304,26 +432,7 @@
}
.hero-buttons .btn-outline:active {
background: rgba(255, 255, 255, 0.08);
}
.hero-img {
flex: none;
width: 100%;
max-width: none;
margin: 0;
display: flex;
justify-content: center;
align-items: flex-end;
}
.hero-img img {
width: min(100%, 460px);
max-width: 100%;
margin: 0 auto -7px;
object-fit: contain;
object-position: center top;
transform: none;
background: rgba(255, 255, 255, 0.14);
}
#intro {
@@ -417,11 +526,6 @@
font-size: 30px;
}
.mobile-menu a.mobile-link-active {
background: var(--yellow);
color: #0a304e;
}
.booking-header {
margin-bottom: 34px;
}
@@ -691,3 +795,27 @@
line-height: 1.1;
}
}
@media (min-width: 769px) {
.hero-img img {
transform: translateX(-120px);
}
}
@media (min-width: 1200px) {
body:has(#hero) .hero-inner {
padding-bottom: 120px;
}
}
/* Ultrawide (1800px)
Only add rules here that are genuinely ultrawide-specific.
Normal desktop, tablet, and mobile breakpoints are handled above. */
@media (min-width: 1800px) {
/* ServiceHero: stop content drifting far right on wide screens.
The component reads this custom property with the normal formula as its
fallback so nothing below 1800px is affected. */
:root {
--sh-copy-left-pad: clamp(420px, 22vw, 550px);
}
}
+283 -24
View File
@@ -19,44 +19,100 @@
#hero {
background: var(--gw-green);
color: #fff;
padding: 44px 50px 0;
padding: 0;
display: flex;
align-items: center;
min-height: 500px;
flex-direction: column;
justify-content: flex-end;
min-height: 65svh;
max-height: 680px;
overflow: hidden;
position: relative;
}
/* Gradient blends the hero image's bottom edge into the green background */
#hero::after {
content: '';
position: absolute;
inset: auto 0 0 42%;
height: 120px;
background: linear-gradient(to top, var(--gw-green) 0%, transparent 100%);
inset: 0;
height: auto;
background: linear-gradient(
to bottom,
transparent 25%,
rgba(33, 48, 33, 0.4) 52%,
rgba(33, 48, 33, 0.88) 70%,
#213021 82%
);
pointer-events: none;
z-index: 1;
}
.hero-inner {
align-items: flex-end;
position: relative;
z-index: 2;
flex: none;
display: block;
width: 100%;
max-width: 700px;
margin: 0 auto;
padding: 0 50px 32px;
gap: 0;
}
.hero-text,
.promise-text {
flex: 1;
}
.hero-text {
padding-bottom: 44px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-bottom: 0;
}
.hero-kicker {
margin: 0 0 14px;
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
}
.hero-subtitle {
margin: -8px 0 26px;
max-width: 460px;
max-width: 480px;
color: rgba(255, 255, 255, 0.86);
font-size: 18px;
line-height: 1.55;
text-align: center;
}
.hero-floating-pill {
display: flex;
position: absolute;
top: 28px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
white-space: nowrap;
padding: 10px 20px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.28);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: #fff;
font-family: var(--font-head);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
line-height: 1;
}
.hero-chips {
display: none;
}
.hero-trust-chip {
@@ -109,25 +165,39 @@
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
}
:global(.hero-cta-arrow) {
font-size: 11px;
margin-left: 4px;
}
.hero-img {
flex: 0 0 46%;
display: flex;
justify-content: flex-end;
align-self: flex-end;
margin-top: 0;
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
flex: none;
display: block;
margin: 0;
}
.hero-img picture {
display: block;
width: 100%;
height: 100%;
}
.hero-img img {
width: min(100%, 530px);
margin: -12px -18px -72px 0;
}
@media (max-width: 1100px) {
.hero-img img {
margin-bottom: -32px;
}
width: 60%;
height: 100%;
max-width: none;
margin: 0;
object-fit: cover;
object-position: center -10%;
transform: none;
}
#intro {
@@ -897,3 +967,192 @@ footer {
.footer-legal a:hover {
opacity: 1;
}
.footer-back-top {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.5;
flex: 0 0 auto;
transition: opacity 0.2s;
}
.footer-back-top:hover {
opacity: 1;
}
/* ── PageHeader shared component ──────────────────────────────────────── */
.page-header {
padding: 72px 0 80px;
}
.ph-inner {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 50px;
text-align: center;
}
/* Green variant */
.page-header--green {
background: var(--gw-green);
color: #fff;
}
.page-header--green .eyebrow {
color: rgba(255, 255, 255, 0.55);
}
/* White variant */
.page-header--white {
background: var(--off-white);
color: var(--text);
}
/* 2-col layout when media slot is used (service pages) */
.ph-inner--grid {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
gap: 52px;
align-items: center;
text-align: left;
}
/* Title */
.ph-title {
margin: 0 0 16px;
font-family: var(--font-head);
font-size: clamp(34px, 4vw, 56px);
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.04em;
}
.page-header--green .ph-title {
color: #fff;
}
.page-header--white .ph-title {
color: #000;
}
/* Subtitle */
.ph-subtitle {
font-size: 17px;
line-height: 1.6;
max-width: 520px;
margin: 0 auto;
}
.page-header--green .ph-subtitle {
color: rgba(255, 255, 255, 0.8);
}
.page-header--white .ph-subtitle {
color: var(--gray);
}
.ph-inner--grid .ph-subtitle {
margin: 0;
}
/* Media column */
.ph-media {
aspect-ratio: 4 / 3;
border-radius: 28px;
overflow: hidden;
background: #f4efe7;
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
}
.ph-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
/* Shared chip styles — used via default slot content */
.ph-chips {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
margin-top: 28px;
}
.ph-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 18px;
border-radius: 999px;
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.01em;
}
.page-header--green .ph-chip {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.14);
color: #fff;
}
.page-header--white .ph-chip {
background: rgba(33, 48, 33, 0.07);
border: 1px solid rgba(33, 48, 33, 0.1);
color: var(--gw-green);
}
.ph-chip--link {
text-decoration: none;
transition: background 0.18s ease;
}
.page-header--green .ph-chip--link:hover {
background: rgba(255, 255, 255, 0.18);
}
.page-header--white .ph-chip--link:hover {
background: rgba(33, 48, 33, 0.12);
}
/* Responsive */
@media (max-width: 1024px) {
.ph-inner--grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
}
}
@media (max-width: 768px) {
.page-header {
padding: 48px 0 56px;
}
.ph-inner {
padding: 0 24px;
}
.ph-inner--grid {
grid-template-columns: 1fr;
gap: 24px;
}
.ph-title {
font-size: clamp(28px, 7vw, 38px);
}
.ph-chips {
gap: 8px;
}
.ph-chip {
font-size: 13px;
padding: 8px 14px;
}
}
+35 -4
View File
@@ -2,6 +2,34 @@
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
border-radius: 4px;
}
@media (prefers-reduced-motion: no-preference) {
.logo::after {
content: '';
position: absolute;
top: 0;
left: -80%;
width: 40%;
height: 100%;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.55) 50%,
rgba(255, 255, 255, 0) 100%
);
transform: skewX(-15deg);
animation: logo-shine 0.7s cubic-bezier(0.22, 1, 0.36, 1) 1s both;
pointer-events: none;
}
@keyframes logo-shine {
from { left: -80%; }
to { left: 140%; }
}
}
.logo img,
@@ -65,11 +93,14 @@
}
.hero-title-main,
.hero-title-connector,
.hero-title-highlight {
.hero-title-connector {
color: #fff;
}
.hero-title-highlight {
color: var(--yellow);
}
.promise-text h2 {
font-size: 42px;
margin-bottom: 20px;
@@ -79,8 +110,8 @@
.info-block p,
.faq details p,
.testimonial-card blockquote {
font-size: 15px;
line-height: 1.7;
font-size: 16px;
line-height: 1.72;
}
.service-card h3,
+21
View File
@@ -32,14 +32,23 @@ export interface NavigationContent {
megaMenuFooter?: LinkItem;
}
export interface HeroChip {
icon: string;
label: string;
}
export interface HeroContent {
title: string;
highlight: string;
mobileTitle?: string;
kicker?: string;
subtitle?: string;
subtitleChips?: HeroChip[];
floatingPill?: string;
primaryCta: CallToAction;
secondaryCta: CallToAction;
imageUrl: string;
desktopImageUrl?: string;
imageAlt: string;
}
@@ -117,6 +126,8 @@ export interface ServiceExtra {
export interface ServiceBenefit {
title: string;
body: string;
badge?: string;
icon?: string;
}
export interface ServiceHighlightImage {
@@ -124,13 +135,21 @@ export interface ServiceHighlightImage {
imageAlt: string;
}
export interface ServiceHighlightPoint {
title: string;
body: string;
}
export interface ServicePageContent {
hero: {
eyebrow: string;
title: string;
subtitle?: string;
paragraphs: string[];
imageUrl: string;
imageAlt: string;
chips?: HeroChip[];
cta?: CallToAction;
};
highlight?: {
eyebrow: string;
@@ -138,6 +157,7 @@ export interface ServicePageContent {
imageUrl: string;
imageAlt: string;
collageImages?: ServiceHighlightImage[];
points?: ServiceHighlightPoint[];
};
pricing: {
title: string;
@@ -148,6 +168,7 @@ export interface ServicePageContent {
};
benefits: {
title: string;
intro?: string;
items: ServiceBenefit[];
};
testimonialsHeading: string;
+35 -1
View File
@@ -10,6 +10,12 @@
import '@fontsource/readex-pro/latin-500.css';
import '@fontsource/readex-pro/latin-600.css';
import '@fontsource/readex-pro/latin-700.css';
import '@fontsource/roboto/latin-400.css';
import '@fontsource/roboto/latin-500.css';
import '@fontsource/roboto/latin-700.css';
import '@fontsource/noto-sans/latin-400.css';
import '@fontsource/noto-sans/latin-500.css';
import '@fontsource/noto-sans/latin-700.css';
import '@fontsource/unbounded/latin-400.css';
import '@fontsource/unbounded/latin-600.css';
import '@fontsource/unbounded/latin-700.css';
@@ -57,9 +63,30 @@
targets.forEach(el => revealObserver!.observe(el));
}
function bounceSection(hash: string) {
const id = hash.startsWith('#') ? hash.slice(1) : hash;
if (!id) return;
// Wait for smooth scroll to finish before playing the bounce
setTimeout(() => {
const el = document.getElementById(id);
if (!el) return;
el.classList.remove('anchor-bounce');
void el.offsetWidth; // force reflow so re-adding the class re-triggers
el.classList.add('anchor-bounce');
el.addEventListener('animationend', () => el.classList.remove('anchor-bounce'), { once: true });
}, 520);
}
onMount(() => {
initClickTracking();
requestAnimationFrame(initReveal);
// Same-page hash clicks aren't caught by afterNavigate
function onHashChange() {
bounceSection(window.location.hash);
}
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
});
function shouldShowSkeleton() {
@@ -83,7 +110,14 @@
$: showRouteSkeleton = shouldShowSkeleton();
afterNavigate(({ from, to }) => {
if (!from || !to || to.url.hash) {
if (!from || !to) return;
if (to.url.hash && from.url.pathname !== to.url.pathname) {
bounceSection(to.url.hash);
return;
}
if (to.url.hash) {
return;
}
-1
View File
@@ -85,7 +85,6 @@
reviewCount: '30'
},
review: data.content.testimonials.map((testimonial) => ({
'@context': 'https://schema.org',
'@type': 'Review',
reviewRating: {
'@type': 'Rating',
+6 -52
View File
@@ -5,6 +5,7 @@
import Header from '$lib/components/Header.svelte';
import BookingPage from '$lib/components/BookingPage.svelte';
import LegalPage from '$lib/components/LegalPage.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import PricingPage from '$lib/components/PricingPage.svelte';
import { aboutPageContent } from '$lib/content/about';
import ServiceLandingPage from '$lib/components/ServiceLandingPage.svelte';
@@ -81,11 +82,7 @@
name: 'Goodwalk Pack Walks',
description: data.page.description,
serviceType: 'Pack walks for small and medium dogs',
provider: {
'@type': 'LocalBusiness',
name: 'Goodwalk',
url: siteUrl
},
provider: { '@id': 'https://www.goodwalk.co.nz/#business' },
areaServed,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`,
@@ -107,11 +104,7 @@
name: 'Goodwalk 1:1 Dog Walks',
description: data.page.description,
serviceType: 'One-on-one dog walking',
provider: {
'@type': 'LocalBusiness',
name: 'Goodwalk',
url: siteUrl
},
provider: { '@id': 'https://www.goodwalk.co.nz/#business' },
areaServed,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`,
@@ -133,11 +126,7 @@
name: 'Goodwalk Puppy Visits',
description: data.page.description,
serviceType: 'In-home puppy visits',
provider: {
'@type': 'LocalBusiness',
name: 'Goodwalk',
url: siteUrl
},
provider: { '@id': 'https://www.goodwalk.co.nz/#business' },
areaServed,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`,
@@ -202,6 +191,7 @@
imageAlt={seoImageAlt}
structuredData={pageStructuredData}
preloadImage={preloadHeroImage}
noindex={data.page.noindex ?? false}
/>
<Header navigation={data.content.navigation} />
@@ -228,11 +218,7 @@
/>
{:else}
<main class="static-page">
<section class="static-page-hero">
<div class="static-page-inner">
<h1>{data.page.title}</h1>
</div>
</section>
<PageHeader variant="white" title={data.page.title} />
</main>
{/if}
@@ -243,36 +229,4 @@
min-height: 50vh;
background: var(--off-white);
}
.static-page-hero {
padding: 96px 0 120px;
}
.static-page-inner {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 50px;
}
h1 {
font-family: var(--font-head);
font-size: 56px;
line-height: 1.05;
letter-spacing: -0.04em;
color: #000;
}
@media (max-width: 768px) {
.static-page-hero {
padding: 56px 0 72px;
}
.static-page-inner {
padding: 0 24px;
}
h1 {
font-size: 34px;
}
}
</style>
+15
View File
@@ -4,6 +4,21 @@ export const GET: RequestHandler = () => {
const body = [
'User-agent: *',
'Allow: /',
'Disallow: /api/',
'Disallow: /contract',
'',
'# AI crawlers — explicitly permitted',
'User-agent: GPTBot',
'Allow: /',
'',
'User-agent: OAI-SearchBot',
'Allow: /',
'',
'User-agent: ClaudeBot',
'Allow: /',
'',
'User-agent: PerplexityBot',
'Allow: /',
'',
'Sitemap: https://www.goodwalk.co.nz/sitemap.xml'
].join('\n');
+14 -11
View File
@@ -7,24 +7,26 @@ interface SitemapRoute {
path: string;
priority: string;
changefreq: string;
lastmod: string;
}
const routes: SitemapRoute[] = [
{ path: '/', priority: '1.0', changefreq: 'weekly' },
{ path: '/pack-walks', priority: '0.9', changefreq: 'monthly' },
{ path: '/dog-walking', priority: '0.9', changefreq: 'monthly' },
{ path: '/puppy-visits', priority: '0.9', changefreq: 'monthly' },
{ path: '/our-pricing', priority: '0.8', changefreq: 'monthly' },
{ path: '/about', priority: '0.7', changefreq: 'monthly' },
{ path: '/contact-us', priority: '0.7', changefreq: 'monthly' },
{ path: '/terms-and-conditions', priority: '0.3', changefreq: 'yearly' },
{ path: '/privacy-policy', priority: '0.3', changefreq: 'yearly' }
{ path: '/', priority: '1.0', changefreq: 'weekly', lastmod: '2026-05-12' },
{ path: '/pack-walks', priority: '0.9', changefreq: 'monthly', lastmod: '2026-05-12' },
{ path: '/dog-walking', priority: '0.9', changefreq: 'monthly', lastmod: '2026-05-12' },
{ path: '/puppy-visits', priority: '0.9', changefreq: 'monthly', lastmod: '2026-05-12' },
{ path: '/our-pricing', priority: '0.8', changefreq: 'monthly', lastmod: '2026-05-12' },
{ path: '/about', priority: '0.7', changefreq: 'monthly', lastmod: '2026-05-12' },
{ path: '/contact-us', priority: '0.7', changefreq: 'monthly', lastmod: '2026-05-12' },
{ path: '/terms-and-conditions', priority: '0.3', changefreq: 'yearly', lastmod: '2026-05-12' },
{ path: '/privacy-policy', priority: '0.3', changefreq: 'yearly', lastmod: '2026-05-12' }
];
const locationRoutes: SitemapRoute[] = locationPages.map((loc) => ({
path: `/locations/${loc.slug}`,
priority: '0.8',
changefreq: 'monthly'
changefreq: 'monthly',
lastmod: '2026-05-12'
}));
export const GET: RequestHandler = () => {
@@ -33,8 +35,9 @@ export const GET: RequestHandler = () => {
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${allRoutes
.map(
({ path, priority, changefreq }) => ` <url>
({ path, priority, changefreq, lastmod }) => ` <url>
<loc>${siteUrl}${path}</loc>
<lastmod>${lastmod}</lastmod>
<changefreq>${changefreq}</changefreq>
<priority>${priority}</priority>
</url>`
+6 -11
View File
@@ -1,15 +1,8 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { describe, expect, it } from 'vitest';
import { GET } from './+server';
describe('sitemap endpoint', () => {
afterEach(() => {
vi.useRealTimers();
});
it('returns a sitemap covering the published routes', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-05-01T09:15:00Z'));
const response = await GET({} as never);
const body = await response.text();
@@ -17,8 +10,10 @@ describe('sitemap endpoint', () => {
expect(body).toContain('<loc>https://www.goodwalk.co.nz/</loc>');
expect(body).toContain('<loc>https://www.goodwalk.co.nz/contact-us</loc>');
expect(body).toContain('<loc>https://www.goodwalk.co.nz/privacy-policy</loc>');
expect(body).toContain('<lastmod>2026-05-01</lastmod>');
expect(body).not.toContain('/locations/');
expect(body.match(/<url>/g)).toHaveLength(9);
expect(body).toContain('<lastmod>2026-05-12</lastmod>');
expect(body).toContain('<loc>https://www.goodwalk.co.nz/locations/mt-eden</loc>');
expect(body).toContain('<loc>https://www.goodwalk.co.nz/locations/kingsland</loc>');
// 9 core pages + 17 location pages
expect(body.match(/<url>/g)).toHaveLength(26);
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

+44
View File
@@ -0,0 +1,44 @@
# Goodwalk — Auckland Dog Walking Service
## About
Goodwalk is Auckland Central's small dog walking specialist, run personally by Alessandra. Services include Tiny Gang pack walks, one-on-one dog walks, and in-home puppy visits across Auckland Central suburbs.
## Key facts
- Founder: Alessandra (all walks run personally)
- Location: Auckland Central, New Zealand
- Phone: (022) 642 1011
- Email: info@goodwalk.co.nz
- Hours: MondayFriday, 8am4pm
- 30+ five-star Google reviews
- Speciality: Small and medium dogs
## Services
- Pack Walks (Tiny Gang): https://www.goodwalk.co.nz/pack-walks
- 1:1 Dog Walks: https://www.goodwalk.co.nz/dog-walking
- Puppy Visits: https://www.goodwalk.co.nz/puppy-visits
- Pricing: https://www.goodwalk.co.nz/our-pricing
## Suburbs served
Mt Eden, Kingsland, Ponsonby, Grey Lynn, Sandringham, Mt Albert, Herne Bay, Morningside, Freemans Bay, Pt Chevalier, Avondale, Three Kings, Hillsborough, Eden Terrace, Balmoral, Arch Hill, Mt Roskill
## Location pages
- https://www.goodwalk.co.nz/locations/mt-eden
- https://www.goodwalk.co.nz/locations/kingsland
- https://www.goodwalk.co.nz/locations/ponsonby
- https://www.goodwalk.co.nz/locations/grey-lynn
- https://www.goodwalk.co.nz/locations/sandringham
- https://www.goodwalk.co.nz/locations/mt-albert
- https://www.goodwalk.co.nz/locations/herne-bay
- https://www.goodwalk.co.nz/locations/morningside
- https://www.goodwalk.co.nz/locations/freemans-bay
- https://www.goodwalk.co.nz/locations/pt-chevalier
- https://www.goodwalk.co.nz/locations/avondale
- https://www.goodwalk.co.nz/locations/three-kings
- https://www.goodwalk.co.nz/locations/hillsborough
- https://www.goodwalk.co.nz/locations/eden-terrace
- https://www.goodwalk.co.nz/locations/balmoral
- https://www.goodwalk.co.nz/locations/arch-hill
- https://www.goodwalk.co.nz/locations/mt-roskill
## Homepage
https://www.goodwalk.co.nz