1 Commits

Author SHA1 Message Date
admin c95d2e8c25 SEO: pricing-rich service schema, AggregateRating, tighter meta
- Service pages now include AggregateOffer in JSON-LD (priceCurrency
  NZD, lowPrice/highPrice/offerCount derived from each service's
  pricing.plans). Unlocks price-rich SERP results.
- Homepage LocalBusiness schema now includes AggregateRating and
  per-testimonial Review entries (5 stars, n reviews). Eligible for
  star ratings in SERPs.
- Puppy Visits meta description rewritten — was 241 chars opening
  with "Puppy Visits Introducing Puppy Visits..." Now a tight 144
  chars with Auckland keyword.
- Removed the dead /about-us static-pages entry; the 301 redirect
  in [slug]/+page.server.ts already routes it to /about, so the
  duplicate metadata was unreachable. Pruned matching dead branches
  in [slug]/+page.svelte and RouteSkeleton.svelte for clarity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 14:06:31 +12:00
10 changed files with 69 additions and 356 deletions
-28
View File
@@ -53,26 +53,6 @@ server {
# nginx does not keep stale upstream IPs in memory.
resolver 127.0.0.11 ipv6=off valid=30s;
# Maintenance mode: when /etc/nginx/conf.d/maintenance.flag exists,
# serve the static "be right back" page with a 503 status. The flag is
# toggled by the deploy script (touch / rm) without reloading nginx.
error_page 503 /maintenance.html;
location = /maintenance.html {
root /var/www/html;
internal;
add_header Cache-Control "no-store" always;
}
# Static assets used only by the maintenance page (logo, etc.). Served
# directly from the nginx html mount so they remain reachable while the
# SvelteKit app is down.
location /m/ {
root /var/www/html;
access_log off;
add_header Cache-Control "public, max-age=3600" always;
}
location ~* /\.(git|env|htaccess) {
deny all;
}
@@ -86,10 +66,6 @@ server {
}
location /api/submit {
if (-f /etc/nginx/conf.d/maintenance.flag) {
return 503;
}
set $goodwalk_mail_api goodwalk_svelte_mail_api:8000;
limit_req zone=goodwalk_limit burst=10 nodelay;
proxy_pass http://$goodwalk_mail_api/submit;
@@ -101,10 +77,6 @@ server {
}
location / {
if (-f /etc/nginx/conf.d/maintenance.flag) {
return 503;
}
set $goodwalk_frontend goodwalk_svelte_app:3000;
proxy_pass http://$goodwalk_frontend;
proxy_http_version 1.1;
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

-189
View File
@@ -1,189 +0,0 @@
<!doctype html>
<html lang="en-NZ">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" />
<title>Be right back | Goodwalk</title>
<style>
:root {
--green: #213021;
--green-soft: #2c3f2c;
--yellow: #ffd100;
--ink: #1a1a1a;
--muted: #4c5056;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
body {
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(255, 209, 0, 0.12), transparent 55%),
radial-gradient(circle at bottom right, rgba(255, 209, 0, 0.08), transparent 60%),
var(--green);
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px;
line-height: 1.5;
}
.stage {
width: 100%;
max-width: 560px;
}
.brand-bar {
display: flex;
justify-content: center;
margin-bottom: 28px;
}
.brand-bar img {
display: block;
height: 44px;
width: auto;
max-width: 100%;
filter: brightness(0) invert(1);
}
.card {
position: relative;
background: #fff;
border-radius: 28px;
padding: 44px 40px 38px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.25);
text-align: center;
overflow: hidden;
}
.card::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 6px;
background: var(--yellow);
}
.paw {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
margin: 4px auto 18px;
border-radius: 50%;
background: rgba(33, 48, 33, 0.08);
color: var(--green);
}
.paw svg {
width: 32px;
height: 32px;
}
.eyebrow {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--green);
margin: 0 0 10px;
}
h1 {
margin: 0 0 14px;
font-size: clamp(28px, 4vw, 38px);
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--ink);
}
p {
margin: 0 0 12px;
color: var(--muted);
font-size: 16px;
}
.meta {
margin-top: 24px;
padding-top: 22px;
border-top: 1px solid rgba(17, 20, 24, 0.08);
font-size: 14px;
color: var(--muted);
}
.meta a {
color: var(--green);
font-weight: 700;
text-decoration: none;
}
.meta a:hover {
text-decoration: underline;
}
.footnote {
margin-top: 22px;
text-align: center;
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
@media (max-width: 480px) {
.card {
padding: 36px 24px 28px;
border-radius: 22px;
}
.brand-bar img {
height: 36px;
}
}
</style>
</head>
<body>
<div class="stage">
<div class="brand-bar">
<img src="/m/logo.png" alt="Goodwalk" width="241" height="48" />
</div>
<main class="card" role="main">
<span class="paw" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 10.5c1.1 0 2-1.3 2-3s-.9-3-2-3-2 1.3-2 3 .9 3 2 3Zm9 0c1.1 0 2-1.3 2-3s-.9-3-2-3-2 1.3-2 3 .9 3 2 3ZM4 13.5c1 0 1.8-1.1 1.8-2.5S5 8.5 4 8.5s-1.8 1.1-1.8 2.5S3 13.5 4 13.5Zm16 0c1 0 1.8-1.1 1.8-2.5S21 8.5 20 8.5s-1.8 1.1-1.8 2.5.8 2.5 1.8 2.5ZM12 13.5c-3.3 0-6 2.5-6 5.4 0 1.4 1.1 2.6 2.5 2.6 1 0 1.5-.4 2.2-.8.4-.2.8-.4 1.3-.4s.9.2 1.3.4c.7.4 1.2.8 2.2.8 1.4 0 2.5-1.2 2.5-2.6 0-2.9-2.7-5.4-6-5.4Z"/>
</svg>
</span>
<p class="eyebrow">Goodwalk</p>
<h1>Be right back!</h1>
<p>We're updating the site — should only take a minute. Thanks for your patience.</p>
<div class="meta">
Need us now? Email <a href="mailto:info@goodwalk.co.nz">info@goodwalk.co.nz</a>
or call <a href="tel:+64226421011">(022) 642 1011</a>.
</div>
</main>
<p class="footnote">Auckland Central dog walking · Tiny Gang pack walks · 1:1 walks · Puppy visits</p>
</div>
<script>
// Auto-recheck once a minute so visitors don't sit on this page forever.
setTimeout(function () {
window.location.reload();
}, 60000);
</script>
</body>
</html>
+14 -109
View File
@@ -114,19 +114,8 @@ else
fi
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/goodwalk-deploy.XXXXXX")"
MAINTENANCE_ACTIVE=0
clear_maintenance_flag() {
if (( MAINTENANCE_ACTIVE )) && (( nginx_args_present )); then
echo "[deploy-remote] Clearing maintenance flag"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
exec -T nginx rm -f /etc/nginx/conf.d/maintenance.flag || true
MAINTENANCE_ACTIVE=0
fi
}
cleanup() {
clear_maintenance_flag
rm -rf "$STAGING_DIR"
}
@@ -193,70 +182,6 @@ if [[ ! -f "$DEPLOY_PATH/.env" ]]; then
fi
fi
merge_env_file() {
local template="$1"
local live="$2"
[[ -f "$template" ]] || { echo "[deploy-remote] No env template at $template, skipping merge"; return 0; }
[[ -f "$live" ]] || { echo "[deploy-remote] No live .env at $live, skipping merge"; return 0; }
local added diffs backup
added="$(mktemp)"
diffs="$(mktemp)"
backup="${live}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
awk -v live="$live" -v added_log="$added" -v diff_log="$diffs" '
function trim(s) { sub(/^[ \t]+/,"",s); sub(/[ \t]+$/,"",s); return s }
BEGIN {
while ((getline line < live) > 0) {
if (line ~ /^[ \t]*#/ || line ~ /^[ \t]*$/) continue
eq = index(line, "=")
if (eq == 0) continue
k = trim(substr(line, 1, eq-1))
v = substr(line, eq+1)
live_keys[k] = v
live_seen[k] = 1
}
close(live)
}
/^[ \t]*#/ || /^[ \t]*$/ { next }
{
eq = index($0, "=")
if (eq == 0) next
k = trim(substr($0, 1, eq-1))
v = substr($0, eq+1)
if (!(k in live_seen)) {
print k "=" v >> added_log
} else if (live_keys[k] != v) {
print k " (template=" v " | live=" live_keys[k] ")" >> diff_log
}
}
' "$template"
if [[ -s "$added" ]]; then
cp "$live" "$backup"
echo "[deploy-remote] Adding env keys present in template but missing from $live:"
sed 's/^/ + /' "$added"
{
printf '\n# Appended by deploy-remote.sh on %s from deploy.env.template\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
cat "$added"
} >> "$live"
echo "[deploy-remote] Backup of previous .env written to $backup"
else
echo "[deploy-remote] .env is up to date with template (no missing keys)"
fi
if [[ -s "$diffs" ]]; then
echo "[deploy-remote] NOTE: these keys exist in both files but values differ. Live values are PRESERVED:"
sed 's/^/ ! /' "$diffs"
echo "[deploy-remote] If a live value is stale (e.g. an old OWNER_EMAIL), edit $live and re-deploy."
fi
rm -f "$added" "$diffs"
}
merge_env_file "$DEPLOY_PATH/deploy.env.template" "$DEPLOY_PATH/.env"
cd "$DEPLOY_PATH"
echo "[deploy-remote] Validating compose configuration"
@@ -269,39 +194,6 @@ if [[ -n "$SERVICE_NAME" ]]; then
fi
fi
if (( nginx_args_present )); then
[[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE"
[[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE"
MAINTENANCE_HTML_SRC="$DEPLOY_PATH/nginx/maintenance.html"
MAINTENANCE_LOGO_SRC="$DEPLOY_PATH/nginx/logo.png"
[[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC"
[[ -f "$MAINTENANCE_LOGO_SRC" ]] || fail "Maintenance logo missing from deployment payload: $MAINTENANCE_LOGO_SRC"
echo "[deploy-remote] Updating shared nginx config (pre-rebuild) so maintenance routing is active"
mkdir -p "$(dirname "$NGINX_TARGET")"
cp "$DEPLOY_PATH/$NGINX_SOURCE" "$NGINX_TARGET"
echo "[deploy-remote] Installing maintenance page assets into nginx container"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
exec -T nginx mkdir -p /var/www/html/m
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
cp "$MAINTENANCE_HTML_SRC" nginx:/var/www/html/maintenance.html
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
cp "$MAINTENANCE_LOGO_SRC" nginx:/var/www/html/m/logo.png
echo "[deploy-remote] Validating nginx configuration"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -t
echo "[deploy-remote] Reloading shared nginx so the new config (incl. maintenance routing) is live"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -s reload
echo "[deploy-remote] Engaging maintenance page (touch maintenance.flag)"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
exec -T nginx touch /etc/nginx/conf.d/maintenance.flag
MAINTENANCE_ACTIVE=1
fi
if [[ -n "$SERVICE_NAME" ]]; then
echo "[deploy-remote] Stopping only the Goodwalk service: $SERVICE_NAME"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop "$SERVICE_NAME" || true
@@ -324,6 +216,19 @@ if [[ -z "$SERVICE_NAME" || "$SERVICE_NAME" == "app" || "$SERVICE_NAME" == "db"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" exec -T app node scripts/sync-homepage-content.mjs
fi
clear_maintenance_flag
if (( nginx_args_present )); then
[[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE"
[[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE"
echo "[deploy-remote] Updating shared nginx config to avoid stale container IPs"
mkdir -p "$(dirname "$NGINX_TARGET")"
cp "$DEPLOY_PATH/$NGINX_SOURCE" "$NGINX_TARGET"
echo "[deploy-remote] Validating nginx configuration"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -t
echo "[deploy-remote] Reloading shared nginx"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -s reload
fi
echo "[deploy-remote] Remote deployment finished"
+1 -1
View File
@@ -13,7 +13,7 @@
function getVariant(path: string) {
if (path === '/') return 'home';
if (serviceRoutes.has(path)) return 'service';
if (path === '/about' || path === '/about-us') return 'about';
if (path === '/about') return 'about';
if (path === '/our-pricing') return 'pricing';
if (path === '/contact-us') return 'contact';
if (legalRoutes.has(path)) return 'legal';
+1 -3
View File
@@ -25,9 +25,7 @@
{/if}
{#if service.href}
<a href={service.href} class="btn btn-green">
Learn more<span class="visually-hidden"> about {service.title}</span>
</a>
<a href={service.href} class="btn btn-green">Learn more</a>
{/if}
</div>
{/each}
+2 -8
View File
@@ -12,9 +12,9 @@ export const staticPages = {
canonicalPath: '/dog-walking'
},
'puppy-visits': {
title: 'Puppy Visits | Puppy Training',
title: 'Puppy Visits | Auckland In-Home Puppy Care | Goodwalk',
description:
'Puppy Visits Introducing Puppy Visits: Building strong foundations for our pack walks! We love puppies! Our puppy home visits are perfect for young pups not quite ready to join the pack and busy owners with hectic schedules. We lay the groundwork for future pack walks, including fun games, potty breaks, and even feeding if required. Let us help your furry friend thrive while you are away!',
'In-home puppy visits across Auckland Central — toilet breaks, feeding, play and gentle early training for pups not yet ready for pack walks.',
canonicalPath: '/puppy-visits'
},
'our-pricing': {
@@ -29,12 +29,6 @@ export const staticPages = {
'Learn more about Kingsland based dog walking company Goodwalk. We offer our Tiny Gang pack walks throughout the Auckland region. Solo (1:1 walks), homestays and more.',
canonicalPath: '/about'
},
'about-us': {
title: 'About Us | Dog Walkers',
description:
'Learn more about Kingsland based dog walking company Goodwalk. We offer our Tiny Gang pack walks throughout the Auckland region. Solo (1:1 walks), homestays and more.',
canonicalPath: '/about'
},
'contact-us': {
title: 'Contact Us',
description: 'Book a Meet & Greet or send a general enquiry to Goodwalk Auckland dog walking services.',
-12
View File
@@ -34,15 +34,3 @@ input,
textarea {
font: inherit;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
+21 -1
View File
@@ -89,7 +89,27 @@
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) => ({
'@type': 'Review',
reviewRating: {
'@type': 'Rating',
ratingValue: '5',
bestRating: '5'
},
author: {
'@type': 'Person',
name: testimonial.reviewer
},
reviewBody: testimonial.quote
}))
},
{
'@context': 'https://schema.org',
+30 -5
View File
@@ -30,6 +30,28 @@
return `${siteUrl}${value.startsWith('/') ? value : `/${value}`}`;
}
function aggregateOfferSchema(plans: { price: string }[]) {
const numericPrices = plans
.map((plan) => Number(plan.price.replace(/[^0-9.]/g, '')))
.filter((value) => Number.isFinite(value) && value > 0);
if (numericPrices.length === 0) {
return null;
}
const lowPrice = Math.min(...numericPrices);
const highPrice = Math.max(...numericPrices);
return {
'@type': 'AggregateOffer',
priceCurrency: 'NZD',
lowPrice: lowPrice.toFixed(2),
highPrice: highPrice.toFixed(2),
offerCount: numericPrices.length,
availability: 'https://schema.org/InStock'
};
}
function breadcrumbSchema(name: string, path: string) {
return {
'@context': 'https://schema.org',
@@ -89,7 +111,8 @@
},
areaServed: 'Auckland Central, New Zealand',
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`
url: `${siteUrl}${data.page.canonicalPath}`,
offers: aggregateOfferSchema(packWalksContent.pricing.plans)
},
breadcrumbSchema('Pack Walks', data.page.canonicalPath)
];
@@ -111,7 +134,8 @@
},
areaServed: 'Auckland Central, New Zealand',
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`
url: `${siteUrl}${data.page.canonicalPath}`,
offers: aggregateOfferSchema(dogWalkingContent.pricing.plans)
},
breadcrumbSchema('1:1 Walks', data.page.canonicalPath)
];
@@ -133,7 +157,8 @@
},
areaServed: 'Auckland Central, New Zealand',
image: absoluteUrl(seoImage),
url: `${siteUrl}${data.page.canonicalPath}`
url: `${siteUrl}${data.page.canonicalPath}`,
offers: aggregateOfferSchema(puppyVisitsContent.pricing.plans)
},
breadcrumbSchema('Puppy Visits', data.page.canonicalPath)
];
@@ -148,7 +173,7 @@
},
breadcrumbSchema('Our Pricing', data.page.canonicalPath)
];
} else if (data.slug === 'about' || data.slug === 'about-us') {
} else if (data.slug === 'about') {
seoImage = aboutPageContent.sections[0].imageUrl;
seoImageAlt = aboutPageContent.sections[0].imageAlt;
pageStructuredData = [
@@ -186,7 +211,7 @@
<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'}
{:else if data.slug === 'about'}
<AboutPage content={data.content} pageContent={aboutPageContent} />
{:else if data.slug === 'terms-and-conditions'}
<LegalPage pageContent={termsAndConditionsContent} />