21 Commits

Author SHA1 Message Date
sage 44ea6cb281 progress.md: confirm domain, DNS, and SSL are live
- Domain resolves via Cloudflare (174.102.118.11 → 10.10.9.230)
- Let's Encrypt SSL valid through July 31, 2026
- Site accessible at https://WilliamsPressureWashingServices.com
2026-05-16 17:07:57 -04:00
sage 7afbe8052a progress.md: confirm honeypot spam protection already implemented 2026-05-16 17:04:53 -04:00
sage ee6f61d51c New custom favicon + mobile nav responsive fix
- New custom favicon: blue gradient water drop with spray burst
  (favicon.png 32/64/256px, favicon.webp, apple-touch-icon 180px)
- Mobile nav (768px): tighter padding (12px), smaller logo (1rem), smaller icon (1.2rem)
- Narrow phones (480px): logo text 0.85rem, logo max-width 60%
- Tiny phones (360px): logo text 0.75rem, logo max-width 55%
- CSS cache buster bumped to v3
2026-05-16 15:59:25 -04:00
John Loper II 02ffcfe15e Add address to schema.org and undercarriage detail to bus fleet service 2026-05-07 20:20:54 -04:00
John Loper II 11686622f8 Add undercarriage inspection & cleaning to bus fleet service 2026-05-07 20:19:57 -04:00
John Loper II 79cfe6b471 Add WebP image optimization with <picture> elements for browser-native format negotiation
- Convert all 14 images to WebP (quality 82)
- Wrap gallery and work section images in <picture> elements with WebP source + JPG/PNG fallback
- Update meta/schema og:image to reference .webp files
- Browsers supporting WebP will load smaller images (~40-60% smaller than JPG)
2026-05-07 13:17:36 -04:00
John Loper II 6c4b2dd434 Update progress.md — mark completed items (SSL, domain, DNS, email, honeypot, favicon) 2026-05-07 01:56:10 -04:00
John Loper II e769efd322 Replace blank favicon with 💧 water droplet icon 2026-05-07 01:54:42 -04:00
John Loper II d679cb9603 Add comma to slogan: 'If it's dirty, we'll clean it' 2026-05-05 14:46:14 -04:00
John Loper II f5010632cc Cache-bust stylesheet with ?v=2
The CSS was updated but nginx caches it for 30 days. Adding version
parameter to force browsers to reload the new CSS.
2026-05-05 13:41:28 -04:00
John Loper II 4ec7d14ef0 Constrain 'Our Work' image grid on desktop
- Add max-width: 900px to work-grid to prevent full-width stretching
- Tighten grid columns from minmax(280px, 1fr) to minmax(220px, 1fr)
- Center grid with margin: 0 auto
- Reduce gap from 24px to 20px
2026-05-05 13:27:06 -04:00
John Loper II c80ab0d609 feat: add 'Our Work' gallery with real photos, hide old Before/After 2026-05-04 18:32:38 -04:00
John Loper II 86db0d594b fix: tighten spacing between privacy text and form fields 2026-05-04 17:52:25 -04:00
John Loper II 7b06c8a872 fix: update privacy text on contact form 2026-05-04 17:35:37 -04:00
John Loper II 8738a7715b feat: add honeypot spam protection to contact form 2026-05-04 17:26:10 -04:00
John Loper II 225fbee7e5 Update progress: contact form + PHPMailer verified working 2026-05-04 17:17:54 -04:00
John Loper II a43df1a3d1 fix: replace contact form timing note with privacy reassurance 2026-05-04 16:44:39 -04:00
John Loper II 7c79772964 config: switch contact form to-walter@gmail.com
- To: waltwilliams87@gmail.com (was john.loper.2@protonmail.com)
- Deployed to 10.10.9.230
2026-05-04 16:22:05 -04:00
John Loper II d9f12e11cd fix: PHPMailer SMTPAutoTLS false — hMailserver STARTTLS without valid cert
- Added SMTPAutoTLS = false to prevent PHPMailer from trying TLS upgrade
- hMailserver advertises STARTTLS but has no valid certificate
- PHPMailer now connects plain-text to 10.10.9.31:25 and delivers successfully
- Test confirmed: email sent to john.loper.2@protonmail.com via hMailserver -> Mailjet relay
2026-05-04 16:13:45 -04:00
John Loper II 379242e4bf feat: re-enable contact form email via hMailserver, test to john.loper.2@protonmail.com
- Restore form submission to send-contact.php (was disabled)
- From: noreply@WilliamsPressureWashingServices.com
- Reply-To: customer email from form
- Subject: Quote Request: [Service] — [Name]
- To: john.loper.2@protonmail.com (testing — swap to Walter's email after)
- SMTP: 10.10.9.31 port 25, no auth (local network)
- Relay: hMailserver → Mailjet (already configured)
- Success message: 'Message Received! We'll be in touch soon. For urgent inquiries, call (740) 502-3120'
- Fallback: mailto fallback if server unreachable
- JS: disable form after submit to prevent double-sends
2026-05-04 14:56:56 -04:00
John Loper II fb1268fe5e docs: update progress with deployment status and hosting details 2026-05-01 21:17:21 -04:00
35 changed files with 480 additions and 80 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"require": {
"phpmailer/phpmailer": "^7.0"
}
}
+51
View File
@@ -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
+84 -1
View File
@@ -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%;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 986 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

+86 -17
View File
@@ -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>
+11 -12
View File
@@ -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';
});
});
}
+130
View File
@@ -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);
});
});
+24 -21
View File
@@ -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? ✅ MonSat 7AM7PM (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)
+34
View File
@@ -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!');
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

+55 -29
View File
@@ -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]);
?>