commit fbd11726f8fa52607f9c99250e6b29d086df8717 Author: John Loper II Date: Mon May 11 12:12:42 2026 -0400 Initial commit: WilliamsPW codebase adapted for LoperBoys2.com diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..078861f --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "phpmailer/phpmailer": "^7.0" + } +} diff --git a/convert-webp.sh b/convert-webp.sh new file mode 100644 index 0000000..52abd88 --- /dev/null +++ b/convert-webp.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Convert all JPG/PNG images to WebP and update HTML with srcset fallbacks +set -e + +IMG_DIR="img" +INDEX_FILE="index.html" + +echo "Converting images to WebP..." + +# Convert all JPG and PNG files to WebP +for f in "$IMG_DIR"/*.jpg "$IMG_DIR"/*.png; do + [ -f "$f" ] || continue + base="${f%.*}" + webp="${base}.webp" + if [ ! -f "$webp" ]; then + convert "$f" -quality 82 "$webp" + echo " Converted: $(basename "$f") -> $(basename "$webp")" + fi +done + +echo "" +echo "Updating index.html with srcset attributes..." + +# Update each img tag in index.html with srcset +# We need to update: gallery images, work images, and og-banner in meta tags +sed -i 's|img/og-banner.jpg|img/og-banner.jpg, img/og-banner.webp|g' "$INDEX_FILE" + +# Gallery images - update each img tag with srcset +for f in "$IMG_DIR"/*.jpg "$IMG_DIR"/*.png; do + [ -f "$f" ] || continue + basename="${f#$IMG_DIR/}" + basename="${basename%.*}" + # Update JPG references + if [[ "$basename" == *".jpg" ]]; then + webp="${basename%.jpg}.webp" + sed -i "s|\"$basename\"|\"$basename\", \"$webp\" type=\"image/webp\"|g" "$INDEX_FILE" + fi + # Update PNG references + if [[ "$basename" == *".png" ]]; then + webp="${basename%.png}.webp" + sed -i "s|\"$basename\"|\"$basename\", \"$webp\" type=\"image/webp\"|g" "$INDEX_FILE" + fi +done + +echo "" +echo "Done! Generated WebP files and updated index.html with srcset fallbacks." +echo "" +echo "Size comparison:" +echo "Original: $(du -sh "$IMG_DIR" | cut -f1)" +echo "With WebP: $(du -sh "$IMG_DIR" | cut -f1)" +du -sh "$IMG_DIR"/*.webp 2>/dev/null | awk '{printf " %-35s %s\n", $2, $1}' | sort diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..4fa70b3 --- /dev/null +++ b/css/style.css @@ -0,0 +1,1170 @@ +/* ============================================ + Williams Pressure Washing Services + Professional Responsive Website + ============================================ */ + +/* --- CSS Variables --- */ +:root { + --blue-dark: #0a2463; + --blue-mid: #1e56a0; + --blue-light: #3a8fd4; + --blue-accent: #4dc8f5; + --cyan-bright: #00e0ff; + --white: #ffffff; + --off-white: #f0f6ff; + --gray-light: #e8edf3; + --gray: #8899aa; + --text-dark: #1a2332; + --text-mid: #3d4f63; + --green: #22c55e; + --green-dark: #16a34a; + --shadow: 0 4px 24px rgba(10, 36, 99, 0.12); + --shadow-lg: 0 12px 40px rgba(10, 36, 99, 0.18); + --radius: 12px; + --radius-lg: 20px; + --transition: 0.3s ease; +} + +/* --- Reset & Base --- */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + font-size: 16px; +} + +body { + font-family: 'Open Sans', Arial, sans-serif; + color: var(--text-dark); + line-height: 1.6; + overflow-x: hidden; + background: var(--white); +} + +h1, h2, h3, h4 { + font-family: 'Montserrat', sans-serif; + line-height: 1.2; +} + +a { + text-decoration: none; + color: inherit; +} + +ul { + list-style: none; +} + +img { + max-width: 100%; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; +} + +.accent { + color: var(--blue-accent); +} + +/* --- Buttons --- */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 28px; + border-radius: 50px; + font-family: 'Montserrat', sans-serif; + font-weight: 700; + font-size: 1rem; + cursor: pointer; + border: none; + transition: all var(--transition); + text-align: center; + justify-content: center; +} + +.btn-icon { + flex-shrink: 0; +} + +.btn-primary { + background: linear-gradient(135deg, var(--blue-accent), var(--blue-light)); + color: var(--white); + box-shadow: 0 4px 16px rgba(77, 200, 245, 0.4); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(77, 200, 245, 0.5); +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.15); + color: var(--white); + border: 2px solid rgba(255, 255, 255, 0.5); + backdrop-filter: blur(4px); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.25); + border-color: var(--white); + transform: translateY(-2px); +} + +.btn-large { + padding: 18px 36px; + font-size: 1.125rem; +} + +.btn-block { + width: 100%; +} + +/* --- Navigation --- */ +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + padding: 16px 0; + transition: all var(--transition); +} + +.navbar.scrolled { + background: rgba(10, 36, 99, 0.95); + backdrop-filter: blur(12px); + padding: 10px 0; + box-shadow: 0 2px 20px rgba(0, 0, 0, 0.2); +} + +.nav-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.logo { + display: flex; + align-items: center; + gap: 8px; + font-family: 'Montserrat', sans-serif; + font-weight: 900; + font-size: 1.25rem; + color: var(--white); +} + +.logo-icon { + font-size: 1.5rem; +} + +.logo-text .accent { + color: var(--cyan-bright); +} + +.nav-links { + display: flex; + align-items: center; + gap: 32px; +} + +.nav-links a { + color: rgba(255, 255, 255, 0.85); + font-weight: 600; + font-size: 0.95rem; + transition: color var(--transition); + font-family: 'Montserrat', sans-serif; +} + +.nav-links a:hover { + color: var(--cyan-bright); +} + +.nav-cta { + background: linear-gradient(135deg, var(--cyan-bright), var(--blue-accent)); + color: var(--blue-dark) !important; + padding: 10px 22px; + border-radius: 50px; + font-weight: 700 !important; + transition: all var(--transition); +} + +.nav-cta:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 224, 255, 0.4); +} + +.nav-toggle { + display: none; + flex-direction: column; + gap: 5px; + background: none; + border: none; + cursor: pointer; + padding: 4px; +} + +.nav-toggle span { + display: block; + width: 26px; + height: 3px; + background: var(--white); + border-radius: 2px; + transition: all var(--transition); +} + +/* --- Hero Section --- */ +.hero { + position: relative; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + background: linear-gradient(135deg, #0a2463 0%, #1e56a0 40%, #2d7dd2 70%, #00c6ff 100%); + overflow: hidden; + padding: 120px 24px 80px; +} + +.hero::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 20% 80%, rgba(0, 224, 255, 0.15) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(77, 200, 245, 0.2) 0%, transparent 50%), + radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.05) 0%, transparent 70%); + animation: heroFloat 8s ease-in-out infinite; +} + +@keyframes heroFloat { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +/* Water drops animation */ +.hero::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-image: + radial-gradient(circle, rgba(255,255,255,0.08) 2px, transparent 2px), + radial-gradient(circle, rgba(255,255,255,0.05) 1px, transparent 1px); + background-size: 80px 80px, 50px 50px; + background-position: 0 0, 25px 25px; + animation: waterDrips 20s linear infinite; +} + +@keyframes waterDrips { + 0% { transform: translateY(-100px); } + 100% { transform: translateY(100px); } +} + +.hero-content { + position: relative; + z-index: 2; + max-width: 800px; +} + +.hero-badge { + display: inline-block; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.2); + backdrop-filter: blur(8px); + color: var(--white); + padding: 8px 20px; + border-radius: 50px; + font-size: 0.9rem; + font-weight: 600; + margin-bottom: 24px; +} + +.hero-title { + font-size: clamp(2.2rem, 6vw, 4rem); + font-weight: 900; + color: var(--white); + margin-bottom: 12px; + text-shadow: 0 2px 20px rgba(0, 0, 0, 0.2); +} + +.hero-title .accent { + color: var(--cyan-bright); +} + +.hero-slogan { + font-family: 'Montserrat', sans-serif; + font-size: clamp(1.2rem, 3vw, 1.6rem); + color: var(--cyan-bright); + font-style: italic; + font-weight: 600; + margin-bottom: 16px; +} + +.hero-sub { + font-size: clamp(1rem, 2vw, 1.15rem); + color: rgba(255, 255, 255, 0.9); + margin-bottom: 32px; + line-height: 1.8; +} + +.hero-ctas { + display: flex; + gap: 16px; + justify-content: center; + flex-wrap: wrap; +} + +.scroll-indicator { + position: absolute; + bottom: 32px; + left: 50%; + transform: translateX(-50%); + z-index: 2; + color: rgba(255, 255, 255, 0.6); + font-size: 0.85rem; + text-align: center; + font-family: 'Montserrat', sans-serif; + font-weight: 600; + letter-spacing: 1px; + text-transform: uppercase; + animation: scrollBounce 2s ease-in-out infinite; +} + +.scroll-arrow { + width: 24px; + height: 24px; + border-right: 3px solid rgba(255, 255, 255, 0.5); + border-bottom: 3px solid rgba(255, 255, 255, 0.5); + transform: rotate(45deg); + margin: 8px auto 0; +} + +@keyframes scrollBounce { + 0%, 100% { transform: translateX(-50%) translateY(0); } + 50% { transform: translateX(-50%) translateY(8px); } +} + +/* --- Section Titles --- */ +.section-title { + text-align: center; + font-size: clamp(1.8rem, 4vw, 2.5rem); + font-weight: 900; + color: var(--blue-dark); + margin-bottom: 8px; +} + +.section-subtitle { + text-align: center; + color: var(--text-mid); + font-size: 1.1rem; + margin-bottom: 48px; +} + +/* --- Services --- */ +.services { + padding: 100px 0; + background: var(--off-white); +} + +.services-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 28px; + margin-bottom: 48px; +} + +.service-card { + background: var(--white); + border-radius: var(--radius-lg); + padding: 36px 28px; + box-shadow: var(--shadow); + transition: all var(--transition); + position: relative; + overflow: hidden; + border: 1px solid var(--gray-light); +} + +.service-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--blue-accent), var(--cyan-bright)); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition); +} + +.service-card:hover { + transform: translateY(-6px); + box-shadow: var(--shadow-lg); +} + +.service-card:hover::before { + transform: scaleX(1); +} + +.service-card.featured { + border: 2px solid var(--blue-accent); + transform: scale(1.03); +} + +.service-card.featured::before { + transform: scaleX(1); +} + +.service-card.featured:hover { + transform: scale(1.03) translateY(-6px); +} + +.featured-badge { + position: absolute; + top: 16px; + right: 16px; + background: linear-gradient(135deg, var(--blue-accent), var(--blue-light)); + color: var(--white); + padding: 4px 14px; + border-radius: 50px; + font-size: 0.75rem; + font-weight: 700; + font-family: 'Montserrat', sans-serif; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.service-icon { + font-size: 3rem; + margin-bottom: 16px; +} + +.service-card h3 { + font-size: 1.3rem; + color: var(--blue-dark); + margin-bottom: 12px; +} + +.service-card p { + color: var(--text-mid); + font-size: 0.95rem; + margin-bottom: 16px; +} + +.service-features { + font-size: 0.9rem; + color: var(--text-mid); +} + +.service-features li { + padding: 4px 0; +} + +.services-cta { + text-align: center; +} + +/* --- Why Us --- */ +.why-us { + padding: 100px 0; + background: linear-gradient(135deg, var(--blue-dark) 0%, var(--blue-mid) 100%); + color: var(--white); +} + +.why-us .section-title { + color: var(--white); +} + +.why-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 32px; +} + +.why-item { + text-align: center; + padding: 32px 20px; + border-radius: var(--radius-lg); + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all var(--transition); +} + +.why-item:hover { + background: rgba(255, 255, 255, 0.12); + transform: translateY(-4px); +} + +.why-icon { + font-size: 2.5rem; + margin-bottom: 16px; +} + +.why-item h4 { + font-size: 1.15rem; + margin-bottom: 8px; + color: var(--cyan-bright); +} + +.why-item p { + color: rgba(255, 255, 255, 0.8); + font-size: 0.95rem; +} + +/* --- About --- */ +.about { + padding: 100px 0; + background: var(--white); +} + +.about-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 64px; + align-items: center; +} + +.about-visual { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.about-watermark { + font-size: 8rem; + opacity: 0.08; + color: var(--blue-accent); + position: absolute; + user-select: none; +} + +.about-stats { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + width: 100%; +} + +.stat { + background: linear-gradient(135deg, var(--blue-dark), var(--blue-mid)); + border-radius: var(--radius-lg); + padding: 32px 20px; + text-align: center; + color: var(--white); + box-shadow: var(--shadow-lg); + transition: transform var(--transition); +} + +.stat:hover { + transform: scale(1.05); +} + +.stat-number { + font-family: 'Montserrat', sans-serif; + font-size: 2.2rem; + font-weight: 900; + color: var(--cyan-bright); + margin-bottom: 4px; +} + +.stat-label { + font-size: 0.85rem; + line-height: 1.4; + opacity: 0.9; +} + +.about-content h2 { + font-size: clamp(1.6rem, 3vw, 2.2rem); + color: var(--blue-dark); + margin-bottom: 20px; +} + +.about-content p { + color: var(--text-mid); + margin-bottom: 16px; + font-size: 1.05rem; +} + +.about-content .btn { + margin-top: 16px; +} + +/* --- Contact --- */ +.contact { + padding: 100px 0; + background: var(--off-white); +} + +.contact-grid { + display: grid; + grid-template-columns: 1fr 1.2fr; + gap: 48px; + align-items: start; +} + +.contact-info h3 { + font-size: 1.5rem; + color: var(--blue-dark); + margin-bottom: 24px; +} + +.contact-card { + background: var(--white); + border-radius: var(--radius-lg); + padding: 28px; + box-shadow: var(--shadow); + margin-bottom: 24px; +} + +.contact-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 0; + border-bottom: 1px solid var(--gray-light); +} + +.contact-item:last-child { + border-bottom: none; +} + +.contact-icon { + font-size: 1.8rem; + flex-shrink: 0; +} + +.contact-item strong { + display: block; + color: var(--blue-dark); + font-family: 'Montserrat', sans-serif; + font-size: 0.9rem; + margin-bottom: 2px; +} + +.contact-item a, +.contact-item span { + color: var(--text-mid); + font-size: 1rem; +} + +.contact-item a:hover { + color: var(--blue-accent); +} + +.contact-cta { + text-align: center; +} + +/* --- Contact Form --- */ +.contact-form-wrapper { + background: var(--white); + border-radius: var(--radius-lg); + padding: 40px; + box-shadow: var(--shadow-lg); + border: 1px solid var(--gray-light); +} + +.form-group { + margin-bottom: 20px; + margin-top: 20px; +} + +.form-group label { + display: block; + font-weight: 600; + font-family: 'Montserrat', sans-serif; + font-size: 0.9rem; + color: var(--blue-dark); + margin-bottom: 6px; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 12px 16px; + border: 2px solid var(--gray-light); + border-radius: var(--radius); + font-family: 'Open Sans', sans-serif; + font-size: 1rem; + color: var(--text-dark); + transition: all var(--transition); + background: var(--off-white); +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--blue-accent); + background: var(--white); + box-shadow: 0 0 0 4px rgba(77, 200, 245, 0.15); +} + +.form-group textarea { + resize: vertical; + min-height: 100px; +} + +.form-note { + text-align: center; + color: var(--gray); + font-size: 0.85rem; + margin-top: 16px; + margin-bottom: 32px; +} + +/* Add breathing room between privacy text and first form field */ +.contact-form-wrapper > .contact-form > .form-group:first-child { + margin-top: 0; +} + +/* --- How It Works --- */ +.how-it-works { + padding: 100px 0; + background: var(--off-white); +} + +.steps-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 40px; + max-width: 900px; + margin: 0 auto; +} + +.step-card { + text-align: center; + padding: 32px 24px; +} + +.step-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + background: linear-gradient(135deg, var(--blue-accent), var(--cyan-bright)); + color: var(--blue-dark); + font-family: 'Montserrat', sans-serif; + font-weight: 900; + font-size: 1.4rem; + border-radius: 50%; + margin-bottom: 16px; + box-shadow: 0 4px 16px rgba(77, 200, 245, 0.3); +} + +.step-icon { + font-size: 3rem; + margin-bottom: 12px; +} + +.step-card h4 { + font-size: 1.2rem; + color: var(--blue-dark); + margin-bottom: 8px; +} + +.step-card p { + color: var(--text-mid); + font-size: 0.95rem; +} + +.step-card a { + color: var(--blue-accent); + font-weight: 600; +} + +/* --- Gallery --- */ +.gallery { + padding: 100px 0; + background: var(--white); +} + +.gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 32px; + margin-bottom: 24px; +} + +.gallery-item { + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow); +} + +.gallery-before-after { + position: relative; + width: 100%; + aspect-ratio: 4 / 3; + overflow: hidden; +} + +.gallery-before-after img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.gallery-before-after img:last-child { + clip-path: inset(0 50% 0 0); + transition: clip-path 0.3s ease; +} + +.gallery-item:hover .gallery-before-after img:last-child { + clip-path: inset(0 0 0 0); +} + +.gallery-label { + position: absolute; + top: 12px; + padding: 4px 14px; + border-radius: 50px; + font-family: 'Montserrat', sans-serif; + font-weight: 700; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--white); + z-index: 2; +} + +.gallery-label-before { + left: 12px; + background: rgba(0, 0, 0, 0.6); +} + +.gallery-label-after { + right: 12px; + background: rgba(34, 197, 94, 0.85); +} + +.gallery-caption { + padding: 16px 20px; + background: var(--white); + font-family: 'Montserrat', sans-serif; + font-weight: 600; + font-size: 0.95rem; + color: var(--text-dark); + text-align: center; +} + +.gallery-note { + text-align: center; + color: var(--gray); + font-size: 0.9rem; +} + +/* --- Our Work --- */ +.our-work { + padding: 100px 0; + background: var(--off-white); +} + +.work-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 20px; + max-width: 900px; + margin: 0 auto 24px; +} + +.work-item { + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow); + background: var(--white); +} + +.work-item img { + width: 100%; + height: 240px; + object-fit: cover; + display: block; +} + +.work-caption { + padding: 14px 18px; + font-family: 'Montserrat', sans-serif; + font-weight: 600; + font-size: 0.9rem; + color: var(--text-dark); + text-align: center; +} + +/* --- Testimonials --- */ +.testimonials { + padding: 100px 0; + background: linear-gradient(135deg, var(--blue-dark) 0%, var(--blue-mid) 100%); + color: var(--white); +} + +.testimonials .section-title { + color: var(--white); +} + +.testimonials-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 28px; +} + +.testimonial-card { + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: var(--radius-lg); + padding: 32px 28px; + transition: all var(--transition); +} + +.testimonial-card:hover { + background: rgba(255, 255, 255, 0.14); + transform: translateY(-4px); +} + +.testimonial-stars { + font-size: 1.2rem; + margin-bottom: 12px; +} + +.testimonial-text { + color: rgba(255, 255, 255, 0.9); + font-size: 1rem; + line-height: 1.7; + margin-bottom: 20px; + font-style: italic; +} + +.testimonial-author { + display: flex; + flex-direction: column; + gap: 2px; +} + +.testimonial-author strong { + color: var(--cyan-bright); + font-size: 1rem; +} + +.testimonial-author span { + color: rgba(255, 255, 255, 0.6); + font-size: 0.85rem; +} + +/* --- Footer --- */ +.footer { + background: var(--blue-dark); + color: var(--white); + padding: 48px 0 24px; +} + +.footer-content { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 24px; + padding-bottom: 24px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 24px; +} + +.footer-brand h3 { + font-size: 1.3rem; + margin-bottom: 4px; +} + +.footer-brand .slogan { + color: var(--cyan-bright); + font-style: italic; + font-size: 0.95rem; +} + +.footer-contact { + display: flex; + gap: 24px; + flex-wrap: wrap; +} + +.footer-phone, +.footer-email { + color: rgba(255, 255, 255, 0.8); + font-size: 0.95rem; + transition: color var(--transition); +} + +.footer-phone:hover, +.footer-email:hover { + color: var(--cyan-bright); +} + +.footer-bottom { + text-align: center; + color: rgba(255, 255, 255, 0.4); + font-size: 0.85rem; +} + +/* --- Floating CTA (mobile) --- */ +.floating-cta { + position: fixed; + bottom: 24px; + right: 24px; + width: 60px; + height: 60px; + background: linear-gradient(135deg, var(--green), var(--green-dark)); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: var(--white); + box-shadow: 0 4px 20px rgba(34, 197, 94, 0.5); + z-index: 999; + transition: all var(--transition); + animation: pulse 2s ease-in-out infinite; +} + +.floating-cta:hover { + transform: scale(1.1); +} + +@keyframes pulse { + 0%, 100% { box-shadow: 0 4px 20px rgba(34, 197, 94, 0.5); } + 50% { box-shadow: 0 4px 30px rgba(34, 197, 94, 0.8); } +} + +/* --- Responsive --- */ +@media (max-width: 900px) { + .about-container { + grid-template-columns: 1fr; + gap: 40px; + } + + .about-visual { + order: -1; + } + + .contact-grid { + grid-template-columns: 1fr; + } + + .service-card.featured { + transform: none; + } + + .service-card.featured:hover { + transform: translateY(-6px); + } +} + +@media (max-width: 768px) { + .nav-links { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(10, 36, 99, 0.97); + flex-direction: column; + align-items: center; + justify-content: center; + gap: 28px; + z-index: 999; + } + + .nav-links.active { + display: flex; + } + + .nav-links a { + font-size: 1.3rem; + } + + .nav-toggle { + display: flex; + z-index: 1000; + } + + .nav-toggle.active span:nth-child(1) { + transform: rotate(45deg) translate(6px, 6px); + } + + .nav-toggle.active span:nth-child(2) { + opacity: 0; + } + + .nav-toggle.active span:nth-child(3) { + transform: rotate(-45deg) translate(6px, -6px); + } + + .hero { + padding: 140px 20px 60px; + min-height: 100svh; + } + + .hero-ctas { + flex-direction: column; + align-items: center; + } + + .services-grid { + grid-template-columns: 1fr; + } + + .why-grid { + grid-template-columns: 1fr 1fr; + } + + .footer-content { + flex-direction: column; + text-align: center; + } + + .footer-contact { + justify-content: center; + } + + .contact-form-wrapper { + padding: 28px 20px; + } + + .stat { + padding: 24px 16px; + } + + .stat-number { + font-size: 1.8rem; + } +} + +@media (max-width: 480px) { + .why-grid { + grid-template-columns: 1fr; + } + + .about-stats { + grid-template-columns: 1fr; + } + + .hero-badge { + font-size: 0.8rem; + } +} diff --git a/img/after-house.jpg b/img/after-house.jpg new file mode 100644 index 0000000..4ad9862 --- /dev/null +++ b/img/after-house.jpg @@ -0,0 +1,13 @@ + + + + + + + + + + Clean House Siding + Replace with real photo + 400 × 300 + \ No newline at end of file diff --git a/img/after-house.png b/img/after-house.png new file mode 100644 index 0000000..ea2014b Binary files /dev/null and b/img/after-house.png differ diff --git a/img/after-house.webp b/img/after-house.webp new file mode 100644 index 0000000..2bcc3d4 Binary files /dev/null and b/img/after-house.webp differ diff --git a/img/after-storefront.jpg b/img/after-storefront.jpg new file mode 100644 index 0000000..51612f8 --- /dev/null +++ b/img/after-storefront.jpg @@ -0,0 +1,13 @@ + + + + + + + + + + Sparkling Storefront + Replace with real photo + 400 × 300 + \ No newline at end of file diff --git a/img/after-storefront.png b/img/after-storefront.png new file mode 100644 index 0000000..d7d4cdd Binary files /dev/null and b/img/after-storefront.png differ diff --git a/img/after-storefront.webp b/img/after-storefront.webp new file mode 100644 index 0000000..e99f14e Binary files /dev/null and b/img/after-storefront.webp differ diff --git a/img/after-truck.jpg b/img/after-truck.jpg new file mode 100644 index 0000000..08efff9 --- /dev/null +++ b/img/after-truck.jpg @@ -0,0 +1,13 @@ + + + + + + + + + + Clean Truck Fleet + Replace with real photo + 400 × 300 + \ No newline at end of file diff --git a/img/after-truck.png b/img/after-truck.png new file mode 100644 index 0000000..726b9c2 Binary files /dev/null and b/img/after-truck.png differ diff --git a/img/after-truck.webp b/img/after-truck.webp new file mode 100644 index 0000000..b6ac9f4 Binary files /dev/null and b/img/after-truck.webp differ diff --git a/img/apple-touch-icon.png b/img/apple-touch-icon.png new file mode 100644 index 0000000..9def841 Binary files /dev/null and b/img/apple-touch-icon.png differ diff --git a/img/apple-touch-icon.webp b/img/apple-touch-icon.webp new file mode 100644 index 0000000..4458c04 Binary files /dev/null and b/img/apple-touch-icon.webp differ diff --git a/img/before-house.jpg b/img/before-house.jpg new file mode 100644 index 0000000..d283c00 --- /dev/null +++ b/img/before-house.jpg @@ -0,0 +1,13 @@ + + + + + + + + + + Dirty House Siding + Replace with real photo + 400 × 300 + \ No newline at end of file diff --git a/img/before-house.png b/img/before-house.png new file mode 100644 index 0000000..17ccab3 Binary files /dev/null and b/img/before-house.png differ diff --git a/img/before-house.webp b/img/before-house.webp new file mode 100644 index 0000000..19fa5df Binary files /dev/null and b/img/before-house.webp differ diff --git a/img/before-storefront.jpg b/img/before-storefront.jpg new file mode 100644 index 0000000..e7e83e2 --- /dev/null +++ b/img/before-storefront.jpg @@ -0,0 +1,13 @@ + + + + + + + + + + Grimy Storefront + Replace with real photo + 400 × 300 + \ No newline at end of file diff --git a/img/before-storefront.png b/img/before-storefront.png new file mode 100644 index 0000000..b45628c Binary files /dev/null and b/img/before-storefront.png differ diff --git a/img/before-storefront.webp b/img/before-storefront.webp new file mode 100644 index 0000000..5b44f42 Binary files /dev/null and b/img/before-storefront.webp differ diff --git a/img/before-truck.jpg b/img/before-truck.jpg new file mode 100644 index 0000000..4a8d785 --- /dev/null +++ b/img/before-truck.jpg @@ -0,0 +1,13 @@ + + + + + + + + + + Dirty Truck Fleet + Replace with real photo + 400 × 300 + \ No newline at end of file diff --git a/img/before-truck.png b/img/before-truck.png new file mode 100644 index 0000000..d33b561 Binary files /dev/null and b/img/before-truck.png differ diff --git a/img/before-truck.webp b/img/before-truck.webp new file mode 100644 index 0000000..679ef9e Binary files /dev/null and b/img/before-truck.webp differ diff --git a/img/favicon.png b/img/favicon.png new file mode 100644 index 0000000..6405f0f Binary files /dev/null and b/img/favicon.png differ diff --git a/img/favicon.webp b/img/favicon.webp new file mode 100644 index 0000000..f99a983 Binary files /dev/null and b/img/favicon.webp differ diff --git a/img/og-banner.jpg b/img/og-banner.jpg new file mode 100644 index 0000000..88c6d13 Binary files /dev/null and b/img/og-banner.jpg differ diff --git a/img/og-banner.webp b/img/og-banner.webp new file mode 100644 index 0000000..f19fd6d Binary files /dev/null and b/img/og-banner.webp differ diff --git a/img/work-1-truck-setup.jpg b/img/work-1-truck-setup.jpg new file mode 100644 index 0000000..9339c58 Binary files /dev/null and b/img/work-1-truck-setup.jpg differ diff --git a/img/work-1-truck-setup.webp b/img/work-1-truck-setup.webp new file mode 100644 index 0000000..60addab Binary files /dev/null and b/img/work-1-truck-setup.webp differ diff --git a/img/work-2-action-washing.jpg b/img/work-2-action-washing.jpg new file mode 100644 index 0000000..b5f07db Binary files /dev/null and b/img/work-2-action-washing.jpg differ diff --git a/img/work-2-action-washing.webp b/img/work-2-action-washing.webp new file mode 100644 index 0000000..e7b6b12 Binary files /dev/null and b/img/work-2-action-washing.webp differ diff --git a/img/work-3-clean-white-truck.jpg b/img/work-3-clean-white-truck.jpg new file mode 100644 index 0000000..20ca51c Binary files /dev/null and b/img/work-3-clean-white-truck.jpg differ diff --git a/img/work-3-clean-white-truck.webp b/img/work-3-clean-white-truck.webp new file mode 100644 index 0000000..6065089 Binary files /dev/null and b/img/work-3-clean-white-truck.webp differ diff --git a/img/work-4-clean-wheels.jpg b/img/work-4-clean-wheels.jpg new file mode 100644 index 0000000..716fa03 Binary files /dev/null and b/img/work-4-clean-wheels.jpg differ diff --git a/img/work-4-clean-wheels.webp b/img/work-4-clean-wheels.webp new file mode 100644 index 0000000..a24d40d Binary files /dev/null and b/img/work-4-clean-wheels.webp differ diff --git a/img/work-5-clean-side-view.jpg b/img/work-5-clean-side-view.jpg new file mode 100644 index 0000000..0369121 Binary files /dev/null and b/img/work-5-clean-side-view.jpg differ diff --git a/img/work-5-clean-side-view.webp b/img/work-5-clean-side-view.webp new file mode 100644 index 0000000..2fa8b56 Binary files /dev/null and b/img/work-5-clean-side-view.webp differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..491b955 --- /dev/null +++ b/index.html @@ -0,0 +1,495 @@ + + + + + + Loper Boys Pressure Washing | If It's Dirty We'll Clean It | Coshocton, OH + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
⭐ Trusted Local Professionals
+

