Compare commits
5 Commits
65bdc8dc20
...
e2c5f38d55
| Author | SHA1 | Date | |
|---|---|---|---|
| e2c5f38d55 | |||
| 8df0e2dfe9 | |||
| c2e6282efa | |||
| c95d2e8c25 | |||
| 9d87d08547 |
@@ -53,6 +53,26 @@ server {
|
|||||||
# nginx does not keep stale upstream IPs in memory.
|
# nginx does not keep stale upstream IPs in memory.
|
||||||
resolver 127.0.0.11 ipv6=off valid=30s;
|
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) {
|
location ~* /\.(git|env|htaccess) {
|
||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
@@ -66,6 +86,10 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /api/submit {
|
location /api/submit {
|
||||||
|
if (-f /etc/nginx/conf.d/maintenance.flag) {
|
||||||
|
return 503;
|
||||||
|
}
|
||||||
|
|
||||||
set $goodwalk_mail_api goodwalk_svelte_mail_api:8000;
|
set $goodwalk_mail_api goodwalk_svelte_mail_api:8000;
|
||||||
limit_req zone=goodwalk_limit burst=10 nodelay;
|
limit_req zone=goodwalk_limit burst=10 nodelay;
|
||||||
proxy_pass http://$goodwalk_mail_api/submit;
|
proxy_pass http://$goodwalk_mail_api/submit;
|
||||||
@@ -77,6 +101,10 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
if (-f /etc/nginx/conf.d/maintenance.flag) {
|
||||||
|
return 503;
|
||||||
|
}
|
||||||
|
|
||||||
set $goodwalk_frontend goodwalk_svelte_app:3000;
|
set $goodwalk_frontend goodwalk_svelte_app:3000;
|
||||||
proxy_pass http://$goodwalk_frontend;
|
proxy_pass http://$goodwalk_frontend;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,189 @@
|
|||||||
|
<!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>
|
||||||
+109
-14
@@ -114,8 +114,19 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/goodwalk-deploy.XXXXXX")"
|
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() {
|
cleanup() {
|
||||||
|
clear_maintenance_flag
|
||||||
rm -rf "$STAGING_DIR"
|
rm -rf "$STAGING_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +193,70 @@ if [[ ! -f "$DEPLOY_PATH/.env" ]]; then
|
|||||||
fi
|
fi
|
||||||
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"
|
cd "$DEPLOY_PATH"
|
||||||
|
|
||||||
echo "[deploy-remote] Validating compose configuration"
|
echo "[deploy-remote] Validating compose configuration"
|
||||||
@@ -194,6 +269,39 @@ if [[ -n "$SERVICE_NAME" ]]; then
|
|||||||
fi
|
fi
|
||||||
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
|
if [[ -n "$SERVICE_NAME" ]]; then
|
||||||
echo "[deploy-remote] Stopping only the Goodwalk service: $SERVICE_NAME"
|
echo "[deploy-remote] Stopping only the Goodwalk service: $SERVICE_NAME"
|
||||||
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop "$SERVICE_NAME" || true
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop "$SERVICE_NAME" || true
|
||||||
@@ -216,19 +324,6 @@ 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
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" exec -T app node scripts/sync-homepage-content.mjs
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( nginx_args_present )); then
|
clear_maintenance_flag
|
||||||
[[ -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"
|
echo "[deploy-remote] Remote deployment finished"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
function getVariant(path: string) {
|
function getVariant(path: string) {
|
||||||
if (path === '/') return 'home';
|
if (path === '/') return 'home';
|
||||||
if (serviceRoutes.has(path)) return 'service';
|
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 === '/our-pricing') return 'pricing';
|
||||||
if (path === '/contact-us') return 'contact';
|
if (path === '/contact-us') return 'contact';
|
||||||
if (legalRoutes.has(path)) return 'legal';
|
if (legalRoutes.has(path)) return 'legal';
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if service.href}
|
{#if service.href}
|
||||||
<a href={service.href} class="btn btn-green">Learn more</a>
|
<a href={service.href} class="btn btn-green">
|
||||||
|
Learn more<span class="visually-hidden"> about {service.title}</span>
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export const staticPages = {
|
|||||||
canonicalPath: '/dog-walking'
|
canonicalPath: '/dog-walking'
|
||||||
},
|
},
|
||||||
'puppy-visits': {
|
'puppy-visits': {
|
||||||
title: 'Puppy Visits | Puppy Training',
|
title: 'Puppy Visits | Auckland In-Home Puppy Care | Goodwalk',
|
||||||
description:
|
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'
|
canonicalPath: '/puppy-visits'
|
||||||
},
|
},
|
||||||
'our-pricing': {
|
'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.',
|
'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'
|
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': {
|
'contact-us': {
|
||||||
title: 'Contact Us',
|
title: 'Contact Us',
|
||||||
description: 'Book a Meet & Greet or send a general enquiry to Goodwalk Auckland dog walking services.',
|
description: 'Book a Meet & Greet or send a general enquiry to Goodwalk Auckland dog walking services.',
|
||||||
|
|||||||
@@ -34,3 +34,15 @@ input,
|
|||||||
textarea {
|
textarea {
|
||||||
font: inherit;
|
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
@@ -89,7 +89,27 @@
|
|||||||
url: `${siteUrl}${service.href}`
|
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',
|
'@context': 'https://schema.org',
|
||||||
|
|||||||
@@ -30,6 +30,28 @@
|
|||||||
return `${siteUrl}${value.startsWith('/') ? value : `/${value}`}`;
|
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) {
|
function breadcrumbSchema(name: string, path: string) {
|
||||||
return {
|
return {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@@ -89,7 +111,8 @@
|
|||||||
},
|
},
|
||||||
areaServed: 'Auckland Central, New Zealand',
|
areaServed: 'Auckland Central, New Zealand',
|
||||||
image: absoluteUrl(seoImage),
|
image: absoluteUrl(seoImage),
|
||||||
url: `${siteUrl}${data.page.canonicalPath}`
|
url: `${siteUrl}${data.page.canonicalPath}`,
|
||||||
|
offers: aggregateOfferSchema(packWalksContent.pricing.plans)
|
||||||
},
|
},
|
||||||
breadcrumbSchema('Pack Walks', data.page.canonicalPath)
|
breadcrumbSchema('Pack Walks', data.page.canonicalPath)
|
||||||
];
|
];
|
||||||
@@ -111,7 +134,8 @@
|
|||||||
},
|
},
|
||||||
areaServed: 'Auckland Central, New Zealand',
|
areaServed: 'Auckland Central, New Zealand',
|
||||||
image: absoluteUrl(seoImage),
|
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)
|
breadcrumbSchema('1:1 Walks', data.page.canonicalPath)
|
||||||
];
|
];
|
||||||
@@ -133,7 +157,8 @@
|
|||||||
},
|
},
|
||||||
areaServed: 'Auckland Central, New Zealand',
|
areaServed: 'Auckland Central, New Zealand',
|
||||||
image: absoluteUrl(seoImage),
|
image: absoluteUrl(seoImage),
|
||||||
url: `${siteUrl}${data.page.canonicalPath}`
|
url: `${siteUrl}${data.page.canonicalPath}`,
|
||||||
|
offers: aggregateOfferSchema(puppyVisitsContent.pricing.plans)
|
||||||
},
|
},
|
||||||
breadcrumbSchema('Puppy Visits', data.page.canonicalPath)
|
breadcrumbSchema('Puppy Visits', data.page.canonicalPath)
|
||||||
];
|
];
|
||||||
@@ -148,7 +173,7 @@
|
|||||||
},
|
},
|
||||||
breadcrumbSchema('Our Pricing', data.page.canonicalPath)
|
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;
|
seoImage = aboutPageContent.sections[0].imageUrl;
|
||||||
seoImageAlt = aboutPageContent.sections[0].imageAlt;
|
seoImageAlt = aboutPageContent.sections[0].imageAlt;
|
||||||
pageStructuredData = [
|
pageStructuredData = [
|
||||||
@@ -186,7 +211,7 @@
|
|||||||
<ServiceLandingPage content={data.content} pageContent={puppyVisitsContent} currentPath={data.page.canonicalPath} />
|
<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'}
|
||||||
<AboutPage content={data.content} pageContent={aboutPageContent} />
|
<AboutPage content={data.content} pageContent={aboutPageContent} />
|
||||||
{:else if data.slug === 'terms-and-conditions'}
|
{:else if data.slug === 'terms-and-conditions'}
|
||||||
<LegalPage pageContent={termsAndConditionsContent} />
|
<LegalPage pageContent={termsAndConditionsContent} />
|
||||||
|
|||||||
Reference in New Issue
Block a user