Onboarding / Deployment Scripts / Marketing updates

This commit is contained in:
2026-05-11 21:02:24 +12:00
parent a90dfb7c66
commit 955a563d14
110 changed files with 9803 additions and 937 deletions
+17 -1
View File
@@ -1,8 +1,24 @@
import { getHomepageContent } from '$lib/server/content';
import { isHomepageHowItWorksEnabled } from '$lib/server/feature-flags';
export async function load() {
const onboardingHosts = new Set(['onboarding.goodwalk.co.nz']);
export async function load({ url }) {
const hostname = url.hostname.toLowerCase();
const siteVariant =
onboardingHosts.has(hostname) || url.searchParams.get('preview') === 'onboarding'
? 'onboarding'
: 'marketing';
if (siteVariant === 'onboarding') {
return {
siteVariant,
isPreview: url.searchParams.get('preview') === 'onboarding'
};
}
return {
siteVariant,
content: await getHomepageContent(),
howItWorksEnabled: isHomepageHowItWorksEnabled()
};
+129 -119
View File
@@ -11,6 +11,7 @@
import ServicesSection from '$lib/components/ServicesSection.svelte';
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
import ValuesSection from '$lib/components/ValuesSection.svelte';
import OnboardingPage from '$lib/components/OnboardingPage.svelte';
import type { PageData } from './$types';
export let data: PageData;
@@ -25,127 +26,136 @@
return `${siteUrl}${value.startsWith('/') ? value : `/${value}`}`;
}
$: homepageStructuredData = [
{
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Goodwalk',
url: siteUrl,
inLanguage: 'en-NZ'
},
{
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: 'Goodwalk',
description:
'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: 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/'],
address: {
'@type': 'PostalAddress',
addressLocality: 'Auckland Central',
addressRegion: 'Auckland',
addressCountry: 'NZ'
},
areaServed: [
'Morningside',
'Kingsland',
'Ponsonby',
'Grey Lynn',
'Mt Albert',
'Mt Eden',
'Sandringham',
'Mt Roskill',
'Arch Hill',
'Freemans Bay',
'Herne Bay',
'Pt Chevalier',
'Avondale',
'Three Kings',
'Hillsborough',
'Eden Terrace',
'Balmoral'
],
openingHoursSpecification: [
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '08:00',
closes: '16:00'
}
],
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: 'Dog Walking Services',
itemListElement: data.content.services.map((service) => ({
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: service.title,
url: `${siteUrl}${service.href}`
$: homepageStructuredData =
data.siteVariant === 'marketing' && data.content
? [
{
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Goodwalk',
url: siteUrl,
inLanguage: 'en-NZ'
},
{
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: 'Goodwalk',
description:
'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: 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/'],
address: {
'@type': 'PostalAddress',
addressLocality: 'Auckland Central',
addressRegion: 'Auckland',
addressCountry: 'NZ'
},
areaServed: [
'Morningside',
'Kingsland',
'Ponsonby',
'Grey Lynn',
'Mt Albert',
'Mt Eden',
'Sandringham',
'Mt Roskill',
'Arch Hill',
'Freemans Bay',
'Herne Bay',
'Pt Chevalier',
'Avondale',
'Three Kings',
'Hillsborough',
'Eden Terrace',
'Balmoral'
],
openingHoursSpecification: [
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '08:00',
closes: '16:00'
}
],
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: 'Dog Walking Services',
itemListElement: data.content.services.map((service) => ({
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: service.title,
url: `${siteUrl}${service.href}`
}
}))
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '5.0',
bestRating: '5',
worstRating: '1',
reviewCount: String(data.content.testimonials.length)
},
review: data.content.testimonials.map((testimonial) => ({
'@context': 'https://schema.org',
'@type': 'Review',
reviewRating: {
'@type': 'Rating',
ratingValue: '5',
bestRating: '5'
},
author: {
'@type': 'Person',
name: testimonial.reviewer
},
reviewBody: testimonial.quote
}))
},
{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: data.content.info.faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer
}
}))
}
}))
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '5.0',
bestRating: '5',
worstRating: '1',
reviewCount: String(data.content.testimonials.length)
},
review: data.content.testimonials.map((testimonial) => ({
'@type': 'Review',
reviewRating: {
'@type': 'Rating',
ratingValue: '5',
bestRating: '5'
},
author: {
'@type': 'Person',
name: testimonial.reviewer
},
reviewBody: testimonial.quote
}))
},
{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: data.content.info.faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer
}
}))
}
];
]
: [];
</script>
<SeoHead
title={data.content.seo.title}
description={data.content.seo.description}
canonicalPath="/"
image={data.content.hero.imageUrl}
imageAlt={data.content.hero.imageAlt}
structuredData={homepageStructuredData}
preloadImage={true}
/>
{#if data.siteVariant === 'onboarding'}
<OnboardingPage preview={data.isPreview} />
{:else}
{@const content = data.content!}
<SeoHead
title={content.seo.title}
description={content.seo.description}
canonicalPath="/"
image={content.hero.imageUrl}
imageAlt={content.hero.imageAlt}
structuredData={homepageStructuredData}
preloadImage={true}
/>
<Header navigation={data.content.navigation} />
<HeroSection hero={data.content.hero} reviewCta={data.content.intro.reviewCta} />
<PromiseSection promise={data.content.promise} />
<ServicesSection services={data.content.services} />
{#if data.howItWorksEnabled}
<HowItWorksSection content={data.content.howItWorks} />
<Header navigation={content.navigation} />
<HeroSection hero={content.hero} reviewCta={content.intro.reviewCta} />
<PromiseSection promise={content.promise} />
<ServicesSection services={content.services} />
{#if data.howItWorksEnabled}
<HowItWorksSection content={content.howItWorks} />
{/if}
<TestimonialsSection testimonials={content.testimonials} seedKey="/" />
<ValuesSection values={content.values} />
<BookingSection booking={content.booking} />
<InfoSection info={content.info} />
<InstagramSection instagram={content.instagram} />
<Footer footer={content.footer} />
{/if}
<TestimonialsSection testimonials={data.content.testimonials} />
<ValuesSection values={data.content.values} />
<BookingSection booking={data.content.booking} />
<InfoSection info={data.content.info} />
<InstagramSection instagram={data.content.instagram} />
<Footer footer={data.content.footer} />
+41 -38
View File
@@ -14,6 +14,7 @@
import { privacyPolicyContent } from '$lib/content/privacy-policy';
import { puppyVisitsContent } from '$lib/content/puppy-visits';
import { termsAndConditionsContent } from '$lib/content/terms-and-conditions';
import { buildAreaServed, buildBreadcrumb, absoluteUrl } from '$lib/seo';
import type { PageData } from './$types';
export let data: PageData;
@@ -22,14 +23,6 @@
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 aggregateOfferSchema(plans: { price: string }[]) {
const numericPrices = plans
.map((plan) => Number(plan.price.replace(/[^0-9.]/g, '')))
@@ -52,26 +45,7 @@
};
}
function breadcrumbSchema(name: string, path: string) {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: siteUrl
},
{
'@type': 'ListItem',
position: 2,
name,
item: `${siteUrl}${path}`
}
]
};
}
const areaServed = buildAreaServed();
let seoImage = defaultSeoImage;
let seoImageAlt = defaultSeoImageAlt;
@@ -90,7 +64,10 @@
description: data.page.description,
url: `${siteUrl}${data.page.canonicalPath}`
},
breadcrumbSchema(data.page.title, data.page.canonicalPath)
buildBreadcrumb([
{ name: 'Home', url: siteUrl },
{ name: data.page.title, path: data.page.canonicalPath }
])
];
if (data.slug === 'pack-walks') {
@@ -109,12 +86,15 @@
name: 'Goodwalk',
url: siteUrl
},
areaServed: 'Auckland Central, New Zealand',
areaServed,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`,
offers: aggregateOfferSchema(packWalksContent.pricing.plans)
},
breadcrumbSchema('Pack Walks', data.page.canonicalPath)
buildBreadcrumb([
{ name: 'Home', url: siteUrl },
{ name: 'Pack Walks', path: data.page.canonicalPath }
])
];
} else if (data.slug === 'dog-walking') {
preloadHeroImage = true;
@@ -132,12 +112,15 @@
name: 'Goodwalk',
url: siteUrl
},
areaServed: 'Auckland Central, New Zealand',
areaServed,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`,
offers: aggregateOfferSchema(dogWalkingContent.pricing.plans)
},
breadcrumbSchema('1:1 Walks', data.page.canonicalPath)
buildBreadcrumb([
{ name: 'Home', url: siteUrl },
{ name: '1:1 Walks', path: data.page.canonicalPath }
])
];
} else if (data.slug === 'puppy-visits') {
preloadHeroImage = true;
@@ -155,12 +138,15 @@
name: 'Goodwalk',
url: siteUrl
},
areaServed: 'Auckland Central, New Zealand',
areaServed,
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`,
offers: aggregateOfferSchema(puppyVisitsContent.pricing.plans)
},
breadcrumbSchema('Puppy Visits', data.page.canonicalPath)
buildBreadcrumb([
{ name: 'Home', url: siteUrl },
{ name: 'Puppy Visits', path: data.page.canonicalPath }
])
];
} else if (data.slug === 'our-pricing') {
pageStructuredData = [
@@ -171,7 +157,10 @@
description: data.page.description,
url: `${siteUrl}${data.page.canonicalPath}`
},
breadcrumbSchema('Our Pricing', data.page.canonicalPath)
buildBreadcrumb([
{ name: 'Home', url: siteUrl },
{ name: 'Our Pricing', path: data.page.canonicalPath }
])
];
} else if (data.slug === 'about') {
seoImage = aboutPageContent.sections[0].imageUrl;
@@ -185,7 +174,21 @@
url: `${siteUrl}${data.page.canonicalPath}`,
image: absoluteUrl(seoImage)
},
breadcrumbSchema('About Us', data.page.canonicalPath)
...(aboutPageContent.faqs && aboutPageContent.faqs.length
? [{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: aboutPageContent.faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: { '@type': 'Answer', text: faq.answer }
}))
}]
: []),
buildBreadcrumb([
{ name: 'Home', url: siteUrl },
{ name: 'About Us', path: data.page.canonicalPath }
])
];
}
}
@@ -212,7 +215,7 @@
{:else if data.slug === 'our-pricing'}
<PricingPage content={data.content} pageContent={ourPricingContent} />
{:else if data.slug === 'about'}
<AboutPage content={data.content} pageContent={aboutPageContent} />
<AboutPage pageContent={aboutPageContent} />
{:else if data.slug === 'terms-and-conditions'}
<LegalPage pageContent={termsAndConditionsContent} />
{:else if data.slug === 'privacy-policy'}
+20 -11
View File
@@ -1,16 +1,22 @@
import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import { aboutPageContent } from '$lib/content/about';
import { dogWalkingContent } from '$lib/content/dog-walking';
import { ourPricingContent } from '$lib/content/our-pricing';
import { packWalksContent } from '$lib/content/pack-walks';
import { puppyVisitsContent } from '$lib/content/puppy-visits';
import { staticPages } from '$lib/content/static-pages';
import SlugPage from './+page.svelte';
import { createStaticRouteData } from '../../test/fixtures';
describe('static slug route page', () => {
it.each([
['pack-walks', 'Join our Tiny Gang!'],
['dog-walking', 'Walks for larger breeds, too!'],
['puppy-visits', 'Introducing Puppy Visits: Building strong foundations for our pack walks!'],
['our-pricing', 'Simple, transparent pricing — no lock-in contracts.'],
['about', 'Who we are'],
['contact-us', "Fill in the form below and we'll be in touch to arrange a free introduction."],
['pack-walks', packWalksContent.hero.title],
['dog-walking', dogWalkingContent.hero.title],
['puppy-visits', puppyVisitsContent.hero.title],
['our-pricing', ourPricingContent.subtitle],
['about', aboutPageContent.sections[0].title],
['contact-us', "Let's meet!"],
['terms-and-conditions', '1. Application of Terms'],
['privacy-policy', 'How we collect your information']
] as const)('renders the %s page branch', (slug, expectedText) => {
@@ -18,6 +24,11 @@ describe('static slug route page', () => {
data: createStaticRouteData(slug)
});
if (slug === 'contact-us') {
expect(screen.getByRole('heading', { name: expectedText })).toBeInTheDocument();
return;
}
expect(screen.getByText(expectedText)).toBeInTheDocument();
});
@@ -26,7 +37,7 @@ describe('static slug route page', () => {
data: createStaticRouteData('about')
});
expect(document.title).toBe('About Us | Dog Walkers | Goodwalk');
expect(document.title).toBe(staticPages.about.title);
expect(document.head.innerHTML).toContain('AboutPage');
});
@@ -37,10 +48,8 @@ describe('static slug route page', () => {
data: createStaticRouteData(slug)
});
expect(
screen.queryByText("Fill in the form below and we'll be in touch to arrange a free introduction.")
).not.toBeInTheDocument();
expect(screen.queryByText('Why people choose us!')).not.toBeInTheDocument();
expect(screen.queryByText("Let's meet!")).not.toBeInTheDocument();
expect(screen.queryByText('What our clients say')).not.toBeInTheDocument();
}
);
+18
View File
@@ -0,0 +1,18 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const onboardingHosts = new Set(['onboarding.goodwalk.co.nz']);
export const load: PageServerLoad = async ({ url }) => {
const hostname = url.hostname.toLowerCase();
const isOnboardingHost =
onboardingHosts.has(hostname) || url.searchParams.get('preview') === 'contract';
if (!isOnboardingHost) {
throw error(404, 'Not found');
}
return {
isPreview: url.searchParams.get('preview') === 'contract',
};
};
+8
View File
@@ -0,0 +1,8 @@
<script lang="ts">
import ContractPage from '$lib/components/ContractPage.svelte';
import type { PageData } from './$types';
export let data: PageData;
</script>
<ContractPage preview={data.isPreview} />
+10 -2
View File
@@ -16,12 +16,19 @@ vi.mock('$lib/server/feature-flags', () => ({
import { load } from './+page.server';
function createLoadEvent(url = 'https://www.goodwalk.co.nz/') {
return {
url: new URL(url)
} as Parameters<typeof load>[0];
}
describe('home page server load', () => {
it('returns homepage content', async () => {
getHomepageContent.mockResolvedValue(homepageContent);
isHomepageHowItWorksEnabled.mockReturnValue(false);
await expect(load()).resolves.toEqual({
await expect(load(createLoadEvent())).resolves.toEqual({
siteVariant: 'marketing',
content: homepageContent,
howItWorksEnabled: false
});
@@ -31,7 +38,8 @@ describe('home page server load', () => {
getHomepageContent.mockResolvedValue(homepageContent);
isHomepageHowItWorksEnabled.mockReturnValue(true);
await expect(load()).resolves.toEqual({
await expect(load(createLoadEvent())).resolves.toEqual({
siteVariant: 'marketing',
content: homepageContent,
howItWorksEnabled: true
});
+5 -4
View File
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import { homepageContent } from '$lib/content/homepage';
import HomePage from './+page.svelte';
import { createHomepageRouteData } from '../test/fixtures';
@@ -9,11 +10,11 @@ describe('home page route', () => {
data: createHomepageRouteData()
});
expect(screen.getAllByText("Your Dog's Day!").length).toBeGreaterThan(0);
expect(document.body.textContent).toContain('Happy pets,');
expect(screen.getByText('Locations & Hours')).toBeInTheDocument();
expect(screen.getByText(homepageContent.hero.highlight)).toBeInTheDocument();
expect(screen.getByText(homepageContent.howItWorks.title)).toBeInTheDocument();
expect(screen.getByText(homepageContent.info.title)).toBeInTheDocument();
expect(screen.queryByLabelText(/General enquiry/i)).not.toBeInTheDocument();
expect(document.title).toBe('Home | Auckland Dog Walking | Goodwalk');
expect(document.title).toBe(homepageContent.seo.title);
expect(document.head.innerHTML).toContain('FAQPage');
expect(document.head.innerHTML).toContain('https://www.goodwalk.co.nz/images/auckland-dog-walking-happy-dog-hero.png');
});
@@ -0,0 +1,17 @@
import { error } from '@sveltejs/kit';
import { locationsBySlug } from '$lib/content/locations';
import { getSharedPageContent } from '$lib/server/content';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const location = locationsBySlug[params.suburb];
if (!location) {
throw error(404, 'Page not found');
}
return {
content: await getSharedPageContent(),
location
};
}
@@ -0,0 +1,26 @@
<script lang="ts">
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
import SeoHead from '$lib/components/SeoHead.svelte';
import LocationPage from '$lib/components/LocationPage.svelte';
import { buildLocationSeo } from '$lib/seo';
import type { PageData } from './$types';
export let data: PageData;
$: location = data.location;
$: seo = buildLocationSeo(location);
</script>
<SeoHead
title={seo.title}
description={seo.description}
canonicalPath={seo.canonicalPath}
image={seo.image}
imageAlt={seo.imageAlt}
structuredData={seo.structuredData}
/>
<Header navigation={data.content.navigation} />
<LocationPage {location} testimonials={data.content.testimonials} />
<Footer footer={data.content.footer} />
+6
View File
@@ -1,4 +1,5 @@
import type { RequestHandler } from './$types';
import { locationPages } from '$lib/content/locations';
const siteUrl = 'https://www.goodwalk.co.nz';
@@ -16,6 +17,11 @@ const routes: SitemapRoute[] = [
{ path: '/our-pricing', priority: '0.8', changefreq: 'monthly' },
{ path: '/about', priority: '0.7', changefreq: 'monthly' },
{ path: '/contact-us', priority: '0.7', changefreq: 'monthly' },
...locationPages.map((loc) => ({
path: `/locations/${loc.slug}`,
priority: '0.8',
changefreq: 'monthly'
})),
{ path: '/terms-and-conditions', priority: '0.3', changefreq: 'yearly' },
{ path: '/privacy-policy', priority: '0.3', changefreq: 'yearly' }
];
+3 -1
View File
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { GET } from './+server';
import { locationPages } from '$lib/content/locations';
describe('sitemap endpoint', () => {
afterEach(() => {
@@ -18,6 +19,7 @@ describe('sitemap endpoint', () => {
expect(body).toContain('<loc>https://www.goodwalk.co.nz/contact-us</loc>');
expect(body).toContain('<loc>https://www.goodwalk.co.nz/privacy-policy</loc>');
expect(body).toContain('<lastmod>2026-05-01</lastmod>');
expect(body.match(/<url>/g)).toHaveLength(9);
expect(body).toContain('<loc>https://www.goodwalk.co.nz/locations/mt-eden</loc>');
expect(body.match(/<url>/g)).toHaveLength(9 + locationPages.length);
});
});