Next.js 14 (App Router) + Tailwind CSS site for Oasis Cleaning of Austin LLC.
- 4 pages: Home, About Us, Reviews, Services
- All-static pages, generated at build time (fast loading)
- Single-codebase backend via Next.js API routes (no extra server)
- Live Google Reviews, refreshed automatically once per week
- "Get a Free Quote" modal that emails the submission to a fixed inbox
- Tap-to-call CTA reused across pages via one
<CallButton />component - Footer/navigation content driven by a single JSON file
- Deploy-ready for Vercel or Cloudflare Pages
# 1. Clone your empty GitHub repo, then copy this project's files into it
git clone https://github.com/<your-username>/<your-repo>.git
cd <your-repo>
# 2. Install dependencies
npm install
# 3. Create your environment file
cp .env.example .env.local
# then open .env.local and fill in the values (see "Environment variables" below)
# 4. Run the dev server
npm run dev
# open http://localhost:3000oasis-cleaning/
├── public/
│ └── images/ # logo, hero photos, manager portrait
├── src/
│ ├── app/ # Next.js App Router pages
│ │ ├── layout.tsx # root layout — wires Navbar + Footer + fonts
│ │ ├── page.tsx # / (Home)
│ │ ├── about/page.tsx # /about
│ │ ├── reviews/page.tsx # /reviews
│ │ ├── services/page.tsx # /services
│ │ ├── globals.css
│ │ └── api/
│ │ ├── quote/route.ts # POST – sends quote-request email
│ │ └── reviews/route.ts # GET cached reviews | POST forces refresh
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Navbar.tsx # sticky top nav (mobile drawer)
│ │ │ └── Footer.tsx
│ │ ├── ui/
│ │ │ ├── Logo.tsx
│ │ │ ├── CallButton.tsx # ← reused on every page
│ │ │ ├── Ornament.tsx
│ │ │ ├── SocialIcons.tsx
│ │ │ └── ServiceIcons.tsx
│ │ └── sections/
│ │ └── QuoteFormButton.tsx # modal + submit-to-API
│ ├── data/ # site-wide JSON content
│ │ ├── site.json # company info, nav, footer, social
│ │ ├── home.json
│ │ ├── about.json
│ │ └── services.json
│ └── lib/
│ └── google-reviews.ts # Places API fetcher (weekly cache)
├── .env.example
├── next.config.js
├── tailwind.config.js
├── tsconfig.json
└── vercel.json # weekly cron job
The footer, nav links, phone number, emails, hours, slogan, and copyright are
all in src/data/site.json — change it once, every page updates.
Page-specific copy lives in src/data/home.json, about.json, services.json.
Create .env.local (locally) and set the same values in Vercel's
Project → Settings → Environment Variables for production.
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SITE_URL |
Full URL of the deployed site, e.g. https://oasiscleaningofaustin.com |
GOOGLE_PLACES_API_KEY |
Google Cloud Places API key |
GOOGLE_PLACE_ID |
The Place ID of the Oasis Cleaning Google Business listing |
SMTP_HOST |
SMTP host (e.g. smtp.gmail.com) |
SMTP_PORT |
465 for SSL, 587 for STARTTLS |
SMTP_SECURE |
true for port 465, false for 587 |
SMTP_USER |
SMTP login email |
SMTP_PASSWORD |
SMTP password / app-password |
QUOTE_RECIPIENT_EMAIL |
Inbox that receives quote-form submissions |
REVALIDATE_SECRET |
Long random string used by the weekly cron |
- Go to https://console.cloud.google.com/ and create a project.
- APIs & Services → Library → enable "Places API".
- APIs & Services → Credentials → create an API key. Restrict it to the Places API.
- Find your Place ID using https://developers.google.com/maps/documentation/places/web-service/place-id
- Enable 2-step verification on the Google account.
- Visit https://myaccount.google.com/apppasswords and generate an "App password".
- Use that 16-char password as
SMTP_PASSWORD.SMTP_USERis the Gmail address.
Want a different provider (SendGrid, Resend, Mailgun)? The
/api/quoteroute uses standard SMTP vianodemailer, so just swap the SMTP credentials.
src/components/ui/CallButton.tsx renders an <a href="tel:+1..."> link in
three variants. The phone number lives in src/data/site.json — change once,
updates everywhere. On mobile, tapping it places the call directly.
src/lib/google-reviews.tscalls the Places "Details" endpoint and asks for the latest reviews.- It filters to reviews with 4+ stars ("positive only") and sorts newest-first.
- The
fetch()call is configured withnext: { revalidate: 604800, tags: ['google-reviews'] }, so Next.js caches the response for 7 days and only hits Google once per week. /api/reviews?secret=YOUR_SECRET(POST) can be hit by a cron to force a refresh.vercel.jsonalready contains a cron entry — replaceREPLACE_WITH_YOUR_REVALIDATE_SECRETwith the value of yourREVALIDATE_SECRETenv var.- Until the Google credentials are configured, the page renders a built-in set of placeholder reviews so the design still looks correct.
src/components/sections/QuoteFormButton.tsxis a Client Component that opens a modal, validates input client-side, and POSTs JSON to/api/quote.src/app/api/quote/route.tsvalidates again on the server and emails the submission toQUOTE_RECIPIENT_EMAILusingnodemailer.- All running inside the same Next.js app — no external backend.
- Push the project to your GitHub repo:
git add . git commit -m "Initial commit" git push origin main
- Go to https://vercel.com/new and import the GitHub repo.
- Framework Preset is auto-detected as "Next.js" — keep defaults.
- Add all the environment variables from section 3 under Environment Variables.
- Click Deploy.
- Edit
vercel.jsonand replaceREPLACE_WITH_YOUR_REVALIDATE_SECRETwith your actualREVALIDATE_SECRETvalue, commit, and push — the weekly cron will start. - Point your domain (e.g.
oasiscleaningofaustin.com) at the Vercel deployment from Project → Settings → Domains.
Cloudflare Pages supports Next.js via the official adapter:
npm install --save-dev @cloudflare/next-on-pages
npx @cloudflare/next-on-pagesThen in Cloudflare Pages:
- Framework preset: Next.js
- Build command:
npx @cloudflare/next-on-pages - Build output directory:
.vercel/output/static - Node version: 18 or 20
- Add the same env variables in Settings → Environment Variables.
Note: Cloudflare Pages uses the Edge runtime by default. The
/api/quoteroute is pinned toruntime = 'nodejs'becausenodemailerneeds Node APIs. Cloudflare will run it on their Node.js compatibility layer. If you hit problems, swapnodemailerfor an HTTP-based provider like Resend or SendGrid Web API and remove theruntimeexport.
- Change the phone number, emails, hours, slogan:
src/data/site.json - Change service list:
src/data/services.json→servicesarray - Change manager statement / name / photo:
src/data/about.json+ replacepublic/images/manager.jpeg - Change a hero image: replace the file in
public/images/with the same name - Add a navigation link: add to
navigationinsrc/data/site.jsonand create a folder insrc/app/<new-route>/page.tsx
npm run dev # start dev server on http://localhost:3000
npm run build # production build
npm run start # start the production server (after build)
npm run lint # lint