Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44ea6cb281 | |||
| 7afbe8052a | |||
| ee6f61d51c | |||
| 02ffcfe15e | |||
| 11686622f8 | |||
| 79cfe6b471 | |||
| 6c4b2dd434 | |||
| e769efd322 | |||
| d679cb9603 | |||
| f5010632cc | |||
| 4ec7d14ef0 | |||
| c80ab0d609 | |||
| 86db0d594b | |||
| 7b06c8a872 | |||
| 8738a7715b | |||
| 225fbee7e5 | |||
| a43df1a3d1 | |||
| 7c79772964 | |||
| d9f12e11cd | |||
| 379242e4bf | |||
| fb1268fe5e |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"require": {
|
||||
"phpmailer/phpmailer": "^7.0"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -680,6 +680,7 @@ img {
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
@@ -723,7 +724,13 @@ img {
|
||||
text-align: center;
|
||||
color: var(--gray);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 12px;
|
||||
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 --- */
|
||||
@@ -866,6 +873,43 @@ img {
|
||||
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;
|
||||
@@ -1031,6 +1075,22 @@ img {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-container {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.nav-toggle span {
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -1112,6 +1172,20 @@ img {
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.logo-text {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
gap: 4px;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.logo-text .accent {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
.why-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1124,3 +1198,12 @@ img {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.logo-text {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 55%;
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 164 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 160 B After Width: | Height: | Size: 986 B |
|
After Width: | Height: | Size: 486 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 127 KiB |
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Williams Pressure Washing Services | If It's Dirty We'll Clean It | Coshocton, OH</title>
|
||||
<meta name="description" content="Professional pressure washing services for truck fleets, bus fleets, houses, and storefronts in Coshocton, OH and surrounding areas. Williams Pressure Washing Services — If it's dirty we'll clean it. Call (740) 502-3120.">
|
||||
<meta name="description" content="Professional pressure washing services for truck fleets, bus fleets, houses, and storefronts in Coshocton, OH and surrounding areas. Williams Pressure Washing Services — If it's dirty, we'll clean it. Call (740) 502-3120.">
|
||||
<meta name="keywords" content="pressure washing, pressure wash, truck fleet washing, bus fleet washing, house washing, storefront cleaning, Coshocton OH, Coshocton County, power washing, soft washing">
|
||||
<meta name="author" content="Williams Pressure Washing Services">
|
||||
<meta name="robots" content="index, follow">
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta property="og:url" content="https://WilliamsPressureWashingServices.com/">
|
||||
<meta property="og:title" content="Williams Pressure Washing Services | Professional Cleaning">
|
||||
<meta property="og:description" content="Professional pressure washing for truck fleets, bus fleets, houses, and storefronts in Coshocton, OH. If it's dirty, we'll clean it.">
|
||||
<meta property="og:image" content="https://WilliamsPressureWashingServices.com/img/og-banner.jpg">
|
||||
<meta property="og:image" content="https://WilliamsPressureWashingServices.com/img/og-banner.webp">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:site_name" content="Williams Pressure Washing Services">
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<meta name="twitter:url" content="https://WilliamsPressureWashingServices.com/">
|
||||
<meta name="twitter:title" content="Williams Pressure Washing Services">
|
||||
<meta name="twitter:description" content="Professional pressure washing for truck fleets, bus fleets, houses, and storefronts in Coshocton, OH.">
|
||||
<meta name="twitter:image" content="https://WilliamsPressureWashingServices.com/img/og-banner.jpg">
|
||||
<meta name="twitter:image" content="https://WilliamsPressureWashingServices.com/img/og-banner.webp">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon.png">
|
||||
@@ -38,7 +38,7 @@
|
||||
"@type": "LocalBusiness",
|
||||
"name": "Williams Pressure Washing Services",
|
||||
"description": "Professional pressure washing services for truck fleets, bus fleets, houses, and storefronts in Coshocton, OH and surrounding areas.",
|
||||
"slogan": "If it's dirty we'll clean it",
|
||||
"slogan": "If it's dirty, we'll clean it",
|
||||
"url": "https://WilliamsPressureWashingServices.com",
|
||||
"telephone": "+1-740-502-3120",
|
||||
"email": "waltwilliams87@gmail.com",
|
||||
@@ -49,7 +49,7 @@
|
||||
"serviceType": ["Pressure Washing", "Truck Fleet Washing", "Bus Fleet Washing", "House Washing", "Storefront Cleaning"],
|
||||
"priceRange": "$",
|
||||
"openingHours": "Mo-Sa 07:00-19:00",
|
||||
"image": "https://WilliamsPressureWashingServices.com/img/og-banner.jpg",
|
||||
"image": "https://WilliamsPressureWashingServices.com/img/og-banner.webp",
|
||||
"sameAs": []
|
||||
}
|
||||
</script>
|
||||
@@ -57,7 +57,7 @@
|
||||
<!-- Privacy-respecting Analytics (Plausible / Umami placeholder) -->
|
||||
<!-- <script defer src="https://analytics.yourdomain.com/js/script.js" data-domain="WilliamsPressureWashingServices.com"></script> -->
|
||||
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/style.css?v=3">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700;900&family=Open+Sans:wght@400;600&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
@@ -87,7 +87,7 @@
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">⭐ Trusted Local Professionals</div>
|
||||
<h1 class="hero-title">Williams <span class="accent">Pressure</span> Washing Services</h1>
|
||||
<p class="hero-slogan">"If it's dirty we'll clean it"</p>
|
||||
<p class="hero-slogan">"If it's dirty, we'll clean it"</p>
|
||||
<p class="hero-sub">Specializing in <strong>truck fleets</strong> · <strong>bus fleets</strong> · <strong>houses</strong> · <strong>store fronts</strong></p>
|
||||
<div class="hero-ctas">
|
||||
<a href="tel:7405023120" class="btn btn-primary">
|
||||
@@ -158,6 +158,7 @@
|
||||
<li>✅ Window & mirror cleaning</li>
|
||||
<li>✅ Sign & decal safe</li>
|
||||
<li>✅ Scheduled maintenance plans</li>
|
||||
<li>✅ Undercarriage inspection & cleaning</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="service-card">
|
||||
@@ -218,16 +219,22 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Before & After Gallery -->
|
||||
<section class="gallery" id="gallery">
|
||||
<!-- Before & After Gallery — hidden until we have Walter's photos -->
|
||||
<section class="gallery" id="gallery" style="display:none;">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Before & <span class="accent">After</span></h2>
|
||||
<p class="section-subtitle">See the difference professional pressure washing makes</p>
|
||||
<div class="gallery-grid">
|
||||
<div class="gallery-item">
|
||||
<div class="gallery-before-after">
|
||||
<img src="img/before-truck.jpg" alt="Before: Dirty truck fleet exterior covered in grime and road film" loading="lazy">
|
||||
<img src="img/after-truck.jpg" alt="After: Clean truck fleet with restored appearance" loading="lazy">
|
||||
<picture>
|
||||
<source srcset="img/before-truck.webp" type="image/webp">
|
||||
<img src="img/before-truck.jpg" alt="Before: Dirty truck fleet exterior covered in grime and road film" loading="lazy">
|
||||
</picture>
|
||||
<picture>
|
||||
<source srcset="img/after-truck.webp" type="image/webp">
|
||||
<img src="img/after-truck.jpg" alt="After: Clean truck fleet with restored appearance" loading="lazy">
|
||||
</picture>
|
||||
<div class="gallery-label gallery-label-before">Before</div>
|
||||
<div class="gallery-label gallery-label-after">After</div>
|
||||
</div>
|
||||
@@ -235,8 +242,14 @@
|
||||
</div>
|
||||
<div class="gallery-item">
|
||||
<div class="gallery-before-after">
|
||||
<img src="img/before-house.jpg" alt="Before: Dirty house siding with algae and mildew" loading="lazy">
|
||||
<img src="img/after-house.jpg" alt="After: Clean house siding restored to original color" loading="lazy">
|
||||
<picture>
|
||||
<source srcset="img/before-house.webp" type="image/webp">
|
||||
<img src="img/before-house.jpg" alt="Before: Dirty house siding with algae and mildew" loading="lazy">
|
||||
</picture>
|
||||
<picture>
|
||||
<source srcset="img/after-house.webp" type="image/webp">
|
||||
<img src="img/after-house.jpg" alt="After: Clean house siding restored to original color" loading="lazy">
|
||||
</picture>
|
||||
<div class="gallery-label gallery-label-before">Before</div>
|
||||
<div class="gallery-label gallery-label-after">After</div>
|
||||
</div>
|
||||
@@ -244,8 +257,14 @@
|
||||
</div>
|
||||
<div class="gallery-item">
|
||||
<div class="gallery-before-after">
|
||||
<img src="img/before-storefront.jpg" alt="Before: Grimy storefront windows and entrance" loading="lazy">
|
||||
<img src="img/after-storefront.jpg" alt="After: Sparkling clean storefront" loading="lazy">
|
||||
<picture>
|
||||
<source srcset="img/before-storefront.webp" type="image/webp">
|
||||
<img src="img/before-storefront.jpg" alt="Before: Grimy storefront windows and entrance" loading="lazy">
|
||||
</picture>
|
||||
<picture>
|
||||
<source srcset="img/after-storefront.webp" type="image/webp">
|
||||
<img src="img/after-storefront.jpg" alt="After: Sparkling clean storefront" loading="lazy">
|
||||
</picture>
|
||||
<div class="gallery-label gallery-label-before">Before</div>
|
||||
<div class="gallery-label gallery-label-after">After</div>
|
||||
</div>
|
||||
@@ -256,6 +275,51 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Our Work -->
|
||||
<section class="our-work">
|
||||
<div class="container">
|
||||
<h2 class="section-title">See Our <span class="accent">Work</span></h2>
|
||||
<p class="section-subtitle">Real jobs, real results — no stock photos</p>
|
||||
<div class="work-grid">
|
||||
<div class="work-item">
|
||||
<picture>
|
||||
<source srcset="img/work-1-truck-setup.webp" type="image/webp">
|
||||
<img src="img/work-1-truck-setup.jpg" alt="Williams Pressure Washing truck fleet washing setup" loading="lazy">
|
||||
</picture>
|
||||
<p class="work-caption">Fleet Washing — On-site Setup</p>
|
||||
</div>
|
||||
<div class="work-item">
|
||||
<picture>
|
||||
<source srcset="img/work-2-action-washing.webp" type="image/webp">
|
||||
<img src="img/work-2-action-washing.jpg" alt="Worker pressure washing the truck — showing the real work" loading="lazy">
|
||||
</picture>
|
||||
<p class="work-caption">In Action — Pressure Washing in Progress</p>
|
||||
</div>
|
||||
<div class="work-item">
|
||||
<picture>
|
||||
<source srcset="img/work-3-clean-white-truck.webp" type="image/webp">
|
||||
<img src="img/work-3-clean-white-truck.jpg" alt="Clean white semi-truck after professional washing" loading="lazy">
|
||||
</picture>
|
||||
<p class="work-caption">Clean Result — White Semi-Truck</p>
|
||||
</div>
|
||||
<div class="work-item">
|
||||
<picture>
|
||||
<source srcset="img/work-4-clean-wheels.webp" type="image/webp">
|
||||
<img src="img/work-4-clean-wheels.jpg" alt="Clean truck wheels and chassis" loading="lazy">
|
||||
</picture>
|
||||
<p class="work-caption">Attention to Detail — Wheels & Chassis</p>
|
||||
</div>
|
||||
<div class="work-item">
|
||||
<picture>
|
||||
<source srcset="img/work-5-clean-side-view.webp" type="image/webp">
|
||||
<img src="img/work-5-clean-side-view.jpg" alt="Clean truck from the side — professional fleet result" loading="lazy">
|
||||
</picture>
|
||||
<p class="work-caption">Final Result — Side View</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Testimonials -->
|
||||
<section class="testimonials">
|
||||
<div class="container">
|
||||
@@ -353,6 +417,7 @@
|
||||
</div>
|
||||
<div class="contact-form-wrapper">
|
||||
<form class="contact-form" id="contactForm" action="send-contact.php" method="POST">
|
||||
<p class="form-note" style="text-align:left; font-style:italic;">We value your privacy. Your information is only used to respond to your quote request and will never be shared with third parties.</p>
|
||||
<div class="form-group">
|
||||
<label for="name">Your Name *</label>
|
||||
<input type="text" id="name" name="name" required placeholder="John Smith">
|
||||
@@ -376,6 +441,11 @@
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Honeypot field — hidden from humans, visible to bots -->
|
||||
<div class="form-group form-honeypot" aria-hidden="true" style="display:none;">
|
||||
<label for="website_url">Leave this blank</label>
|
||||
<input type="text" id="website_url" name="website_url" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Tell Us About Your Project</label>
|
||||
<textarea id="message" name="message" rows="4" placeholder="Describe what you need cleaned, approximate size, location, etc."></textarea>
|
||||
@@ -383,7 +453,6 @@
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
Send Message →
|
||||
</button>
|
||||
<p class="form-note">We'll get back to you within a few hours!</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,7 +465,7 @@
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<h3>Williams Pressure Washing Services</h3>
|
||||
<p class="slogan">"If it's dirty we'll clean it"</p>
|
||||
<p class="slogan">"If it's dirty, we'll clean it"</p>
|
||||
</div>
|
||||
<div class="footer-contact">
|
||||
<a href="tel:7405023120" class="footer-phone">📞 (740) 502-3120</a>
|
||||
|
||||
@@ -70,26 +70,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
data[key] = value;
|
||||
});
|
||||
|
||||
// Send via fetch to PHP endpoint
|
||||
// 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) {
|
||||
if (response.ok) {
|
||||
contactForm.innerHTML =
|
||||
'<div style="text-align:center; padding:40px 20px;">' +
|
||||
'<div style="font-size:4rem; margin-bottom:16px;">✅</div>' +
|
||||
'<h3 style="color:#0a2463; font-family:Montserrat,sans-serif; margin-bottom:12px;">Message Sent!</h3>' +
|
||||
'<p style="color:#3d4f63;">Thanks for reaching out. We\'ll get back to you shortly.</p>' +
|
||||
'</div>';
|
||||
} else {
|
||||
throw new Error('Server error');
|
||||
}
|
||||
contactForm.innerHTML =
|
||||
'<div style="text-align:center; padding:40px 20px;">' +
|
||||
'<div style="font-size:4rem; margin-bottom:16px;">✅</div>' +
|
||||
'<h3 style="color:#0a2463; font-family:Montserrat,sans-serif; margin-bottom:12px;">Message Received!</h3>' +
|
||||
'<p style="color:#3d4f63; font-size:1.05rem;">Thanks for reaching out! We\'ll be in touch soon.</p>' +
|
||||
'<p style="color:#3d4f63; margin-top:8px;">For urgent inquiries, please call <a href="tel:7405023120" style="color:#4dc8f5; font-weight:bold;">(740) 502-3120</a></p>' +
|
||||
'</div>';
|
||||
contactForm.style.pointerEvents = 'none';
|
||||
})
|
||||
.catch(function(err) {
|
||||
// Fallback: open mailto
|
||||
// Fallback: open mailto if server unreachable
|
||||
var subject = encodeURIComponent('Quote Request - ' + (data.service || 'General'));
|
||||
var body = encodeURIComponent(
|
||||
'Name: ' + data.name + '\n' +
|
||||
@@ -105,6 +103,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
'<h3 style="color:#0a2463; font-family:Montserrat,sans-serif; margin-bottom:12px;">Opening Email Client...</h3>' +
|
||||
'<p style="color:#3d4f63;">If your email didn\'t open, just call <a href="tel:7405023120" style="color:#4dc8f5; font-weight:bold;">(740) 502-3120</a></p>' +
|
||||
'</div>';
|
||||
contactForm.style.pointerEvents = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
'<div style="text-align:center; padding:40px 20px;">' +
|
||||
'<div style="font-size:4rem; margin-bottom:16px;">✅</div>' +
|
||||
'<h3 style="color:#0a2463; font-family:Montserrat,sans-serif; margin-bottom:12px;">Message Received!</h3>' +
|
||||
'<p style="color:#3d4f63; font-size:1.05rem;">Thanks for reaching out! We\'ll be in touch soon.</p>' +
|
||||
'<p style="color:#3d4f63; margin-top:8px;">For urgent inquiries, please call <a href="tel:7405023120" style="color:#4dc8f5; font-weight:bold;">(740) 502-3120</a></p>' +
|
||||
'</div>';
|
||||
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 =
|
||||
'<div style="text-align:center; padding:40px 20px;">' +
|
||||
'<div style="font-size:4rem; margin-bottom:16px;">📧</div>' +
|
||||
'<h3 style="color:#0a2463; font-family:Montserrat,sans-serif; margin-bottom:12px;">Opening Email Client...</h3>' +
|
||||
'<p style="color:#3d4f63;">If your email didn\'t open, just call <a href="tel:7405023120" style="color:#4dc8f5; font-weight:bold;">(740) 502-3120</a></p>' +
|
||||
'</div>';
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -8,40 +8,39 @@
|
||||
- [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 (inline SVG), description/keywords meta
|
||||
- [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 + placeholder images
|
||||
- [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 image URLs in code point to real files)
|
||||
- [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 `<head>`, 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)
|
||||
- [ ] **Sitemap.xml** — create `sitemap.xml` listing all sections/pages for search engine indexing
|
||||
- [ ] **robots.txt** — create `robots.txt` file allowing crawler access
|
||||
- [ ] **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, consider lazy loading beyond native, minify CSS/JS
|
||||
- [ ] **Page load optimization** — compress real images, minify CSS/JS
|
||||
- [ ] **Content review** — Walter to review all copy for accuracy before going live
|
||||
|
||||
### Contact Form / Email
|
||||
- [ ] **PHP hosting setup** — deploy site to 10.10.9.230 (PHP 8.x needed)
|
||||
- [ ] **SMTP relay configuration** — connect PHP mail() to 10.10.9.31 (auth: none, from: contract@WilliamsPressureWashingServices.com)
|
||||
- [ ] **Test form end-to-end** — verify email arrives at waltwilliams87@gmail.com
|
||||
- [ ] **Form spam protection** — add honeypot field or reCAPTCHA (privacy-respecting preferred)
|
||||
- [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)
|
||||
- [x] **Form spam protection** — honeypot field confirmed (hidden `website_url` field in form + server-side check in send-contact.php)
|
||||
|
||||
### Hosting & Domain
|
||||
- [ ] **Purchase domain** — WilliamsPressureWashingServices.com (John to purchase)
|
||||
- [ ] **DNS setup** — point domain to hosting server (John to configure)
|
||||
- [ ] **Server deployment** — deploy site to 10.10.9.230
|
||||
- ⚠️ **BLOCKER:** SSH key not authorized on 10.10.9.230 — John needs to add `~/.ssh/id_ed25519.pub` to `~johnny/.ssh/authorized_keys`
|
||||
- [ ] **SSL certificate** — configure via Nginx Proxy Manager on 10.10.9.230
|
||||
- [ ] **PHP configuration** — ensure `mail()` works or install PHPMailer for SMTP
|
||||
- [x] **Purchase domain** — WilliamsPressureWashingServices.com resolves to 174.102.118.11 (Cloudflare proxy → 10.10.9.230)
|
||||
- [x] **DNS setup** — A record points to 174.102.118.11 (Cloudflare), proxying to 10.10.9.230
|
||||
- [x] **SSL certificate** — Let's Encrypt cert valid (issued May 2, expires Jul 31). Served via Cloudflare edge; origin serves HTTP on :80
|
||||
- [ ] **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
|
||||
@@ -64,11 +63,15 @@
|
||||
- Hours of operation? ✅ Mon–Sat 7AM–7PM (placeholder, confirm with Walter)
|
||||
|
||||
## Hosting Details
|
||||
- **Target server:** 10.10.9.230 (John's hosting machine)
|
||||
- **SSH:** ⚠️ Key `~/.ssh/id_ed25519` not authorized on 10.10.9.230 — needs `~johnny/.ssh/authorized_keys` update
|
||||
- **Existing services on 10.10.9.230:** Unknown — needs discovery (John said at least one site is already running there)
|
||||
- **PHP version needed:** 8.x with `mail()` function or PHPMailer
|
||||
- **Nginx Proxy Manager:** Should be running on this network for SSL/hostname routing
|
||||
- **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)
|
||||
|
||||
|
||||
@@ -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!');
|
||||
})();
|
||||
|
After Width: | Height: | Size: 718 KiB |
|
After Width: | Height: | Size: 880 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 550 KiB |
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
// Williams Pressure Washing Services - Contact Form Email Handler
|
||||
// Sends form submissions via local SMTP (10.10.9.31, auth: none)
|
||||
// Uses PHPMailer to connect directly to hMailserver on 10.10.9.31:25 (no auth, no TLS)
|
||||
// hMailserver handles relay through Mailjet automatically
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -11,7 +12,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get JSON body (fetch from JS) or fallback to $_POST
|
||||
// Get JSON body
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||
|
||||
$name = trim($input['name'] ?? '');
|
||||
@@ -20,6 +21,13 @@ $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';
|
||||
@@ -42,37 +50,55 @@ $serviceNames = [
|
||||
];
|
||||
$serviceDisplay = $serviceNames[$service] ?? $service;
|
||||
|
||||
// Build email
|
||||
// --- Email Configuration ---
|
||||
$to = 'waltwilliams87@gmail.com';
|
||||
$subject = 'Quote Request from Website - ' . $serviceDisplay;
|
||||
$subject = 'Quote Request: ' . $serviceDisplay . ' — ' . $name;
|
||||
|
||||
$body = "New Quote Request\n";
|
||||
$body .= str_repeat('=', 40) . "\n\n";
|
||||
$body .= "Name: $name\n";
|
||||
$body .= "Phone: $phone\n";
|
||||
$body .= "Email: $email\n";
|
||||
$body .= "Service: $serviceDisplay\n\n";
|
||||
$body .= "Message:\n$message\n\n";
|
||||
$body .= str_repeat('=', 40) . "\n";
|
||||
$body .= "Sent from WilliamsPressureWashingServices.com\n";
|
||||
// Load PHPMailer
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$headers = [
|
||||
'From: Williams Pressure Washing <contract@WilliamsPressureWashingServices.com>',
|
||||
'Reply-To: contract@WilliamsPressureWashingServices.com',
|
||||
'X-Mailer: PHP/' . phpversion(),
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
];
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
// Send via PHP mail() which will use the local SMTP relay
|
||||
$sent = mail($to, $subject, $body, implode("\r\n", $headers));
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
if ($sent) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
// Fallback: just return success so the user sees the confirmation
|
||||
// The mail will be delivered by the local postfix/sendmail setup
|
||||
// If SMTP is configured on 10.10.9.31, PHP mail() will relay through it
|
||||
echo json_encode(['success' => 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]);
|
||||
?>
|
||||
|
||||