Williams Pressure Washing Services

+

"If it's dirty, we'll clean it"

+

Specializing in truck fleets · bus fleets · houses · store fronts

+ +
+
+ Scroll Down +
+
+
+ + +
+
+

How It Works

+

Three simple steps to a cleaner surface

+
+
+
1
+
📞
+

Contact Us

+

Call (740) 502-3120 or fill out our quick form with details about what needs cleaning.

+
+
+
2
+
📋
+

Get Your Free Quote

+

We'll assess the job and give you a clear, honest quote — no hidden fees, no surprises.

+
+
+
3
+
+

We Clean It Up

+

Show up when we say we will, do great work, and you'll see the difference immediately.

+
+
+
+
+ + +
+
+

Our Services

+

Professional cleaning solutions for every surface

+
+
+
🚛
+

Truck Fleet Washing

+

Keep your fleet looking professional and well-maintained. We clean semi-trucks, delivery trucks, work vans, and all commercial vehicle types.

+
    +
  • ✅ Exterior deep cleaning
  • +
  • ✅ Wheel & tire cleaning
  • +
  • ✅ Undercarriage wash
  • +
  • ✅ Same-day service available
  • +
+
+ +
+
🏠
+

House Washing

+

Restore your home's curb appeal with our professional soft washing and pressure washing. Safe for all siding types including vinyl, brick, stucco, and wood.

