4.0.1 - fixes

This commit is contained in:
2026-05-02 19:44:45 +12:00
parent b0bb692972
commit 07c754da12
34 changed files with 497 additions and 233 deletions
+24
View File
@@ -26,10 +26,34 @@
rel="stylesheet"
/>
</noscript>
<link
rel="preconnect"
href="https://cdnjs.cloudflare.com"
crossorigin="anonymous"
/>
<link
rel="preload"
as="style"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
media="print"
onload="this.media='all'"
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<noscript>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</noscript>
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-K7TLSFJVP1"></script>
<script>
+9 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
import { reveal } from '$lib/actions/reveal';
import { getImageMetadata } from '$lib/image-metadata';
import type { AboutPageContent, SiteSharedContent } from '$lib/types';
export let content: SiteSharedContent;
@@ -29,7 +30,14 @@
</div>
<div class="about-media">
<img src={section.imageUrl} alt={section.imageAlt} loading="lazy" />
<img
src={section.imageUrl}
alt={section.imageAlt}
width={getImageMetadata(section.imageUrl)?.width}
height={getImageMetadata(section.imageUrl)?.height}
loading="lazy"
decoding="async"
/>
</div>
</div>
</section>
+4 -1
View File
@@ -18,7 +18,10 @@
src="/images/goodwalk-auckland-dog-walking-logo.png"
alt="Goodwalk Auckland dog walking service logo"
class="footer-logo"
height="28"
width="241"
height="48"
loading="lazy"
decoding="async"
/>
<p>{footer.brandText}</p>
<div class="social-links">
+7 -1
View File
@@ -119,7 +119,13 @@
media="(max-width: 768px)"
srcset="/images/goodwalk-auckland-dog-walking-logo-mobile.png"
/>
<img src="/images/goodwalk-auckland-dog-walking-logo.png" alt="Goodwalk Auckland dog walking service logo" height="21" />
<img
src="/images/goodwalk-auckland-dog-walking-logo.png"
alt="Goodwalk Auckland dog walking service logo"
width="241"
height="48"
decoding="async"
/>
</picture>
</a>
+4
View File
@@ -1,10 +1,12 @@
<script lang="ts">
import { getImageMetadata } from '$lib/image-metadata';
import type { HeroContent } from '$lib/types';
export let hero: HeroContent;
$: titleParts = splitTitle(hero.title);
$: mobileTitle = hero.mobileTitle?.trim() || `${hero.title} ${hero.highlight}`.trim();
$: heroImage = getImageMetadata(hero.imageUrl);
function splitTitle(title: string) {
const trimmed = title.trim();
@@ -48,6 +50,8 @@
<img
src={hero.imageUrl}
alt={hero.imageAlt}
width={heroImage?.width}
height={heroImage?.height}
loading="eager"
fetchpriority="high"
decoding="async"
+11 -1
View File
@@ -1,7 +1,10 @@
<script lang="ts">
import { getImageMetadata } from '$lib/image-metadata';
import type { PromiseContent } from '$lib/types';
export let promise: PromiseContent;
$: promiseImage = getImageMetadata(promise.imageUrl);
</script>
<section id="promise">
@@ -21,7 +24,14 @@
</div>
<div class="promise-img">
<img src={promise.imageUrl} alt={promise.imageAlt} />
<img
src={promise.imageUrl}
alt={promise.imageAlt}
width={promiseImage?.width}
height={promiseImage?.height}
loading="lazy"
decoding="async"
/>
</div>
</div>
</section>
+8
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { getImageMetadata } from '$lib/image-metadata';
export let title: string;
export let description: string;
export let canonicalPath: string;
@@ -31,6 +33,7 @@
$: pageTitle = fullTitle(title);
$: canonicalUrl = absoluteUrl(canonicalPath);
$: imageUrl = absoluteUrl(image);
$: imageMeta = getImageMetadata(image);
</script>
<svelte:head>
@@ -62,8 +65,13 @@
<meta property="og:image" content={imageUrl} />
<meta property="og:image:secure_url" content={imageUrl} />
<meta property="og:image:alt" content={imageAlt} />
{#if imageMeta}
<meta property="og:image:width" content={String(imageMeta.width)} />
<meta property="og:image:height" content={String(imageMeta.height)} />
{/if}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@goodwalk.nz" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
@@ -3,10 +3,14 @@
import { reveal } from '$lib/actions/reveal';
import BookingSection from '$lib/components/BookingSection.svelte';
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
import { getImageMetadata } from '$lib/image-metadata';
import type { ServicePageContent, SiteSharedContent } from '$lib/types';
export let content: SiteSharedContent;
export let pageContent: ServicePageContent;
$: heroImage = getImageMetadata(pageContent.hero.imageUrl);
$: highlightImage = pageContent.highlight ? getImageMetadata(pageContent.highlight.imageUrl) : null;
</script>
<main class="service-page">
@@ -25,6 +29,8 @@
<img
src={pageContent.hero.imageUrl}
alt={pageContent.hero.imageAlt}
width={heroImage?.width}
height={heroImage?.height}
loading="eager"
fetchpriority="high"
decoding="async"
@@ -45,7 +51,10 @@
<img
src={pageContent.highlight.imageUrl}
alt={pageContent.highlight.imageAlt}
width={highlightImage?.width}
height={highlightImage?.height}
loading="lazy"
decoding="async"
/>
</div>
</div>
@@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { reveal } from '$lib/actions/reveal';
import Icon from '$lib/components/Icon.svelte';
import { getImageMetadata } from '$lib/image-metadata';
import type { TestimonialContent } from '$lib/types';
export let testimonials: TestimonialContent[];
@@ -123,11 +124,14 @@
<div class="testimonial-photo-wrap">
<div class="testimonial-photo-frame">
{#if index === activeIndex}
{@const imageMeta = getImageMetadata(testimonial.imageUrl)}
<img
class="testimonial-photo"
src={testimonial.imageUrl}
alt={`${testimonial.reviewer}'s dog`}
loading={activeIndex === 0 ? 'eager' : 'lazy'}
width={imageMeta?.width}
height={imageMeta?.height}
loading="lazy"
decoding="async"
/>
{/if}
+85 -10
View File
@@ -1,21 +1,48 @@
import { fireEvent, render } from '@testing-library/svelte';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { afterEach, describe, expect, it, vi } from 'vitest';
import TestimonialsSection from './TestimonialsSection.svelte';
import { homepageContent } from '$lib/content/homepage';
import type { TestimonialContent } from '$lib/types';
const expectedMappedSlides = [
{ reviewer: 'Kate', src: '/images/archie-auckland-dog-walking-review.png' },
{ reviewer: 'Estelle', src: '/images/monty-auckland-dog-walking-review.png' },
{ reviewer: 'Ross', src: '/images/otis-auckland-dog-walking-review.png' },
{ reviewer: 'Nina', src: '/images/wallace-auckland-dog-walking-review.png' }
];
function getActiveSlide(container: HTMLElement) {
return container.querySelector('.testimonial-slide-active') as HTMLElement;
}
function getActiveReviewer(container: HTMLElement) {
return getActiveSlide(container).querySelector('.testimonial-author-name')?.textContent;
}
function getActiveImage(container: HTMLElement) {
return getActiveSlide(container).querySelector('img') as HTMLImageElement;
}
describe('TestimonialsSection', () => {
afterEach(() => {
vi.useRealTimers();
});
it('uses the mapped local image assets for known testimonials', () => {
it('maps all known testimonial images to the local PNG assets', async () => {
const { container } = render(TestimonialsSection, {
testimonials: homepageContent.testimonials
});
const activeImage = container.querySelector('.testimonial-slide-active img') as HTMLImageElement;
const nextButton = screen.getByRole('button', { name: /next testimonial/i });
expect(activeImage.getAttribute('src')).toBe('/images/archie-auckland-dog-walking-review.jpg');
for (const [index, slide] of expectedMappedSlides.entries()) {
expect(getActiveReviewer(container)).toBe(slide.reviewer);
expect(getActiveImage(container).getAttribute('src')).toBe(slide.src);
if (index < expectedMappedSlides.length - 1) {
await fireEvent.click(nextButton);
}
}
});
it('moves to the next testimonial on arrow click and auto-rotation', async () => {
@@ -25,16 +52,64 @@ describe('TestimonialsSection', () => {
testimonials: homepageContent.testimonials
});
const nextButton = container.querySelector('.testimonial-arrow-right') as HTMLButtonElement;
const activeReviewer = () =>
(container.querySelector('.testimonial-slide-active h6 strong') as HTMLElement).textContent;
const nextButton = screen.getByRole('button', { name: /next testimonial/i });
expect(activeReviewer()).toBe('Kate');
expect(getActiveReviewer(container)).toBe('Kate');
await fireEvent.click(nextButton);
expect(activeReviewer()).toBe('Estelle');
expect(getActiveReviewer(container)).toBe('Estelle');
await vi.advanceTimersByTimeAsync(5000);
expect(activeReviewer()).toBe('Ross');
expect(getActiveReviewer(container)).toBe('Ross');
});
it('wraps to the last testimonial when navigating backwards from the first slide', async () => {
const { container } = render(TestimonialsSection, {
testimonials: homepageContent.testimonials
});
const previousButton = screen.getByRole('button', { name: /previous testimonial/i });
expect(getActiveReviewer(container)).toBe('Kate');
await fireEvent.click(previousButton);
expect(getActiveReviewer(container)).toBe('Nina');
expect(getActiveImage(container).getAttribute('src')).toBe(
'/images/wallace-auckland-dog-walking-review.png'
);
});
it('keeps custom testimonial images and filters out testimonials with no image', async () => {
const customTestimonials: TestimonialContent[] = [
...homepageContent.testimonials,
{
reviewer: 'Casey',
detail: "Poppy's mum",
quote: 'Thoughtful updates and a very happy dog after every walk.',
imageUrl: '/images/custom-casey-review.png'
},
{
reviewer: 'Jordan',
detail: "Scout's dad",
quote: 'Should be hidden because there is no image.'
}
];
const { container } = render(TestimonialsSection, {
testimonials: customTestimonials
});
const nextButton = screen.getByRole('button', { name: /next testimonial/i });
expect(container.querySelectorAll('.testimonial-slide')).toHaveLength(5);
for (let step = 0; step < 4; step += 1) {
await fireEvent.click(nextButton);
}
expect(getActiveReviewer(container)).toBe('Casey');
expect(getActiveImage(container).getAttribute('src')).toBe('/images/custom-casey-review.png');
expect(screen.queryByText('Jordan')).not.toBeInTheDocument();
});
});
+1 -1
View File
@@ -53,7 +53,7 @@ export const homepageContent: HomePageContent = {
'Offering tailored pack walks for small and medium dogs, and one-on-one walks for large breeds. Our walkers give personalised attention to each dog, easing stress, anxiety and ensuring a quality experience. Our expertise in small-medium breeds ensures tailored care for their unique needs. Join our',
emphasis: 'TINY GANG!',
cta: { label: 'Book now', href: '#newlead', variant: 'green' },
imageUrl: '/images/auckland-dog-walking-happy-dogs-happy-humans.png',
imageUrl: '/images/auckland-dog-walking-happy-dogs-happy-humans.webp',
imageAlt: 'Woman cuddling a dog for Goodwalk Auckland dog walking services'
},
services: [
+31
View File
@@ -0,0 +1,31 @@
export interface ImageMetadata {
width: number;
height: number;
}
const imageMetadata: Record<string, ImageMetadata> = {
'/images/goodwalk-auckland-dog-walking-logo.png': { width: 241, height: 48 },
'/images/goodwalk-auckland-dog-walking-logo-mobile.png': { width: 206, height: 41 },
'/images/auckland-dog-walking-happy-dog-hero.png': { width: 500, height: 500 },
'/images/auckland-dog-walking-happy-dogs-happy-humans.webp': { width: 1222, height: 1312 },
'/images/archie-auckland-dog-walking-review.png': { width: 1122, height: 1402 },
'/images/monty-auckland-dog-walking-review.png': { width: 1254, height: 1254 },
'/images/otis-auckland-dog-walking-review.png': { width: 1254, height: 1254 },
'/images/wallace-auckland-dog-walking-review.png': { width: 1254, height: 1254 },
'/images/auckland-small-dog-pack-walk.jpg': { width: 640, height: 480 },
'/images/tiny-gang-auckland-dog-pack.jpg': { width: 1024, height: 297 },
'/images/auckland-large-dog-one-on-one-walk.jpg': { width: 1024, height: 970 },
'/images/auckland-dogs-outdoor-pack.jpg': { width: 1024, height: 297 },
'/images/auckland-puppy-home-visit.jpg': { width: 640, height: 427 },
'/images/auckland-pack-walk-dog.jpg': { width: 480, height: 640 },
'/images/auckland-dog-group-outing.jpg': { width: 640, height: 480 },
'/images/goodwalk-dog-walker-alessandra.png': { width: 640, height: 640 }
};
export function getImageMetadata(src: string | undefined | null): ImageMetadata | null {
if (!src) {
return null;
}
return imageMetadata[src] ?? null;
}
+12
View File
@@ -79,4 +79,16 @@ describe('content server helpers', () => {
footer: homepageContent.footer
});
});
it('returns cloned shared page sections instead of original references', async () => {
vi.mocked(getPool).mockReturnValue(null);
const result = await getSharedPageContent();
expect(result.navigation).not.toBe(homepageContent.navigation);
expect(result.services).not.toBe(homepageContent.services);
expect(result.testimonials).not.toBe(homepageContent.testimonials);
expect(result.booking).not.toBe(homepageContent.booking);
expect(result.footer).not.toBe(homepageContent.footer);
});
});
+3
View File
@@ -1,5 +1,8 @@
header {
position: relative;
z-index: 100;
isolation: isolate;
overflow: visible;
background: var(--green);
}
+9 -1
View File
@@ -17,6 +17,14 @@
const siteUrl = 'https://www.goodwalk.co.nz';
function absoluteUrl(value: string) {
if (value.startsWith('http://') || value.startsWith('https://')) {
return value;
}
return `${siteUrl}${value.startsWith('/') ? value : `/${value}`}`;
}
$: homepageStructuredData = [
{
'@context': 'https://schema.org',
@@ -33,7 +41,7 @@
'Professional dog walking services across Auckland Central, including pack walks, 1:1 walks, and puppy visits.',
url: siteUrl,
logo: `${siteUrl}/images/goodwalk-auckland-dog-walking-logo.png`,
image: data.content.hero.imageUrl,
image: absoluteUrl(data.content.hero.imageUrl),
email: 'info@goodwalk.co.nz',
telephone: '+64-22-642-1011',
sameAs: ['https://www.instagram.com/goodwalk.nz/', 'https://g.page/r/CUsvrWPhkYrAEB0/'],
+12 -4
View File
@@ -22,6 +22,14 @@
const defaultSeoImage = '/images/auckland-dog-walking-happy-dog-hero.png';
const defaultSeoImageAlt = 'Goodwalk Auckland dog walking services';
function absoluteUrl(value: string) {
if (value.startsWith('http://') || value.startsWith('https://')) {
return value;
}
return `${siteUrl}${value.startsWith('/') ? value : `/${value}`}`;
}
function breadcrumbSchema(name: string, path: string) {
return {
'@context': 'https://schema.org',
@@ -80,7 +88,7 @@
url: siteUrl
},
areaServed: 'Auckland Central, New Zealand',
image: seoImage,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`
},
breadcrumbSchema('Pack Walks', data.page.canonicalPath)
@@ -102,7 +110,7 @@
url: siteUrl
},
areaServed: 'Auckland Central, New Zealand',
image: seoImage,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`
},
breadcrumbSchema('1:1 Walks', data.page.canonicalPath)
@@ -124,7 +132,7 @@
url: siteUrl
},
areaServed: 'Auckland Central, New Zealand',
image: seoImage,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`
},
breadcrumbSchema('Puppy Visits', data.page.canonicalPath)
@@ -150,7 +158,7 @@
name: data.page.title,
description: data.page.description,
url: `${siteUrl}${data.page.canonicalPath}`,
image: seoImage
image: absoluteUrl(seoImage)
},
breadcrumbSchema('About Us', data.page.canonicalPath)
];