Design language
This commit is contained in:
@@ -10,6 +10,12 @@
|
||||
import '@fontsource/readex-pro/latin-500.css';
|
||||
import '@fontsource/readex-pro/latin-600.css';
|
||||
import '@fontsource/readex-pro/latin-700.css';
|
||||
import '@fontsource/roboto/latin-400.css';
|
||||
import '@fontsource/roboto/latin-500.css';
|
||||
import '@fontsource/roboto/latin-700.css';
|
||||
import '@fontsource/noto-sans/latin-400.css';
|
||||
import '@fontsource/noto-sans/latin-500.css';
|
||||
import '@fontsource/noto-sans/latin-700.css';
|
||||
import '@fontsource/unbounded/latin-400.css';
|
||||
import '@fontsource/unbounded/latin-600.css';
|
||||
import '@fontsource/unbounded/latin-700.css';
|
||||
@@ -57,9 +63,30 @@
|
||||
targets.forEach(el => revealObserver!.observe(el));
|
||||
}
|
||||
|
||||
function bounceSection(hash: string) {
|
||||
const id = hash.startsWith('#') ? hash.slice(1) : hash;
|
||||
if (!id) return;
|
||||
// Wait for smooth scroll to finish before playing the bounce
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.classList.remove('anchor-bounce');
|
||||
void el.offsetWidth; // force reflow so re-adding the class re-triggers
|
||||
el.classList.add('anchor-bounce');
|
||||
el.addEventListener('animationend', () => el.classList.remove('anchor-bounce'), { once: true });
|
||||
}, 520);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initClickTracking();
|
||||
requestAnimationFrame(initReveal);
|
||||
|
||||
// Same-page hash clicks aren't caught by afterNavigate
|
||||
function onHashChange() {
|
||||
bounceSection(window.location.hash);
|
||||
}
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
return () => window.removeEventListener('hashchange', onHashChange);
|
||||
});
|
||||
|
||||
function shouldShowSkeleton() {
|
||||
@@ -83,7 +110,14 @@
|
||||
$: showRouteSkeleton = shouldShowSkeleton();
|
||||
|
||||
afterNavigate(({ from, to }) => {
|
||||
if (!from || !to || to.url.hash) {
|
||||
if (!from || !to) return;
|
||||
|
||||
if (to.url.hash && from.url.pathname !== to.url.pathname) {
|
||||
bounceSection(to.url.hash);
|
||||
return;
|
||||
}
|
||||
|
||||
if (to.url.hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
reviewCount: '30'
|
||||
},
|
||||
review: data.content.testimonials.map((testimonial) => ({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Review',
|
||||
reviewRating: {
|
||||
'@type': 'Rating',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import BookingPage from '$lib/components/BookingPage.svelte';
|
||||
import LegalPage from '$lib/components/LegalPage.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import PricingPage from '$lib/components/PricingPage.svelte';
|
||||
import { aboutPageContent } from '$lib/content/about';
|
||||
import ServiceLandingPage from '$lib/components/ServiceLandingPage.svelte';
|
||||
@@ -81,11 +82,7 @@
|
||||
name: 'Goodwalk Pack Walks',
|
||||
description: data.page.description,
|
||||
serviceType: 'Pack walks for small and medium dogs',
|
||||
provider: {
|
||||
'@type': 'LocalBusiness',
|
||||
name: 'Goodwalk',
|
||||
url: siteUrl
|
||||
},
|
||||
provider: { '@id': 'https://www.goodwalk.co.nz/#business' },
|
||||
areaServed,
|
||||
image: absoluteUrl(seoImage),
|
||||
url: `${siteUrl}${data.page.canonicalPath}`,
|
||||
@@ -107,11 +104,7 @@
|
||||
name: 'Goodwalk 1:1 Dog Walks',
|
||||
description: data.page.description,
|
||||
serviceType: 'One-on-one dog walking',
|
||||
provider: {
|
||||
'@type': 'LocalBusiness',
|
||||
name: 'Goodwalk',
|
||||
url: siteUrl
|
||||
},
|
||||
provider: { '@id': 'https://www.goodwalk.co.nz/#business' },
|
||||
areaServed,
|
||||
image: absoluteUrl(seoImage),
|
||||
url: `${siteUrl}${data.page.canonicalPath}`,
|
||||
@@ -133,11 +126,7 @@
|
||||
name: 'Goodwalk Puppy Visits',
|
||||
description: data.page.description,
|
||||
serviceType: 'In-home puppy visits',
|
||||
provider: {
|
||||
'@type': 'LocalBusiness',
|
||||
name: 'Goodwalk',
|
||||
url: siteUrl
|
||||
},
|
||||
provider: { '@id': 'https://www.goodwalk.co.nz/#business' },
|
||||
areaServed,
|
||||
image: absoluteUrl(seoImage),
|
||||
url: `${siteUrl}${data.page.canonicalPath}`,
|
||||
@@ -202,6 +191,7 @@
|
||||
imageAlt={seoImageAlt}
|
||||
structuredData={pageStructuredData}
|
||||
preloadImage={preloadHeroImage}
|
||||
noindex={data.page.noindex ?? false}
|
||||
/>
|
||||
|
||||
<Header navigation={data.content.navigation} />
|
||||
@@ -228,11 +218,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<main class="static-page">
|
||||
<section class="static-page-hero">
|
||||
<div class="static-page-inner">
|
||||
<h1>{data.page.title}</h1>
|
||||
</div>
|
||||
</section>
|
||||
<PageHeader variant="white" title={data.page.title} />
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
@@ -243,36 +229,4 @@
|
||||
min-height: 50vh;
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.static-page-hero {
|
||||
padding: 96px 0 120px;
|
||||
}
|
||||
|
||||
.static-page-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-head);
|
||||
font-size: 56px;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.04em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.static-page-hero {
|
||||
padding: 56px 0 72px;
|
||||
}
|
||||
|
||||
.static-page-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,21 @@ export const GET: RequestHandler = () => {
|
||||
const body = [
|
||||
'User-agent: *',
|
||||
'Allow: /',
|
||||
'Disallow: /api/',
|
||||
'Disallow: /contract',
|
||||
'',
|
||||
'# AI crawlers — explicitly permitted',
|
||||
'User-agent: GPTBot',
|
||||
'Allow: /',
|
||||
'',
|
||||
'User-agent: OAI-SearchBot',
|
||||
'Allow: /',
|
||||
'',
|
||||
'User-agent: ClaudeBot',
|
||||
'Allow: /',
|
||||
'',
|
||||
'User-agent: PerplexityBot',
|
||||
'Allow: /',
|
||||
'',
|
||||
'Sitemap: https://www.goodwalk.co.nz/sitemap.xml'
|
||||
].join('\n');
|
||||
|
||||
@@ -7,24 +7,26 @@ interface SitemapRoute {
|
||||
path: string;
|
||||
priority: string;
|
||||
changefreq: string;
|
||||
lastmod: 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' }
|
||||
{ path: '/', priority: '1.0', changefreq: 'weekly', lastmod: '2026-05-12' },
|
||||
{ path: '/pack-walks', priority: '0.9', changefreq: 'monthly', lastmod: '2026-05-12' },
|
||||
{ path: '/dog-walking', priority: '0.9', changefreq: 'monthly', lastmod: '2026-05-12' },
|
||||
{ path: '/puppy-visits', priority: '0.9', changefreq: 'monthly', lastmod: '2026-05-12' },
|
||||
{ path: '/our-pricing', priority: '0.8', changefreq: 'monthly', lastmod: '2026-05-12' },
|
||||
{ path: '/about', priority: '0.7', changefreq: 'monthly', lastmod: '2026-05-12' },
|
||||
{ path: '/contact-us', priority: '0.7', changefreq: 'monthly', lastmod: '2026-05-12' },
|
||||
{ path: '/terms-and-conditions', priority: '0.3', changefreq: 'yearly', lastmod: '2026-05-12' },
|
||||
{ path: '/privacy-policy', priority: '0.3', changefreq: 'yearly', lastmod: '2026-05-12' }
|
||||
];
|
||||
|
||||
const locationRoutes: SitemapRoute[] = locationPages.map((loc) => ({
|
||||
path: `/locations/${loc.slug}`,
|
||||
priority: '0.8',
|
||||
changefreq: 'monthly'
|
||||
changefreq: 'monthly',
|
||||
lastmod: '2026-05-12'
|
||||
}));
|
||||
|
||||
export const GET: RequestHandler = () => {
|
||||
@@ -33,8 +35,9 @@ export const GET: RequestHandler = () => {
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${allRoutes
|
||||
.map(
|
||||
({ path, priority, changefreq }) => ` <url>
|
||||
({ path, priority, changefreq, lastmod }) => ` <url>
|
||||
<loc>${siteUrl}${path}</loc>
|
||||
<lastmod>${lastmod}</lastmod>
|
||||
<changefreq>${changefreq}</changefreq>
|
||||
<priority>${priority}</priority>
|
||||
</url>`
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { GET } from './+server';
|
||||
|
||||
describe('sitemap endpoint', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns a sitemap covering the published routes', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-01T09:15:00Z'));
|
||||
|
||||
const response = await GET({} as never);
|
||||
const body = await response.text();
|
||||
|
||||
@@ -17,8 +10,10 @@ describe('sitemap endpoint', () => {
|
||||
expect(body).toContain('<loc>https://www.goodwalk.co.nz/</loc>');
|
||||
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).not.toContain('/locations/');
|
||||
expect(body.match(/<url>/g)).toHaveLength(9);
|
||||
expect(body).toContain('<lastmod>2026-05-12</lastmod>');
|
||||
expect(body).toContain('<loc>https://www.goodwalk.co.nz/locations/mt-eden</loc>');
|
||||
expect(body).toContain('<loc>https://www.goodwalk.co.nz/locations/kingsland</loc>');
|
||||
// 9 core pages + 17 location pages
|
||||
expect(body.match(/<url>/g)).toHaveLength(26);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user