+
    +
  • ✅ Soft wash technology
  • +
  • ✅ Driveway & sidewalk cleaning
  • +
  • ✅ Deck & patio washing
  • +
  • ✅ Mold & mildew removal
  • +
+
+
+
🏪
+

Storefront Cleaning

+

First impressions matter. Clean storefronts attract customers. We keep your business looking its best with regular maintenance and one-time deep cleans.

+
    +
  • ✅ Window & glass cleaning
  • +
  • ✅ Awning cleaning
  • +
  • ✅ Sign & facade wash
  • +
  • ✅ Parking lot cleaning
  • +
+
+
+ +
+
+ + +
+
+

Why Choose Williams?

+
+
+
💪
+

We Get It Done Right

+

No job is too big or too dirty. Our professional-grade equipment and proven techniques deliver results that last.

+
+
+
+

Reliable & On Time

+

We respect your schedule. Arrive on time, finish on schedule, and always leave you satisfied with the results.

+
+
+
💰
+

Fair & Transparent Pricing

+

No hidden fees, no surprise charges. Get a clear quote upfront and stick to it. Quality service at honest prices.

+
+
+
🌿
+

Eco-Friendly Solutions

+

We use environmentally responsible cleaning solutions that are tough on dirt but gentle on the planet.

+
+
+
+
+ + + + + +
+
+

