4.0.1 - fixes
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
header {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
isolation: isolate;
|
||||
overflow: visible;
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
|
||||
@@ -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/'],
|
||||
|
||||
@@ -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)
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user