2 Commits

Author SHA1 Message Date
admin 71cdc809c6 Commit 2026-05-05 22:47:14 +12:00
admin 0eed557f95 4.2.0 — deploy hardening and visual consistency pass
Make the production deploy foolproof against the shared nginx container's read-only bind mounts on the Digital Ocean droplet. The previous maintenance flow tried to docker-cp/docker-exec into /var/www/html and /etc/nginx/conf.d, both of which are mounted :ro on prod, and /var/www/html happens to point at the WordPress html dir — so writes either failed silently or risked scribbling into another site's tree. Maintenance assets and the engagement flag are now written directly to host paths (/docker/nginx/maintenance and /docker/nginx/conf.d/maintenance.flag) that nginx already sees through its existing bind mounts, so the script no longer depends on a writable container layer, survives container rebuilds, and works regardless of read_only settings. A pre-flight check verifies the maintenance bind mount is actually present on the nginx container and fails fast with a clear "run the one-time setup" message if it isn't, instead of silently serving stale content. The nginx config now serves maintenance.html and /m/ from a dedicated /var/www/maintenance root rather than sharing the WordPress html dir.

On the front end, hero images on Pack Walks, 1:1 Walks and Puppy Visits were rendering at whatever aspect ratio their source files happened to have, so one page felt tall, another wide, another oversized. They are now locked to a 4:3 frame with object-fit: cover, matching the About Us section images, which were given the same treatment. The About Us body grid was also alternating between 0.7fr/1.3fr and 1.3fr/0.7fr columns depending on whether a section was reversed, which made the copy width jump between sections; both layouts are now an even 50/50 split, with the existing order swap still handling the image-left vs image-right alternation.

The reveal-on-scroll action used to require 18% of an element to intersect before fading it in, with an additional -8% bottom margin, which meant the section directly below a service-page hero stayed invisible on initial load until the user scrolled — making the page look blank below the hero on navigation. The action now does a synchronous bounding-rect check on mount and reveals anything already in the viewport immediately, falling back to the IntersectionObserver for everything below the fold.