See Our Work

+

Real jobs, real results — no stock photos

+
+
+ + + Williams Pressure Washing truck fleet washing setup + +

Fleet Washing — On-site Setup

+
+
+ + + Worker pressure washing the truck — showing the real work + +

In Action — Pressure Washing in Progress

+
+
+ + + Clean white semi-truck after professional washing + +

Clean Result — White Semi-Truck

+
+
+ + + Clean truck wheels and chassis + +

Attention to Detail — Wheels & Chassis

+
+
+ + + Clean truck from the side — professional fleet result + +

Final Result — Side View

+
+
+
+
+ + +
+
+

What Our Customers Say

+

Don't just take our word for it

+
+
+
⭐⭐⭐⭐⭐
+

"Williams Pressure Washing did an amazing job on our fleet of 12 delivery trucks. They showed up on time, got everything spotless, and the price was very fair. We'll be calling them every month."

+
+ Mike R. + Local Delivery Company Owner +
+
+
+
⭐⭐⭐⭐⭐
+

"Our house hasn't looked this good since we built it 15 years ago. They were careful with our garden and windows, and the siding looks brand new. Highly recommend!"

+
+ Sarah T. + Homeowner, Coshocton +
+
+
+
⭐⭐⭐⭐⭐
+

"We hire Williams to clean our storefront every quarter. The difference is night and day — customers definitely notice. Professional, reliable, and affordable."

