initial commit
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
---
|
||||
name: local-business-website
|
||||
description: Build production static websites for local service businesses — blueprint-first workflow, mobile-first CSS patterns, local SEO integration, phased implementation. Absorbed small-business-website (directory structure, CSS architecture, gallery/pricing table patterns).
|
||||
---
|
||||
|
||||
## Trigger
|
||||
|
||||
Use this skill when the user asks to build, design, or architect a static website for a local service business (HVAC, handyman, plumbing, landscaping, roofing, electrical, etc.). Also load it when the user references a website blueprint or a multi-page brochure site for a local business.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Gather Requirements
|
||||
|
||||
Before any code, collect:
|
||||
- Business name and owner
|
||||
- Service lines (primary and secondary)
|
||||
- Service areas (cities/neighborhoods — critical for local SEO)
|
||||
- USPs (24/7 emergency, family-owned, flat-rate pricing, etc.)
|
||||
- Phone number and email
|
||||
- Any specific pages needed beyond the standard set
|
||||
|
||||
If the user is terse (common for this user), ask directly rather than guessing business type or features.
|
||||
|
||||
### 2. Produce a BLUEPRINT.md
|
||||
|
||||
Write a comprehensive blueprint document saved to the project directory BEFORE writing any code. Cover:
|
||||
- URL structure for every page
|
||||
- Technology choices (static HTML/CSS/JS, no frameworks, no build step — keep it fast and portable)
|
||||
- Global header and footer design (desktop + mobile behavior)
|
||||
- Every page: section-by-section layout with copy guidance, CTA placement, and psychology notes
|
||||
- Mobile-specific rules (touch targets, sticky bars, typography)
|
||||
- Local SEO checklist: where each service area city appears (H1, H2, footer, image alt text)
|
||||
- Color palette and typography spec
|
||||
- Implementation phases: each phase produces a deployable increment
|
||||
|
||||
The blueprint is the source of truth. All implementation decisions trace back to it.
|
||||
|
||||
### 3. Build in Phases
|
||||
|
||||
Standard phase order for local business sites:
|
||||
|
||||
| Phase | Deliverable | Priority |
|
||||
|-------|------------|----------|
|
||||
| 1 | Global shell: base HTML, CSS custom properties, header, footer, mobile nav, tap-to-call bar, shared JS | Foundation |
|
||||
| 2 | Emergency/urgent landing page (if applicable) | Highest conversion |
|
||||
| 3 | Contact/quote form page | Lead capture |
|
||||
| 4 | Homepage (all sections) | Main entry point |
|
||||
| 5+ | Secondary service pages | Supporting pages |
|
||||
| Final | Polish: schema.org, meta tags, image optimization, performance | SEO finish |
|
||||
|
||||
Each phase produces a working, self-contained increment. Never deliver a half-built page.
|
||||
|
||||
## CSS Architecture (Mobile-First)
|
||||
|
||||
### Custom Properties
|
||||
|
||||
Always use CSS custom properties on `:root`. Standard palette for local business sites:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-primary: #1E40AF; /* Blue — trust */
|
||||
--color-secondary: #15803D; /* Green — growth/outdoors */
|
||||
--color-emergency: #DC2626; /* Red — urgency only */
|
||||
--color-dark: #111827; /* Near-black body text */
|
||||
--color-medium: #6B7280; /* Secondary text */
|
||||
--color-light: #F9FAFB; /* Page background */
|
||||
--color-white: #FFFFFF;
|
||||
--color-border: #E5E7EB;
|
||||
|
||||
--font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--max-width: 72rem;
|
||||
--header-height: 64px;
|
||||
--callbar-height: 48px;
|
||||
}
|
||||
```
|
||||
|
||||
Use system font stack — no Google Fonts. Zero FOUT, fastest load.
|
||||
|
||||
### Mobile-First Rules
|
||||
|
||||
- Body text: 16px minimum (prevents iOS zoom on input focus)
|
||||
- All tappable elements: minimum 44×44px
|
||||
- Buttons: full-width on mobile
|
||||
- Phone numbers: ALWAYS `<a href="tel:...">` — never plain text
|
||||
- Section padding: 48px top/bottom on mobile
|
||||
|
||||
### Header (Sticky)
|
||||
|
||||
Three zones:
|
||||
1. **Desktop (≥768px):** Logo left, nav center, phone + CTA button right. Sticky, shrinks on scroll.
|
||||
2. **Mobile (<768px):** Logo left, hamburger right. Hamburger opens slide-down drawer with all links + emergency call button.
|
||||
3. **Mobile nav drawer:** Full-viewport overlay below header. Links at 18px, bold, with rounded tap targets. Close on link tap.
|
||||
|
||||
Pattern:
|
||||
```html
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a href="/" class="logo">...</a>
|
||||
<nav class="main-nav">...</nav>
|
||||
<div class="header-right">...</div>
|
||||
<button class="hamburger">...</button>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="mobile-nav">...</nav>
|
||||
```
|
||||
|
||||
### Tap-to-Call Bar (Mobile Only)
|
||||
|
||||
CRITICAL for service businesses. A sticky bar below the header on mobile (<768px) with phone icon + "Call Now: (XXX) XXX-XXXX — 24/7" that dials on tap. Hidden on desktop via CSS media query.
|
||||
|
||||
```html
|
||||
<a href="tel:+1XXXXXXXXXX" class="callbar">
|
||||
<svg><!-- phone icon --></svg>
|
||||
Call Now: (XXX) XXX-XXXX — 24/7
|
||||
</a>
|
||||
```
|
||||
|
||||
CSS: `position: fixed; top: var(--header-height);` — sits between header and content. Use accent/emergency background color.
|
||||
|
||||
The body gets `padding-top: calc(var(--header-height) + var(--callbar-height))` on mobile, `padding-top: var(--header-height)` on desktop.
|
||||
|
||||
### Footer
|
||||
|
||||
Three-section grid on desktop (brand, nav, contact), stacked on mobile. Must include service area cities in natural language: "📍 Proudly serving: City1 · City2 · City3"
|
||||
|
||||
## Local SEO Patterns
|
||||
|
||||
### Where to Place Service Areas
|
||||
|
||||
| Element | Location | Format |
|
||||
|---------|----------|--------|
|
||||
| H1 | Homepage hero | "Service in City1 & Surrounding Areas" |
|
||||
| H2 | Service area section | "Serving City1, City2 & Beyond" |
|
||||
| Body list | Dedicated section on main service page | Bullet points with context ("City1 — 30-min response") |
|
||||
| Footer | Global footer | Single line, all pages |
|
||||
| Image alt text | Gallery/service photos | "Fence installation in City1 TN" |
|
||||
| Meta description | `<head>` per page | Include primary city |
|
||||
| Schema.org | JSON-LD `LocalBusiness` | `areaServed` array |
|
||||
|
||||
### Schema.org Template
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"name": "Business Name",
|
||||
"telephone": "(XXX) XXX-XXXX",
|
||||
"areaServed": ["City1", "City2", "City3"],
|
||||
"hasOfferCatalog": {
|
||||
"@type": "OfferCatalog",
|
||||
"name": "Service Category",
|
||||
"itemListElement": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Include on every page.
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
- Do NOT create separate pages for each service area city — spammy, adds no value
|
||||
- Do NOT link city names to dedicated city pages
|
||||
- Do NOT keyword-stuff city names into every heading
|
||||
- One well-optimized service area mention beats six low-quality city pages
|
||||
|
||||
## JavaScript (Vanilla, No Dependencies)
|
||||
|
||||
Three pieces of shared JS:
|
||||
|
||||
1. **Mobile nav toggle:** hamburger click → open/close drawer, animate hamburger icon, set `body overflow: hidden` when open, close on link tap
|
||||
2. **Header shrink on scroll:** add `.scrolled` class when `scrollY > 80`
|
||||
3. **Active nav highlighting:** match `window.location.pathname` against nav links
|
||||
|
||||
Keep it simple. No frameworks. `DOMContentLoaded` wrapper. All vanilla.
|
||||
|
||||
## Quote/Contact Form Pattern
|
||||
|
||||
Use Web3Forms (free tier, simple API) for form-to-email on static sites. No backend needed.
|
||||
|
||||
### Form Fields (6-field standard)
|
||||
- Name* (text, required)
|
||||
- Phone* (tel, required) — phone is THE critical field for local businesses
|
||||
- Email (email, optional) — don't require; some local customers don't use email
|
||||
- Service Needed* (select, required) — routes the lead mentally, helps owner prioritize
|
||||
- Project Description* (textarea, required) — gives context before callback
|
||||
- Lead Source (select, optional) — helps track which marketing works
|
||||
|
||||
### Validation Pattern
|
||||
Client-side only. On submit: check required fields, add `.error` class to empty inputs (red border), add `.visible` to error spans. On input: remove error classes — instant feedback.
|
||||
|
||||
### States
|
||||
- **Loading:** button disabled + CSS spinner (swaps button text for animated spinner via `display` toggle)
|
||||
- **Success:** form hidden, success card shown. Includes customer's first name + emergency phone CTA
|
||||
- **Error:** red banner displayed, button re-enabled for retry
|
||||
|
||||
### Web3Forms Setup
|
||||
1. Create free account at https://web3forms.com/
|
||||
2. Replace `YOUR_ACCESS_KEY` in the form script
|
||||
3. Form POSTs to `https://api.web3forms.com/submit`
|
||||
4. Emails go to the address configured in dashboard
|
||||
|
||||
Full implementation reference at `references/airrepairteam-blueprint.md`.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **Don't use Google Fonts** — system font stack is faster and looks native on every device
|
||||
- **Don't use iframe Google Maps** — they kill page speed. Use a static image or CSS map
|
||||
- **Don't add a calendar/scheduler to forms** — adds complexity, needs backend, breaks when not maintained. Simple POST-to-email via Formspree or Web3Forms is enough
|
||||
- **Don't require email on quote forms** — some local customers don't use email. Phone is the required field
|
||||
- **Don't build landing pages with nav links** — emergency landing pages should have ONE exit: the phone number
|
||||
- **Don't use frameworks for simple brochure sites** — Tailwind or raw CSS is fine, but React/Vue/Next.js is overkill and adds hosting complexity the owner can't maintain
|
||||
|
||||
## Support Files
|
||||
|
||||
- `references/airrepairteam-blueprint.md` — Full blueprint example: 6-page HVAC/handyman site with section-by-section layout, mobile rules, and SEO checklist
|
||||
- `references/blueprint-template.md` — Empty website blueprint template (absorbed from small-business-website)
|
||||
- `references/contact-form.md` — Complete Web3Forms contact form pattern with HTML, CSS, and JS (absorbed from small-business-website)
|
||||
- `templates/style.css` — Copyable base CSS: custom properties, reset, header, callbar, footer, responsive breakpoints
|
||||
- `templates/main.js` — Copyable base JS: mobile nav toggle, header shrink, active link detection, smooth scroll
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```text
|
||||
BusinessName/
|
||||
├── BLUEPRINT.md
|
||||
├── index.html
|
||||
├── css/style.css
|
||||
├── js/main.js
|
||||
├── contact/index.html
|
||||
├── service-pages/...
|
||||
├── images/
|
||||
```
|
||||
|
||||
## Global CSS Architecture (Section Order)
|
||||
|
||||
All styles go in `css/style.css`. Structure:
|
||||
|
||||
```text
|
||||
1. CSS custom properties (colors, typography, spacing, layout vars)
|
||||
2. Reset
|
||||
3. Utility classes
|
||||
4. Buttons (btn, btn-primary, btn-emergency, btn-outline, btn-large, btn-full)
|
||||
5. Phone link (.phone-link)
|
||||
6. Header (.site-header, .logo, .main-nav, .header-right, .hamburger)
|
||||
7. Mobile nav drawer (.mobile-nav)
|
||||
8. Tap-to-call bar (.callbar — mobile only, hidden on desktop)
|
||||
9. Page sections (.page-section, .section-heading, .section-subheading)
|
||||
10. Footer (.site-footer, .footer-grid, .footer-service-areas)
|
||||
11. Page-specific section styles
|
||||
12. Responsive: Tablet (768px) — grid layouts, header changes, hide mobile elements
|
||||
13. Responsive: Desktop (1024px) — larger type, wider grids
|
||||
```
|
||||
|
||||
## Emergency Landing Page (if applicable)
|
||||
|
||||
For service businesses with 24/7 emergency offerings, create a standalone stripped-down page:
|
||||
- Zero external resources — all CSS inline, no JS files, no images, no fonts
|
||||
- ~7KB total page weight, sub-1s cold load
|
||||
- ONE goal: phone call. Phone number is the only prominent interactive element
|
||||
- Red/urgent accent, pulsing emergency badge
|
||||
- No navigation links (people click them and bounce)
|
||||
- "No after-hours fees" prominently addressed
|
||||
|
||||
## Before/After Gallery (if applicable)
|
||||
|
||||
For service businesses where visual proof drives conversions:
|
||||
- 2×2 or 4-col grid of before/after pairs side-by-side
|
||||
- Filter bar by service category (All, Fences, Lawns, etc.)
|
||||
- Filter JS: `data-category` attributes, toggles `display:none`
|
||||
- Place gallery filter script BEFORE main.js in the HTML
|
||||
- "Before"/"After" labels on each image
|
||||
|
||||
## Pricing Table (if applicable)
|
||||
|
||||
- Responsive table: collapses to label-value rows on mobile using `data-label` attributes
|
||||
- Use "Flat Rate" or "Starting at $XX" language — don't lock in exact numbers
|
||||
- Disclaimer: "Final price confirmed before any work begins"
|
||||
- Wrap in a card with border and shadow for visual weight
|
||||
|
||||
## After Build Checklist
|
||||
|
||||
1. Search all files for `XXX-XXXX` and replace with real phone number
|
||||
2. Replace placeholder email
|
||||
3. Get Web3Forms access key, update contact page
|
||||
4. Replace emoji/placeholder images with real photos
|
||||
5. Fill in actual pricing if applicable
|
||||
6. Serve via nginx
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# AirRepairTeam — Worked Blueprint Example
|
||||
|
||||
Full blueprint at: `/mnt/seagate8tb/Websites/AirRepairTeam/BLUEPRINT.md`
|
||||
|
||||
## What This Example Demonstrates
|
||||
|
||||
A 6-page static website for a multi-trade contractor (HVAC + Handyman/Yardwork) serving 6 Knoxville-area cities. The blueprint covers every page section-by-section with copy psychology, CTA placement, mobile behavior, and local SEO integration.
|
||||
|
||||
## Key Patterns Illustrated
|
||||
|
||||
### Dual-Service Routing (Homepage Section 3)
|
||||
When a business has two distinct service lines with different customer psychologies (urgent vs. comparison-shopping), the homepage hero routes traffic with two equal-weight CTAs, and Section 3 presents them as side-by-side cards with visual differentiation (cool blue for HVAC, warm green for handyman).
|
||||
|
||||
### Emergency Landing Page (`/hvac/emergency`)
|
||||
Stripped-down page for Google Ads traffic. No nav links. One goal: phone call. The phone number appears twice (hero + sticky bottom bar). Red/urgent accent. Must load in under 1 second — no images, no frameworks, inline CSS only if needed.
|
||||
|
||||
### Psychology-Driven Service Pages
|
||||
|
||||
**HVAC page** (high-urgency, high-ticket):
|
||||
- Hero leads with emergency language and "no after-hours fees"
|
||||
- Pricing table builds trust (most HVAC companies hide pricing)
|
||||
- "What to do in an HVAC emergency" section provides SEO content + conversion nudge
|
||||
- Phone is always the primary CTA
|
||||
|
||||
**Handyman page** (comparison shoppers):
|
||||
- Hero is a photo of completed work
|
||||
- Before/after gallery is THE conversion driver — juxtoposition proves competence
|
||||
- 3-step "How It Works" removes friction for first-time customers
|
||||
- Lighter, friendlier tone throughout
|
||||
|
||||
### Local SEO Integration (6 Cities)
|
||||
|
||||
Cities appear exactly 5 ways across the site:
|
||||
1. H1 on homepage: "Knoxville & Surrounding Areas"
|
||||
2. H2 on HVAC page: "HVAC Repair in Knoxville, Powell, Halls & Beyond"
|
||||
3. Body bullets on HVAC page with fake "response times" per city (makes it useful, not spammy)
|
||||
4. Footer on every page: "📍 Proudly serving: Knoxville · Powell · Halls · Corryton · Fountain City · Karns"
|
||||
5. Image alt text: "Fence installation in Powell TN"
|
||||
|
||||
No separate city pages. No city-page links. One consistent signal.
|
||||
|
||||
### Quote Form Design
|
||||
- 6 fields: Name*, Phone*, Email (optional), Service (dropdown)*, Project description*, Lead source
|
||||
- No calendar, no file upload, no address, no CAPTCHA (initially)
|
||||
- Success state still shows phone number (for emergency callers who used the form)
|
||||
- Posts to Formspree/Web3Forms — no backend
|
||||
|
||||
### Web3Forms Form Pattern
|
||||
|
||||
Used on the contact page. Key implementation details:
|
||||
|
||||
**Form handler:** POST to `https://api.web3forms.com/submit` with an access key. The access key is configured in Web3Forms dashboard (free tier available). Form data is emailed directly to the address configured there.
|
||||
|
||||
**Client-side validation pattern:**
|
||||
```js
|
||||
function validate() {
|
||||
var valid = true;
|
||||
function showErr(name) {
|
||||
document.getElementById('field-' + name).classList.add('error');
|
||||
document.getElementById('error-' + name).classList.add('visible');
|
||||
}
|
||||
if (!nameField.value.trim()) { showErr('name'); valid = false; }
|
||||
if (!phoneField.value.trim()) { showErr('phone'); valid = false; }
|
||||
if (!detailsField.value.trim()) { showErr('details'); valid = false; }
|
||||
if (!serviceSelect.value) { showErr('service'); valid = false; }
|
||||
return valid;
|
||||
}
|
||||
```
|
||||
|
||||
**Error clearing on input:** Each input gets an `input` event listener that removes `.error` from itself and `.visible` from its error span — gives instant feedback as the user types.
|
||||
|
||||
**Loading state:** Button gets `disabled` + `.loading` class. CSS swaps button text for a CSS-only spinner.
|
||||
|
||||
**Success state:** Form `display: none`, success div gets `.visible`. Message includes the customer's first name (extracted from the name field) and a prominent "if this is an emergency, call now" note with phone number.
|
||||
|
||||
**Error state:** Red banner at top of form card with "Something went wrong" message. Button re-enables for retry.
|
||||
|
||||
**CORS note:** Fetch to Web3Forms will be blocked on `file://` origins (browser security). Works correctly on any real domain. Test form behavior locally by manually toggling success/error state classes.
|
||||
|
||||
### Mobile-Specific Decisions
|
||||
- Sticky tap-to-call bar below header on all pages (hidden on desktop)
|
||||
- Both the nav bar AND callbar remain sticky — combined ~100px height, acceptable tradeoff
|
||||
- Buttons full-width on mobile
|
||||
- Phone numbers always `<a href="tel:...">`
|
||||
- Touch targets minimum 44px
|
||||
|
||||
### Implementation Phases
|
||||
1. Global shell (header, footer, CSS, JS, mobile nav, callbar)
|
||||
2. Emergency HVAC landing page
|
||||
3. Contact/quote page
|
||||
4. Homepage (all 7 sections)
|
||||
5. HVAC service page
|
||||
6. Handyman service page
|
||||
7. Polish (schema, meta, images, performance)
|
||||
@@ -0,0 +1,81 @@
|
||||
# Website Blueprint Template
|
||||
|
||||
Use this format when designing a website before building. Write the blueprint first, then build from it.
|
||||
|
||||
---
|
||||
|
||||
## Business Context
|
||||
- **Client:** [name]
|
||||
- **Business:** [type — HVAC, handyman, plumbing, etc.]
|
||||
- **USPs:** [2-4 unique selling propositions — 24/7, family-owned, flat-rate, fast response]
|
||||
- **Service Areas:** [city list]
|
||||
|
||||
## URL Structure
|
||||
```
|
||||
/ → Homepage
|
||||
/service-a → Service page A
|
||||
/service-a/landing → Standalone landing page (for ads)
|
||||
/service-b → Service page B
|
||||
/contact → Contact / Quote Request
|
||||
```
|
||||
|
||||
## Technology
|
||||
- Static HTML/CSS/JS — no backend
|
||||
- Form submissions via Web3Forms (free tier)
|
||||
- No frameworks, no build step
|
||||
- System font stack, CSS custom properties
|
||||
|
||||
---
|
||||
|
||||
## Per-Page Blueprint
|
||||
|
||||
For each page, list sections top-to-bottom with:
|
||||
- Section name
|
||||
- Visual elements (icons, photos, layout)
|
||||
- Copy and headlines (with SEO keywords)
|
||||
- CTAs (buttons, phone links)
|
||||
- Psychology notes (what the visitor feels, what they need to see)
|
||||
|
||||
### Global Header
|
||||
```
|
||||
Layout: Logo | Nav links | Phone | CTA button
|
||||
Mobile: Logo | Hamburger + sticky tap-to-call bar below
|
||||
```
|
||||
|
||||
### Global Footer
|
||||
```
|
||||
Layout: Brand | Service links | Contact | Service areas
|
||||
SEO: All service area cities in footer on every page
|
||||
```
|
||||
|
||||
### Page: [Name]
|
||||
#### Section 1: Hero
|
||||
- Headline: [H1 with primary keyword + city]
|
||||
- Subcopy: [1-2 lines]
|
||||
- CTAs: [primary CTA, secondary CTA]
|
||||
- Psychology: [what the visitor is thinking]
|
||||
|
||||
#### Section 2: [Name]
|
||||
- ...
|
||||
|
||||
[Repeat for all sections]
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Usage |
|
||||
|------|-------|-------|
|
||||
| Primary | hex | primary CTAs, brand elements |
|
||||
| Secondary | hex | secondary CTAs |
|
||||
| Emergency | hex | urgency CTAs if applicable |
|
||||
| Dark | hex | body text, dark sections |
|
||||
| Light | hex | page background |
|
||||
|
||||
## Implementation Priority
|
||||
1. Global shell (header, footer, CSS, JS)
|
||||
2. [Highest-conversion page]
|
||||
3. [Contact form]
|
||||
4. [Homepage]
|
||||
5. [Remaining service pages]
|
||||
6. Polish (schema, meta, images)
|
||||
@@ -0,0 +1,135 @@
|
||||
# Contact Form Pattern (Web3Forms)
|
||||
|
||||
Static-site contact form that emails the business owner directly. No backend, no calendar, no payment.
|
||||
|
||||
## Form Fields
|
||||
|
||||
Keep it SHORT. Every extra field costs conversions.
|
||||
|
||||
| Field | Required | Type | Notes |
|
||||
|-------|----------|------|-------|
|
||||
| Name | Yes | text | `autocomplete="name"` |
|
||||
| Phone | Yes | tel | THE most important field for phone-first businesses |
|
||||
| Email | No | email | Optional — don't require it, it adds friction |
|
||||
| Service Needed | Yes | select | Dropdown with all service types + "Other" |
|
||||
| Project Details | Yes | textarea | 3-line minimum, gives context before callback |
|
||||
| Lead Source | No | select | Google/Facebook/Neighbor/Other — helps measure marketing |
|
||||
|
||||
**Deliberately missing:** address, calendar, file upload, CAPTCHA (add only if spam), budget field.
|
||||
|
||||
## HTML Structure
|
||||
|
||||
```html
|
||||
<div class="form-card" id="quote-form-wrapper">
|
||||
<div class="form-error" id="form-error" role="alert">
|
||||
Something went wrong. Please try again or call us directly.
|
||||
</div>
|
||||
|
||||
<form id="quote-form" novalidate>
|
||||
<!-- fields with .form-group > .form-label + .form-input/.form-select/.form-textarea + .field-error -->
|
||||
<button type="submit" class="btn submit-btn">
|
||||
<span class="btn-text">Get My Free Quote</span>
|
||||
<span class="spinner"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Success state (hidden by default) -->
|
||||
<div class="form-success" id="form-success">
|
||||
<div class="check-icon">✓</div>
|
||||
<h3>Thanks, <span id="success-name">friend</span>!</h3>
|
||||
<p>We'll reach out within 2 hours.</p>
|
||||
<div class="emergency-note">
|
||||
<strong>🚨 Emergency?</strong> Call now: <a href="tel:...">(XXX) XXX-XXXX</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## CSS States
|
||||
|
||||
```css
|
||||
.form-input.error, .form-select.error, .form-textarea.error {
|
||||
border-color: var(--color-emergency);
|
||||
box-shadow: 0 0 0 3px rgba(220,38,38,0.1);
|
||||
}
|
||||
.field-error { display: none; }
|
||||
.field-error.visible { display: block; color: var(--color-emergency); }
|
||||
.form-error { display: none; background: var(--color-emergency-light); }
|
||||
.form-error.visible { display: block; }
|
||||
.form-success { display: none; }
|
||||
.form-success.visible { display: block; }
|
||||
.submit-btn.loading .spinner { display: inline-block; }
|
||||
.submit-btn.loading .btn-text { display: none; }
|
||||
```
|
||||
|
||||
## JavaScript
|
||||
|
||||
```javascript
|
||||
var WEB3FORMS_KEY = 'YOUR_ACCESS_KEY'; // Replace with real key
|
||||
var WEB3FORMS_URL = 'https://api.web3forms.com/submit';
|
||||
|
||||
// Validation
|
||||
function validate() {
|
||||
var valid = true;
|
||||
if (!nameField.value.trim()) { showErr('name'); valid = false; }
|
||||
if (!phoneField.value.trim()) { showErr('phone'); valid = false; }
|
||||
if (!detailsField.value.trim()) { showErr('details'); valid = false; }
|
||||
if (!serviceField.value) { showErr('service'); valid = false; }
|
||||
return valid;
|
||||
}
|
||||
|
||||
// Clear errors on input
|
||||
inputs.forEach(function(el) {
|
||||
el.addEventListener('input', function() {
|
||||
el.classList.remove('error');
|
||||
getErrorEl(el.name).classList.remove('visible');
|
||||
});
|
||||
});
|
||||
|
||||
// Submit
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (!validate()) return;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.add('loading');
|
||||
|
||||
var payload = new FormData(form);
|
||||
payload.append('access_key', WEB3FORMS_KEY);
|
||||
payload.append('subject', 'New Quote Request — Business Name');
|
||||
payload.append('from_name', 'Business Website');
|
||||
|
||||
fetch(WEB3FORMS_URL, { method: 'POST', body: payload })
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
document.getElementById('success-name').textContent =
|
||||
nameField.value.trim().split(' ')[0] || 'friend';
|
||||
form.style.display = 'none';
|
||||
successDiv.classList.add('visible');
|
||||
} else {
|
||||
throw new Error(data.message || 'Submission failed');
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.classList.remove('loading');
|
||||
errorBanner.classList.add('visible');
|
||||
console.error('Form error:', err);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
1. Go to https://web3forms.com/ — create free account
|
||||
2. Copy access key
|
||||
3. Replace `YOUR_ACCESS_KEY` in the form script
|
||||
4. Configure destination email in Web3Forms dashboard
|
||||
|
||||
## Testing
|
||||
|
||||
- `file://` CORS blocks fetch() — expected. Test from a real web server.
|
||||
- Click submit with empty fields → validation errors on all required fields
|
||||
- Fill all required fields, submit → loading spinner → success card (if key valid) or error banner (if invalid key)
|
||||
- Success card shows customer's first name + emergency phone fallback
|
||||
@@ -0,0 +1,74 @@
|
||||
/* ============================================================
|
||||
Local Business Website — Base JavaScript Template
|
||||
Mobile nav toggle, header scroll shrink, active page detection.
|
||||
Copy as js/main.js — zero dependencies, vanilla JS.
|
||||
============================================================ */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
/* ---- Elements ---- */
|
||||
var hamburger = document.querySelector('.hamburger');
|
||||
var mobileNav = document.querySelector('.mobile-nav');
|
||||
var header = document.querySelector('.site-header');
|
||||
|
||||
/* ---- Mobile Nav Toggle ---- */
|
||||
if (hamburger && mobileNav) {
|
||||
hamburger.addEventListener('click', function () {
|
||||
var isOpen = mobileNav.classList.toggle('open');
|
||||
hamburger.classList.toggle('active');
|
||||
hamburger.setAttribute('aria-expanded', isOpen);
|
||||
document.body.style.overflow = isOpen ? 'hidden' : '';
|
||||
});
|
||||
|
||||
/* Close nav when a link is tapped */
|
||||
var navLinks = mobileNav.querySelectorAll('a');
|
||||
navLinks.forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
mobileNav.classList.remove('open');
|
||||
hamburger.classList.remove('active');
|
||||
hamburger.setAttribute('aria-expanded', 'false');
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Header Shrink on Scroll ---- */
|
||||
if (header) {
|
||||
window.addEventListener('scroll', function () {
|
||||
if (window.scrollY > 80) {
|
||||
header.classList.add('scrolled');
|
||||
} else {
|
||||
header.classList.remove('scrolled');
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/* ---- Active Nav Link Highlighting ---- */
|
||||
(function () {
|
||||
var currentPath = window.location.pathname.replace(/\/$/, '') || '/';
|
||||
var navLinks = document.querySelectorAll('.main-nav a, .footer-nav a');
|
||||
navLinks.forEach(function (link) {
|
||||
var href = link.getAttribute('href');
|
||||
if (!href) return;
|
||||
var linkPath = href.replace(/\/$/, '') || '/';
|
||||
if (linkPath === '/' && currentPath !== '/') return;
|
||||
if (currentPath.startsWith(linkPath) && linkPath !== '/') {
|
||||
link.classList.add('active');
|
||||
} else if (currentPath === '/' && linkPath === '/') {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- Smooth Scroll for Anchor Links ---- */
|
||||
document.querySelectorAll('a[href^="#"]').forEach(function (anchor) {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
var target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,304 @@
|
||||
/* ============================================================
|
||||
Local Business Website — Base CSS Template
|
||||
Mobile-first. No frameworks. No build step.
|
||||
Copy this, customize :root variables, build your pages.
|
||||
============================================================ */
|
||||
|
||||
/* ---- CSS Custom Properties ---- */
|
||||
:root {
|
||||
/* Colors — customize these per-client */
|
||||
--color-primary: #1E40AF; /* Blue — trust, main brand */
|
||||
--color-primary-light:#DBEAFE;
|
||||
--color-secondary: #15803D; /* Green — growth, second service line */
|
||||
--color-secondary-light:#DCFCE7;
|
||||
--color-emergency: #DC2626; /* Red — urgency CTAs only */
|
||||
--color-emergency-light:#FEE2E2;
|
||||
--color-dark: #111827; /* Near-black — body text */
|
||||
--color-dark-soft: #1F2937;
|
||||
--color-medium: #6B7280; /* Secondary text */
|
||||
--color-light: #F9FAFB; /* Page background */
|
||||
--color-white: #FFFFFF;
|
||||
--color-border: #E5E7EB;
|
||||
--color-accent: #D97706; /* Amber/gold — stars, subtle highlights */
|
||||
|
||||
/* Typography */
|
||||
--font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
--line-height-body: 1.65;
|
||||
|
||||
/* Spacing scale */
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-2xl: 3rem;
|
||||
--space-3xl: 4rem;
|
||||
|
||||
/* Layout */
|
||||
--max-width: 72rem;
|
||||
--header-height: 64px;
|
||||
--callbar-height: 48px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
}
|
||||
|
||||
/* ---- Reset ---- */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-stack);
|
||||
font-size: 1rem;
|
||||
line-height: var(--line-height-body);
|
||||
color: var(--color-dark);
|
||||
background: var(--color-light);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding-top: calc(var(--header-height) + var(--callbar-height));
|
||||
}
|
||||
|
||||
img { max-width: 100%; height: auto; display: block; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
ul, ol { list-style: none; }
|
||||
button { font: inherit; cursor: pointer; border: none; background: none; }
|
||||
|
||||
/* ---- Utility Classes ---- */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute; width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden; clip: rect(0,0,0,0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
|
||||
/* ---- Buttons ---- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
gap: var(--space-sm); padding: 0.75rem 1.5rem;
|
||||
font-weight: 600; font-size: 1rem; line-height: 1;
|
||||
border-radius: 0.5rem; transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary); color: var(--color-white);
|
||||
}
|
||||
.btn-primary:hover, .btn-primary:focus-visible { background: #1E3A8A; }
|
||||
|
||||
.btn-emergency {
|
||||
background: var(--color-emergency); color: var(--color-white);
|
||||
}
|
||||
.btn-emergency:hover, .btn-emergency:focus-visible { background: #B91C1C; }
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary); color: var(--color-white);
|
||||
}
|
||||
.btn-secondary:hover, .btn-secondary:focus-visible { background: #166534; }
|
||||
|
||||
.btn-outline {
|
||||
background: var(--color-white); color: var(--color-primary);
|
||||
border: 2px solid var(--color-primary);
|
||||
}
|
||||
.btn-outline:hover, .btn-outline:focus-visible {
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.btn-large { padding: 1rem 2rem; font-size: 1.125rem; }
|
||||
.btn-full { width: 100%; }
|
||||
|
||||
/* ---- Phone Link ---- */
|
||||
.phone-link {
|
||||
display: inline-flex; align-items: center; gap: var(--space-xs);
|
||||
font-weight: 700; color: var(--color-primary); font-size: 1.125rem;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
.phone-link:hover, .phone-link:focus-visible { color: var(--color-emergency); }
|
||||
.phone-link svg { width: 1.25rem; height: 1.25rem; flex-shrink: 0; }
|
||||
|
||||
/* ================================================================
|
||||
HEADER (sticky)
|
||||
================================================================ */
|
||||
.site-header {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
height: var(--header-height); background: var(--color-white);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: height var(--transition-fast), padding var(--transition-fast);
|
||||
}
|
||||
.site-header.scrolled { --header-height: 56px; }
|
||||
|
||||
.header-inner {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo {
|
||||
display: flex; align-items: center; gap: var(--space-sm);
|
||||
font-weight: 800; font-size: 1.25rem; color: var(--color-dark);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logo-icon {
|
||||
width: 2rem; height: 2rem; background: var(--color-primary);
|
||||
color: var(--color-white); border-radius: 0.5rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* Desktop nav */
|
||||
.main-nav {
|
||||
display: none; /* hidden on mobile */
|
||||
align-items: center; gap: var(--space-xl);
|
||||
}
|
||||
.main-nav a {
|
||||
font-weight: 500; color: var(--color-dark-soft);
|
||||
padding: var(--space-xs) 0; border-bottom: 2px solid transparent;
|
||||
transition: color var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
.main-nav a:hover, .main-nav a.active {
|
||||
color: var(--color-primary); border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.header-right { display: none; align-items: center; gap: var(--space-lg); }
|
||||
|
||||
/* Hamburger */
|
||||
.hamburger {
|
||||
display: flex; flex-direction: column; justify-content: center; gap: 5px;
|
||||
width: 44px; height: 44px; padding: 10px;
|
||||
}
|
||||
.hamburger span {
|
||||
display: block; width: 24px; height: 2px;
|
||||
background: var(--color-dark); border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.hamburger.active span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
|
||||
.hamburger.active span:nth-child(2) { opacity: 0; }
|
||||
.hamburger.active span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
|
||||
|
||||
/* Mobile nav drawer */
|
||||
.mobile-nav {
|
||||
display: none; position: fixed; top: var(--header-height);
|
||||
left: 0; right: 0; bottom: 0; background: var(--color-white);
|
||||
z-index: 99; flex-direction: column; padding: var(--space-xl) var(--space-lg);
|
||||
gap: var(--space-sm); overflow-y: auto;
|
||||
}
|
||||
.mobile-nav.open { display: flex; }
|
||||
.mobile-nav a {
|
||||
display: block; padding: var(--space-md); font-size: 1.125rem;
|
||||
font-weight: 600; color: var(--color-dark); border-radius: 0.5rem;
|
||||
}
|
||||
.mobile-nav a:hover { background: var(--color-light); }
|
||||
.mobile-nav .btn { margin-top: var(--space-md); }
|
||||
|
||||
/* ================================================================
|
||||
TAP-TO-CALL BAR (mobile only)
|
||||
================================================================ */
|
||||
.callbar {
|
||||
position: fixed; top: var(--header-height); left: 0; right: 0; z-index: 98;
|
||||
height: var(--callbar-height); background: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; font-size: 1rem; gap: var(--space-sm);
|
||||
text-decoration: none;
|
||||
}
|
||||
.callbar:hover { background: #1E3A8A; }
|
||||
.callbar.emergency { background: var(--color-emergency); }
|
||||
.callbar.emergency:hover { background: #B91C1C; }
|
||||
.callbar svg { width: 1.25rem; height: 1.25rem; flex-shrink: 0; }
|
||||
|
||||
/* ================================================================
|
||||
PAGE SECTIONS
|
||||
================================================================ */
|
||||
.page-section { padding: var(--space-3xl) 0; }
|
||||
.page-section-alt { background: var(--color-white); }
|
||||
.page-section-dark { background: var(--color-dark); color: var(--color-white); }
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.75rem; font-weight: 800; text-align: center;
|
||||
margin-bottom: var(--space-xs); color: var(--color-dark); line-height: 1.2;
|
||||
}
|
||||
.section-subheading {
|
||||
font-size: 1.125rem; color: var(--color-medium); text-align: center;
|
||||
margin-bottom: var(--space-2xl); max-width: 36rem;
|
||||
margin-left: auto; margin-right: auto;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
FOOTER
|
||||
================================================================ */
|
||||
.site-footer {
|
||||
background: var(--color-dark-soft); color: var(--color-white);
|
||||
padding: var(--space-3xl) 0 var(--space-xl);
|
||||
}
|
||||
.footer-grid {
|
||||
display: grid; grid-template-columns: 1fr; gap: var(--space-2xl);
|
||||
}
|
||||
.footer-brand p { color: #D1D5DB; margin-top: var(--space-sm); line-height: 1.7; }
|
||||
.footer-brand .logo { color: var(--color-white); font-size: 1.375rem; }
|
||||
|
||||
.footer-nav h4 {
|
||||
font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
color: #9CA3AF; margin-bottom: var(--space-md);
|
||||
}
|
||||
.footer-nav a { display: block; color: #D1D5DB; padding: var(--space-xs) 0; }
|
||||
.footer-nav a:hover { color: var(--color-white); }
|
||||
|
||||
.footer-contact p {
|
||||
display: flex; align-items: center; gap: var(--space-sm);
|
||||
color: #D1D5DB; margin-bottom: var(--space-sm);
|
||||
}
|
||||
.footer-contact .phone-link { color: var(--color-white); font-size: 1.25rem; }
|
||||
|
||||
.footer-service-areas {
|
||||
margin-top: var(--space-2xl); padding-top: var(--space-xl);
|
||||
border-top: 1px solid #374151; text-align: center;
|
||||
color: #9CA3AF; font-size: 0.9375rem;
|
||||
}
|
||||
.footer-service-areas span { color: #D1D5DB; }
|
||||
|
||||
.footer-bottom {
|
||||
margin-top: var(--space-xl); padding-top: var(--space-lg);
|
||||
border-top: 1px solid #374151; text-align: center;
|
||||
color: #6B7280; font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
RESPONSIVE: Tablet+ (768px)
|
||||
================================================================ */
|
||||
@media (min-width: 768px) {
|
||||
body { padding-top: var(--header-height); }
|
||||
.hamburger { display: none; }
|
||||
.main-nav { display: flex; }
|
||||
.header-right { display: flex; }
|
||||
.callbar { display: none; }
|
||||
.mobile-nav { display: none !important; }
|
||||
.footer-grid { grid-template-columns: 2fr 1fr 1fr; }
|
||||
.section-heading { font-size: 2.25rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.section-heading { font-size: 2.75rem; }
|
||||
.section-subheading { font-size: 1.25rem; }
|
||||
}
|
||||
Reference in New Issue
Block a user