The "Explore our services" block on About Us was a bespoke icon-tile grid that did not match the homepage's "What we do" cards; it now reuses the shared ServicesSection component (with the heading exposed as a prop), so both pages produce identical card layout, descriptions, "from $" prices, and Learn more CTAs. The footer Explore column was missing the About Us link — added between Our Pricing and Contact Us so it propagates through the homepage content sync into PostgreSQL on the next deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 20:54:56 +12:00
20 changed files with 729 additions and 133 deletions
+4
View File
@@ -0,0 +1,4 @@
* text=auto eol=lf
*.sh text eol=lf
*.ps1 text eol=crlf
+66
View File
@@ -37,6 +37,9 @@ containers untouched.
`deploy.ps1`. Keep using the root script directly.
- [scripts/deploy-remote.sh](scripts/deploy-remote.sh)
- Server-side helper that updates only the `goodwalk-svelte` compose project.
- [scripts/deploy-from-git.sh](scripts/deploy-from-git.sh)
- Standalone server-side entrypoint that pulls from Git, then runs the same
compose/nginx deployment steps on the server.
- [docker-compose.prod.yml](docker-compose.prod.yml)
- Production compose file for the new Svelte app, mail API, and Postgres.
- `scripts/export-homepage-content.mjs`
@@ -129,6 +132,69 @@ To rebuild and restart only one service, for example the mail API:
powershell -ExecutionPolicy Bypass -File .\deploy.ps1 -Force -Service mail-api
```
## Remote Git deploy
If you want the production server to pull straight from Gitea instead of
receiving an uploaded tarball from Windows, use
[scripts/deploy-from-git.sh](scripts/deploy-from-git.sh) on the server.
Recommended credential setup for a private HTTPS repo:
```bash
umask 077
cat > ~/.netrc <<'EOF'
machine g.sublogue.com
login YOUR_GITEA_USERNAME
password YOUR_READ_ONLY_TOKEN
EOF
chmod 600 ~/.netrc
```
Install the script on the server and make it executable:
```bash
install -m 0755 scripts/deploy-from-git.sh /usr/local/bin/goodwalk-deploy
```
The remote host must have `git` and `docker`. A host-level `node` install is
optional; if it is missing, the script will export homepage content using a
temporary `node:22-alpine` container instead.
Run a full deploy from the repo:
```bash
/usr/local/bin/goodwalk-deploy \
--repo-url https://g.sublogue.com/admin/gw-svelte.git \
--branch main \
--deploy-path /docker/goodwalk-svelte \
--compose-file docker-compose.prod.yml \
--project-name goodwalk-svelte \
--nginx-source nginx/goodwalk.co.nz.svelte.conf.example \
--nginx-target /docker/nginx/conf.d/goodwalk.co.nz.conf \
--nginx-compose-file /docker/nginx/docker-compose.yml \
--nginx-project-name nginx \
--maintenance-host-dir /docker/nginx/maintenance \
--maintenance-flag /docker/nginx/conf.d/maintenance.flag
```
Deploy a specific commit or tag:
```bash
/usr/local/bin/goodwalk-deploy \
--repo-url https://g.sublogue.com/admin/gw-svelte.git \
--branch main \
--ref <commit-or-tag> \
--deploy-path /docker/goodwalk-svelte \
--compose-file docker-compose.prod.yml \
--project-name goodwalk-svelte \
--nginx-source nginx/goodwalk.co.nz.svelte.conf.example \
--nginx-target /docker/nginx/conf.d/goodwalk.co.nz.conf \
--nginx-compose-file /docker/nginx/docker-compose.yml \
--nginx-project-name nginx \
--maintenance-host-dir /docker/nginx/maintenance \
--maintenance-flag /docker/nginx/conf.d/maintenance.flag
```
## Homepage content sync
Local development can feel fresher than production because production reads the
+15 -1
View File
@@ -24,6 +24,12 @@ $NginxConfigSource = 'nginx/goodwalk.co.nz.svelte.conf.example'
$NginxConfigTarget = '/docker/nginx/conf.d/goodwalk.co.nz.conf'
$NginxComposeFile = '/docker/nginx/docker-compose.yml'
$NginxProjectName = 'nginx'
# Host paths used for the maintenance page. The directory must be bind-mounted
# into the shared nginx container at /var/www/maintenance:ro (see DEPLOYMENT.md).
# The flag file lives in the existing conf.d bind mount; nginx ignores non-.conf
# files, so it does not pollute the include glob.
$MaintenanceHostDir = '/docker/nginx/maintenance'
$MaintenanceFlagPath = '/docker/nginx/conf.d/maintenance.flag'
# Optional deployment settings.
$VerifyUrl = 'https://www.goodwalk.co.nz/api/health'
@@ -169,6 +175,8 @@ Assert-NotBlank -Name 'NginxConfigSource' -Value $NginxConfigSource
Assert-NotBlank -Name 'NginxConfigTarget' -Value $NginxConfigTarget
Assert-NotBlank -Name 'NginxComposeFile' -Value $NginxComposeFile
Assert-NotBlank -Name 'NginxProjectName' -Value $NginxProjectName
Assert-NotBlank -Name 'MaintenanceHostDir' -Value $MaintenanceHostDir
Assert-NotBlank -Name 'MaintenanceFlagPath' -Value $MaintenanceFlagPath
if (-not [string]::IsNullOrWhiteSpace($Service)) {
$Service = $Service.Trim()
@@ -206,6 +214,8 @@ Write-Host "[deploy] Remote compose file: $ComposeFileName"
Write-Host "[deploy] Docker project name: $DockerProjectName"
Write-Host "[deploy] Shared nginx config: $NginxConfigTarget"
Write-Host "[deploy] Shared nginx compose file: $NginxComposeFile"
Write-Host "[deploy] Maintenance host dir: $MaintenanceHostDir (must be bind-mounted at /var/www/maintenance:ro)"
Write-Host "[deploy] Maintenance flag path: $MaintenanceFlagPath"
Write-Host "[deploy] SSH target: $sshTarget"
Write-Host "[deploy] SSH config: $SshConfigPath"
if (-not [string]::IsNullOrWhiteSpace($Service)) {
@@ -272,7 +282,11 @@ try {
'--nginx-compose-file',
$NginxComposeFile,
'--nginx-project-name',
$NginxProjectName
$NginxProjectName,
'--maintenance-host-dir',
$MaintenanceHostDir,
'--maintenance-flag',
$MaintenanceFlagPath
) + $(if (-not [string]::IsNullOrWhiteSpace($Service)) { @('--service', $Service) } else { @() }))
Write-Host ''
+4 -4
View File
@@ -59,16 +59,16 @@ server {
error_page 503 /maintenance.html;
location = /maintenance.html {
root /var/www/html;
root /var/www/maintenance;
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.
# from a dedicated bind mount so they remain reachable while the
# SvelteKit app is down and do not collide with any other site's html dir.
location /m/ {
root /var/www/html;
root /var/www/maintenance;
access_log off;
add_header Cache-Control "public, max-age=3600" always;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "goodwalk-svelte-port",
"version": "4.1.0",
"version": "4.2.0",
"private": true,
"type": "module",
"scripts": {
+500
View File
@@ -0,0 +1,500 @@
#!/usr/bin/env bash
set -Eeuo pipefail
REPO_URL=""
BRANCH="main"
REF=""
DEPLOY_PATH=""
COMPOSE_FILE=""
PROJECT_NAME=""
SERVICE_NAME=""
NGINX_SOURCE=""
NGINX_TARGET=""
NGINX_COMPOSE_FILE=""
NGINX_PROJECT_NAME=""
MAINTENANCE_HOST_DIR=""
MAINTENANCE_FLAG_PATH=""
VERIFY_URL="https://www.goodwalk.co.nz/api/health"
SKIP_SITE_CHECK=0
usage() {
cat <<'EOF'
Usage:
deploy-from-git.sh --repo-url <url> --branch <name> --deploy-path <path> --compose-file <name> --project-name <name>
deploy-from-git.sh --repo-url <url> [--branch <name>] [--ref <commit-or-tag>] --deploy-path <path> --compose-file <name> --project-name <name> [--service <name>]
deploy-from-git.sh --repo-url <url> [--branch <name>] [--ref <commit-or-tag>] --deploy-path <path> --compose-file <name> --project-name <name> \
[--service <name>] [--nginx-source <path>] [--nginx-target <path>] \
[--nginx-compose-file <path>] [--nginx-project-name <name>] \
[--maintenance-host-dir <path>] [--maintenance-flag <path>] \
[--verify-url <url>] [--skip-site-check]
This script clones or fetches the application repo on the server, exports the
homepage content payload, updates only the main Goodwalk compose project, and
optionally updates the shared nginx stack plus maintenance mode handling.
Authentication for private HTTPS repos is expected to come from ~/.netrc,
git-credential, or another Git-supported credential mechanism already present
on the server.
EOF
}
fail() {
echo "[deploy-git] ERROR: $*" >&2
exit 1
}
assert_command() {
command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' is not installed on the server"
}
run_homepage_export() {
local export_script="$1"
local output_path="$2"
if command -v node >/dev/null 2>&1; then
node --experimental-strip-types "$export_script" "$output_path"
return
fi
echo "[deploy-git] Host node not found; exporting homepage content via temporary node:22-alpine container"
docker run --rm \
-v "$CHECKOUT_DIR:/app" \
-w /app \
node:22-alpine \
node --experimental-strip-types "${export_script#/app/}" "${output_path#/app/}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--repo-url)
REPO_URL="${2:-}"
shift 2
;;
--branch)
BRANCH="${2:-}"
shift 2
;;
--ref)
REF="${2:-}"
shift 2
;;
--deploy-path)
DEPLOY_PATH="${2:-}"
shift 2
;;
--compose-file)
COMPOSE_FILE="${2:-}"
shift 2
;;
--project-name)
PROJECT_NAME="${2:-}"
shift 2
;;
--service)
SERVICE_NAME="${2:-}"
shift 2
;;
--nginx-source)
NGINX_SOURCE="${2:-}"
shift 2
;;
--nginx-target)
NGINX_TARGET="${2:-}"
shift 2
;;
--nginx-compose-file)
NGINX_COMPOSE_FILE="${2:-}"
shift 2
;;
--nginx-project-name)
NGINX_PROJECT_NAME="${2:-}"
shift 2
;;
--maintenance-host-dir)
MAINTENANCE_HOST_DIR="${2:-}"
shift 2
;;
--maintenance-flag)
MAINTENANCE_FLAG_PATH="${2:-}"
shift 2
;;
--verify-url)
VERIFY_URL="${2:-}"
shift 2
;;
--skip-site-check)
SKIP_SITE_CHECK=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail "Unknown argument: $1"
;;
esac
done
[[ -n "$REPO_URL" ]] || fail "--repo-url is required"
[[ -n "$BRANCH" ]] || fail "--branch is required"
[[ -n "$DEPLOY_PATH" ]] || fail "--deploy-path is required"
[[ -n "$COMPOSE_FILE" ]] || fail "--compose-file is required"
[[ -n "$PROJECT_NAME" ]] || fail "--project-name is required"
if [[ -n "$SERVICE_NAME" ]]; then
SERVICE_NAME="$(printf '%s' "$SERVICE_NAME" | xargs)"
fi
[[ "$DEPLOY_PATH" != "/" ]] || fail "Refusing to deploy to /"
nginx_args=("$NGINX_SOURCE" "$NGINX_TARGET" "$NGINX_COMPOSE_FILE" "$NGINX_PROJECT_NAME")
nginx_args_present=0
for value in "${nginx_args[@]}"; do
if [[ -n "$value" ]]; then
nginx_args_present=1
break
fi
done
if (( nginx_args_present )); then
[[ -n "$NGINX_SOURCE" ]] || fail "--nginx-source is required when nginx deployment is enabled"
[[ -n "$NGINX_TARGET" ]] || fail "--nginx-target is required when nginx deployment is enabled"
[[ -n "$NGINX_COMPOSE_FILE" ]] || fail "--nginx-compose-file is required when nginx deployment is enabled"
[[ -n "$NGINX_PROJECT_NAME" ]] || fail "--nginx-project-name is required when nginx deployment is enabled"
[[ -n "$MAINTENANCE_HOST_DIR" ]] || fail "--maintenance-host-dir is required when nginx deployment is enabled"
[[ -n "$MAINTENANCE_FLAG_PATH" ]] || fail "--maintenance-flag is required when nginx deployment is enabled"
fi
assert_command git
assert_command docker
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose)
else
fail "Docker Compose is not installed on the server"
fi
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/goodwalk-git-deploy.XXXXXX")"
CHECKOUT_DIR="$WORK_DIR/repo"
PAYLOAD_DIR="$WORK_DIR/payload"
MAINTENANCE_ACTIVE=0
clear_maintenance_flag() {
if (( MAINTENANCE_ACTIVE )) && (( nginx_args_present )); then
echo "[deploy-git] Clearing maintenance flag at $MAINTENANCE_FLAG_PATH"
rm -f "$MAINTENANCE_FLAG_PATH" || true
MAINTENANCE_ACTIVE=0
fi
}
cleanup() {
clear_maintenance_flag
rm -rf "$WORK_DIR"
}
copy_checkout_to_payload() {
mkdir -p "$PAYLOAD_DIR"
if command -v rsync >/dev/null 2>&1; then
rsync -a \
--exclude '.git' \
--exclude '.env' \
--exclude '.env.*' \
"$CHECKOUT_DIR"/ "$PAYLOAD_DIR"/
return
fi
while IFS= read -r -d '' item; do
relative_path="${item#"$CHECKOUT_DIR"/}"
case "$relative_path" in
.git|.git/*|.env|.env.*)
continue
;;
esac
destination="$PAYLOAD_DIR/$relative_path"
if [[ -d "$item" ]]; then
mkdir -p "$destination"
continue
fi
mkdir -p "$(dirname "$destination")"
cp -f "$item" "$destination"
done < <(find "$CHECKOUT_DIR" -mindepth 1 -print0)
}
copy_payload_to_deploy() {
mkdir -p "$DEPLOY_PATH"
if command -v rsync >/dev/null 2>&1; then
rsync -a \
--exclude '.env' \
--exclude '.env.*' \
"$PAYLOAD_DIR"/ "$DEPLOY_PATH"/
return
fi
while IFS= read -r -d '' item; do
relative_path="${item#"$PAYLOAD_DIR"/}"
if [[ "$relative_path" == ".env" || "$relative_path" == .env.* ]]; then
continue
fi
destination="$DEPLOY_PATH/$relative_path"
if [[ -d "$item" ]]; then
mkdir -p "$destination"
continue
fi
mkdir -p "$(dirname "$destination")"
cp -f "$item" "$destination"
done < <(find "$PAYLOAD_DIR" -mindepth 1 -print0)
}
merge_env_file() {
local template="$1"
local live="$2"
[[ -f "$template" ]] || { echo "[deploy-git] No env template at $template, skipping merge"; return 0; }
[[ -f "$live" ]] || { echo "[deploy-git] 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-git] Adding env keys present in template but missing from $live:"
sed 's/^/ + /' "$added"
{
printf '\n# Appended by deploy-from-git.sh on %s from deploy.env.template\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
cat "$added"
} >> "$live"
echo "[deploy-git] Backup of previous .env written to $backup"
else
echo "[deploy-git] .env is up to date with template (no missing keys)"
fi
if [[ -s "$diffs" ]]; then
echo "[deploy-git] NOTE: these keys exist in both files but values differ. Live values are PRESERVED:"
sed 's/^/ ! /' "$diffs"
echo "[deploy-git] If a live value is stale, edit $live and re-deploy."
fi
rm -f "$added" "$diffs"
}
check_site() {
if (( SKIP_SITE_CHECK )) || [[ -z "$VERIFY_URL" ]]; then
return 0
fi
echo "[deploy-git] Checking production site: $VERIFY_URL"
if command -v curl >/dev/null 2>&1; then
local http_code
if http_code="$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 30 -L "$VERIFY_URL" 2>/dev/null)"; then
echo "[deploy-git] Site responded with HTTP $http_code"
else
echo "[deploy-git] WARNING: production site check failed for $VERIFY_URL" >&2
fi
return 0
fi
if command -v wget >/dev/null 2>&1; then
if wget --spider --server-response --timeout=30 "$VERIFY_URL" >/tmp/goodwalk-site-check.$$ 2>&1; then
awk '/^ HTTP\// { code=$2 } END { if (code != "") printf "[deploy-git] Site responded with HTTP %s\n", code }' /tmp/goodwalk-site-check.$$
else
echo "[deploy-git] WARNING: production site check failed for $VERIFY_URL" >&2
fi
rm -f /tmp/goodwalk-site-check.$$
return 0
fi
echo "[deploy-git] WARNING: curl/wget not available; skipping site check" >&2
}
trap cleanup EXIT
echo "[deploy-git] Deploying main Goodwalk stack from Git"
echo "[deploy-git] Repo URL: $REPO_URL"
echo "[deploy-git] Branch: $BRANCH"
if [[ -n "$REF" ]]; then
echo "[deploy-git] Requested ref: $REF"
fi
echo "[deploy-git] Target deployment path: $DEPLOY_PATH"
echo "[deploy-git] Compose file: $COMPOSE_FILE"
echo "[deploy-git] Docker project: $PROJECT_NAME"
if [[ -n "$SERVICE_NAME" ]]; then
echo "[deploy-git] Target service: $SERVICE_NAME"
fi
if (( nginx_args_present )); then
echo "[deploy-git] Nginx config source: $NGINX_SOURCE"
echo "[deploy-git] Nginx config target: $NGINX_TARGET"
echo "[deploy-git] Nginx compose file: $NGINX_COMPOSE_FILE"
echo "[deploy-git] Nginx project: $NGINX_PROJECT_NAME"
echo "[deploy-git] Maintenance host dir: $MAINTENANCE_HOST_DIR"
echo "[deploy-git] Maintenance flag path: $MAINTENANCE_FLAG_PATH"
fi
echo "[deploy-git] Cloning repository into: $CHECKOUT_DIR"
git clone "$REPO_URL" "$CHECKOUT_DIR"
git -C "$CHECKOUT_DIR" fetch --tags --prune origin
if [[ -n "$REF" ]]; then
git -C "$CHECKOUT_DIR" checkout --detach "$REF"
else
git -C "$CHECKOUT_DIR" checkout -B "$BRANCH" "origin/$BRANCH"
fi
DEPLOYED_REVISION="$(git -C "$CHECKOUT_DIR" rev-parse HEAD)"
echo "[deploy-git] Using repo revision: $DEPLOYED_REVISION"
EXPORT_SCRIPT="$CHECKOUT_DIR/scripts/export-homepage-content.mjs"
[[ -f "$EXPORT_SCRIPT" ]] || fail "Homepage export script not found: $EXPORT_SCRIPT"
echo "[deploy-git] Exporting current homepage content for PostgreSQL sync"
run_homepage_export "/app/scripts/export-homepage-content.mjs" "/app/deploy-data/homepage-content.json"
echo "[deploy-git] Preparing deployment payload"
copy_checkout_to_payload
[[ -f "$PAYLOAD_DIR/$COMPOSE_FILE" ]] || fail "Compose file missing from repo checkout: $COMPOSE_FILE"
if [[ -f "$DEPLOY_PATH/.env" ]]; then
echo "[deploy-git] Preserving existing $DEPLOY_PATH/.env"
fi
echo "[deploy-git] Copying application files into $DEPLOY_PATH"
copy_payload_to_deploy
[[ -f "$DEPLOY_PATH/$COMPOSE_FILE" ]] || fail "Compose file missing after copy: $DEPLOY_PATH/$COMPOSE_FILE"
if [[ ! -f "$DEPLOY_PATH/.env" ]]; then
if [[ -f "$DEPLOY_PATH/deploy.env.template" ]]; then
echo "[deploy-git] No remote .env found. Creating $DEPLOY_PATH/.env from deploy.env.template"
cp "$DEPLOY_PATH/deploy.env.template" "$DEPLOY_PATH/.env"
else
fail "Remote .env is missing and deploy.env.template was not present"
fi
fi
merge_env_file "$DEPLOY_PATH/deploy.env.template" "$DEPLOY_PATH/.env"
cd "$DEPLOY_PATH"
echo "[deploy-git] Validating compose configuration"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" config >/dev/null
if [[ -n "$SERVICE_NAME" ]]; then
AVAILABLE_SERVICES="$("${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" config --services)"
if ! grep -Fxq "$SERVICE_NAME" <<<"$AVAILABLE_SERVICES"; then
fail "Service '$SERVICE_NAME' was not found in $COMPOSE_FILE. Available services: $(tr '\n' ',' <<<"$AVAILABLE_SERVICES" | sed 's/,$//')"
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"
NGINX_CID="$(docker ps -qf name=^nginx$ | head -n1 || true)"
[[ -n "$NGINX_CID" ]] || fail "Shared nginx container is not running (expected name 'nginx'). Bring it up before deploying."
if ! docker inspect -f '{{range .Mounts}}{{.Source}}|{{.Destination}}{{println}}{{end}}' "$NGINX_CID" \
| grep -Fxq "${MAINTENANCE_HOST_DIR}|/var/www/maintenance"; then
fail "nginx container is missing the maintenance bind mount.
Expected: ${MAINTENANCE_HOST_DIR}:/var/www/maintenance:ro
One-time setup on the droplet:
mkdir -p ${MAINTENANCE_HOST_DIR}/m
# add this volume to ${NGINX_COMPOSE_FILE}:
# - ${MAINTENANCE_HOST_DIR}:/var/www/maintenance:ro
${COMPOSE_CMD[*]} -p ${NGINX_PROJECT_NAME} -f ${NGINX_COMPOSE_FILE} up -d"
fi
FLAG_DIR="$(dirname "$MAINTENANCE_FLAG_PATH")"
[[ -d "$FLAG_DIR" ]] || fail "Maintenance flag directory does not exist on host: $FLAG_DIR"
echo "[deploy-git] Writing maintenance assets to host bind dir: $MAINTENANCE_HOST_DIR"
mkdir -p "$MAINTENANCE_HOST_DIR/m"
install -m 0644 "$MAINTENANCE_HTML_SRC" "$MAINTENANCE_HOST_DIR/maintenance.html"
install -m 0644 "$MAINTENANCE_LOGO_SRC" "$MAINTENANCE_HOST_DIR/m/logo.png"
echo "[deploy-git] 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-git] Validating nginx configuration"
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -t
echo "[deploy-git] 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-git] Engaging maintenance page via host flag: $MAINTENANCE_FLAG_PATH"
: > "$MAINTENANCE_FLAG_PATH"
MAINTENANCE_ACTIVE=1
fi
if [[ -n "$SERVICE_NAME" ]]; then
echo "[deploy-git] Stopping only the Goodwalk service: $SERVICE_NAME"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop "$SERVICE_NAME" || true
echo "[deploy-git] Rebuilding and starting only the Goodwalk service: $SERVICE_NAME"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --build "$SERVICE_NAME"
else
echo "[deploy-git] Stopping only the Goodwalk project containers"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop || true
echo "[deploy-git] Rebuilding and starting only the Goodwalk project containers"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --build --remove-orphans
fi
echo "[deploy-git] Current Goodwalk container status"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" ps
if [[ -z "$SERVICE_NAME" || "$SERVICE_NAME" == "app" || "$SERVICE_NAME" == "db" ]]; then
echo "[deploy-git] Syncing homepage content into PostgreSQL"
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" exec -T app node scripts/sync-homepage-content.mjs
fi
clear_maintenance_flag
check_site
echo "[deploy-git] Remote deployment finished"
echo "[deploy-git] Deployed revision: $DEPLOYED_REVISION"
+46 -15
View File
@@ -10,6 +10,8 @@ NGINX_SOURCE=""
NGINX_TARGET=""
NGINX_COMPOSE_FILE=""
NGINX_PROJECT_NAME=""
MAINTENANCE_HOST_DIR=""
MAINTENANCE_FLAG_PATH=""
usage() {
cat <<'EOF'
@@ -18,7 +20,8 @@ Usage:
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name> [--service <name>]
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name> \
[--service <name>] [--nginx-source <path>] [--nginx-target <path>] \
[--nginx-compose-file <path>] [--nginx-project-name <name>]
[--nginx-compose-file <path>] [--nginx-project-name <name>] \
[--maintenance-host-dir <path>] [--maintenance-flag <path>]
This script only updates the main Goodwalk compose project at the specified
deployment path. It does not touch unrelated Docker projects or global Docker
@@ -69,6 +72,14 @@ while [[ $# -gt 0 ]]; do
NGINX_PROJECT_NAME="${2:-}"
shift 2
;;
--maintenance-host-dir)
MAINTENANCE_HOST_DIR="${2:-}"
shift 2
;;
--maintenance-flag)
MAINTENANCE_FLAG_PATH="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
@@ -103,6 +114,8 @@ if (( nginx_args_present )); then
[[ -n "$NGINX_TARGET" ]] || fail "--nginx-target is required when nginx deployment is enabled"
[[ -n "$NGINX_COMPOSE_FILE" ]] || fail "--nginx-compose-file is required when nginx deployment is enabled"
[[ -n "$NGINX_PROJECT_NAME" ]] || fail "--nginx-project-name is required when nginx deployment is enabled"
[[ -n "$MAINTENANCE_HOST_DIR" ]] || fail "--maintenance-host-dir is required when nginx deployment is enabled"
[[ -n "$MAINTENANCE_FLAG_PATH" ]] || fail "--maintenance-flag is required when nginx deployment is enabled"
fi
if docker compose version >/dev/null 2>&1; then
@@ -118,9 +131,8 @@ 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
echo "[deploy-remote] Clearing maintenance flag at $MAINTENANCE_FLAG_PATH"
rm -f "$MAINTENANCE_FLAG_PATH" || true
MAINTENANCE_ACTIVE=0
fi
}
@@ -144,6 +156,8 @@ if (( nginx_args_present )); then
echo "[deploy-remote] Nginx config target: $NGINX_TARGET"
echo "[deploy-remote] Nginx compose file: $NGINX_COMPOSE_FILE"
echo "[deploy-remote] Nginx project: $NGINX_PROJECT_NAME"
echo "[deploy-remote] Maintenance host dir: $MAINTENANCE_HOST_DIR"
echo "[deploy-remote] Maintenance flag path: $MAINTENANCE_FLAG_PATH"
fi
echo "[deploy-remote] Staging archive in: $STAGING_DIR"
@@ -278,27 +292,44 @@ if (( nginx_args_present )); then
[[ -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"
# Pre-flight: the nginx container must have a bind mount whose source is
# MAINTENANCE_HOST_DIR and whose destination is /var/www/maintenance. If the
# one-time droplet setup has not been done, fail fast with a clear message
# rather than silently serving stale content.
NGINX_CID="$(docker ps -qf name=^nginx$ | head -n1 || true)"
[[ -n "$NGINX_CID" ]] || fail "Shared nginx container is not running (expected name 'nginx'). Bring it up before deploying."
if ! docker inspect -f '{{range .Mounts}}{{.Source}}|{{.Destination}}{{println}}{{end}}' "$NGINX_CID" \
| grep -Fxq "${MAINTENANCE_HOST_DIR}|/var/www/maintenance"; then
fail "nginx container is missing the maintenance bind mount.
Expected: ${MAINTENANCE_HOST_DIR}:/var/www/maintenance:ro
One-time setup on the droplet:
mkdir -p ${MAINTENANCE_HOST_DIR}/m
# add this volume to ${NGINX_COMPOSE_FILE}:
# - ${MAINTENANCE_HOST_DIR}:/var/www/maintenance:ro
${COMPOSE_CMD[*]} -p ${NGINX_PROJECT_NAME} -f ${NGINX_COMPOSE_FILE} up -d"
fi
FLAG_DIR="$(dirname "$MAINTENANCE_FLAG_PATH")"
[[ -d "$FLAG_DIR" ]] || fail "Maintenance flag directory does not exist on host: $FLAG_DIR"
echo "[deploy-remote] Writing maintenance assets to host bind dir: $MAINTENANCE_HOST_DIR"
mkdir -p "$MAINTENANCE_HOST_DIR/m"
install -m 0644 "$MAINTENANCE_HTML_SRC" "$MAINTENANCE_HOST_DIR/maintenance.html"
install -m 0644 "$MAINTENANCE_LOGO_SRC" "$MAINTENANCE_HOST_DIR/m/logo.png"
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
echo "[deploy-remote] Engaging maintenance page via host flag: $MAINTENANCE_FLAG_PATH"
: > "$MAINTENANCE_FLAG_PATH"
MAINTENANCE_ACTIVE=1
fi
+19
View File
@@ -25,6 +25,25 @@ export function reveal(node: HTMLElement, options: RevealOptions = {}) {
node.style.setProperty('--reveal-distance', `${settings.distance}px`);
node.classList.add('reveal-ready');
// If the element is already visible at all in the initial viewport,
// reveal it immediately so the first section below the hero doesn't
// appear blank on page load.
const initialCheck = () => {
const rect = node.getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
if (rect.top < viewportHeight && rect.bottom > 0) {
node.classList.add('reveal-visible');
return true;
}
return false;
};
if (initialCheck()) {
return {
destroy() {}
};
}
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
+7 -90
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
import { reveal } from '$lib/actions/reveal';
import ServicesSection from '$lib/components/ServicesSection.svelte';
import { getImageMetadata } from '$lib/image-metadata';
import type { AboutPageContent, SiteSharedContent } from '$lib/types';
@@ -43,24 +43,7 @@
</section>
{/each}
<section use:reveal={{ delay: 40 }} class="about-services reveal-block">
<div class="about-inner">
<div class="about-section-heading">
<h2>{pageContent.servicesTitle}</h2>
</div>
<div class="about-service-grid">
{#each content.services as service}
<a class="about-service-card" href={service.href}>
<div class="about-service-icon" aria-hidden="true">
<Icon name={service.icon} />
</div>
<span>{service.title}</span>
</a>
{/each}
</div>
</div>
</section>
<ServicesSection services={content.services} heading={pageContent.servicesTitle} />
<section use:reveal={{ delay: 70 }} class="about-contact reveal-block">
<div class="about-inner">
@@ -98,7 +81,6 @@
}
.about-hero h1,
.about-section-heading h2,
.about-copy h2,
.about-contact-card h2 {
margin: 0;
@@ -109,8 +91,7 @@
color: #000;
}
.about-hero h1,
.about-section-heading {
.about-hero h1 {
text-align: center;
}
@@ -127,13 +108,13 @@
.about-section-grid {
display: grid;
grid-template-columns: minmax(0, 0.7fr) minmax(0, 1.3fr);
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 44px;
align-items: center;
}
.about-section-reverse {
grid-template-columns: minmax(0, 1.3fr) minmax(0, 0.7fr);
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.about-section-reverse .about-copy {
@@ -159,6 +140,8 @@
display: block;
width: 100%;
max-width: 460px;
aspect-ratio: 4 / 3;
height: auto;
margin-left: auto;
margin-right: auto;
border-radius: 28px;
@@ -180,65 +163,6 @@
transform: translate3d(0, 0, 0);
}
.about-services {
padding: 0 0 88px;
}
.about-section-heading {
margin-bottom: 34px;
}
.about-section-heading h2 {
font-size: clamp(28px, 3vw, 40px);
}
.about-service-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
}
.about-service-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
min-height: 200px;
padding: 28px 24px;
border-radius: 28px;
background: #fff;
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
color: #000;
text-align: center;
text-decoration: none;
transition:
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease;
}
@media (hover: hover) {
.about-service-card:hover {
transform: translateY(-6px) scale(1.012);
box-shadow: 0 20px 40px rgba(17, 20, 24, 0.09);
}
}
.about-service-card:active {
transform: translateY(-1px) scale(0.992);
}
.about-service-icon {
font-size: 42px;
color: #000;
}
.about-service-card span {
font-family: var(--font-head);
font-size: 24px;
line-height: 1.2;
}
.about-contact {
padding: 0 0 88px;
}
@@ -281,7 +205,6 @@
order: initial;
}
.about-service-grid,
.about-contact-grid {
grid-template-columns: 1fr;
}
@@ -297,7 +220,6 @@
}
.about-section,
.about-services,
.about-contact {
padding-bottom: 64px;
}
@@ -313,7 +235,6 @@
}
.about-copy h2,
.about-section-heading h2,
.about-contact-card h2 {
font-size: 30px;
}
@@ -323,10 +244,6 @@
line-height: 1.7;
}
.about-service-card {
min-height: 168px;
}
.about-contact-card {
padding: 30px 24px;
}
+1 -1
View File
@@ -372,7 +372,7 @@
id="location"
name="location"
required
placeholder="Neighborhood, street..."
placeholder="Suburb, street..."
class:input-invalid={errors.location}
on:input={() => clearError('location')}
/>
+19 -1
View File
@@ -9,6 +9,24 @@
{ label: 'Facebook', href: 'https://facebook.com/goodwalk.nz', external: true },
{ label: 'Google', href: 'https://g.page/r/CUsvrWPhkYrAEB0', external: true }
];
const aboutLink: LinkItem = { label: 'About Us', href: '/about' };
function withAboutLink(links: LinkItem[]) {
if (links.some((link) => link.href === aboutLink.href || link.label === aboutLink.label)) {
return links;
}
const contactIndex = links.findIndex((link) => link.href === '/contact-us');
if (contactIndex === -1) {
return [...links, aboutLink];
}
return [...links.slice(0, contactIndex), aboutLink, ...links.slice(contactIndex)];
}
$: navigationLinks = withAboutLink(footer.navigationLinks);
</script>
<footer>
@@ -40,7 +58,7 @@
<div class="footer-explore">
<p class="footer-col-label">Explore</p>
<ul class="footer-nav">
{#each footer.navigationLinks as link}
{#each navigationLinks as link}
<li>
<a
href={link.href}
+20
View File
@@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import Footer from './Footer.svelte';
import { homepageContent } from '$lib/content/homepage';
describe('Footer', () => {
it('adds the About Us link when footer content omits it', () => {
const footer = {
...homepageContent.footer,
navigationLinks: homepageContent.footer.navigationLinks.filter((link) => link.href !== '/about')
};
render(Footer, { footer });
const aboutLinks = screen.getAllByRole('link', { name: 'About Us' });
expect(aboutLinks).toHaveLength(1);
expect(aboutLinks[0]).toHaveAttribute('href', '/about');
});
});
@@ -391,6 +391,11 @@
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
}
.service-hero-media img {
aspect-ratio: 4 / 3;
height: auto;
}
.service-highlight {
padding: 0 0 96px;
}
+2 -1
View File
@@ -4,12 +4,13 @@
import type { IconCard } from '$lib/types';
export let services: IconCard[];
export let heading = 'What we do';
</script>
<section id="services" use:reveal={{ delay: 20 }} class="reveal-block">
<div class="services-inner">
<h2 class="section-heading">What we do</h2>
<h2 class="section-heading">{heading}</h2>
<div class="services-grid">
{#each services as service}
+1 -1
View File
@@ -6,7 +6,7 @@ export const aboutPageContent: AboutPageContent = {
{
title: 'Who we are',
body: [
"At GoodWalk, we're not your average dog walking service. We're a team of passionate dog lovers dedicated to providing top-notch care for your furry friends. Specializing in small dogs, we understand their unique needs firsthand, being small dog owners ourselves!",
"At GoodWalk, we're not your average dog walking service. We're a team of passionate dog lovers dedicated to providing top-notch care for your furry friends. Specialising in small dogs, we understand their unique needs firsthand, being small dog owners ourselves!",
"Our commitment to excellence has quickly made us a leader in Auckland Central's dog-walking scene. From pack walks to one-on-one sessions, we ensure the happiness and well-being of every dog in our care."
],
imageUrl: '/images/auckland-pack-walk-dog.jpg',
+8 -8
View File
@@ -6,7 +6,7 @@ export const dogWalkingContent: ServicePageContent = {
title: 'Walks for larger breeds, too!',
paragraphs: [
'Looking for personal attention for your big pup? Our professional dog walking service is perfect for larger dogs who walk well on leash and have good recall. Safety is paramount, so we only accommodate dogs with no reactivity issues. At Goodwalk, we pride ourselves on building relationships with both you and your furry family member.',
"Give your dog his best life while joining our growing community of happy pet parents! For those seeking extra care, we offer specialized one-on-one walks tailored to your dog's individual needs and personality"
"Give your dog his best life while joining our growing community of happy pet parents! For those seeking extra care, we offer specialised one-on-one walks tailored to your dog's individual needs and personality"
],
imageUrl:
'/images/auckland-large-dog-one-on-one-walk.jpg',
@@ -14,7 +14,7 @@ export const dogWalkingContent: ServicePageContent = {
},
highlight: {
eyebrow: '▼・ᴥ・▼',
title: 'Personalized adventures for your dog!',
title: 'Personalised adventures for your dog!',
imageUrl: '/images/auckland-dogs-outdoor-pack.jpg',
imageAlt: 'Goodwalk dogs gathered together outdoors'
},
@@ -46,16 +46,16 @@ export const dogWalkingContent: ServicePageContent = {
title: 'Benefits of our 1:1 walks',
items: [
{
title: 'Individualized Attention',
body: 'Large breeds receive personalized care and undivided attention from the walker, addressing their unique needs and preferences without competition from other dogs.'
title: 'Individualised Attention',
body: 'Large breeds receive personalised care and undivided attention from the walker, addressing their unique needs and preferences without competition from other dogs.'
},
{
title: 'Tailored Exercise',
body: 'Walkers can customize the pace, duration, and route of the walk to accommodate the energy levels and physical abilities of large breeds, providing an appropriate level of exercise tailored to their specific needs.'
body: 'Walkers can customise the pace, duration, and route of the walk to accommodate the energy levels and physical abilities of large breeds, providing an appropriate level of exercise tailored to their specific needs.'
},
{
title: 'Bonding and Socialization',
body: 'During one-on-one walks, large breeds bond closely with their walker and socialize with people and animals encountered, promoting confidence and social skills'
title: 'Bonding and Socialisation',
body: 'During one-on-one walks, large breeds bond closely with their walker and socialise with people and animals encountered, promoting confidence and social skills'
},
{
title: 'Enhanced safety',
@@ -63,7 +63,7 @@ export const dogWalkingContent: ServicePageContent = {
},
{
title: 'Training Opportunities',
body: 'One-on-one walks offer dedicated time for training and reinforcement of good behaviors, such as loose leash walking or obedience commands, helping large breeds develop and maintain positive habits.'
body: 'One-on-one walks offer dedicated time for training and reinforcement of good behaviours, such as loose leash walking or obedience commands, helping large breeds develop and maintain positive habits.'
},
{
title: 'Stress Reduction',
+3 -2
View File
@@ -88,7 +88,7 @@ export const homepageContent: HomePageContent = {
icon: 'fas fa-heart',
title: 'Kindness',
body:
'With gentle care and genuine affection, we make every walk a calm, happy experience. We use positive reinforcement to encourage good behavior because kindness is at the heart of everything we do.'
'With gentle care and genuine affection, we make every walk a calm, happy experience. We use positive reinforcement to encourage good behaviour because kindness is at the heart of everything we do.'
},
{
icon: 'fas fa-camera',
@@ -176,7 +176,7 @@ export const homepageContent: HomePageContent = {
nearbyCta: { label: 'Book a Meet & Greet', href: '#newlead' },
hoursLabel: 'Opening Hours',
hours: 'Monday to Friday, 8am - 4pm.',
faqTitle: "FAQ's",
faqTitle: 'FAQs',
faqs: [
{
question: 'Can any dog use your service?',
@@ -219,6 +219,7 @@ export const homepageContent: HomePageContent = {
{ label: '1:1 Walks', href: '/dog-walking' },
{ label: 'Puppy Visits', href: '/puppy-visits' },
{ label: 'Our Pricing', href: '/our-pricing' },
{ label: 'About Us', href: '/about' },
{ label: 'Contact Us', href: '/contact-us' }
],
contactLinks: [
+5 -5
View File
@@ -59,11 +59,11 @@ export const packWalksContent: ServicePageContent = {
title: 'Tiny Gang membership benefits',
items: [
{
title: 'Socialization with other dogs',
body: 'Tiny Gang pack walks help small and medium-sized dogs mingle and learn social skills from each other, boosting their confidence and positive behavior.'
title: 'Socialisation with other dogs',
body: 'Tiny Gang pack walks help small and medium-sized dogs mingle and learn social skills from each other, boosting their confidence and positive behaviour.'
},
{
title: 'Taliored peace',
title: 'Tailored pace',
body: 'Our handlers can adjust the pace and intensity of the walk to suit the energy levels and abilities of small and medium-sized dogs, ensuring a pleasant and enjoyable experience for all participants.'
},
{
@@ -75,8 +75,8 @@ export const packWalksContent: ServicePageContent = {
body: 'Tiny Gang pack walks foster stronger bonds between dogs and their walker, as well as between the dogs themselves, enhancing trust and companionship among the group.'
},
{
title: 'Individualized attention',
body: 'Small pack sizes allow for more personalized care and attention from the walker, addressing the unique needs and preferences of small and medium-sized breeds.'
title: 'Individualised attention',
body: 'Small pack sizes allow for more personalised care and attention from the walker, addressing the unique needs and preferences of small and medium-sized breeds.'
},
{
title: 'Safety',
+2 -2
View File
@@ -46,10 +46,10 @@ export const puppyVisitsContent: ServicePageContent = {
},
{
title: 'Reduce anxiety',
body: "With time your pup will know when to expect a visit, reducing the chances of accidents while you're away. With regular visits, your pup will feel loved and secure, minimizing any time spent at home alone."
body: "With time your pup will know when to expect a visit, reducing the chances of accidents while you're away. With regular visits, your pup will feel loved and secure, minimising any time spent at home alone."
},
{
title: 'Expert advise',
title: 'Expert advice',
body: "As experienced dog pawrents, we've been through it all with many adorable puppies. Consider us your go-to for any questions or concerns as your furry friend grows up."
}
]
+1 -1
View File
@@ -86,7 +86,7 @@ export const termsAndConditionsContent: LegalPageContent = {
{
type: 'list',
content: [
'5.1 Following the assessment walks, you must select your preferred permanent days of the week for walks, with a minimum commitment of one walk per week. Dogs must attend on these chosen days regularly for a minimum period of at least 6 months. Walks for dogs that show aggressive behavior may be cancelled with immediate effect.',
'5.1 Following the assessment walks, you must select your preferred permanent days of the week for walks, with a minimum commitment of one walk per week. Dogs must attend on these chosen days regularly for a minimum period of at least 6 months. Walks for dogs that show aggressive behaviour may be cancelled with immediate effect.',
'5.2 Walks will not take place during severe weather conditions such as high winds, heavy snow, heavy rain, thunder, and lightning. In these cases, your dog will be returned to your residence, or the walk may be shortened, cancelled or rescheduled.',
'5.3 If you decide to cancel a walk due to bad weather or heat, you agree to pay for the cancelled walk in full. However, we will endeavour to walk dogs in heavy rain or hot weather as long as we consider that it is safe for the dogs and our dog walkers.',
'5.4 As part of our service, we will work with you to reinforce recall training, leash training, and car behaviour for your dog, using positive reinforcement methods. However, we do not provide individual training sessions and are not responsible for training your dog or for its behaviour.',