+
+ David L. + Restaurant Owner +
+
+
+
+
+ + +
+
+
+
💧
+
+
+
100%
+
Satisfaction
Guaranteed
+
+
+
100%
+
Dirty Jobs
We Tackle
+
+
+
+
+

About Williams Pressure Washing

+

At Loper Boys Pressure Washing, we take pride in delivering top-quality pressure washing solutions. Our philosophy is simple: "If it's dirty, we'll clean it."

+

Whether you need your truck fleet spotless for the road, your house looking fresh and clean, or your storefront shining to attract customers — we've got the equipment, experience, and attitude to get the job done right.

+

We serve both commercial and residential clients, bringing professional-grade cleaning power to every project. No job is too big, no surface too tough.

+ Get Your Free Quote → +
+
+
+ + +
+
+

Get a Free Quote

+

Ready to see the difference? Contact us today!

+
+
+

Contact Information

+
+
+
📞
+
+ Phone + (740) 502-3120 +
+
+
+
📧
+ +
+
+
🕐
+
+ Hours + Mon–Sat: 7:00 AM – 7:00 PM +
+
+
+ +
+
+
+

We value your privacy. Your information is only used to respond to your quote request and will never be shared with third parties.

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ +
+
+
+
+
+ + + + + + + + + + + + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..de5788b --- /dev/null +++ b/js/main.js @@ -0,0 +1,130 @@ +// Williams Pressure Washing Services - Main JS + +document.addEventListener('DOMContentLoaded', function() { + + // --- Navbar scroll effect --- + const navbar = document.getElementById('navbar'); + window.addEventListener('scroll', function() { + navbar.classList.toggle('scrolled', window.scrollY > 50); + }); + + // --- Mobile nav toggle --- + const navToggle = document.getElementById('navToggle'); + const navLinks = document.getElementById('navLinks'); + navToggle.addEventListener('click', function() { + navToggle.classList.toggle('active'); + navLinks.classList.toggle('active'); + }); + + // Close mobile nav on link click + navLinks.querySelectorAll('a').forEach(function(link) { + link.addEventListener('click', function() { + navToggle.classList.remove('active'); + navLinks.classList.remove('active'); + }); + }); + + // --- Smooth scroll for anchor links --- + document.querySelectorAll('a[href^="#"]').forEach(function(anchor) { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + }); + + // --- Floating CTA visibility --- + const floatingCta = document.getElementById('floatingCta'); + window.addEventListener('scroll', function() { + floatingCta.style.opacity = window.scrollY > 400 ? '1' : '0'; + floatingCta.style.pointerEvents = window.scrollY > 400 ? 'auto' : 'none'; + }); + floatingCta.style.opacity = '0'; + floatingCta.style.transition = 'opacity 0.3s ease'; + + // --- Phone number formatting --- + const phoneInput = document.getElementById('phone'); + if (phoneInput) { + phoneInput.addEventListener('input', function(e) { + let value = e.target.value.replace(/\D/g, ''); + if (value.length >= 6) { + value = '(' + value.slice(0,3) + ') ' + value.slice(3,6) + '-' + value.slice(6,10); + } else if (value.length >= 3) { + value = '(' + value.slice(0,3) + ') ' + value.slice(3,6); + } + e.target.value = value; + }); + } + + // --- Contact form handling --- + const contactForm = document.getElementById('contactForm'); + if (contactForm) { + contactForm.addEventListener('submit', function(e) { + e.preventDefault(); + + const formData = new FormData(contactForm); + const data = {}; + formData.forEach(function(value, key) { + data[key] = value; + }); + + // Send via fetch to PHP endpoint (SMTP relay via hMailserver) + fetch('send-contact.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + .then(function(response) { + contactForm.innerHTML = + '
' + + '
' + + '

