- 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:
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user