diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d81d5e6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf + +*.sh text eol=lf +*.ps1 text eol=crlf diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 3228ece..807ce9b 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -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 \ + --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 diff --git a/scripts/deploy-from-git.sh b/scripts/deploy-from-git.sh new file mode 100644 index 0000000..4f99a1e --- /dev/null +++ b/scripts/deploy-from-git.sh @@ -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 --branch --deploy-path --compose-file --project-name + deploy-from-git.sh --repo-url [--branch ] [--ref ] --deploy-path --compose-file --project-name [--service ] + deploy-from-git.sh --repo-url [--branch ] [--ref ] --deploy-path --compose-file --project-name \ + [--service ] [--nginx-source ] [--nginx-target ] \ + [--nginx-compose-file ] [--nginx-project-name ] \ + [--maintenance-host-dir ] [--maintenance-flag ] \ + [--verify-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" diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index 2c10c16..be5b016 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -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);