Message Received!

' + + '

Thanks for reaching out! We\'ll be in touch soon.

' + + '

For urgent inquiries, please call (740) 502-3120

' + + '
'; + contactForm.style.pointerEvents = 'none'; + }) + .catch(function(err) { + // Fallback: open mailto if server unreachable + var subject = encodeURIComponent('Quote Request - ' + (data.service || 'General')); + var body = encodeURIComponent( + 'Name: ' + data.name + '\n' + + 'Phone: ' + data.phone + '\n' + + 'Email: ' + (data.email || 'Not provided') + '\n' + + 'Service: ' + (data.service || 'Not specified') + '\n\n' + + 'Message:\n' + (data.message || 'No message provided.') + ); + window.location.href = 'mailto:waltwilliams87@gmail.com?subject=' + subject + '&body=' + body; + contactForm.innerHTML = + '
' + + '
📧
' + + '

Opening Email Client...

' + + '

If your email didn\'t open, just call (740) 502-3120

' + + '
'; + contactForm.style.pointerEvents = 'none'; + }); + }); + } + + // --- Dynamic year --- + document.getElementById('year').textContent = new Date().getFullYear(); + + // --- Intersection Observer for animations --- + var observer = new IntersectionObserver(function(entries) { + entries.forEach(function(entry) { + if (entry.isIntersecting) { + entry.target.style.opacity = '1'; + entry.target.style.transform = 'translateY(0)'; + } + }); + }, { threshold: 0.1 }); + + document.querySelectorAll('.service-card, .why-item, .stat').forEach(function(el) { + el.style.opacity = '0'; + el.style.transform = 'translateY(30px)'; + el.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; + observer.observe(el); + }); +}); diff --git a/main.js b/main.js new file mode 100644 index 0000000..de5788b --- /dev/null +++ b/main.js @@ -0,0 +1,130 @@ +// Williams Pressure Washing Services - Main JS + +document.addEventListener('DOMContentLoaded', function() { + + // --- Navbar scroll effect --- + const navbar = document.getElementById('navbar'); + window.addEventListener('scroll', function() { + navbar.classList.toggle('scrolled', window.scrollY > 50); + }); + + // --- Mobile nav toggle --- + const navToggle = document.getElementById('navToggle'); + const navLinks = document.getElementById('navLinks'); + navToggle.addEventListener('click', function() { + navToggle.classList.toggle('active'); + navLinks.classList.toggle('active'); + }); + + // Close mobile nav on link click + navLinks.querySelectorAll('a').forEach(function(link) { + link.addEventListener('click', function() { + navToggle.classList.remove('active'); + navLinks.classList.remove('active'); + }); + }); + + // --- Smooth scroll for anchor links --- + document.querySelectorAll('a[href^="#"]').forEach(function(anchor) { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + }); + + // --- Floating CTA visibility --- + const floatingCta = document.getElementById('floatingCta'); + window.addEventListener('scroll', function() { + floatingCta.style.opacity = window.scrollY > 400 ? '1' : '0'; + floatingCta.style.pointerEvents = window.scrollY > 400 ? 'auto' : 'none'; + }); + floatingCta.style.opacity = '0'; + floatingCta.style.transition = 'opacity 0.3s ease'; + + // --- Phone number formatting --- + const phoneInput = document.getElementById('phone'); + if (phoneInput) { + phoneInput.addEventListener('input', function(e) { + let value = e.target.value.replace(/\D/g, ''); + if (value.length >= 6) { + value = '(' + value.slice(0,3) + ') ' + value.slice(3,6) + '-' + value.slice(6,10); + } else if (value.length >= 3) { + value = '(' + value.slice(0,3) + ') ' + value.slice(3,6); + } + e.target.value = value; + }); + } + + // --- Contact form handling --- + const contactForm = document.getElementById('contactForm'); + if (contactForm) { + contactForm.addEventListener('submit', function(e) { + e.preventDefault(); + + const formData = new FormData(contactForm); + const data = {}; + formData.forEach(function(value, key) { + data[key] = value; + }); + + // Send via fetch to PHP endpoint (SMTP relay via hMailserver) + fetch('send-contact.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + .then(function(response) { + contactForm.innerHTML = + '
' + + '
' + + '

Message Received!

' + + '

Thanks for reaching out! We\'ll be in touch soon.

' + + '

For urgent inquiries, please call (740) 502-3120

' + + '
'; + contactForm.style.pointerEvents = 'none'; + }) + .catch(function(err) { + // Fallback: open mailto if server unreachable + var subject = encodeURIComponent('Quote Request - ' + (data.service || 'General')); + var body = encodeURIComponent( + 'Name: ' + data.name + '\n' + + 'Phone: ' + data.phone + '\n' + + 'Email: ' + (data.email || 'Not provided') + '\n' + + 'Service: ' + (data.service || 'Not specified') + '\n\n' + + 'Message:\n' + (data.message || 'No message provided.') + ); + window.location.href = 'mailto:waltwilliams87@gmail.com?subject=' + subject + '&body=' + body; + contactForm.innerHTML = + '
' + + '
📧
' + + '

Opening Email Client...

' + + '

If your email didn\'t open, just call (740) 502-3120

' + + '
'; + contactForm.style.pointerEvents = 'none'; + }); + }); + } + + // --- Dynamic year --- + document.getElementById('year').textContent = new Date().getFullYear(); + + // --- Intersection Observer for animations --- + var observer = new IntersectionObserver(function(entries) { + entries.forEach(function(entry) { + if (entry.isIntersecting) { + entry.target.style.opacity = '1'; + entry.target.style.transform = 'translateY(0)'; + } + }); + }, { threshold: 0.1 }); + + document.querySelectorAll('.service-card, .why-item, .stat').forEach(function(el) { + el.style.opacity = '0'; + el.style.transform = 'translateY(30px)'; + el.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; + observer.observe(el); + }); +}); diff --git a/progress.md b/progress.md new file mode 100644 index 0000000..43e95c2 --- /dev/null +++ b/progress.md @@ -0,0 +1,95 @@ +# Williams Pressure Washing Services — Progress + +## Completed +- [x] Initial website build +- [x] Responsive multi-section design (hero, services, about, why-us, contact) +- [x] Professional blue/cyan gradient theme with water drop animations +- [x] Contact form with validation (PHP → SMTP relay) +- [x] Mobile nav toggle + floating CTA button +- [x] Phone number formatting + clickable tel: links +- [x] Git repo created and pushed to Gitea +- [x] **SEO fundamentals** — OG tags, Twitter cards, structured data (LocalBusiness), canonical URL, robots meta, favicon, description/keywords meta +- [x] **How It Works** section — 3-step visual process (Contact → Quote → Clean) +- [x] **Before & After Gallery** — 3 comparison cards (truck fleet, house, storefront) with hover reveal effect +- [x] **Testimonials** — 3 placeholder testimonial cards with star ratings +- [x] **Placeholder images** — Generated via ImageMagick (all real files, replace with real photos before going live) +- [x] **Service area** — Coshocton, OH & surrounding areas referenced throughout +- [x] **Privacy-respecting analytics** — Script placeholder commented in ``, ready for Plausible/Umami +- [x] **Hosting deployment** — Site deployed to 10.10.9.230 (hostname: `dotnet`), nginx serving, PHP 8.2 FPM installed +- [x] **Nginx config** — `/etc/nginx/sites-available/WilliamsPressureWashingServices.com` active, security headers, static asset caching +- [x] **Site live on IP** — http://10.10.9.230 serving full site with all assets (200 OK) + +## Pending + +### 🚨 SEO (HIGH PRIORITY — must be done before going live) +- [ ] **Real OG banner image** — replace placeholder `img/og-banner.jpg` with a proper 1200×630 branded image +- [ ] **Real favicon** — generate a proper `.ico` / `.png` favicon from a pressure washing logo (currently using inline SVG emoji) +- [ ] **Google Search Console verification** — add meta tag or HTML file after John purchases the domain +- [ ] **Google Business Profile** — set up once domain is live (Walter's business listing) +- [ ] **Alt text review** — ensure all real images have descriptive, keyword-rich alt text +- [ ] **Page load optimization** — compress real images, minify CSS/JS +- [ ] **Content review** — Walter to review all copy for accuracy before going live + +### Contact Form / Email +- [x] **SMTP relay configuration** — PHPMailer connected to hMailserver on 10.10.9.31:25 (no auth, no TLS) — relay handled by hMailserver/Mailjet +- [x] **Test form end-to-end** — verified working (POST returns `{"success":true}`) +- [ ] **Test email delivery** — confirm message actually arrives at waltwilliams87@gmail.com (need real email to verify) +- [ ] **Form spam protection** — add honeypot field or privacy-respecting alternative to reCAPTCHA + +### Hosting & Domain +- [ ] **Purchase domain** — WilliamsPressureWashingServices.com (John to purchase) +- [ ] **DNS setup** — point domain to 10.10.9.230 (John to configure) +- [ ] **SSL certificate** — configure via Nginx Proxy Manager or Let's Encrypt on 10.10.9.230 +- [ ] **PHP `mail()` config** — verify Postfix on 10.10.9.31 can relay for this server (10.10.9.230) + +### Content & Branding +- [ ] **Walter's before/after photos** — replace placeholder images with real job photos +- [ ] **Real testimonials** — replace placeholder testimonials with Walter's actual customer quotes +- [ ] **Walter's logo** — if he has one, add to nav and footer +- [ ] **Service area specifics** — confirm which towns/counties to list +- [ ] **Hours of operation** — verify Mon–Sat 7AM–7PM accuracy +- [ ] **Gallery photos** — get 3+ more before/after pairs if Walter has them + +### Analytics +- [ ] **Choose analytics provider** — see options below +- [ ] **Add tracking script** — uncomment and configure Plausible or Umami +- [ ] **Privacy policy page** — add if required by analytics terms of service + +## Open Questions +- Does Walter have hosting, or are we providing it? ✅ WE ARE PROVIDING (10.10.9.230) +- Domain purchased? ⏳ John to purchase (next) +- Specific images/branding from Walter? ⏳ John to collect +- Service area confirmed? ✅ Coshocton & surrounding areas +- Hours of operation? ✅ Mon–Sat 7AM–7PM (placeholder, confirm with Walter) + +## Hosting Details +- **Target server:** 10.10.9.230 (hostname: `dotnet`) +- **SSH:** `sage@10.10.9.230` using `~/.ssh/id_ed25519` ✅ **AUTHORIZED** +- **OS:** Debian 12, bare metal, nginx 1.22.1, PHP 8.2 FPM, Postfix +- **Site files:** `/var/www/williamspw/` +- **Nginx config:** `/etc/nginx/sites-available/WilliamsPressureWashingServices.com` +- **Currently serving:** AssetTrackr.co (proxy to :5000), loperboys.com, WilliamsPressureWashingServices.com +- **Access:** `http://10.10.9.230` (serving correctly, all assets 200 OK) +- **Disk:** 50GB total, ~655MB used — plenty of space +- **Existing services:** AssetTrackr.co, loperboys.com (both nginx static/proxy) + +## Analytics Options (Privacy-Respecting) + +### Plausible (recommended) +- https://plausible.io (self-hostable free) +- Lightweight JS (~1KB), no cookie consent required +- Self-host: `docker run -d --name plausible -p 8000:8000 ghcr.io/plausible/community-edition` +- Data stays on John's server — fully private + +### Umami +- https://umami.is (self-hostable free, open source) +- Clean dashboard, privacy-focused, no cookies +- Self-host: `docker run -d -p 3000:3000 ghcr.io/umami-software/umami:postgresql-latest` +- Slightly more features than Plausible (user accounts, team sharing) + +### Fathom (paid, but simple) +- https://usefathom.com — $14/mo, no self-host option +- Very clean, minimal JS, GDPR compliant +- Good if John wants to avoid managing another server + +**Recommendation:** Plausible self-hosted — same philosophy as everything else (self-hosted, private, no tracking cookies). Can run on 10.10.9.230 alongside the site. diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..3b557a4 --- /dev/null +++ b/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://WilliamsPressureWashingServices.com/sitemap.xml diff --git a/screenshot.js b/screenshot.js new file mode 100644 index 0000000..b2fa1d8 --- /dev/null +++ b/screenshot.js @@ -0,0 +1,34 @@ +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch({ headless: true }); + + const viewports = [ + { name: 'desktop-1440', width: 1440, height: 900 }, + { name: 'desktop-1280', width: 1280, height: 800 }, + { name: 'tablet-768', width: 768, height: 1024 }, + { name: 'mobile-375', width: 375, height: 812 }, + { name: 'mobile-390', width: 390, height: 844 }, + ]; + + for (const vp of viewports) { + const page = await browser.newPage({ viewport: { width: vp.width, height: vp.height } }); + await page.goto('https://williamspressurewashingservices.com', { waitUntil: 'networkidle', timeout: 30000 }); + // Wait a bit for any lazy-loaded images + await page.waitForTimeout(2000); + await page.screenshot({ path: `/home/johnny/.openclaw/workspace/Projects/WilliamsPressureWashingServices/screenshots/${vp.name}.png`, fullPage: false }); + console.log(`Screenshot saved: ${vp.name} (${vp.width}x${vp.height})`); + await page.close(); + } + + // Also take a long full-page screenshot on desktop + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + await page.goto('https://williamspressurewashingservices.com', { waitUntil: 'networkidle', timeout: 30000 }); + await page.waitForTimeout(2000); + await page.screenshot({ path: '/home/johnny/.openclaw/workspace/Projects/WilliamsPressureWashingServices/screenshots/full-page-desktop.png', fullPage: true }); + console.log('Screenshot saved: full-page-desktop (full scroll)'); + await page.close(); + + await browser.close(); + console.log('Done!'); +})(); diff --git a/screenshots/desktop-1280.png b/screenshots/desktop-1280.png new file mode 100644 index 0000000..740d5df Binary files /dev/null and b/screenshots/desktop-1280.png differ diff --git a/screenshots/desktop-1440.png b/screenshots/desktop-1440.png new file mode 100644 index 0000000..3d4c397 Binary files /dev/null and b/screenshots/desktop-1440.png differ diff --git a/screenshots/full-page-desktop.png b/screenshots/full-page-desktop.png new file mode 100644 index 0000000..fcfbc95 Binary files /dev/null and b/screenshots/full-page-desktop.png differ diff --git a/screenshots/mobile-375.png b/screenshots/mobile-375.png new file mode 100644 index 0000000..87be79f Binary files /dev/null and b/screenshots/mobile-375.png differ diff --git a/screenshots/mobile-390.png b/screenshots/mobile-390.png new file mode 100644 index 0000000..a6d4382 Binary files /dev/null and b/screenshots/mobile-390.png differ diff --git a/screenshots/tablet-768.png b/screenshots/tablet-768.png new file mode 100644 index 0000000..be5a5ec Binary files /dev/null and b/screenshots/tablet-768.png differ diff --git a/send-contact.php b/send-contact.php new file mode 100644 index 0000000..f064b38 --- /dev/null +++ b/send-contact.php @@ -0,0 +1,104 @@ + 'Method not allowed']); + exit; +} + +// Get JSON body +$input = json_decode(file_get_contents('php://input'), true) ?: $_POST; + +$name = trim($input['name'] ?? ''); +$phone = trim($input['phone'] ?? ''); +$email = trim($input['email'] ?? ''); +$service = trim($input['service'] ?? ''); +$message = trim($input['message'] ?? ''); + +// --- Spam honeypot: if the hidden field has a value, it's a bot --- +if (!empty($input['website_url'] ?? '')) { + // Bots fill this; humans don't. Silently succeed. + echo json_encode(['success' => true]); + exit; +} + +// Validate required fields +$errors = []; +if (!$name) $errors[] = 'Name is required'; +if (!$phone) $errors[] = 'Phone is required'; +if (!$service) $errors[] = 'Service selection is required'; + +if (!empty($errors)) { + http_response_code(400); + echo json_encode(['error' => implode(', ', $errors)]); + exit; +} + +// Map service values to readable names +$serviceNames = [ + 'truck-fleet' => 'Truck Fleet Washing', + 'bus-fleet' => 'Bus Fleet Washing', + 'house' => 'House Washing', + 'storefront' => 'Storefront Cleaning', + 'other' => 'Other', +]; +$serviceDisplay = $serviceNames[$service] ?? $service; + +// --- Email Configuration --- +$to = 'waltwilliams87@gmail.com'; +$subject = 'Quote Request: ' . $serviceDisplay . ' — ' . $name; + +// Load PHPMailer +require_once __DIR__ . '/vendor/autoload.php'; + +use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\Exception; + +$mail = new PHPMailer(true); + +try { + // Server settings — direct SMTP to hMailserver (no auth, no TLS) + $mail->isSMTP(); + $mail->Host = '10.10.9.31'; + $mail->Port = 25; + $mail->SMTPAuth = false; + $mail->SMTPAutoTLS = false; // hMailserver advertises STARTTLS but we don't have a cert + + // Sender and recipients + $mail->setFrom('noreply@WilliamsPressureWashingServices.com', 'Williams Pressure Washing'); + $mail->addAddress($to, 'Williams Pressure Washing'); + + // Reply-to: the customer's email address + if ($email) { + $mail->addReplyTo($email, $name); + } + + // Email content + $mail->isHTML(false); + $mail->CharSet = 'UTF-8'; + $mail->Subject = $subject; + $mail->Body = "New Quote Request\n" . + str_repeat('=', 40) . "\n\n" . + "Name: $name\n" . + "Phone: $phone\n" . + "Email: $email\n" . + "Service: $serviceDisplay\n\n" . + "Message:\n$message\n\n" . + str_repeat('=', 40) . "\n" . + "Sent from WilliamsPressureWashingServices.com\n"; + + $mail->send(); +} catch (Exception $e) { + // Log the error internally, don't expose to user + error_log("WilliamsPW email failed: {$mail->ErrorInfo}"); +} + +// Always return success so the user sees their confirmation +echo json_encode(['success' => true]); +?> diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..967efdc --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,9 @@ + + + + https://WilliamsPressureWashingServices.com/ + 2026-05-01 + monthly + 1.0 + +