General enquries feature

This commit is contained in:
2026-05-04 20:32:24 +12:00
parent bf9331bb5b
commit fa1bc1a615
27 changed files with 657 additions and 164 deletions
+2 -1
View File
@@ -70,7 +70,8 @@ mkdir -p /docker/goodwalk-svelte
It is created from [deploy.env.template](deploy.env.template). Current template contents:
```env
APP_VERSION=4.0.1
APP_VERSION=4.0.2
ENABLE_GENERAL_ENQUIRIES=false
TZ=Pacific/Auckland
POSTGRES_DB=goodwalk
+1 -1
View File
@@ -1,4 +1,4 @@
ARG APP_VERSION=4.0.1
ARG APP_VERSION=4.0.2
FROM node:22-alpine AS builder
ARG APP_VERSION
+2 -1
View File
@@ -1,4 +1,4 @@
APP_VERSION=4.0.1
APP_VERSION=4.0.2
TZ=Pacific/Auckland
POSTGRES_DB=goodwalk
@@ -10,6 +10,7 @@ RESEND_API_KEY=re_hcDByLp8_HEBW93wDirr7o9g16FgCeYNF
OWNER_EMAIL=mattcohen0@gmail.com
FROM_EMAIL=GoodWalk <info@goodwalk.co.nz>
REPLY_TO=info@goodwalk.co.nz
ENABLE_GENERAL_ENQUIRIES=false
FORM_MIN_SECONDS=4
FORM_MAX_SECONDS=7200
+6 -4
View File
@@ -3,13 +3,14 @@ services:
build:
context: .
args:
APP_VERSION: ${APP_VERSION:-4.0.1}
APP_VERSION: ${APP_VERSION:-4.0.2}
container_name: goodwalk_svelte_app
environment:
APP_VERSION: ${APP_VERSION:-4.0.1}
APP_VERSION: ${APP_VERSION:-4.0.2}
DATABASE_URL: postgresql://${POSTGRES_USER:-goodwalk}:${POSTGRES_PASSWORD_URLENCODED:-goodwalk}@db:5432/${POSTGRES_DB:-goodwalk}
NODE_ENV: production
PORT: 3000
ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false}
TZ: ${TZ:-Pacific/Auckland}
depends_on:
- db
@@ -24,14 +25,15 @@ services:
build:
context: ./mail-api
args:
APP_VERSION: ${APP_VERSION:-4.0.1}
APP_VERSION: ${APP_VERSION:-4.0.2}
container_name: goodwalk_svelte_mail_api
environment:
APP_VERSION: ${APP_VERSION:-4.0.1}
APP_VERSION: ${APP_VERSION:-4.0.2}
RESEND_API_KEY: ${RESEND_API_KEY}
OWNER_EMAIL: ${OWNER_EMAIL}
FROM_EMAIL: ${FROM_EMAIL:-GoodWalk <bookings@goodwalk.co.nz>}
REPLY_TO: ${REPLY_TO:-aless@goodwalk.co.nz}
ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false}
FORM_MIN_SECONDS: ${FORM_MIN_SECONDS:-4}
FORM_MAX_SECONDS: ${FORM_MAX_SECONDS:-7200}
RATE_LIMIT_WINDOW_SECONDS: ${RATE_LIMIT_WINDOW_SECONDS:-900}
+6 -4
View File
@@ -3,12 +3,13 @@ services:
build:
context: .
args:
APP_VERSION: ${APP_VERSION:-4.0.1}
APP_VERSION: ${APP_VERSION:-4.0.2}
environment:
APP_VERSION: ${APP_VERSION:-4.0.1}
APP_VERSION: ${APP_VERSION:-4.0.2}
DATABASE_URL: postgresql://${POSTGRES_USER:-goodwalk}:${POSTGRES_PASSWORD:-goodwalk}@db:5432/${POSTGRES_DB:-goodwalk}
NODE_ENV: production
PORT: ${APP_PORT:-3000}
ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false}
TZ: ${TZ:-Pacific/Auckland}
depends_on:
- db
@@ -18,13 +19,14 @@ services:
build:
context: ./mail-api
args:
APP_VERSION: ${APP_VERSION:-4.0.1}
APP_VERSION: ${APP_VERSION:-4.0.2}
environment:
APP_VERSION: ${APP_VERSION:-4.0.1}
APP_VERSION: ${APP_VERSION:-4.0.2}
RESEND_API_KEY: ${RESEND_API_KEY}
OWNER_EMAIL: ${OWNER_EMAIL}
FROM_EMAIL: ${FROM_EMAIL:-GoodWalk <bookings@goodwalk.co.nz>}
REPLY_TO: ${REPLY_TO:-aless@goodwalk.co.nz}
ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false}
TZ: ${TZ:-Pacific/Auckland}
restart: unless-stopped
+1 -1
View File
@@ -1,4 +1,4 @@
ARG APP_VERSION=4.0.1
ARG APP_VERSION=4.0.2
FROM python:3.12-slim
ARG APP_VERSION
Binary file not shown.
+172 -58
View File
@@ -89,6 +89,7 @@ def _load_config() -> dict:
"owner_email": os.environ["OWNER_EMAIL"],
"from_email": os.environ.get("FROM_EMAIL", "GoodWalk <bookings@goodwalk.co.nz>"),
"reply_to": os.environ.get("REPLY_TO", "aless@goodwalk.co.nz"),
"enable_general_enquiries": os.environ.get("ENABLE_GENERAL_ENQUIRIES", "false").strip().lower() in {"1", "true", "yes", "on", "enabled"},
"max_attempts": max(1, int(os.environ.get("MAIL_MAX_ATTEMPTS", "3"))),
"form_min_seconds": max(1, int(os.environ.get("FORM_MIN_SECONDS", "4"))),
"form_max_seconds": max(60, int(os.environ.get("FORM_MAX_SECONDS", "7200"))),
@@ -105,6 +106,7 @@ resend.api_key = _config["resend_api_key"]
OWNER_EMAIL = _config["owner_email"]
FROM_EMAIL = _config["from_email"]
REPLY_TO = _config["reply_to"]
ENABLE_GENERAL_ENQUIRIES = _config["enable_general_enquiries"]
MAX_SEND_ATTEMPTS = _config["max_attempts"]
FORM_MIN_SECONDS = _config["form_min_seconds"]
FORM_MAX_SECONDS = _config["form_max_seconds"]
@@ -116,12 +118,13 @@ RATE_LIMIT_MIN_INTERVAL_SECONDS = _config["rate_limit_min_interval_seconds"]
LOGO_URL = "https://www.goodwalk.co.nz/images/goodwalk-auckland-dog-walking-logo.png"
logger.info(
"Mail API config: version=%r timezone=%r from=%r reply_to=%r owner=%r max_attempts=%d form_min=%ss form_max=%ss rate_window=%ss per_ip=%d per_email=%d min_interval=%ss",
"Mail API config: version=%r timezone=%r from=%r reply_to=%r owner=%r general_enquiries=%r max_attempts=%d form_min=%ss form_max=%ss rate_window=%ss per_ip=%d per_email=%d min_interval=%ss",
APP_VERSION,
os.environ.get("TZ", "system-default"),
FROM_EMAIL,
REPLY_TO,
OWNER_EMAIL,
ENABLE_GENERAL_ENQUIRIES,
MAX_SEND_ATTEMPTS,
FORM_MIN_SECONDS,
FORM_MAX_SECONDS,
@@ -165,11 +168,12 @@ async def _request_logging_middleware(request: Request, call_next):
class BookingSubmission(BaseModel):
enquiryType: str = "booking"
fullName: str
email: EmailStr
phone: str
petName: str
location: str
petName: str = ""
location: str = ""
message: str = ""
services: list[str] = []
website: str = ""
@@ -300,6 +304,85 @@ def _is_honeypot_triggered(data: BookingSubmission) -> bool:
return bool(_trimmed(data.website))
def _is_general_enquiry(data: BookingSubmission) -> bool:
return _trimmed(data.enquiryType).lower() == "general"
def _enquiry_type_label(data: BookingSubmission) -> str:
return "General enquiry" if _is_general_enquiry(data) else "Booking enquiry"
def _validate_submission(request_id: str, data: BookingSubmission) -> None:
enquiry_type = _trimmed(data.enquiryType).lower()
if enquiry_type not in {"booking", "general"}:
logger.warning("[%s] rejected: invalid enquiryType=%r", request_id, data.enquiryType)
raise HTTPException(
status_code=400,
detail="Please choose a valid enquiry type and try again.",
)
if not _trimmed(data.fullName):
logger.warning("[%s] rejected: missing full name", request_id)
raise HTTPException(
status_code=400,
detail="Please enter your full name.",
)
if not _trimmed(data.phone):
logger.warning("[%s] rejected: missing phone number", request_id)
raise HTTPException(
status_code=400,
detail="Please enter your contact number.",
)
if _is_general_enquiry(data):
if not ENABLE_GENERAL_ENQUIRIES:
logger.warning("[%s] rejected: general enquiries are disabled", request_id)
raise HTTPException(
status_code=403,
detail="General enquiries are currently unavailable through this form.",
)
if not _trimmed(data.message):
logger.warning("[%s] rejected: missing general enquiry message", request_id)
raise HTTPException(
status_code=400,
detail="Please tell us how we can help.",
)
return
if not _trimmed(data.petName):
logger.warning("[%s] rejected: missing pet name", request_id)
raise HTTPException(
status_code=400,
detail="Please enter your dog's name.",
)
if not _trimmed(data.location):
logger.warning("[%s] rejected: missing location", request_id)
raise HTTPException(
status_code=400,
detail="Please enter your location.",
)
def _normalize_submission(data: BookingSubmission) -> None:
data.enquiryType = "general" if _is_general_enquiry(data) else "booking"
data.fullName = _trimmed(data.fullName)
data.phone = _trimmed(data.phone)
data.petName = _trimmed(data.petName)
data.location = _trimmed(data.location)
data.message = _trimmed(data.message)
data.referrer = _trimmed(data.referrer)
data.page = _trimmed(data.page)
data.services = [_trimmed(service) for service in data.services if _trimmed(service)]
if _is_general_enquiry(data):
data.petName = ""
data.location = ""
data.services = []
def _parse_ua(ua: str) -> str:
if not ua:
return "Unknown"
@@ -360,8 +443,46 @@ def _logo_header(badge_html: str = "", subtitle: str = "") -> str:
def client_email(data: BookingSubmission) -> str:
is_general = _is_general_enquiry(data)
services_text = ", ".join(data.services) if data.services else "Not specified"
message_row = _detail_row("About the dog", data.message) if data.message else ""
enquiry_summary_rows = [
_detail_row("Your name", data.fullName),
_detail_row("Email", str(data.email)),
_detail_row("Phone", data.phone),
_detail_row("Type", _enquiry_type_label(data)),
]
if is_general:
if data.message:
enquiry_summary_rows.append(_detail_row("Message", data.message))
intro_html = (
"We&rsquo;ve received your message and we will be in touch shortly."
)
next_steps_html = (
"We will review your message and reply within 1 business day."
)
logo_subtitle = "General enquiries and dog walking support"
else:
enquiry_summary_rows.extend(
[
_detail_row("Dog&rsquo;s name", data.petName),
_detail_row("Location", data.location),
_detail_row("Services", services_text),
]
)
if data.message:
enquiry_summary_rows.append(_detail_row("About the dog", data.message))
intro_html = (
"We&rsquo;ve received your enquiry and we will be in touch shortly to arrange "
"a <strong style=\"color:#213021;\">Meet &amp; Greet</strong> with you and "
f"{data.petName}."
)
next_steps_html = (
"We will review your details and reach out within 1 business day "
"to schedule a free Meet &amp; Greet. No commitment required &mdash; just a "
f"chance for {data.petName} to make a new best friend."
)
logo_subtitle = "Professional dog walking services"
return f"""<!DOCTYPE html>
<html lang="en">
@@ -380,7 +501,7 @@ def client_email(data: BookingSubmission) -> str:
style="max-width:600px;width:100%;border-radius:16px;overflow:hidden;
box-shadow:0 4px 24px rgba(0,0,0,0.08);">
{_logo_header(subtitle="Professional dog walking services")}
{_logo_header(subtitle=logo_subtitle)}
<!-- Body -->
<tr>
@@ -392,9 +513,7 @@ def client_email(data: BookingSubmission) -> str:
</h1>
<p style="margin:0 0 32px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:16px;color:#555;line-height:1.65;">
We&rsquo;ve received your enquiry and we will be in touch shortly to arrange
a <strong style="color:#213021;">Meet &amp; Greet</strong> with you and
{data.petName}.
{intro_html}
</p>
<!-- Details card -->
@@ -408,13 +527,7 @@ def client_email(data: BookingSubmission) -> str:
Your enquiry summary
</div>
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
{_detail_row("Your name", data.fullName)}
{_detail_row("Email", data.email)}
{_detail_row("Phone", data.phone)}
{_detail_row("Dog&rsquo;s name", data.petName)}
{_detail_row("Location", data.location)}
{_detail_row("Services", services_text)}
{message_row}
{"".join(enquiry_summary_rows)}
</table>
</td>
</tr>
@@ -431,9 +544,7 @@ def client_email(data: BookingSubmission) -> str:
</div>
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;color:#666;line-height:1.6;">
We will review your details and reach out within 1 business days
to schedule a free Meet &amp; Greet. No commitment required &mdash; just a
chance for {data.petName} to make a new best friend.
{next_steps_html}
</div>
</td>
</tr>
@@ -469,17 +580,20 @@ def client_email(data: BookingSubmission) -> str:
def owner_email(data: BookingSubmission, ip: str, browser: str) -> str:
is_general = _is_general_enquiry(data)
services_text = ", ".join(data.services) if data.services else ""
now = datetime.now()
submitted_at = now.strftime("%d %b %Y at %I:%M %p").lstrip("0")
first_name = data.fullName.split()[0] if data.fullName.strip() else "them"
email_title = "New GoodWalk Enquiry" if is_general else "New GoodWalk Lead"
message_label = "Message" if is_general else "About the dog"
message_block = f"""
<tr>
<td colspan="2" style="padding:16px 0 0;">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:11px;font-weight:700;letter-spacing:0.08em;color:#888;
text-transform:uppercase;margin-bottom:8px;">About the dog</div>
text-transform:uppercase;margin-bottom:8px;">{message_label}</div>
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;color:#444;line-height:1.6;background:#f0efe9;
border-radius:8px;padding:14px 16px;">{data.message}</div>
@@ -490,7 +604,7 @@ def owner_email(data: BookingSubmission, ip: str, browser: str) -> str:
padding:10px 28px;">
<span style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:18px;font-weight:700;color:#213021;">
&#x1F4E9;&nbsp; New lead!
&#x1F4E9;&nbsp; New enquiry!
</span>
</div>
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
@@ -500,13 +614,29 @@ def owner_email(data: BookingSubmission, ip: str, browser: str) -> str:
referrer_row = _meta_row("Came from", data.referrer) if data.referrer else _meta_row("Came from", "Direct / bookmark")
page_row = _meta_row("Page", data.page) if data.page else ""
detail_heading = "Enquiry details" if is_general else "Dog &amp; services"
detail_rows = [_detail_row("Type", _enquiry_type_label(data))]
if is_general:
if data.petName:
detail_rows.append(_detail_row("Dog", data.petName))
if data.location:
detail_rows.append(_detail_row("Location", data.location))
else:
detail_rows.extend(
[
_detail_row("Dog", data.petName),
_detail_row("Location", data.location),
_detail_row("Services", services_text),
]
)
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>New GoodWalk Lead</title>
<title>{email_title}</title>
</head>
<body style="margin:0;padding:0;background:#f2f2f0;">
@@ -597,36 +727,13 @@ def owner_email(data: BookingSubmission, ip: str, browser: str) -> str:
<!-- Dog & service details -->
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:11px;font-weight:700;letter-spacing:0.1em;color:#888;
text-transform:uppercase;margin-bottom:16px;">Dog &amp; services</div>
text-transform:uppercase;margin-bottom:16px;">{detail_heading}</div>
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
style="background:#f8f7f4;border-radius:12px;margin-bottom:28px;">
<tr><td style="padding:24px 28px;">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;width:80px;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Dog</td>
<td style="padding:6px 0 6px 16px;font-size:15px;font-weight:600;
color:#213021;font-family:-apple-system,BlinkMacSystemFont,
'Segoe UI',sans-serif;vertical-align:top;">{data.petName}</td>
</tr>
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Location</td>
<td style="padding:6px 0 6px 16px;font-size:14px;font-weight:500;color:#213021;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">{data.location}</td>
</tr>
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Services</td>
<td style="padding:6px 0 6px 16px;font-size:14px;color:#444;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">{services_text}</td>
</tr>
{"".join(detail_rows)}
{message_block}
</table>
</td></tr>
@@ -681,7 +788,7 @@ def owner_email(data: BookingSubmission, ip: str, browser: str) -> str:
border-top:1px solid #e8e8e4;">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:12px;color:#bbb;">
Sent automatically by GoodWalk booking form
Sent automatically by GoodWalk enquiry form
</div>
</td>
</tr>
@@ -753,15 +860,6 @@ async def submit_booking(data: BookingSubmission, request: Request):
ip = _get_ip(request)
browser = _parse_ua(request.headers.get("user-agent", ""))
name_parts = data.fullName.strip().split()
first_name = name_parts[0] if name_parts else "there"
logger.info(
"[%s] /submit: email=%s ip=%s browser=%r dog=%s services=%s page=%r",
request_id, data.email, ip, browser, data.petName, data.services, data.page,
)
logger.debug("[%s] full payload: %s", request_id, data.model_dump())
await _enforce_submit_rate_limits(request_id, ip, str(data.email))
_enforce_form_timing(request_id, data)
@@ -779,6 +877,18 @@ async def submit_booking(data: BookingSubmission, request: Request):
"ignored": True,
}
_validate_submission(request_id, data)
_normalize_submission(data)
name_parts = data.fullName.split()
first_name = name_parts[0] if name_parts else "there"
logger.info(
"[%s] /submit: type=%s email=%s ip=%s browser=%r dog=%s services=%s page=%r",
request_id, data.enquiryType, data.email, ip, browser, data.petName, data.services, data.page,
)
logger.debug("[%s] full payload: %s", request_id, data.model_dump())
failures: list[dict] = []
try:
@@ -787,7 +897,7 @@ async def submit_booking(data: BookingSubmission, request: Request):
"from": FROM_EMAIL,
"to": [data.email],
"reply_to": REPLY_TO,
"subject": f"We received your enquiry, {first_name}! 🐾",
"subject": f"We received your {'general enquiry' if _is_general_enquiry(data) else 'enquiry'}, {first_name}! 🐾",
"html": client_email(data),
},
label="client_email",
@@ -807,7 +917,11 @@ async def submit_booking(data: BookingSubmission, request: Request):
"from": FROM_EMAIL,
"to": [OWNER_EMAIL],
"reply_to": data.email,
"subject": f"New GoodWalk lead — {data.fullName} ({data.petName})",
"subject": (
f"New GoodWalk general enquiry — {data.fullName}"
if _is_general_enquiry(data)
else f"New GoodWalk lead — {data.fullName} ({data.petName})"
),
"html": owner_email(data, ip, browser),
},
label="owner_email",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "goodwalk-svelte-port",
"version": "4.0.1",
"version": "4.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "goodwalk-svelte-port",
"version": "4.0.1",
"version": "4.0.2",
"dependencies": {
"canvas-confetti": "^1.9.4",
"pg": "^8.13.1"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "goodwalk-svelte-port",
"version": "4.0.1",
"version": "4.0.2",
"private": true,
"type": "module",
"scripts": {
+10 -3
View File
@@ -4,6 +4,7 @@
import type { BookingContent } from '$lib/types';
export let booking: BookingContent;
export let allowGeneralEnquiry = false;
const email = 'info@goodwalk.co.nz';
const phone = '(022) 642 1011';
@@ -12,8 +13,14 @@
<main class="booking-page">
<section class="booking-page-hero">
<div class="booking-page-inner">
<h1>Book a Meet &amp; Greet</h1>
<p class="booking-page-sub">Fill in the form below and we'll be in touch to arrange a free introduction.</p>
<h1>Contact Us</h1>
<p class="booking-page-sub">
{#if allowGeneralEnquiry}
Fill in the form below to book a Meet &amp; Greet or send a general enquiry.
{:else}
Fill in the form below and we'll be in touch to arrange a free introduction.
{/if}
</p>
<div class="booking-page-contact">
<a href="mailto:{email}" class="booking-contact-link">
<Icon name="fas fa-envelope" />
@@ -27,7 +34,7 @@
</div>
</section>
<BookingSection {booking} />
<BookingSection {booking} {allowGeneralEnquiry} />
</main>
<style>
+175 -70
View File
@@ -7,10 +7,13 @@
import type { BookingContent } from '$lib/types';
export let booking: BookingContent;
export let allowGeneralEnquiry = false;
type EnquiryType = 'booking' | 'general';
let step = 1;
$: headingParts = splitBookingTitle(booking.title);
let enquiryType: EnquiryType = 'booking';
let fullName = '';
let email = '';
let phone = '';
@@ -59,12 +62,26 @@
const defaultDogIntro =
'Tell us about your dog and where you are based so we can plan the right Meet & Greet.';
const defaultGeneralIntro =
'Need to send feedback, make a complaint, or ask a business question? Choose general enquiry and tell us what you need.';
const defaultGeneralSubtitle =
'Almost there — just your contact details so we can reply properly to your message.';
$: dogIntro = booking.dogIntro?.trim() || defaultDogIntro;
$: hasBanner = Boolean(booking.subtitle?.trim());
$: generalIntro = booking.generalIntro?.trim() || defaultGeneralIntro;
$: hasServices = booking.serviceOptions.length > 0;
$: if (!allowGeneralEnquiry && enquiryType === 'general') {
enquiryType = 'booking';
}
$: isGeneralEnquiry = allowGeneralEnquiry && enquiryType === 'general';
$: ownerSubtitle = isGeneralEnquiry
? booking.generalSubtitle?.trim() || defaultGeneralSubtitle
: booking.subtitle;
$: ownerStepLabel = booking.ownerStepLabel?.trim() || 'Owner Details';
$: dogStepLabel = booking.dogStepLabel?.trim() || 'Your dog';
$: firstStepLabel = isGeneralEnquiry ? 'Your enquiry' : dogStepLabel;
$: firstStepIntro = isGeneralEnquiry ? generalIntro : dogIntro;
$: successPetName = petName.trim() || 'your dog';
onMount(() => {
formStartedAt = Date.now();
@@ -99,22 +116,47 @@
selectedServices = selectedServices.filter((item) => item !== service);
}
function validateDogStep(): boolean {
function setEnquiryType(nextType: EnquiryType) {
enquiryType = nextType;
if (nextType === 'general') {
petName = '';
location = '';
selectedServices = [];
}
errors = {};
}
function validateFirstStep(): boolean {
const next: Record<string, string> = {};
if (!petName.trim()) next.petName = "Please enter your dog's name";
if (!location.trim()) next.location = 'Please enter your location';
if (isGeneralEnquiry) {
if (!message.trim()) next.message = 'Please tell us how we can help';
} else {
if (!petName.trim()) next.petName = "Please enter your dog's name";
if (!location.trim()) next.location = 'Please enter your location';
}
errors = next;
if (next.petName) { petNameInput?.focus(); return false; }
if (next.location) { locationInput?.focus(); return false; }
if (next.petName) {
petNameInput?.focus();
return false;
}
if (next.location) {
locationInput?.focus();
return false;
}
if (next.message) {
return false;
}
return true;
}
function goToOwnerStep() {
if (!validateDogStep()) return;
if (!validateFirstStep()) return;
errors = {};
step = 2;
}
@@ -152,13 +194,19 @@
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fullName, email, phone, petName, location, message,
services: selectedServices,
enquiryType,
fullName,
email,
phone,
petName: isGeneralEnquiry ? '' : petName,
location: isGeneralEnquiry ? '' : location,
message,
services: isGeneralEnquiry ? [] : selectedServices,
website,
formStartedAt,
referrer: document.referrer,
page: window.location.href,
}),
})
});
if (!res.ok) {
@@ -185,8 +233,9 @@
{#if submitted}
<SuccessModal
firstName={fullName.split(' ')[0]}
{petName}
petName={successPetName}
{email}
{enquiryType}
onClose={() => (submitted = false)}
/>
{/if}
@@ -194,6 +243,7 @@
{#if showErrorModal}
<ErrorModal
detail={submitErrorDetail}
{enquiryType}
onClose={() => (showErrorModal = false)}
onRetry={() => (showErrorModal = false)}
/>
@@ -212,7 +262,7 @@
on:click={() => (step = 1)}
>
<span class="booking-step-number">1</span>
<span class="booking-step-label">{dogStepLabel}</span>
<span class="booking-step-label">{firstStepLabel}</span>
</button>
<span class="booking-step-divider" aria-hidden="true"></span>
<button
@@ -247,70 +297,120 @@
{#if step === 1}
<div class="booking-panel">
{#if dogIntro}
<div class="booking-panel-banner">{dogIntro}</div>
{#if firstStepIntro}
<div class="booking-panel-banner">{firstStepIntro}</div>
{/if}
<div class:booking-card-grid-with-banner={Boolean(dogIntro)} class="booking-card-grid booking-card-grid-dog">
<div class="booking-field-card" class:booking-field-card-invalid={errors.petName}>
<label for="petName">
<Icon name="fas fa-dog" />&nbsp;Pet's Name <span class="booking-required">*</span>
</label>
<input
bind:this={petNameInput}
bind:value={petName}
type="text"
id="petName"
name="petName"
required
placeholder="Your dog's name"
class:input-invalid={errors.petName}
on:input={() => clearError('petName')}
/>
{#if errors.petName}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.petName}
<div class:booking-card-grid-with-banner={Boolean(firstStepIntro)} class="booking-card-grid booking-card-grid-dog">
{#if allowGeneralEnquiry}
<div class="booking-field-card booking-field-card-full">
<label>
<Icon name="fas fa-comments" />&nbsp;Enquiry type
</label>
<div class="booking-toggle-group" role="radiogroup" aria-label="Enquiry type">
<label class="booking-toggle-option">
<input
type="radio"
name="enquiryType"
value="booking"
checked={enquiryType === 'booking'}
on:change={() => setEnquiryType('booking')}
/>
<span class="booking-toggle-indicator" aria-hidden="true"></span>
<span>Book a Meet &amp; Greet</span>
</label>
<label class="booking-toggle-option">
<input
type="radio"
name="enquiryType"
value="general"
checked={enquiryType === 'general'}
on:change={() => setEnquiryType('general')}
/>
<span class="booking-toggle-indicator" aria-hidden="true"></span>
<span>General enquiry</span>
</label>
</div>
<p class="booking-help-text">
General enquiries cover feedback, complaints, business enquiries, and other non-booking messages.
</p>
{/if}
</div>
</div>
{/if}
<div class="booking-field-card booking-field-card-wide" class:booking-field-card-invalid={errors.location}>
<label for="location">
<Icon name="fas fa-location-dot" />&nbsp;Location <span class="booking-required">*</span>
{#if !isGeneralEnquiry}
<div class="booking-field-card" class:booking-field-card-invalid={errors.petName}>
<label for="petName">
<Icon name="fas fa-dog" />&nbsp;Dog's Name <span class="booking-required">*</span>
</label>
<input
bind:this={petNameInput}
bind:value={petName}
type="text"
id="petName"
name="petName"
required
placeholder="Your dog's name"
class:input-invalid={errors.petName}
on:input={() => clearError('petName')}
/>
{#if errors.petName}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.petName}
</p>
{/if}
</div>
<div class="booking-field-card booking-field-card-wide" class:booking-field-card-invalid={errors.location}>
<label for="location">
<Icon name="fas fa-location-dot" />&nbsp;Location <span class="booking-required">*</span>
</label>
<input
bind:this={locationInput}
bind:value={location}
type="text"
id="location"
name="location"
required
placeholder="Neighborhood, street..."
class:input-invalid={errors.location}
on:input={() => clearError('location')}
/>
{#if errors.location}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.location}
</p>
{/if}
</div>
{/if}
<div class="booking-field-card booking-field-card-full" class:booking-field-card-invalid={errors.message}>
<label for="message">
<Icon name="fas fa-comment" />&nbsp;{isGeneralEnquiry ? 'Your Message' : 'About Your Dog'}
{#if isGeneralEnquiry}<span class="booking-required">*</span>{/if}
</label>
<input
bind:this={locationInput}
bind:value={location}
type="text"
id="location"
name="location"
required
placeholder="Neighborhood, street..."
class:input-invalid={errors.location}
on:input={() => clearError('location')}
/>
{#if errors.location}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.location}
</p>
{/if}
</div>
<div class="booking-field-card booking-field-card-full">
<label for="message"><Icon name="fas fa-comment" />&nbsp;About Your Dog</label>
<textarea
bind:value={message}
id="message"
name="message"
rows="4"
placeholder="Describe your pet, any special needs, or anything we should know."
placeholder={isGeneralEnquiry
? 'Tell us if this is feedback, a complaint, a business enquiry, or anything else we should know.'
: 'Describe your pet, any special needs, or anything we should know.'}
class:input-invalid={errors.message}
on:input={() => clearError('message')}
></textarea>
{#if errors.message}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.message}
</p>
{/if}
</div>
</div>
{#if hasServices}
{#if hasServices && !isGeneralEnquiry}
<div class="booking-service-row">
<span class="booking-service-label"><Icon name="fas fa-paw" />&nbsp;Services</span>
<div class="booking-service-options">
@@ -340,19 +440,24 @@
</button>
</div>
{:else}
<input type="hidden" name="petName" value={petName} />
<input type="hidden" name="location" value={location} />
<input type="hidden" name="enquiryType" value={enquiryType} />
{#if !isGeneralEnquiry}
<input type="hidden" name="petName" value={petName} />
<input type="hidden" name="location" value={location} />
{/if}
<input type="hidden" name="message" value={message} />
{#each selectedServices as service}
<input type="hidden" name="services" value={service} />
{/each}
{#if !isGeneralEnquiry}
{#each selectedServices as service}
<input type="hidden" name="services" value={service} />
{/each}
{/if}
<div class="booking-panel">
{#if hasBanner}
<div class="booking-panel-banner">{booking.subtitle}</div>
{#if ownerSubtitle}
<div class="booking-panel-banner">{ownerSubtitle}</div>
{/if}
<div class:booking-card-grid-with-banner={hasBanner} class="booking-card-grid booking-card-grid-owner">
<div class:booking-card-grid-with-banner={Boolean(ownerSubtitle)} class="booking-card-grid booking-card-grid-owner">
<div class="booking-field-card booking-field-card-group booking-field-card-full">
<div class="booking-field-group booking-field-group-owner">
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.fullName}>
+69 -3
View File
@@ -16,6 +16,8 @@ describe('BookingSection', () => {
booking: homepageContent.booking
});
expect(screen.queryByLabelText(/General enquiry/i)).not.toBeInTheDocument();
await fireEvent.click(container.querySelector('.booking-next-button')!);
expect(screen.getByText("Please enter your dog's name")).toBeInTheDocument();
@@ -27,7 +29,7 @@ describe('BookingSection', () => {
booking: homepageContent.booking
});
await fireEvent.input(screen.getByLabelText(/Pet's Name/i), {
await fireEvent.input(screen.getByLabelText(/Dog's Name/i), {
target: { value: 'Maya' }
});
await fireEvent.input(screen.getByLabelText(/Location/i), {
@@ -56,7 +58,7 @@ describe('BookingSection', () => {
await fireEvent.click(screen.getByLabelText('Pack Walks'));
await fireEvent.click(screen.getByLabelText('Other Services'));
await fireEvent.input(screen.getByLabelText(/Pet's Name/i), {
await fireEvent.input(screen.getByLabelText(/Dog's Name/i), {
target: { value: 'Maya' }
});
await fireEvent.input(screen.getByLabelText(/Location/i), {
@@ -91,6 +93,7 @@ describe('BookingSection', () => {
const payload = JSON.parse(fetchMock.mock.calls[0][1].body as string);
expect(payload).toMatchObject({
enquiryType: 'booking',
fullName: 'Alex Walker',
email: 'alex@example.com',
phone: '021 123 4567',
@@ -113,6 +116,69 @@ describe('BookingSection', () => {
);
});
it('allows general enquiries without dog or service details', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({})
});
vi.stubGlobal('fetch', fetchMock);
const { container } = render(BookingSection, {
booking: homepageContent.booking,
allowGeneralEnquiry: true
});
await fireEvent.input(screen.getByLabelText(/Dog's Name/i), {
target: { value: 'Maya' }
});
await fireEvent.input(screen.getByLabelText(/Location/i), {
target: { value: 'Grey Lynn' }
});
await fireEvent.click(screen.getByLabelText('Pack Walks'));
await fireEvent.click(screen.getByLabelText(/General enquiry/i));
expect(screen.queryByLabelText(/Dog's Name/i)).not.toBeInTheDocument();
expect(screen.queryByText('Pack Walks')).not.toBeInTheDocument();
await fireEvent.click(container.querySelector('.booking-next-button')!);
expect(screen.getByText('Please tell us how we can help')).toBeInTheDocument();
await fireEvent.input(screen.getByLabelText(/Your Message/i), {
target: { value: 'I would like to discuss a business partnership.' }
});
await fireEvent.click(container.querySelector('.booking-next-button')!);
await fireEvent.input(screen.getByLabelText(/Full Name/i), {
target: { value: 'Alex Walker' }
});
await fireEvent.input(screen.getByLabelText(/^Email/i), {
target: { value: 'alex@example.com' }
});
await fireEvent.input(screen.getByLabelText(/Contact #/i), {
target: { value: '021 123 4567' }
});
await fireEvent.click(container.querySelector('.booking-submit-button')!);
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
const payload = JSON.parse(fetchMock.mock.calls[0][1].body as string);
expect(payload).toMatchObject({
enquiryType: 'general',
fullName: 'Alex Walker',
email: 'alex@example.com',
phone: '021 123 4567',
petName: '',
location: '',
message: 'I would like to discuss a business partnership.',
services: []
});
expect(screen.getByRole('dialog', { name: /Enquiry confirmed/i })).toBeInTheDocument();
expect(screen.getByText(/Your message is with us!/i)).toBeInTheDocument();
});
it('shows the API error message when submission fails', async () => {
vi.stubGlobal(
'fetch',
@@ -126,7 +192,7 @@ describe('BookingSection', () => {
booking: homepageContent.booking
});
await fireEvent.input(screen.getByLabelText(/Pet's Name/i), {
await fireEvent.input(screen.getByLabelText(/Dog's Name/i), {
target: { value: 'Maya' }
});
await fireEvent.input(screen.getByLabelText(/Location/i), {
+6 -2
View File
@@ -1,14 +1,18 @@
<script lang="ts">
export let email = 'info@goodwalk.co.nz';
export let enquiryType: 'booking' | 'general' = 'booking';
export let onClose: () => void;
export let onRetry: (() => void) | null = null;
export let detail = '';
$: isGeneralEnquiry = enquiryType === 'general';
$: mailtoHref =
`mailto:${email}` +
`?subject=${encodeURIComponent('Booking enquiry')}` +
`?subject=${encodeURIComponent(isGeneralEnquiry ? 'General enquiry' : 'Booking enquiry')}` +
`&body=${encodeURIComponent(
'Hi Aless,\n\nI tried to submit the booking form but it didnt go through. Here are my details:\n\nName:\nPhone:\nDogs name:\nLocation:\n\nThanks!'
isGeneralEnquiry
? 'Hi Aless,\n\nI tried to submit the contact form but it didnt go through. Here are my details:\n\nName:\nPhone:\nMessage:\n\nThanks!'
: 'Hi Aless,\n\nI tried to submit the booking form but it didnt go through. Here are my details:\n\nName:\nPhone:\nDogs name:\nLocation:\n\nThanks!'
)}`;
function handleKeydown(event: KeyboardEvent) {
+28 -8
View File
@@ -5,8 +5,11 @@
export let firstName: string;
export let petName: string;
export let email: string;
export let enquiryType: 'booking' | 'general' = 'booking';
export let onClose: () => void;
$: isGeneralEnquiry = enquiryType === 'general';
onMount(() => {
const duration = 3200;
const end = Date.now() + duration;
@@ -44,7 +47,7 @@
class="modal-backdrop"
role="dialog"
aria-modal="true"
aria-label="Booking confirmed"
aria-label={isGeneralEnquiry ? 'Enquiry confirmed' : 'Booking confirmed'}
on:click|self={onClose}
on:keydown={(e) => e.key === 'Escape' && onClose()}
tabindex="-1"
@@ -56,18 +59,35 @@
<div class="modal-paw" aria-hidden="true">🐾</div>
<h2 class="modal-heading">You&rsquo;re on our radar!</h2>
<h2 class="modal-heading">
{#if isGeneralEnquiry}
Your message is with us!
{:else}
You&rsquo;re on our radar!
{/if}
</h2>
<p class="modal-body">
Thanks, <strong>{firstName}</strong>! We&rsquo;ve sent a confirmation to
<strong>{email}</strong> and Aless will be in touch soon to arrange a
Meet &amp; Greet with <strong>{petName}</strong>.
</p>
{#if isGeneralEnquiry}
<p class="modal-body">
Thanks, <strong>{firstName}</strong>! We&rsquo;ve sent a confirmation to
<strong>{email}</strong> and Aless will be in touch soon about your enquiry.
</p>
{:else}
<p class="modal-body">
Thanks, <strong>{firstName}</strong>! We&rsquo;ve sent a confirmation to
<strong>{email}</strong> and Aless will be in touch soon to arrange a
Meet &amp; Greet with <strong>{petName}</strong>.
</p>
{/if}
<div class="modal-divider"></div>
<p class="modal-sub">
In the meantime, feel free to follow along on Instagram for daily walks and happy dogs!
{#if isGeneralEnquiry}
We aim to reply within 1 business day.
{:else}
In the meantime, feel free to follow along on Instagram for daily walks and happy dogs!
{/if}
</p>
<button class="modal-btn" type="button" on:click={onClose}>
+5 -1
View File
@@ -158,10 +158,14 @@ export const homepageContent: HomePageContent = {
title: "Let's meet!",
subtitle:
"Almost there — just your contact details so we can reach out to arrange your free, no-obligation Meet & Greet.",
generalSubtitle:
"Almost there — just your contact details so we can reply properly to your message.",
formAction: '/contact-us',
serviceOptions: ['Pack Walks', '1:1 Walks', 'Puppy Visits', 'Other Services'],
ownerStepLabel: 'Your details',
dogStepLabel: 'Your dog'
dogStepLabel: 'Your dog',
generalIntro:
'Got feedback, a complaint, or a business enquiry? Choose general enquiry and send us the details without filling in dog or service information.'
},
info: {
title: 'Locations & Hours',
+1 -1
View File
@@ -37,7 +37,7 @@ export const staticPages = {
},
'contact-us': {
title: 'Contact Us',
description: 'Book a Meet & Greet with Goodwalk Auckland dog walking services.',
description: 'Book a Meet & Greet or send a general enquiry to Goodwalk Auckland dog walking services.',
canonicalPath: '/contact-us'
},
'terms-and-conditions': {
+21
View File
@@ -0,0 +1,21 @@
function parseBooleanFlag(value: string | undefined, defaultValue = false) {
if (value == null) {
return defaultValue;
}
const normalized = value.trim().toLowerCase();
if (['1', 'true', 'yes', 'on', 'enabled'].includes(normalized)) {
return true;
}
if (['0', 'false', 'no', 'off', 'disabled'].includes(normalized)) {
return false;
}
return defaultValue;
}
export function isGeneralEnquiryEnabled() {
return parseBooleanFlag(process.env.ENABLE_GENERAL_ENQUIRIES, false);
}
+69
View File
@@ -191,6 +191,13 @@
color: #34363a;
}
.booking-help-text {
margin: 14px 0 0;
font-size: 13px;
line-height: 1.5;
color: #666;
}
.booking-required {
color: var(--yellow);
}
@@ -238,6 +245,68 @@
box-shadow: 0 10px 30px rgba(17, 20, 24, 0.04);
}
.booking-toggle-group {
display: flex;
flex-wrap: wrap;
gap: 14px;
}
.booking-toggle-option {
display: inline-flex;
align-items: center;
gap: 12px;
min-height: 56px;
padding: 14px 18px;
border: 3px solid #111;
border-radius: 18px;
font-family: var(--font-head);
font-size: 15px;
font-weight: 700;
color: #34363a;
cursor: pointer;
transition:
background 0.2s,
border-color 0.2s,
transform 0.15s ease;
}
.booking-toggle-option:hover {
background: #f6f0e5;
}
.booking-toggle-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.booking-toggle-indicator {
width: 24px;
height: 24px;
border: 3px solid #111;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
transition:
background 0.2s,
border-color 0.2s;
}
.booking-toggle-option input:checked + .booking-toggle-indicator {
border-color: #111;
background: var(--yellow);
}
.booking-toggle-option input:checked + .booking-toggle-indicator::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #111;
}
.booking-service-options {
display: flex;
+17
View File
@@ -469,6 +469,23 @@
gap: 12px 18px;
}
.booking-toggle-group {
flex-direction: column;
}
.booking-toggle-option {
width: 100%;
border-width: 2px;
border-radius: 16px;
padding: 14px 16px;
}
.booking-toggle-indicator {
width: 22px;
height: 22px;
border-width: 2px;
}
.booking-check-option {
font-size: 15px;
}
+2
View File
@@ -77,11 +77,13 @@ export interface TestimonialContent {
export interface BookingContent {
title: string;
subtitle: string;
generalSubtitle?: string;
formAction: string;
serviceOptions: string[];
ownerStepLabel?: string;
dogStepLabel?: string;
dogIntro?: string;
generalIntro?: string;
}
export interface ServicePricingPlan {
+13 -2
View File
@@ -1,5 +1,6 @@
import { error, redirect } from '@sveltejs/kit';
import { staticPages, type StaticPageSlug } from '$lib/content/static-pages';
import { isGeneralEnquiryEnabled } from '$lib/server/feature-flags';
import { getSharedPageContent } from '$lib/server/content';
export async function load({ params }) {
@@ -12,14 +13,24 @@ export async function load({ params }) {
}
const slug = params.slug as StaticPageSlug;
const page = staticPages[slug];
const generalEnquiryEnabled = isGeneralEnquiryEnabled();
const sourcePage = staticPages[slug];
if (!page) {
if (!sourcePage) {
throw error(404, 'Page not found');
}
const page =
slug === 'contact-us' && !generalEnquiryEnabled
? {
...sourcePage,
description: 'Book a Meet & Greet with Goodwalk Auckland dog walking services.'
}
: sourcePage;
return {
content: await getSharedPageContent(),
generalEnquiryEnabled,
page,
slug
};
+1 -1
View File
@@ -193,7 +193,7 @@
{:else if data.slug === 'privacy-policy'}
<LegalPage pageContent={privacyPolicyContent} />
{:else if data.slug === 'contact-us'}
<BookingPage booking={data.content.booking} />
<BookingPage booking={data.content.booking} allowGeneralEnquiry={data.generalEnquiryEnabled} />
{:else}
<main class="static-page">
<section class="static-page-hero">
@@ -15,6 +15,7 @@ import { load } from './+page.server';
describe('static slug page server load', () => {
beforeEach(() => {
getSharedPageContent.mockReset();
vi.unstubAllEnvs();
});
it('redirects the legacy about-us slug to /about', async () => {
@@ -42,8 +43,35 @@ describe('static slug page server load', () => {
await expect(load({ params: { slug: 'pack-walks' } } as never)).resolves.toEqual({
content: sharedPageContent,
generalEnquiryEnabled: false,
page: staticPages['pack-walks'],
slug: 'pack-walks'
});
});
it('keeps general enquiries disabled on contact-us by default', async () => {
getSharedPageContent.mockResolvedValue(sharedPageContent);
await expect(load({ params: { slug: 'contact-us' } } as never)).resolves.toEqual({
content: sharedPageContent,
generalEnquiryEnabled: false,
page: {
...staticPages['contact-us'],
description: 'Book a Meet & Greet with Goodwalk Auckland dog walking services.'
},
slug: 'contact-us'
});
});
it('enables general enquiries on contact-us when the env flag is turned on', async () => {
vi.stubEnv('ENABLE_GENERAL_ENQUIRIES', 'enabled');
getSharedPageContent.mockResolvedValue(sharedPageContent);
await expect(load({ params: { slug: 'contact-us' } } as never)).resolves.toEqual({
content: sharedPageContent,
generalEnquiryEnabled: true,
page: staticPages['contact-us'],
slug: 'contact-us'
});
});
});
+17
View File
@@ -43,4 +43,21 @@ describe('static slug route page', () => {
expect(screen.queryByText('Why people choose us!')).not.toBeInTheDocument();
}
);
it('shows the general enquiry option on the contact page only', () => {
const { rerender } = render(SlugPage, {
data: {
...createStaticRouteData('contact-us'),
generalEnquiryEnabled: true
}
});
expect(screen.getByLabelText(/General enquiry/i)).toBeInTheDocument();
rerender({
data: createStaticRouteData('pack-walks')
});
expect(screen.queryByLabelText(/General enquiry/i)).not.toBeInTheDocument();
});
});
+1
View File
@@ -12,6 +12,7 @@ describe('home page route', () => {
expect(screen.getAllByText("Your Dog's Day!").length).toBeGreaterThan(0);
expect(document.body.textContent).toContain('Happy pets,');
expect(screen.getByText('Locations & Hours')).toBeInTheDocument();
expect(screen.queryByLabelText(/General enquiry/i)).not.toBeInTheDocument();
expect(document.title).toBe('Home | Auckland Dog Walking | Goodwalk');
expect(document.head.innerHTML).toContain('FAQPage');
expect(document.head.innerHTML).toContain('https://www.goodwalk.co.nz/images/auckland-dog-walking-happy-dog-hero.png');
+1
View File
@@ -18,6 +18,7 @@ export function createHomepageRouteData() {
export function createStaticRouteData(slug: StaticPageSlug) {
return {
content: sharedPageContent,
generalEnquiryEnabled: false,
page: staticPages[slug],
slug
};