A production-style marketing and booking website for Ortega's House Cleaning, a family-owned house cleaning business serving the Seattle/Tacoma area.
This project was built through Web Impact, a University of Washington club that partners with small local businesses to create free websites while giving club members real client project experience. Ortega's Cleaning did not have an established digital presence, so this site gives the business a public home for services, trust-building content, reviews, contact information, and booking requests.
The live product is more than a static marketing site. Customers can submit booking requests, receive a booking ID by email, use that ID to view their booking later, cancel a booking, and send quote/contact messages. The owner receives email notifications for new bookings, cancellations, and contact messages.
- What The Site Does
- Tech Stack
- Project Structure
- Route Architecture
- Data And Email Flows
- Environment Variables
- Local Development
- External Services
- Deployment
- Maintenance Notes
- Known Limitations And Risks
- Recreating The Project From Scratch
The website has four main responsibilities:
- Present Ortega's House Cleaning as a trustworthy local cleaning business.
- Explain service offerings, service areas, company background, reviews, and gallery photos.
- Accept booking requests and store them in MongoDB.
- Send transactional email notifications through EmailJS.
Core customer flows:
- A visitor lands on the homepage, reviews services and areas served, then clicks a booking CTA.
- A customer submits the booking form on
/book. - The booking is saved in MongoDB.
- The customer receives a confirmation email containing the booking ID.
- The owner receives a new booking email with the full booking details.
- The customer can return to
/book, enter their booking ID, and view the booking page. - The customer can cancel from the booking page.
- Cancellation updates the MongoDB document and sends cancellation emails to both customer and owner.
- A visitor can submit the contact form on
/contact. - Contact messages are emailed to the owner, and the customer receives a confirmation email.
This is a Next.js App Router project.
| Category | Technology |
|---|---|
| Framework | Next.js 16.1.1 |
| UI | React 19.2.3, React DOM 19.2.3 |
| Styling | Tailwind CSS v4, global CSS variables in app/globals.css |
| Animation | Framer Motion |
| Database | MongoDB with Mongoose |
| EmailJS Node SDK | |
| CMS | Sanity Studio and next-sanity |
| Maps | Leaflet and React Leaflet |
| Package manager | npm, with package-lock.json committed |
| Linting | ESLint with Next core web vitals config |
There is currently no automated test script.
.
|-- app/
| |-- api/
| | |-- book/
| | | |-- route.js
| | | `-- [id]/
| | | |-- route.js
| | | `-- cancel/route.js
| | `-- contact/route.js
| |-- about/page.js
| |-- book/
| | |-- page.js
| | `-- [id]/
| | |-- page.js
| | `-- cancel/page.js
| |-- contact/page.js
| |-- gallery/page.js
| |-- reviews/page.js
| |-- services/page.js
| |-- studio/[[...tool]]/page.jsx
| |-- layout.js
| |-- page.js
| `-- globals.css
|-- components/
| |-- booking/
| |-- buttons/
| |-- contact/
| |-- gallery/
| `-- shared site components
|-- public/
| |-- gallery/
| `-- images and SVG assets
|-- sanity/
| |-- lib/
| |-- schemaTypes/
| `-- desk.js
|-- sanity.config.js
|-- sanity.cli.js
|-- package.json
`-- README.md
Important organization notes:
app/contains the App Router pages and API route handlers.components/contains reusable UI components, grouped by feature where useful.components/booking/contains the booking form, booking lookup box, layout wrappers, field wrapper, and terms content.components/contact/contains the quote/contact form, FAQ item, and contact banner.components/gallery/contains the carousel, gallery images, pips, arrows, statistics, and gallery section components.app/lib/contactInfo.jscentralizes owner contact environment variables for server-rendered components.app/lib/email/sendEmail.jsbuilds all booking, cancellation, and contact email HTML.sanity/contains the Sanity client, schema, image helpers, live helpers, and studio desk structure.
| Route | File | Purpose |
|---|---|---|
/ |
app/page.js |
Homepage with navbar, hero, service preview, areas served, featured reviews, and CTA banner. |
/about |
app/about/page.js |
Business story, 20+ years of service messaging, core values, and booking CTA. |
/services |
app/services/page.js |
Residential, commercial, deep cleaning, recurring services, service areas, and value props. |
/gallery |
app/gallery/page.js |
Photo carousel and gallery statistics using local images in public/gallery. |
/contact |
app/contact/page.js |
Owner phone/email, quote form, and FAQ. |
/reviews |
app/reviews/page.js |
Sanity-powered review list and Google Maps review link. |
/book |
app/book/page.js |
Booking form, terms and conditions, and existing booking lookup. |
/book/[id] |
app/book/[id]/page.js |
Server-rendered booking confirmation/details page for a MongoDB booking ID. |
/book/[id]/cancel |
app/book/[id]/cancel/page.js |
Cancellation confirmation page. |
/studio/[[...tool]] |
app/studio/[[...tool]]/page.jsx |
Embedded Sanity Studio. |
| Route | Method | File | Purpose |
|---|---|---|---|
/api/book |
POST |
app/api/book/route.js |
Save a booking to MongoDB and email customer/owner. |
/api/book/[id] |
GET |
app/api/book/[id]/route.js |
Return booking JSON if found and not cancelled. |
/api/book/[id]/cancel |
POST |
app/api/book/[id]/cancel/route.js |
Mark a booking cancelled and email customer/owner. |
/api/contact |
POST |
app/api/contact/route.js |
Validate a contact message and email customer/owner. |
The current UI does not rely on GET /api/book/[id] for booking lookup. components/booking/BookingLookup.js navigates directly to /book/[id], and app/book/[id]/page.js queries MongoDB server-side.
The booking schema is currently defined inline in each MongoDB-using route/page:
{
name: String,
email: String,
phone: String,
address: String,
serviceLocation: String,
frequency: String,
status: String
}Expected values from the booking form:
name: customer full name.email: customer email address.phone: customer phone number.address: service address.serviceLocation:"Home"or"Office".frequency:"One Time","Weekly","Biweekly","Monthly","Move In", or"Move Out".status: starts as"Active"and changes to"cancelled"when cancelled.
MongoDB creates the _id. That _id is the booking ID emailed to the customer.
Main files:
components/booking/BookingForm.jsapp/api/book/route.jsapp/lib/email/sendEmail.js
Flow:
- The customer fills out
name,phone,email,address,serviceLocation, andfrequency. - Client-side validation checks required fields, email format, and 10-digit phone number.
- The client posts the form JSON to
POST /api/book. - The API route connects to MongoDB using
MONGO_URI. - The API creates and saves a
Bookingdocument. sendCustomerEmail(booking, "booking")sends the customer confirmation email.sendOwnerEmail(booking, "booking")sends the owner notification email.- The API returns
{ id, message: "Saved" }with status201. - The client redirects to
/book/[id].
The customer email includes the booking ID, service location, frequency, and owner contact email. The owner email includes the booking ID, status, name, email, phone, address, service location, and frequency.
Main files:
components/booking/BookingLookup.jsapp/book/[id]/page.js
Flow:
- The customer enters a booking ID on
/book. - The lookup component trims the ID and navigates to
/book/[id]. - The booking page connects to MongoDB and runs
Booking.findById(id).lean(). - If the booking does not exist or has
status === "cancelled", the page renders a not-found/cancelled message. - If the booking exists, the page shows the customer name, service location, frequency, preparation instructions, contact tag, and cancel button.
Main files:
components/buttons/CancelBooking.jsapp/api/book/[id]/cancel/route.jsapp/book/[id]/cancel/page.jsapp/lib/email/sendEmail.js
Flow:
- The customer opens the cancel modal from
/book/[id]. - On confirmation, the client sends
POST /api/book/[id]/cancel. - The API connects to MongoDB and finds the booking by ID.
- If the booking does not exist, the API returns
404. - If found, the API sets
statusto"cancelled"and saves. - Cancellation emails are sent to the customer and owner.
- The client redirects to
/book/[id]/cancel.
The customer cancellation email includes the booking ID and a warning to contact the business if they did not request the cancellation. The owner cancellation email includes the full booking details.
Main files:
components/contact/ContactForm.jsapp/api/contact/route.jsapp/lib/email/sendEmail.js
Flow:
- The visitor submits
name,email,serviceType, andmessage. - Client-side validation checks required fields and email format.
- The API trims fields and repeats required-field/email validation server-side.
- The owner receives a "New Contact Message" email with the submitted details.
- The customer receives a confirmation email.
- No contact messages are stored in MongoDB.
app/lib/email/sendEmail.js uses @emailjs/nodejs and one dynamic EmailJS template. The template must support these variables:
| Template Variable | Meaning |
|---|---|
to_email |
Recipient email address. |
subject |
Email subject line. |
reply_to |
Reply-to address. |
html_message |
Full HTML email body generated by the app. |
The email helper builds six types of messages:
- Customer booking confirmation.
- Owner booking notification.
- Customer cancellation confirmation.
- Owner cancellation notice.
- Customer contact acknowledgement.
- Owner contact message notification.
User-provided values are passed through escapeHtml() before being interpolated into email HTML.
Create .env.local for local development. Environment files are intentionally ignored by .gitignore.
NEXT_PUBLIC_SANITY_PROJECT_ID=
NEXT_PUBLIC_SANITY_DATASET=
NEXT_PUBLIC_SANITY_API_VERSION=2026-04-01
MONGO_URI=
EMAILJS_SERVICE_ID=
EMAILJS_TEMPLATE_ID=
EMAILJS_PUBLIC_KEY=
EMAILJS_PRIVATE_KEY=
OWNER_EMAIL=
EMAILJS_OWNER_EMAIL=
OWNER_PHONE=| Variable | Required | Used By | Notes |
|---|---|---|---|
MONGO_URI |
Yes for booking features | Booking API routes and booking detail page | MongoDB connection string. |
EMAILJS_SERVICE_ID |
Yes for email | app/lib/email/sendEmail.js |
EmailJS service ID. |
EMAILJS_TEMPLATE_ID |
Yes for email | app/lib/email/sendEmail.js |
The dynamic template ID. The code explicitly throws if missing. |
EMAILJS_PUBLIC_KEY |
Yes for email | app/lib/email/sendEmail.js |
EmailJS public key. |
EMAILJS_PRIVATE_KEY |
Yes for email | app/lib/email/sendEmail.js |
EmailJS private key for server-side sending. |
OWNER_EMAIL |
Yes for owner emails and display | app/lib/contactInfo.js, email helper, footer/contact pages |
Preferred owner email variable. |
EMAILJS_OWNER_EMAIL |
Optional fallback | app/lib/contactInfo.js |
Used only if OWNER_EMAIL is not set. |
OWNER_PHONE |
Yes for phone display | app/lib/contactInfo.js, footer/contact pages |
Used for display and tel: links. |
NEXT_PUBLIC_SANITY_PROJECT_ID |
Yes for reviews/studio | Sanity config and client | Public Sanity project ID. |
NEXT_PUBLIC_SANITY_DATASET |
Yes for reviews/studio | Sanity config and client | Usually production. |
NEXT_PUBLIC_SANITY_API_VERSION |
Optional | Sanity config and client | Defaults to 2026-04-01. |
Never commit real credentials. If any credentials ever appear in source or commit history, rotate them before deploying or sharing the repository.
- Node.js compatible with Next.js 16 and Sanity 5. Node
20.19+or22.12+is recommended. - npm.
- A MongoDB database.
- An EmailJS account with a dynamic HTML template.
- A Sanity project and dataset if you want reviews and Studio to work.
npm ciUse npm ci because package-lock.json is committed.
Create .env.local in the project root and fill in the variables from Environment Variables.
npm run devOpen http://localhost:3000.
Sanity Studio is mounted at http://localhost:3000/studio.
npm run dev # Start the Next.js development server
npm run build # Create a production build
npm run start # Start the production server after building
npm run lint # Run ESLintThere is no npm test script yet.
MongoDB stores bookings only. Contact form messages are not persisted.
To set up MongoDB:
- Create a MongoDB Atlas cluster or local MongoDB database.
- Create a database user with read/write access.
- Add the connection string to
MONGO_URI. - Make sure the deployment environment can reach the database.
The code does not specify a collection name. Mongoose will use the Booking model and its default collection naming.
EmailJS sends all customer and owner emails.
To set up EmailJS:
- Create an EmailJS service.
- Create one template that renders
html_messageas HTML. - Add template variables for
to_email,subject,reply_to, andhtml_message. - Copy the service ID, template ID, public key, and private key into environment variables.
- Test booking, cancellation, and contact flows in development before deploying.
Sanity stores customer reviews.
Important files:
sanity.config.jssanity.cli.jssanity/schemaTypes/reviewType.jssanity/lib/client.jsapp/reviews/page.jscomponents/WhatOurClientsSay.js
The review schema contains:
reviewer: required string.reviewText: required text.numStars: required integer from 1 to 5.featuredOnHomepage: boolean; homepage shows up to 3 featured reviews.
The Studio runs inside the Next.js app at /studio.
The homepage service area map uses React Leaflet and OpenStreetMap tiles. Marker images are loaded from the Leaflet CDN at runtime. The map is dynamically imported because Leaflet depends on browser APIs and cannot render during server-side rendering.
The service area list is hard-coded in components/AreasWeServeSection.js.
The repository has no custom deployment config such as vercel.json, Dockerfile, Netlify config, or GitHub Actions workflow. The simplest deployment target is Vercel because this is a Next.js project.
Production deployment needs:
- Install dependencies with npm.
- Run
npm run build. - Run
npm run startor deploy through a Next-compatible hosting platform. - Provide all required environment variables in the hosting provider.
- Confirm the host supports server-side route handlers, MongoDB outbound connections, and EmailJS outbound requests.
Recommended production checks:
npm run lint
npm run buildAfter deploy, manually test:
- Homepage loads.
- Navigation works on desktop and mobile.
/services,/gallery,/about,/contact,/reviews, and/bookload.- Contact form sends owner and customer emails.
- Booking form saves to MongoDB and sends owner/customer emails.
- Booking ID opens
/book/[id]. - Cancellation updates the booking status and sends cancellation emails.
- Sanity reviews load on homepage and
/reviews. /studioopens for authorized Sanity users.
- Business copy is mostly hard-coded in page/component files.
- Services are defined in
components/ServicesHomePage.jsandcomponents/ServicesPageCards.js. - Service areas are defined in
components/AreasWeServeSection.js. - FAQs are defined in
app/contact/page.js. - Terms and conditions are defined in
components/booking/TermsConditions.js. - Gallery images are imported manually in
components/gallery/GalleryMain.js; adding/removing images requires updating imports,numPips, and renderedGalleryImageelements. - Reviews are managed through Sanity Studio.
- Owner email and phone should be changed through environment variables, not hard-coded.
- Footer address currently uses placeholder text and should be updated before production use.
The booking schema and MongoDB connection helper are duplicated across:
app/api/book/route.jsapp/api/book/[id]/route.jsapp/api/book/[id]/cancel/route.jsapp/book/[id]/page.js
A future cleanup should move the schema/model and connectDB() into shared server-only modules, such as:
app/lib/db/connect.jsapp/lib/models/Booking.js
That would reduce drift and make schema changes safer.
app/layout.js sets the title to Ortega's Cleaning, but the description is still the default create-next-app text. Update the metadata before public launch.
Suggested metadata:
export const metadata = {
title: "Ortega's House Cleaning",
description: "Family-owned residential and commercial cleaning services serving King County and Pierce County.",
};Gallery JPG files are stored in public/gallery. Several are large, so image compression may improve performance. Because the gallery imports local images through next/image, Next can optimize rendering, but smaller source assets will still help.
Global brand colors and theme tokens live in app/globals.css. Most layout/styling uses Tailwind utility classes directly in components.
These are current implementation details that future maintainers should understand.
- Booking validation is mostly client-side.
POST /api/booktrusts the request body and saves it directly. - The booking schema has no required fields, enums, indexes, timestamps, or data retention fields.
- Email sending happens after the booking is saved. If saving succeeds but email fails, the API returns an error even though a booking exists.
- Contact emails are not stored anywhere. If email delivery fails, the message is lost.
- Anyone with a valid booking ID can view the booking detail page and cancel the booking.
GET /api/book/[id]returns the full booking document, including customer PII.- Cancellation is not idempotent. Repeated cancellation requests can re-send cancellation emails.
- There is no rate limiting, CAPTCHA, spam prevention, CSRF protection, or authentication for form endpoints.
- Invalid MongoDB ObjectId values are not consistently handled and may produce generic server errors.
- There is no
.env.examplefile. - There are no automated tests.
- The project has no documented privacy policy, data retention policy, admin dashboard, or owner-side booking management UI.
- Audit the source and git history for accidental credentials before making the repository public.
To recreate this project from scratch, build these pieces in order:
- Create a Next.js App Router app with JavaScript, npm, ESLint, Tailwind CSS, and a root
app/layout.js. - Add a global layout that renders page content and a persistent footer.
- Create marketing pages for homepage, about, services, gallery, contact, reviews, and booking.
- Build reusable components for navbar, footer, service cards, CTAs, review cards, gallery carousel, contact form, booking form, booking lookup, and cancel modal.
- Add local image assets under
public/and gallery images underpublic/gallery/. - Add MongoDB and Mongoose.
- Define a
Bookingmodel with customer contact fields, service fields, and status. - Create
POST /api/bookto save a booking and return the MongoDB ID. - Create
/book/[id]to read a booking by ID and show a customer-friendly confirmation page. - Create
POST /api/book/[id]/cancelto mark a booking cancelled. - Add EmailJS server-side sending with one dynamic HTML template.
- Send customer and owner emails for booking, cancellation, and contact flows.
- Create
POST /api/contactwith server-side validation and email notifications. - Add Sanity, define a
reviewschema, mount Studio at/studio, and fetch reviews in the homepage/reviews pages. - Add React Leaflet for the areas-served map, using dynamic imports to avoid SSR issues.
- Add all required environment variables locally and in production.
- Run lint/build checks and manually test all user flows.
This project is a client-facing Web Impact website for a real local cleaning business. The main maintenance responsibility is keeping public business content accurate while protecting customer data in the booking flow. Before a long-term production launch, prioritize server-side booking validation, removal/rotation of any accidental secrets, shared MongoDB model utilities, better booking access controls, metadata updates, and a small automated test suite for the booking/contact APIs.