- 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:
@@ -0,0 +1 @@
|
|||||||
|
{"sessionId":"00a1ac5c-2553-49fc-9cf5-c6ecf5a89a7c","pid":13584,"acquiredAt":1777925044532}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(kill %1)",
|
||||||
|
"Bash(pkill -f \"vite dev\")",
|
||||||
|
"Bash(npm run *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "goodwalk-svelte-port",
|
"name": "goodwalk-svelte-port",
|
||||||
"version": "4.0.2",
|
"version": "4.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -56,6 +56,17 @@
|
|||||||
return normalizePath($page.url.pathname) === normalizePath(href);
|
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() {
|
function handleViewportChange() {
|
||||||
if (window.innerWidth > 768) {
|
if (window.innerWidth > 768) {
|
||||||
mobileMenuOpen = false;
|
mobileMenuOpen = false;
|
||||||
@@ -79,6 +90,7 @@
|
|||||||
href={link.href}
|
href={link.href}
|
||||||
target={linkTarget(link.external)}
|
target={linkTarget(link.external)}
|
||||||
rel={linkRel(link.external)}
|
rel={linkRel(link.external)}
|
||||||
|
aria-current={ariaCurrent(link.href)}
|
||||||
class:nav-link-active={isActiveLink(link.href, i === 0 && Boolean(navigation.megaMenuServices?.length))}
|
class:nav-link-active={isActiveLink(link.href, i === 0 && Boolean(navigation.megaMenuServices?.length))}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
@@ -96,6 +108,7 @@
|
|||||||
href={service.href}
|
href={service.href}
|
||||||
target={linkTarget(service.href.startsWith('http'))}
|
target={linkTarget(service.href.startsWith('http'))}
|
||||||
rel={linkRel(service.href.startsWith('http'))}
|
rel={linkRel(service.href.startsWith('http'))}
|
||||||
|
aria-current={ariaCurrent(service.href)}
|
||||||
class="mega-service"
|
class="mega-service"
|
||||||
>
|
>
|
||||||
<div class="mega-icon">
|
<div class="mega-icon">
|
||||||
@@ -183,6 +196,7 @@
|
|||||||
href={link.href}
|
href={link.href}
|
||||||
target={linkTarget(link.external)}
|
target={linkTarget(link.external)}
|
||||||
rel={linkRel(link.external)}
|
rel={linkRel(link.external)}
|
||||||
|
aria-current={ariaCurrent(link.href)}
|
||||||
class:mobile-link-active={isActiveLink(link.href)}
|
class:mobile-link-active={isActiveLink(link.href)}
|
||||||
on:click={closeMenu}
|
on:click={closeMenu}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
const dogCutoutSrc = '/images/smiling-dogs-instagram-cta.png';
|
const dogCutoutSrc = '/images/smiling-dogs-instagram-cta.png';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section id="instagram">
|
<aside id="instagram" aria-label="Follow Goodwalk on Instagram">
|
||||||
<div class="instagram-stage">
|
<div class="instagram-stage">
|
||||||
<div class="instagram-panel">
|
<div class="instagram-panel">
|
||||||
<div class="instagram-copy">
|
<div class="instagram-copy">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<img class="instagram-dog" src={dogCutoutSrc} alt="" loading="lazy" decoding="async" />
|
<img class="instagram-dog" src={dogCutoutSrc} alt="" loading="lazy" decoding="async" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</aside>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#instagram {
|
#instagram {
|
||||||
|
|||||||
@@ -65,10 +65,8 @@
|
|||||||
<meta property="og:image" content={imageUrl} />
|
<meta property="og:image" content={imageUrl} />
|
||||||
<meta property="og:image:secure_url" content={imageUrl} />
|
<meta property="og:image:secure_url" content={imageUrl} />
|
||||||
<meta property="og:image:alt" content={imageAlt} />
|
<meta property="og:image:alt" content={imageAlt} />
|
||||||
{#if imageMeta}
|
<meta property="og:image:width" content={String(imageMeta?.width ?? 1200)} />
|
||||||
<meta property="og:image:width" content={String(imageMeta.width)} />
|
<meta property="og:image:height" content={String(imageMeta?.height ?? 630)} />
|
||||||
<meta property="og:image:height" content={String(imageMeta.height)} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:site" content="@goodwalk.nz" />
|
<meta name="twitter:site" content="@goodwalk.nz" />
|
||||||
|
|||||||
@@ -8,9 +8,30 @@
|
|||||||
|
|
||||||
export let content: SiteSharedContent;
|
export let content: SiteSharedContent;
|
||||||
export let pageContent: ServicePageContent;
|
export let pageContent: ServicePageContent;
|
||||||
|
export let currentPath = '';
|
||||||
|
|
||||||
$: heroImage = getImageMetadata(pageContent.hero.imageUrl);
|
$: heroImage = getImageMetadata(pageContent.hero.imageUrl);
|
||||||
$: highlightImage = pageContent.highlight ? getImageMetadata(pageContent.highlight.imageUrl) : null;
|
$: 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>
|
</script>
|
||||||
|
|
||||||
<main class="service-page">
|
<main class="service-page">
|
||||||
@@ -131,6 +152,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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} />
|
<TestimonialsSection heading={pageContent.testimonialsHeading} testimonials={content.testimonials} />
|
||||||
<BookingSection booking={pageContent.booking} />
|
<BookingSection booking={pageContent.booking} />
|
||||||
</main>
|
</main>
|
||||||
@@ -150,6 +204,146 @@
|
|||||||
padding: 72px 0 96px;
|
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 {
|
.service-hero-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
|
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
|
||||||
@@ -450,7 +644,8 @@
|
|||||||
.service-hero-grid,
|
.service-hero-grid,
|
||||||
.service-plan-grid,
|
.service-plan-grid,
|
||||||
.service-plan-grid-three,
|
.service-plan-grid-three,
|
||||||
.service-benefit-grid {
|
.service-benefit-grid,
|
||||||
|
.service-related-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +666,8 @@
|
|||||||
.service-hero-grid,
|
.service-hero-grid,
|
||||||
.service-plan-grid,
|
.service-plan-grid,
|
||||||
.service-plan-grid-three,
|
.service-plan-grid-three,
|
||||||
.service-benefit-grid {
|
.service-benefit-grid,
|
||||||
|
.service-related-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
@@ -482,7 +678,8 @@
|
|||||||
|
|
||||||
.service-highlight,
|
.service-highlight,
|
||||||
.service-pricing,
|
.service-pricing,
|
||||||
.service-benefits {
|
.service-benefits,
|
||||||
|
.service-related {
|
||||||
padding-bottom: 72px;
|
padding-bottom: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,18 @@
|
|||||||
activeIndex = 0;
|
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() {
|
function showPrevious() {
|
||||||
if (!slides.length) {
|
if (!slides.length) {
|
||||||
return;
|
return;
|
||||||
@@ -158,7 +170,7 @@
|
|||||||
<img
|
<img
|
||||||
class="testimonial-photo"
|
class="testimonial-photo"
|
||||||
src={testimonial.imageUrl}
|
src={testimonial.imageUrl}
|
||||||
alt={`${testimonial.reviewer}'s dog`}
|
alt={testimonialAlt(testimonial)}
|
||||||
width={imageMeta?.width}
|
width={imageMeta?.width}
|
||||||
height={imageMeta?.height}
|
height={imageMeta?.height}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -179,11 +179,11 @@
|
|||||||
<Header navigation={data.content.navigation} />
|
<Header navigation={data.content.navigation} />
|
||||||
|
|
||||||
{#if data.slug === 'pack-walks'}
|
{#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'}
|
{: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'}
|
{: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'}
|
{:else if data.slug === 'our-pricing'}
|
||||||
<PricingPage content={data.content} pageContent={ourPricingContent} />
|
<PricingPage content={data.content} pageContent={ourPricingContent} />
|
||||||
{:else if data.slug === 'about' || data.slug === 'about-us'}
|
{:else if data.slug === 'about' || data.slug === 'about-us'}
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
const siteUrl = 'https://www.goodwalk.co.nz';
|
const siteUrl = 'https://www.goodwalk.co.nz';
|
||||||
const routes = [
|
|
||||||
'/',
|
interface SitemapRoute {
|
||||||
'/pack-walks',
|
path: string;
|
||||||
'/dog-walking',
|
priority: string;
|
||||||
'/puppy-visits',
|
changefreq: string;
|
||||||
'/our-pricing',
|
}
|
||||||
'/about',
|
|
||||||
'/contact-us',
|
const routes: SitemapRoute[] = [
|
||||||
'/terms-and-conditions',
|
{ path: '/', priority: '1.0', changefreq: 'weekly' },
|
||||||
'/privacy-policy'
|
{ 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 = () => {
|
export const GET: RequestHandler = () => {
|
||||||
@@ -19,11 +26,11 @@ export const GET: RequestHandler = () => {
|
|||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
${routes
|
${routes
|
||||||
.map(
|
.map(
|
||||||
(path) => ` <url>
|
({ path, priority, changefreq }) => ` <url>
|
||||||
<loc>${siteUrl}${path}</loc>
|
<loc>${siteUrl}${path}</loc>
|
||||||
<lastmod>${lastmod}</lastmod>
|
<lastmod>${lastmod}</lastmod>
|
||||||
<changefreq>${path === '/' ? 'weekly' : 'monthly'}</changefreq>
|
<changefreq>${changefreq}</changefreq>
|
||||||
<priority>${path === '/' ? '1.0' : '0.8'}</priority>
|
<priority>${priority}</priority>
|
||||||
</url>`
|
</url>`
|
||||||
)
|
)
|
||||||
.join('\n')}
|
.join('\n')}
|
||||||
|
|||||||
Reference in New Issue
Block a user