- Add "Explore our other services" block to service landing pages

with colour-tinted cards + Our Pricing entry (grey "All services" pill)
- Wrap homepage Instagram CTA in <aside aria-label="...">
- Always emit og:image:width/height with 1200x630 fallback when
  image metadata is unknown
- Add aria-current="page" to active desktop, mega-menu, and mobile
  nav links (exact-path match only — not "Services" parent)
- Richer testimonial alt text derived from dog name in the detail
  field (e.g. "Archie, a happy Goodwalk dog walking client...")
- Tier sitemap.xml priorities: home 1.0, services 0.9, pricing 0.8,
  about/contact 0.7, legal 0.3 (yearly changefreq)
- Bump to 4.1.0
This commit is contained in:
2026-05-05 08:12:36 +12:00
parent 04bca98ef8
commit 65bdc8dc20
10 changed files with 265 additions and 27 deletions
+1
View File
@@ -0,0 +1 @@
{"sessionId":"00a1ac5c-2553-49fc-9cf5-c6ecf5a89a7c","pid":13584,"acquiredAt":1777925044532}
+9
View File
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(kill %1)",
"Bash(pkill -f \"vite dev\")",
"Bash(npm run *)"
]
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "goodwalk-svelte-port",
"version": "4.0.2",
"version": "4.1.0",
"private": true,
"type": "module",
"scripts": {
+14
View File
@@ -56,6 +56,17 @@
return normalizePath($page.url.pathname) === normalizePath(href);
}
function isExactPageMatch(href: string) {
if (!href || href.startsWith('http') || href.startsWith('#')) {
return false;
}
return normalizePath($page.url.pathname) === normalizePath(href);
}
function ariaCurrent(href: string): 'page' | undefined {
return isExactPageMatch(href) ? 'page' : undefined;
}
function handleViewportChange() {
if (window.innerWidth > 768) {
mobileMenuOpen = false;
@@ -79,6 +90,7 @@
href={link.href}
target={linkTarget(link.external)}
rel={linkRel(link.external)}
aria-current={ariaCurrent(link.href)}
class:nav-link-active={isActiveLink(link.href, i === 0 && Boolean(navigation.megaMenuServices?.length))}
>
{link.label}
@@ -96,6 +108,7 @@
href={service.href}
target={linkTarget(service.href.startsWith('http'))}
rel={linkRel(service.href.startsWith('http'))}
aria-current={ariaCurrent(service.href)}
class="mega-service"
>
<div class="mega-icon">
@@ -183,6 +196,7 @@
href={link.href}
target={linkTarget(link.external)}
rel={linkRel(link.external)}
aria-current={ariaCurrent(link.href)}
class:mobile-link-active={isActiveLink(link.href)}
on:click={closeMenu}
>
+2 -2
View File
@@ -7,7 +7,7 @@
const dogCutoutSrc = '/images/smiling-dogs-instagram-cta.png';
</script>
<section id="instagram">
<aside id="instagram" aria-label="Follow Goodwalk on Instagram">
<div class="instagram-stage">
<div class="instagram-panel">
<div class="instagram-copy">
@@ -25,7 +25,7 @@
<img class="instagram-dog" src={dogCutoutSrc} alt="" loading="lazy" decoding="async" />
</div>
</div>
</section>
</aside>
<style>
#instagram {
+2 -4
View File
@@ -65,10 +65,8 @@
<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 property="og:image:width" content={String(imageMeta?.width ?? 1200)} />
<meta property="og:image:height" content={String(imageMeta?.height ?? 630)} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@goodwalk.nz" />
+200 -3
View File
@@ -8,9 +8,30 @@
export let content: SiteSharedContent;
export let pageContent: ServicePageContent;
export let currentPath = '';
$: heroImage = getImageMetadata(pageContent.hero.imageUrl);
$: highlightImage = pageContent.highlight ? getImageMetadata(pageContent.highlight.imageUrl) : null;
$: relatedServices = content.services.filter((s) => s.href && s.href !== currentPath);
$: relatedCards = [
...relatedServices.map((s) => ({
icon: s.icon,
title: s.title,
body: s.body,
href: s.href as string,
priceFrom: s.priceFrom,
pill: undefined as string | undefined
})),
{
icon: 'fas fa-tags',
title: 'Our Pricing',
body: 'Compare every service and add-on in one place.',
href: '/our-pricing',
priceFrom: undefined,
pill: 'All services'
}
];
</script>
<main class="service-page">
@@ -131,6 +152,39 @@
</div>
</section>
{#if relatedServices.length}
<section use:reveal class="service-related reveal-block" aria-label="Other services">
<div class="service-inner">
<div class="service-section-heading">
<h2>Explore our other services</h2>
</div>
<div class="service-related-grid">
{#each relatedCards as card, i}
<a class="service-related-card service-related-tint-{i % 4}" href={card.href}>
<div class="service-related-icon" aria-hidden="true">
<Icon name={card.icon} />
</div>
<h3>{card.title}</h3>
{#if card.body}
<p>{card.body}</p>
{/if}
<div class="service-related-meta">
{#if card.priceFrom}
<span class="service-related-price">{card.priceFrom}</span>
{/if}
{#if card.pill}
<span class="service-related-pill">{card.pill}</span>
{/if}
</div>
<span class="service-related-link" aria-hidden="true">Learn more →</span>
</a>
{/each}
</div>
</div>
</section>
{/if}
<TestimonialsSection heading={pageContent.testimonialsHeading} testimonials={content.testimonials} />
<BookingSection booking={pageContent.booking} />
</main>
@@ -150,6 +204,146 @@
padding: 72px 0 96px;
}
.service-related {
padding: 0 0 96px;
}
.service-related-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
}
.service-related-card {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
padding: 28px 26px;
border-radius: 28px;
background: #fff;
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
color: #000;
text-decoration: none;
overflow: hidden;
transition:
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease;
}
.service-related-card::before {
content: '';
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: var(--card-accent, var(--yellow));
}
.service-related-tint-0 {
--card-accent: var(--yellow);
}
.service-related-tint-0 .service-related-icon {
background: #fff3c6;
color: #5a4500;
}
.service-related-tint-1 {
--card-accent: var(--green);
}
.service-related-tint-1 .service-related-icon {
background: #dce6dc;
color: var(--green);
}
.service-related-tint-2 {
--card-accent: #c98a3f;
}
.service-related-tint-2 .service-related-icon {
background: #efe4d1;
color: var(--green);
}
.service-related-tint-3 {
--card-accent: #9ca3af;
}
.service-related-tint-3 .service-related-icon {
background: #f3f4f6;
color: #4b5563;
}
@media (hover: hover) {
.service-related-card:hover {
transform: translateY(-6px) scale(1.012);
box-shadow: 0 20px 40px rgba(17, 20, 24, 0.09);
}
}
.service-related-card:active {
transform: translateY(-1px) scale(0.992);
}
.service-related-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: #efe4d1;
color: var(--green);
font-size: 18px;
margin-bottom: 8px;
}
.service-related-card h3 {
margin: 0;
font-family: var(--font-head);
font-size: 22px;
line-height: 1.2;
}
.service-related-card p {
margin: 0;
color: #34363a;
font-size: 15px;
line-height: 1.55;
}
.service-related-meta {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.service-related-price {
font-family: var(--font-head);
font-weight: 700;
color: var(--green);
font-size: 14px;
}
.service-related-pill {
display: inline-block;
padding: 4px 12px;
border-radius: 999px;
background: #f3f4f6;
color: #4b5563;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
}
.service-related-link {
margin-top: auto;
padding-top: 6px;
color: var(--green);
font-family: var(--font-head);
font-weight: 700;
font-size: 14px;
}
.service-hero-grid {
display: grid;
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
@@ -450,7 +644,8 @@
.service-hero-grid,
.service-plan-grid,
.service-plan-grid-three,
.service-benefit-grid {
.service-benefit-grid,
.service-related-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -471,7 +666,8 @@
.service-hero-grid,
.service-plan-grid,
.service-plan-grid-three,
.service-benefit-grid {
.service-benefit-grid,
.service-related-grid {
grid-template-columns: 1fr;
gap: 24px;
}
@@ -482,7 +678,8 @@
.service-highlight,
.service-pricing,
.service-benefits {
.service-benefits,
.service-related {
padding-bottom: 72px;
}
+13 -1
View File
@@ -59,6 +59,18 @@
activeIndex = 0;
}
function dogNameFromDetail(detail: string) {
const match = detail.match(/^([^']+)/);
return match ? match[1].trim() : '';
}
function testimonialAlt(testimonial: TestimonialSlide) {
const dog = dogNameFromDetail(testimonial.detail);
return dog
? `${dog}, a happy Goodwalk dog walking client in Auckland`
: `${testimonial.reviewer}'s dog after a Goodwalk Auckland dog walk`;
}
function showPrevious() {
if (!slides.length) {
return;
@@ -158,7 +170,7 @@
<img
class="testimonial-photo"
src={testimonial.imageUrl}
alt={`${testimonial.reviewer}'s dog`}
alt={testimonialAlt(testimonial)}
width={imageMeta?.width}
height={imageMeta?.height}
loading="lazy"
+3 -3
View File
@@ -179,11 +179,11 @@
<Header navigation={data.content.navigation} />
{#if data.slug === 'pack-walks'}
<ServiceLandingPage content={data.content} pageContent={packWalksContent} />
<ServiceLandingPage content={data.content} pageContent={packWalksContent} currentPath={data.page.canonicalPath} />
{:else if data.slug === 'dog-walking'}
<ServiceLandingPage content={data.content} pageContent={dogWalkingContent} />
<ServiceLandingPage content={data.content} pageContent={dogWalkingContent} currentPath={data.page.canonicalPath} />
{:else if data.slug === 'puppy-visits'}
<ServiceLandingPage content={data.content} pageContent={puppyVisitsContent} />
<ServiceLandingPage content={data.content} pageContent={puppyVisitsContent} currentPath={data.page.canonicalPath} />
{:else if data.slug === 'our-pricing'}
<PricingPage content={data.content} pageContent={ourPricingContent} />
{:else if data.slug === 'about' || data.slug === 'about-us'}
+20 -13
View File
@@ -1,16 +1,23 @@
import type { RequestHandler } from './$types';
const siteUrl = 'https://www.goodwalk.co.nz';
const routes = [
'/',
'/pack-walks',
'/dog-walking',
'/puppy-visits',
'/our-pricing',
'/about',
'/contact-us',
'/terms-and-conditions',
'/privacy-policy'
interface SitemapRoute {
path: string;
priority: string;
changefreq: 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' }
];
export const GET: RequestHandler = () => {
@@ -19,11 +26,11 @@ export const GET: RequestHandler = () => {
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${routes
.map(
(path) => ` <url>
({ path, priority, changefreq }) => ` <url>
<loc>${siteUrl}${path}</loc>
<lastmod>${lastmod}</lastmod>
<changefreq>${path === '/' ? 'weekly' : 'monthly'}</changefreq>
<priority>${path === '/' ? '1.0' : '0.8'}</priority>
<changefreq>${changefreq}</changefreq>
<priority>${priority}</priority>
</url>`
)
.join('\n')}