Migrate to Stack A: PHP-fpm + nginx + supervisord, drop flat HTML + Python API

- Remove old flat HTML pages (index, about, blog, contact, reviews, services/*, locations/*)
- Remove Python/Flask API container (api/)
- Remove old root nginx.conf and components/
- Add infra/: full nginx.conf (http block at /etc/nginx/nginx.conf), php-fpm-pool.conf (TCP listen), supervisord.conf, entrypoint.sh (auto-generates ALTCHA_HMAC_KEY)
- Add src/: PHP router, page/service/location/blog templates, contact handler, altcha handler, promo endpoint, SQLite data files
- Rewrite Dockerfile: single container, tini PID 1, healthcheck, all env vars declared
- Update docker-compose.yml: port 8096, env_file, healthcheck
- Update .dockerignore: exclude .env.*, include robots.txt/sitemap.xml/404.html/500.html
- Update assets: tokens.css, promo-popup.css/js, altcha.min.js, refactored form.js/main.js

Verified: all 17 routes 200, protection audit PASS, Resend confirmed working

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Concept Agent
2026-05-29 18:56:56 +02:00
parent 88ed4e6bda
commit 81feccdc1a
61 changed files with 2460 additions and 5747 deletions
+12
View File
@@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/altcha.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
exit;
}
echo json_encode(VYC_Altcha::create_challenge());
+45
View File
@@ -0,0 +1,45 @@
<?php
class VYC_Altcha {
private static function key(): string {
return getenv('ALTCHA_HMAC_KEY') ?: '';
}
public static function is_enabled(): bool {
return !empty(self::key());
}
public static function create_challenge(int $maxnumber = 100000): array {
$salt = bin2hex(random_bytes(12));
$number = random_int(0, $maxnumber);
$challenge = hash('sha256', $salt . $number);
$signature = hash_hmac('sha256', $challenge, self::key());
return [
'algorithm' => 'SHA-256',
'challenge' => $challenge,
'maxnumber' => $maxnumber,
'salt' => $salt,
'signature' => $signature,
];
}
public static function verify(string $payload): bool {
if (!self::is_enabled()) return true;
if (empty($payload)) return false;
$data = json_decode(base64_decode($payload, true), true);
if (!is_array($data)) return false;
foreach (['algorithm', 'challenge', 'number', 'salt', 'signature'] as $key) {
if (!isset($data[$key])) return false;
}
if ($data['algorithm'] !== 'SHA-256') return false;
$expected_sig = hash_hmac('sha256', $data['challenge'], self::key());
if (!hash_equals($expected_sig, $data['signature'])) return false;
$expected_challenge = hash('sha256', $data['salt'] . $data['number']);
return hash_equals($expected_challenge, $data['challenge']);
}
}
+102
View File
@@ -0,0 +1,102 @@
</main>
<footer class="site-footer">
<div class="footer-top">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<div class="footer-logo-text">Floor It<span>.</span></div>
<p>Western New York's hardwood floor specialists. Serving Buffalo and surrounding communities since 1994.</p>
<div class="footer-contact-list">
<div class="footer-contact-item">
<div class="footer-contact-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/></svg>
</div>
<div>
<a href="tel:+17166021429">(716) 602-1429</a>
</div>
</div>
<div class="footer-contact-item">
<div class="footer-contact-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
</div>
<div>
<a href="mailto:floorithardwoods@gmail.com">floorithardwoods@gmail.com</a>
</div>
</div>
</div>
</div>
<div class="footer-col">
<h5>Services</h5>
<ul>
<li><a href="/services/floor-refinishing/">Floor Refinishing</a></li>
<li><a href="/services/floor-restoration/">Floor Restoration</a></li>
<li><a href="/services/floor-sanding/">Floor Sanding</a></li>
<li><a href="/services/floor-installation/">Floor Installation</a></li>
</ul>
</div>
<div class="footer-col">
<h5>Locations</h5>
<ul>
<li><a href="/locations/buffalo/">Buffalo</a></li>
<li><a href="/locations/amherst/">Amherst</a></li>
<li><a href="/locations/williamsville/">Williamsville</a></li>
<li><a href="/locations/clarence/">Clarence</a></li>
<li><a href="/locations/east-amherst/">East Amherst</a></li>
<li><a href="/locations/lancaster/">Lancaster</a></li>
</ul>
</div>
<div class="footer-col">
<h5>Company</h5>
<ul>
<li><a href="/about/">About Us</a></li>
<li><a href="/reviews/">Reviews</a></li>
<li><a href="/blog/">Blog</a></li>
<li><a href="/contact/">Contact</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="footer-bottom">
<div class="container">
<p>&copy; <?= date('Y') ?> Floor It Hardwood Floors. All rights reserved. Buffalo, NY.</p>
</div>
</div>
</footer>
<link rel="stylesheet" href="/assets/css/promo-popup.css">
<div id="promo-overlay" style="display:none">
<div id="promo-box">
<button id="promo-close" aria-label="Close">&times;</button>
<span class="promo-badge">Limited Time</span>
<h2>Summer Refinishing Special: Up to 15% Off Through June 30, 2026</h2>
<p class="promo-sub">Save up to 15% on hardwood floor refinishing. Book by June 30 to lock in your discount.</p>
<form id="promo-form" novalidate>
<div class="promo-field">
<label for="promo-email">Email Address</label>
<input type="email" id="promo-email" name="email" placeholder="you@email.com" required>
</div>
<div class="promo-field">
<label for="promo-phone">Phone Number</label>
<input type="tel" id="promo-phone" name="phone" placeholder="(716) 000-0000" required>
</div>
<div class="promo-field">
<label for="promo-sqft">Square Footage (approx.)</label>
<input type="number" id="promo-sqft" name="sqft" placeholder="e.g. 800" min="50" max="50000" required>
</div>
<button type="submit" id="promo-submit" class="promo-submit">Claim My Discount</button>
<p class="promo-error" id="promo-error"></p>
</form>
<div class="promo-success" id="promo-success">
<p><strong>You are locked in.</strong><br>We will reach out within 24 hours to schedule your estimate.</p>
</div>
<p class="promo-fine">Offer valid through June 30, 2026. Buffalo, NY area only.</p>
</div>
</div>
<script src="/assets/js/main.js" defer></script>
<script src="/assets/js/form.js" defer></script>
<script src="/assets/js/promo-popup.js" defer></script>
</body>
</html>
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
$db_path = __DIR__ . '/../data/pages.sqlite';
$db = new SQLite3($db_path, SQLITE3_OPEN_READONLY);
$db->busyTimeout(3000);
$nav_result = $db->query("SELECT slug, nav_label FROM pages WHERE in_nav=1 ORDER BY nav_order");
$nav_items = [];
while ($row = $nav_result->fetchArray(SQLITE3_ASSOC)) {
$nav_items[] = $row;
}
$db->close();
$canonical = $canonical ?? '';
$page_title = htmlspecialchars($page_title ?? 'Floor It Hardwood Floors', ENT_QUOTES);
$page_meta = htmlspecialchars($page_meta ?? '', ENT_QUOTES);
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $page_title ?></title>
<meta name="description" content="<?= $page_meta ?>">
<?php if ($canonical): ?><link rel="canonical" href="<?= htmlspecialchars($canonical, ENT_QUOTES) ?>">
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/tokens.css">
<link rel="stylesheet" href="/assets/css/main.css">
<link rel="stylesheet" href="/assets/css/components.css">
</head>
<body>
<div id="promo-topbar" role="complementary" aria-label="Summer promotion">
<span id="promo-topbar-text">Summer Refinishing Savings: Save up to 15% through June 30, 2026</span>
<button id="promo-topbar-btn" type="button">Get Offer</button>
<button id="promo-topbar-close" type="button" aria-label="Dismiss promotion">&times;</button>
</div>
<header id="site-header" class="site-header">
<div class="container">
<div class="header-inner">
<a href="/" class="header-logo" aria-label="Floor It Hardwood Floors">
<img src="/assets/images/logo-header.png" alt="Floor It Hardwood Floors" style="height:42px;width:auto;object-fit:contain;">
</a>
<nav class="header-nav" aria-label="Main navigation">
<?php foreach ($nav_items as $item):
$href = $item['slug'] === 'home' ? '/' : '/' . $item['slug'] . '/';
?><a href="<?= htmlspecialchars($href, ENT_QUOTES) ?>"><?= htmlspecialchars($item['nav_label'], ENT_QUOTES) ?></a>
<?php endforeach; ?>
</nav>
<div class="header-cta">
<a href="tel:+17166021429" class="header-phone" aria-label="Call (716) 602-1429">(716) 602-1429</a>
<a href="/contact/" class="btn btn--primary btn--sm">Get Estimate</a>
<button class="header-menu-btn" aria-label="Open menu" aria-expanded="false">
<span></span><span></span><span></span>
</button>
</div>
</div>
</div>
<div class="mobile-nav" id="mobileNav" aria-hidden="true">
<div class="mobile-nav-overlay" id="mobileNavOverlay"></div>
<div class="mobile-nav-panel">
<button class="mobile-nav-close" aria-label="Close menu" id="mobileNavClose">&#x2715;</button>
<nav class="mobile-nav-links">
<?php foreach ($nav_items as $item):
$href = $item['slug'] === 'home' ? '/' : '/' . $item['slug'] . '/';
?><a href="<?= htmlspecialchars($href, ENT_QUOTES) ?>"><?= htmlspecialchars($item['nav_label'], ENT_QUOTES) ?></a>
<?php endforeach; ?>
</nav>
<div class="mobile-nav-cta">
<a href="tel:+17166021429" class="mobile-nav-phone">(716) 602-1429</a>
<a href="/contact/" class="btn btn--primary btn--full">Request Free Estimate</a>
</div>
</div>
</div>
</header>
<main>
+196
View File
@@ -0,0 +1,196 @@
<?php
/**
* Floor It Hardwood Floors — contact form handler.
*
* Pipeline:
* 1. Read JSON body (32KB cap)
* 2. Validate required fields
* 3. Honeypot + time-on-page checks
* 4. Altcha server-side verify
* 5. Sliding-window per-IP rate limit (file-backed in /var/www/html/src/api/data/rate-limits/)
* 6. POST to Resend → email to contact address
* 7. JSON response
*
* Configuration is read entirely from environment variables — set these in
* .env or the runtime environment. No hardcoded keys in this file.
*/
declare(strict_types=1);
require_once __DIR__ . '/altcha.php';
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
// ─── Config from environment ────────────────────────────────────────
$RESEND_API_KEY = getenv('RESEND_API_KEY') ?: '';
$FROM_EMAIL = getenv('FROM_EMAIL') ?: 'noreply@floorithardwoods.com';
$TO_EMAIL = getenv('TO_EMAIL') ?: 'floorithardwoodfloors@gmail.com';
$RATE_LIMIT = (int)(getenv('RATE_LIMIT_PER_IP_PER_10MIN') ?: 5);
$TIME_MIN_SECONDS = (int)(getenv('TIME_MIN_SECONDS') ?: 3);
// ─── Helpers ────────────────────────────────────────────────────────
function send_json(array $data, int $status = 200): void {
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_SLASHES);
exit;
}
function fail(string $msg, int $status = 200): void {
send_json(['ok' => false, 'error' => $msg], $status);
}
function client_ip(): string {
$trust = (getenv('TRUST_PROXY') === '1');
if ($trust) {
$fwd = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($fwd) return trim(explode(',', $fwd)[0]);
}
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
function rate_limit_check(string $ip, int $max, int $window_seconds): bool {
$dir = '/var/www/html/src/api/data/rate-limits';
if (!is_dir($dir)) @mkdir($dir, 0700, true);
$file = $dir . '/' . preg_replace('/[^a-zA-Z0-9._:-]/', '_', $ip);
$now = time();
$events = [];
if (is_file($file)) {
$raw = @file_get_contents($file);
$events = $raw ? array_filter(array_map('intval', explode("\n", $raw))) : [];
$events = array_filter($events, fn($t) => $now - $t <= $window_seconds);
}
if (count($events) >= $max) return false;
$events[] = $now;
@file_put_contents($file, implode("\n", $events), LOCK_EX);
return true;
}
function http_post_json(string $url, array $headers, array $body, int $timeout = 15): array {
$ch = curl_init($url);
$payload = json_encode($body, JSON_UNESCAPED_SLASHES);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json'], $headers),
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => $timeout,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
return ['status' => $code, 'body' => $resp, 'err' => $err];
}
// ─── Method gate ────────────────────────────────────────────────────
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
fail('Method Not Allowed', 405);
}
// ─── Read + decode JSON body (capped to prevent DoS via huge payloads) ─
$MAX_BODY_BYTES = 32 * 1024; // 32 KB is plenty for this form
$raw = '';
$in = fopen('php://input', 'rb');
if ($in) {
$raw = stream_get_contents($in, $MAX_BODY_BYTES + 1);
fclose($in);
}
if (strlen($raw) > $MAX_BODY_BYTES) {
fail('Payload too large.', 413);
}
$body = json_decode($raw, true);
if (!is_array($body)) {
fail('Invalid request payload.');
}
$request_id = bin2hex(random_bytes(6));
$ip = client_ip();
// ─── Rate limit ─────────────────────────────────────────────────────
if (!rate_limit_check($ip, $RATE_LIMIT, 600)) {
error_log("[floorit.form] rate_limited request_id=$request_id ip=$ip");
fail('Too many requests. Please wait a few minutes.', 429);
}
// ─── Field extraction + validation ──────────────────────────────────
$name = trim((string)($body['name'] ?? ''));
$email = trim((string)($body['email'] ?? ''));
$phone = trim((string)($body['phone'] ?? ''));
$message = trim((string)($body['message'] ?? ''));
$website = trim((string)($body['website'] ?? '')); // honeypot
$form_loaded_at = trim((string)($body['form_loaded_at'] ?? ''));
$altcha_payload = trim((string)($body['altcha'] ?? ''));
$errors = [];
if (mb_strlen($name) < 2 || mb_strlen($name) > 80) $errors[] = 'name';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'email';
if (mb_strlen($phone) > 20) $errors[] = 'phone';
if (mb_strlen($message) > 2000) $errors[] = 'message';
if ($errors) {
error_log("[floorit.form] validation_error request_id=$request_id fields=" . implode(',', $errors));
fail('Please check the form fields and try again.');
}
// ─── Honeypot ───────────────────────────────────────────────────────
if ($website !== '') {
error_log("[floorit.form] honeypot_triggered request_id=$request_id ip=$ip");
send_json(['ok' => true, 'ref' => $request_id]); // pretend success
}
// ─── Time-on-page ───────────────────────────────────────────────────
$flagged_review = false;
if ($form_loaded_at !== '' && ctype_digit(ltrim($form_loaded_at, '-'))) {
$loaded_ms = (int)$form_loaded_at;
$elapsed = (microtime(true) * 1000 - $loaded_ms) / 1000.0;
if ($elapsed < $TIME_MIN_SECONDS) $flagged_review = true;
}
// ─── Altcha verify ──────────────────────────────────────────────────
if (!VYC_Altcha::verify($altcha_payload)) {
error_log("[floorit.form] altcha_verification_failed request_id=$request_id");
fail('Spam check failed.');
}
// ─── Compose email body ─────────────────────────────────────────────
$subject_name = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $name);
$subject_prefix = $flagged_review ? '[REVIEW] ' : '';
$subject = "{$subject_prefix}New estimate request from {$subject_name}";
$text_body =
"A new estimate request came in through floorithardwoods.com.\n\n" .
"Name: {$name}\n" .
"Email: {$email}\n" .
"Phone: " . ($phone ?: 'not provided') . "\n\n" .
"Message:\n" . ($message ?: '(no message)') . "\n\n" .
"Submitted at: " . gmdate('Y-m-d\TH:i:s\Z') . "\n" .
"Request id: {$request_id}\n";
$html_body = nl2br(htmlspecialchars($text_body, ENT_QUOTES, 'UTF-8'));
// ─── Send via Resend ────────────────────────────────────────────────
if ($RESEND_API_KEY !== '') {
$r = http_post_json(
'https://api.resend.com/emails',
["Authorization: Bearer {$RESEND_API_KEY}"],
[
'from' => $FROM_EMAIL,
'to' => [$TO_EMAIL],
'reply_to' => $email,
'subject' => $subject,
'text' => $text_body,
'html' => $html_body,
]
);
if ($r['status'] >= 300) {
error_log("[floorit.form] resend_send_failed request_id=$request_id status={$r['status']} body=" . substr($r['body'] ?? '', 0, 300));
fail('Could not send the message. Please call (716) 602-1429 directly.');
}
$resend_id = (json_decode($r['body'] ?? '', true)['id'] ?? '');
error_log("[floorit.form] resend_send_ok request_id=$request_id resend_id=$resend_id");
} else {
// Pre-launch / no Resend key: log only, return success
error_log("[floorit.form] resend_skipped_no_key request_id=$request_id\nSubject: $subject\n$text_body");
}
send_json(['ok' => true, 'ref' => $request_id]);
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
View File
View File
Binary file not shown.
Binary file not shown.
+73
View File
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
header('Content-Type: application/json');
$email = trim(strip_tags($_POST['email'] ?? ''));
$phone = trim(strip_tags($_POST['phone'] ?? ''));
$sqft = trim(strip_tags($_POST['sqft'] ?? ''));
if (!$email || !$phone || !$sqft) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'All fields are required.']);
exit;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Invalid email address.']);
exit;
}
$sqft_int = (int) preg_replace('/[^0-9]/', '', $sqft);
if ($sqft_int < 50 || $sqft_int > 50000) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Please enter a valid square footage.']);
exit;
}
$api_key = getenv('RESEND_API_KEY');
$from = getenv('FROM_EMAIL') ?: 'Floor It Hardwood Floors <webleads@floorithardwoods.com>';
$to_email = getenv('TO_EMAIL') ?: 'floorithardwoods@gmail.com';
if (!$api_key) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Server configuration error.']);
exit;
}
$body = "Summer Refinishing Savings Lead\n\nEmail: {$email}\nPhone: {$phone}\nSquare Footage: {$sqft_int} sq ft\n\nOffer: Save up to 15% off through June 30, 2026.";
$payload = json_encode([
'from' => $from,
'to' => [$to_email],
'subject' => "Summer Promo Lead: {$sqft_int} sq ft from {$email}",
'text' => $body,
]);
$ch = curl_init('https://api.resend.com/emails');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $api_key,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 200 && $status < 300) {
echo json_encode(['ok' => true]);
} else {
http_response_code(502);
echo json_encode(['ok' => false, 'error' => 'Something went wrong. Please call (716) 602-1429.']);
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
$type = preg_replace('/[^a-z_]/', '', strtolower($_GET['type'] ?? 'page'));
$slug = preg_replace('/[^a-z0-9-]/', '', strtolower($_GET['slug'] ?? 'home'));
$allowed = ['page', 'service', 'location', 'blog', 'blog_post'];
if (!in_array($type, $allowed, true)) {
$type = 'page';
$slug = 'home';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $type === 'page' && $slug === 'contact') {
require __DIR__ . '/contact.php';
exit;
}
$template = match($type) {
'service' => __DIR__ . '/templates/service.php',
'location' => __DIR__ . '/templates/location.php',
'blog', 'blog_post' => __DIR__ . '/templates/blog.php',
default => __DIR__ . '/templates/page.php',
};
if (!file_exists($template)) {
http_response_code(404);
require __DIR__ . '/templates/page.php';
exit;
}
require $template;
+102
View File
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
$db = new SQLite3(__DIR__ . '/../data/blog.sqlite', SQLITE3_OPEN_READONLY);
$db->busyTimeout(3000);
if ($slug === 'blog') {
// Blog listing
$posts_result = $db->query("SELECT slug, title, excerpt, created_at FROM posts WHERE published=1 ORDER BY created_at DESC");
$posts = [];
while ($row = $posts_result->fetchArray(SQLITE3_ASSOC)) { $posts[] = $row; }
$db->close();
$page_title = 'Hardwood Floor Tips & Advice | Floor It Hardwood Floors Blog';
$page_meta = 'Expert hardwood floor tips from Floor It Hardwood Floors. Refinishing, restoration, care advice for Buffalo and Erie County homeowners.';
require __DIR__ . '/../components/_header.php';
?>
<section class="page-hero section section--dark">
<div class="container">
<div class="page-hero-content">
<span class="eyebrow">Hardwood Floor Tips</span>
<h1>The Floor It Blog</h1>
<p class="lead">Expert advice on hardwood floor refinishing, restoration, and care from the Buffalo area's most experienced team.</p>
</div>
</div>
</section>
<section class="section section--light">
<div class="container">
<?php if ($posts): ?>
<div class="grid grid--3col blog-grid">
<?php foreach ($posts as $post): ?>
<article class="card card--blog">
<div class="card-body">
<h2><a href="/blog/<?= htmlspecialchars($post['slug'], ENT_QUOTES) ?>/"><?= htmlspecialchars($post['title'], ENT_QUOTES) ?></a></h2>
<p><?= htmlspecialchars($post['excerpt'] ?? '', ENT_QUOTES) ?></p>
<a href="/blog/<?= htmlspecialchars($post['slug'], ENT_QUOTES) ?>/" class="card-link">Read more &rarr;</a>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>No posts yet. Check back soon.</p>
<?php endif; ?>
</div>
</section>
<section class="section section--amber cta-section">
<div class="container cta-section-inner">
<div class="cta-section-text">
<h2>Questions About Your Floors?</h2>
<p>Contact our team for a free estimate and expert advice.</p>
</div>
<a href="/contact/" class="btn btn--primary btn--lg">Request an Estimate</a>
</div>
</section>
<?php
} else {
// Individual post
$post = $db->querySingle("SELECT * FROM posts WHERE slug='" . SQLite3::escapeString($slug) . "' AND published=1", true);
$db->close();
if (!$post) {
http_response_code(404);
$page_title = '404: Post Not Found | Floor It Hardwood Floors';
$page_meta = '';
require __DIR__ . '/../components/_header.php';
echo '<section class="section section--light"><div class="container"><h1>Post Not Found</h1><p><a href="/blog/">Back to blog</a></p></div></section>';
require __DIR__ . '/../components/_footer.php';
exit;
}
$page_title = $post['title'] . ' | Floor It Hardwood Floors';
$page_meta = $post['excerpt'] ?? '';
require __DIR__ . '/../components/_header.php';
?>
<section class="page-hero section section--dark">
<div class="container">
<div class="page-hero-content">
<span class="eyebrow">Floor It Blog</span>
<h1><?= htmlspecialchars($post['title'], ENT_QUOTES) ?></h1>
<p class="lead"><?= htmlspecialchars($post['excerpt'] ?? '', ENT_QUOTES) ?></p>
</div>
</div>
</section>
<section class="section section--light">
<div class="container blog-post-body">
<?= $post['body_html'] ?>
<p style="margin-top:2rem;"><a href="/blog/">Back to blog</a></p>
</div>
</section>
<section class="section section--amber cta-section">
<div class="container cta-section-inner">
<div class="cta-section-text">
<h2>Ready to Restore Your Floors?</h2>
<p>Request a free estimate from our Buffalo team.</p>
</div>
<a href="/contact/" class="btn btn--primary btn--lg">Get a Free Estimate</a>
</div>
</section>
<?php
}
require __DIR__ . '/../components/_footer.php'; ?>
+122
View File
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
$db = new SQLite3(__DIR__ . '/../data/locations.sqlite', SQLITE3_OPEN_READONLY);
$db->busyTimeout(3000);
$loc = $db->querySingle("SELECT * FROM locations WHERE slug='" . SQLite3::escapeString($slug) . "'", true);
$db->close();
if (!$loc) {
http_response_code(404);
$page_title = '404: Location Not Found | Floor It Hardwood Floors';
$page_meta = '';
require __DIR__ . '/../components/_header.php';
echo '<section class="section section--light"><div class="container"><h1>Location Not Found</h1><p><a href="/locations/">View all locations</a></p></div></section>';
require __DIR__ . '/../components/_footer.php';
exit;
}
$page_title = $loc['title'];
$page_meta = $loc['meta_description'];
$body = json_decode($loc['body_json'] ?? '{}', true) ?? [];
$faqs = json_decode($loc['faqs_json'] ?? '[]', true) ?? [];
$city = $loc['city'];
require __DIR__ . '/../components/_header.php';
?>
<section class="page-hero section section--dark">
<div class="container">
<div class="page-hero-content">
<span class="eyebrow"><?= htmlspecialchars($loc['hero_eyebrow'] ?? '', ENT_QUOTES) ?></span>
<h1><?= htmlspecialchars($loc['hero_h1'] ?? '', ENT_QUOTES) ?></h1>
<p class="lead"><?= htmlspecialchars($loc['hero_lead'] ?? '', ENT_QUOTES) ?></p>
<a href="/contact/" class="btn btn--primary btn--lg">Request a <?= htmlspecialchars($city, ENT_QUOTES) ?> Estimate</a>
</div>
</div>
</section>
<?php if (!empty($body['overview_h2'])): ?>
<section class="section section--light">
<div class="container">
<div class="section-header">
<?php if (!empty($body['overview_eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($body['overview_eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h2><?= htmlspecialchars($body['overview_h2'], ENT_QUOTES) ?></h2>
<div class="divider"></div>
<?php if (!empty($body['overview_body_1'])): ?><p><?= htmlspecialchars($body['overview_body_1'], ENT_QUOTES) ?></p><?php endif; ?>
<?php if (!empty($body['overview_body_2'])): ?><p style="margin-top:1rem;color:var(--smoke);"><?= htmlspecialchars($body['overview_body_2'], ENT_QUOTES) ?></p><?php endif; ?>
</div>
<?php
$stats = [];
for ($i = 1; $i <= 3; $i++) {
if (!empty($body["stat_{$i}_num"])) {
$stats[] = ['num' => $body["stat_{$i}_num"], 'label' => $body["stat_{$i}_label"] ?? '', 'sub' => $body["stat_{$i}_sub"] ?? ''];
}
}
if ($stats): ?>
<div class="stats-row">
<?php foreach ($stats as $stat): ?>
<div class="stat-item">
<strong><?= htmlspecialchars($stat['num'], ENT_QUOTES) ?></strong>
<span><?= htmlspecialchars($stat['label'], ENT_QUOTES) ?></span>
<?php if ($stat['sub']): ?><small><?= htmlspecialchars($stat['sub'], ENT_QUOTES) ?></small><?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</section>
<?php endif; ?>
<?php if (!empty($body['services_intro'])): ?>
<section class="section section--dark">
<div class="container">
<div class="section-header">
<span class="eyebrow">Services in <?= htmlspecialchars($city, ENT_QUOTES) ?></span>
<h2>Hardwood Floor Services in <?= htmlspecialchars($city, ENT_QUOTES) ?>, NY</h2>
<p class="lead"><?= htmlspecialchars($body['services_intro'], ENT_QUOTES) ?></p>
</div>
<div class="grid grid--3col services-local-grid">
<?php for ($i = 1; $i <= 3; $i++): $tk = "service_{$i}_title"; $bk = "service_{$i}_body"; ?>
<?php if (!empty($body[$tk])): ?>
<div class="card card--service-local">
<h3><?= htmlspecialchars($body[$tk], ENT_QUOTES) ?></h3>
<p><?= htmlspecialchars($body[$bk] ?? '', ENT_QUOTES) ?></p>
</div>
<?php endif; ?>
<?php endfor; ?>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($faqs): ?>
<section class="section section--light">
<div class="container faq-section">
<div class="section-header">
<span class="eyebrow"><?= htmlspecialchars($city, ENT_QUOTES) ?> FAQs</span>
<h2>Common Questions from <?= htmlspecialchars($city, ENT_QUOTES) ?> Homeowners</h2>
</div>
<div class="faq-list">
<?php foreach ($faqs as $faq): ?>
<details class="faq-item">
<summary><?= htmlspecialchars($faq['q'] ?? '', ENT_QUOTES) ?></summary>
<p><?= htmlspecialchars($faq['a'] ?? '', ENT_QUOTES) ?></p>
</details>
<?php endforeach; ?>
</div>
</div>
</section>
<?php endif; ?>
<section class="section section--amber cta-section">
<div class="container cta-section-inner">
<div class="cta-section-text">
<h2><?= htmlspecialchars($body['form_h2'] ?? "Request a {$city} Floor Estimate", ENT_QUOTES) ?></h2>
<p>We respond to all <?= htmlspecialchars($city, ENT_QUOTES) ?> inquiries within 24 hours.</p>
</div>
<a href="/contact/" class="btn btn--primary btn--lg"><?= htmlspecialchars($body['form_submit'] ?? 'Send Estimate Request', ENT_QUOTES) ?></a>
</div>
</section>
<?php require __DIR__ . '/../components/_footer.php'; ?>
+391
View File
@@ -0,0 +1,391 @@
<?php
declare(strict_types=1);
$db = new SQLite3(__DIR__ . '/../data/pages.sqlite', SQLITE3_OPEN_READONLY);
$db->busyTimeout(3000);
$page = $db->querySingle("SELECT * FROM pages WHERE slug='" . SQLite3::escapeString($slug) . "'", true);
$db->close();
if (!$page) {
http_response_code(404);
$page_title = '404: Page Not Found | Floor It Hardwood Floors';
$page_meta = '';
require __DIR__ . '/../components/_header.php';
echo '<section class="section section--light"><div class="container"><h1>Page Not Found</h1><p><a href="/">Return home</a></p></div></section>';
require __DIR__ . '/../components/_footer.php';
exit;
}
$page_title = $page['title'];
$page_meta = $page['meta_description'];
$sections = json_decode($page['sections_json'], true) ?? [];
require __DIR__ . '/../components/_header.php';
foreach ($sections as $s) {
$t = $s['type'] ?? '';
switch ($t) {
case 'hero_video':
$stats = $s['stats'] ?? [];
?>
<section class="hero" aria-label="Hero">
<div class="hero-video-wrap">
<video autoplay muted loop playsinline poster="<?= htmlspecialchars($s['poster'] ?? '', ENT_QUOTES) ?>" aria-hidden="true">
<source src="<?= htmlspecialchars($s['video_webm'] ?? '', ENT_QUOTES) ?>" type="video/webm">
<source src="<?= htmlspecialchars($s['video_mp4'] ?? '', ENT_QUOTES) ?>" type="video/mp4">
</video>
<div class="hero-overlay"></div>
</div>
<div class="container">
<div class="hero-content">
<div class="hero-eyebrow">
<div class="hero-eyebrow-line"></div>
<span><?= htmlspecialchars($s['eyebrow'] ?? '', ENT_QUOTES) ?></span>
</div>
<h1><?= htmlspecialchars($s['h1'] ?? '', ENT_QUOTES) ?></h1>
<p class="hero-sub"><?= htmlspecialchars($s['lead'] ?? '', ENT_QUOTES) ?></p>
<div class="hero-actions">
<a href="<?= htmlspecialchars($s['cta_primary_href'] ?? '/contact/', ENT_QUOTES) ?>" class="btn btn--primary btn--lg"><?= htmlspecialchars($s['cta_primary'] ?? 'Get Estimate', ENT_QUOTES) ?></a>
<?php if (!empty($s['cta_secondary'])): ?>
<a href="<?= htmlspecialchars($s['cta_secondary_href'] ?? '#', ENT_QUOTES) ?>" class="btn btn--ghost btn--lg"><?= htmlspecialchars($s['cta_secondary'], ENT_QUOTES) ?></a>
<?php endif; ?>
</div>
<?php if ($stats): ?>
<div class="hero-trust">
<?php foreach ($stats as $stat): ?>
<div class="hero-trust-stat">
<strong><?= htmlspecialchars($stat['num'] ?? '', ENT_QUOTES) ?></strong>
<span><?= htmlspecialchars($stat['label'] ?? '', ENT_QUOTES) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</section>
<?php
break;
case 'hero_simple':
?>
<section class="page-hero section section--dark">
<div class="container">
<div class="page-hero-content">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h1><?= htmlspecialchars($s['h1'] ?? '', ENT_QUOTES) ?></h1>
<?php if (!empty($s['lead'])): ?><p class="lead"><?= htmlspecialchars($s['lead'], ENT_QUOTES) ?></p><?php endif; ?>
</div>
</div>
</section>
<?php
break;
case 'services_grid':
$sdb = new SQLite3(__DIR__ . '/../data/services.sqlite', SQLITE3_OPEN_READONLY);
$svc_result = $sdb->query("SELECT slug, service_name, hero_eyebrow, hero_lead, hero_image FROM services ORDER BY id");
$services = [];
while ($row = $svc_result->fetchArray(SQLITE3_ASSOC)) { $services[] = $row; }
$sdb->close();
$svc_images = [
'floor-refinishing' => '/assets/images/project-1-after.webp',
'floor-restoration' => '/assets/images/project-2-before.webp',
'floor-sanding' => '/assets/images/project-3-before.webp',
'floor-installation' => '/assets/images/project-1-before.webp',
];
?>
<section class="section section--light" id="services">
<div class="container">
<div class="section-header">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<?php if (!empty($s['h2'])): ?><h2><?= htmlspecialchars($s['h2'], ENT_QUOTES) ?></h2><?php endif; ?>
<?php if (!empty($s['lead'])): ?><p class="lead"><?= htmlspecialchars($s['lead'], ENT_QUOTES) ?></p><?php endif; ?>
</div>
<div class="grid grid--4col services-grid">
<?php foreach ($services as $svc): ?>
<a href="/services/<?= htmlspecialchars($svc['slug'], ENT_QUOTES) ?>/" class="card card--service">
<?php $img = $svc_images[$svc['slug']] ?? '/assets/images/project-1-after.webp'; ?>
<div class="card-img-wrap"><img src="<?= $img ?>" alt="<?= htmlspecialchars($svc['service_name'], ENT_QUOTES) ?>" loading="lazy"></div>
<div class="card-body">
<h3><?= htmlspecialchars($svc['service_name'], ENT_QUOTES) ?></h3>
<p><?= htmlspecialchars($svc['hero_lead'], ENT_QUOTES) ?></p>
<span class="card-link">Learn more &rarr;</span>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
</section>
<?php
break;
case 'process_steps':
$steps = $s['steps'] ?? [];
?>
<section class="section section--dark" id="process">
<div class="container">
<div class="section-header section-header--center">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h2><?= htmlspecialchars($s['h2'] ?? '', ENT_QUOTES) ?></h2>
</div>
<div class="grid grid--3col process-grid">
<?php foreach ($steps as $step): ?>
<div class="process-step">
<div class="process-num"><?= htmlspecialchars($step['num'] ?? '', ENT_QUOTES) ?></div>
<h3><?= htmlspecialchars($step['title'] ?? '', ENT_QUOTES) ?></h3>
<p><?= htmlspecialchars($step['body'] ?? '', ENT_QUOTES) ?></p>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<?php
break;
case 'about_preview':
?>
<section class="section section--alt" id="about-preview">
<div class="container about-preview-inner">
<div class="about-preview-text">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h2><?= htmlspecialchars($s['h2'] ?? '', ENT_QUOTES) ?></h2>
<div class="divider"></div>
<?php if (!empty($s['lead'])): ?><p class="lead"><?= htmlspecialchars($s['lead'], ENT_QUOTES) ?></p><?php endif; ?>
<?php if (!empty($s['body'])): ?><p style="margin-top:1.25rem;color:var(--smoke);"><?= htmlspecialchars($s['body'], ENT_QUOTES) ?></p><?php endif; ?>
<?php if (!empty($s['cta'])): ?>
<div class="cta-group mt-8">
<a href="<?= htmlspecialchars($s['cta_href'] ?? '/about/', ENT_QUOTES) ?>" class="btn btn--primary"><?= htmlspecialchars($s['cta'], ENT_QUOTES) ?></a>
</div>
<?php endif; ?>
</div>
<div class="about-preview-img">
<img src="/assets/images/project-1-after.webp" alt="Hardwood floor refinishing in Western New York" loading="lazy">
</div>
</div>
</section>
<?php
break;
case 'gallery':
?>
<section class="section section--bark" id="gallery">
<div class="container">
<div class="section-header section-header--center">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h2><?= htmlspecialchars($s['h2'] ?? '', ENT_QUOTES) ?></h2>
<?php if (!empty($s['lead'])): ?><p class="lead"><?= htmlspecialchars($s['lead'], ENT_QUOTES) ?></p><?php endif; ?>
</div>
<div class="gallery-grid">
<div class="gallery-item"><img src="/assets/images/project-1-before.webp" alt="Before refinishing" loading="lazy"><span class="gallery-item-label">Before</span></div>
<div class="gallery-item"><img src="/assets/images/project-1-after.webp" alt="After refinishing" loading="lazy"><span class="gallery-item-label gallery-item-label--after">After</span></div>
<div class="gallery-item"><img src="/assets/images/project-2-before.webp" alt="Before restoration" loading="lazy"><span class="gallery-item-label">Before</span></div>
<div class="gallery-item"><img src="/assets/images/project-3-before.webp" alt="Refinishing in progress" loading="lazy"><span class="gallery-item-label">In Progress</span></div>
</div>
</div>
</section>
<?php
break;
case 'cta_banner':
?>
<section class="section section--amber cta-section">
<div class="container cta-section-inner">
<div class="cta-section-text">
<h2><?= htmlspecialchars($s['h2'] ?? '', ENT_QUOTES) ?></h2>
<?php if (!empty($s['body'])): ?><p><?= htmlspecialchars($s['body'], ENT_QUOTES) ?></p><?php endif; ?>
</div>
<a href="<?= htmlspecialchars($s['cta_href'] ?? '/contact/', ENT_QUOTES) ?>" class="btn btn--primary btn--lg"><?= htmlspecialchars($s['cta'] ?? 'Get Estimate', ENT_QUOTES) ?></a>
</div>
</section>
<?php
break;
case 'contact_form':
?>
<section class="section section--light" id="contact">
<div class="container contact-layout">
<div class="contact-info">
<h2><?= htmlspecialchars($s['h2'] ?? 'Send Us a Message', ENT_QUOTES) ?></h2>
<div class="contact-detail"><strong>Phone</strong><a href="tel:+17166021429"><?= htmlspecialchars($s['phone'] ?? '(716) 602-1429', ENT_QUOTES) ?></a></div>
<div class="contact-detail"><strong>Email</strong><a href="mailto:<?= htmlspecialchars($s['email'] ?? '', ENT_QUOTES) ?>"><?= htmlspecialchars($s['email'] ?? '', ENT_QUOTES) ?></a></div>
<div class="contact-detail"><strong>Response Time</strong><span>Within 24 hours</span></div>
<div class="contact-detail"><strong>Service Area</strong><span>Buffalo and Erie County, NY</span></div>
</div>
<form class="contact-form" id="contactForm" action="/contact/" method="POST" novalidate>
<div class="form-group">
<label for="name">Full Name <span aria-hidden="true">*</span></label>
<input type="text" id="name" name="name" required autocomplete="name" placeholder="Your name">
</div>
<div class="form-group">
<label for="email">Email Address <span aria-hidden="true">*</span></label>
<input type="email" id="email" name="email" required autocomplete="email" placeholder="your@email.com">
</div>
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="tel" id="phone" name="phone" autocomplete="tel" placeholder="(716) 555-0000">
</div>
<div class="form-group">
<label for="message">Message <span aria-hidden="true">*</span></label>
<textarea id="message" name="message" required rows="5" placeholder="Tell us about your floors and what you need..."></textarea>
</div>
<input type="text" name="website" style="display:none;" tabindex="-1" autocomplete="off">
<input type="hidden" name="form_loaded_at" id="form_loaded_at">
<div id="altcha-widget"></div>
<button type="submit" id="contactFormSubmit" class="btn btn--primary btn--full">Send Message</button>
<div id="formStatus" role="status" aria-live="polite"></div>
</form>
</div>
</section>
<script src="/assets/js/altcha.min.js"></script>
<script src="/assets/js/form.js"></script>
<?php
break;
case 'story':
?>
<section class="section section--light">
<div class="container about-story">
<div class="about-story-text">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h2><?= htmlspecialchars($s['h2'] ?? '', ENT_QUOTES) ?></h2>
<div class="divider"></div>
<?php if (!empty($s['lead'])): ?><p class="lead"><?= htmlspecialchars($s['lead'], ENT_QUOTES) ?></p><?php endif; ?>
<?php if (!empty($s['body'])): ?><p style="margin-top:1.25rem;color:var(--smoke);"><?= htmlspecialchars($s['body'], ENT_QUOTES) ?></p><?php endif; ?>
</div>
<div class="about-story-img">
<img src="/assets/images/refinishing-machine.webp" alt="Floor It refinishing equipment" loading="lazy">
</div>
</div>
</section>
<?php
break;
case 'credentials':
?>
<section class="section section--dark">
<div class="container">
<div class="section-header section-header--center">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h2><?= htmlspecialchars($s['h2'] ?? '', ENT_QUOTES) ?></h2>
</div>
<div class="grid grid--4col credentials-grid">
<div class="credential-item"><strong>30+</strong><span>Years in Business</span></div>
<div class="credential-item"><strong>75+</strong><span>Years Combined Experience</span></div>
<div class="credential-item"><strong>500+</strong><span>Projects Completed</span></div>
<div class="credential-item"><strong>4.9/5</strong><span>Google Rating</span></div>
</div>
</div>
</section>
<?php
break;
case 'service_areas':
$ldb = new SQLite3(__DIR__ . '/../data/locations.sqlite', SQLITE3_OPEN_READONLY);
$loc_result = $ldb->query("SELECT slug, city FROM locations ORDER BY id");
$locations = [];
while ($row = $loc_result->fetchArray(SQLITE3_ASSOC)) { $locations[] = $row; }
$ldb->close();
?>
<section class="section section--light">
<div class="container">
<div class="section-header section-header--center">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h2><?= htmlspecialchars($s['h2'] ?? '', ENT_QUOTES) ?></h2>
<?php if (!empty($s['body'])): ?><p class="lead"><?= htmlspecialchars($s['body'], ENT_QUOTES) ?></p><?php endif; ?>
</div>
<div class="locations-pill-list">
<?php foreach ($locations as $loc): ?>
<a href="/locations/<?= htmlspecialchars($loc['slug'], ENT_QUOTES) ?>/" class="location-pill"><?= htmlspecialchars($loc['city'], ENT_QUOTES) ?>, NY</a>
<?php endforeach; ?>
</div>
</div>
</section>
<?php
break;
case 'testimonials_grid':
case 'testimonials_preview':
$tdb = new SQLite3(__DIR__ . '/../data/testimonials.sqlite', SQLITE3_OPEN_READONLY);
$limit = ($t === 'testimonials_preview') ? 3 : 20;
$featured = ($t === 'testimonials_preview') ? 'WHERE featured=1' : '';
$t_result = $tdb->query("SELECT * FROM testimonials {$featured} ORDER BY id LIMIT {$limit}");
$testimonials = [];
while ($row = $t_result->fetchArray(SQLITE3_ASSOC)) { $testimonials[] = $row; }
$tdb->close();
?>
<section class="section section--alt" id="reviews">
<div class="container">
<div class="section-header section-header--center">
<?php if (!empty($s['eyebrow'])): ?><span class="eyebrow"><?= htmlspecialchars($s['eyebrow'], ENT_QUOTES) ?></span><?php endif; ?>
<h2><?= htmlspecialchars($s['h2'] ?? '', ENT_QUOTES) ?></h2>
</div>
<div class="grid grid--3col testimonials-grid">
<?php foreach ($testimonials as $review): ?>
<div class="testimonial">
<div class="testimonial-stars"><span>&#9733;</span><span>&#9733;</span><span>&#9733;</span><span>&#9733;</span><span>&#9733;</span></div>
<p class="testimonial-text">"<?= htmlspecialchars($review['quote'], ENT_QUOTES) ?>"</p>
<div class="testimonial-author">
<strong><?= htmlspecialchars($review['author'], ENT_QUOTES) ?></strong>
<span><?= htmlspecialchars($review['location'] ?? '', ENT_QUOTES) ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<?php
break;
case 'blog_listing':
$bdb = new SQLite3(__DIR__ . '/../data/blog.sqlite', SQLITE3_OPEN_READONLY);
$b_result = $bdb->query("SELECT slug, title, excerpt, created_at FROM posts WHERE published=1 ORDER BY created_at DESC");
$posts = [];
while ($row = $b_result->fetchArray(SQLITE3_ASSOC)) { $posts[] = $row; }
$bdb->close();
?>
<section class="section section--light">
<div class="container">
<div class="grid grid--3col blog-grid">
<?php foreach ($posts as $post): ?>
<article class="card card--blog">
<div class="card-body">
<h2><a href="/blog/<?= htmlspecialchars($post['slug'], ENT_QUOTES) ?>/"><?= htmlspecialchars($post['title'], ENT_QUOTES) ?></a></h2>
<p><?= htmlspecialchars($post['excerpt'] ?? '', ENT_QUOTES) ?></p>
<a href="/blog/<?= htmlspecialchars($post['slug'], ENT_QUOTES) ?>/" class="card-link">Read more &rarr;</a>
</div>
</article>
<?php endforeach; ?>
</div>
</div>
</section>
<?php
break;
case 'locations_grid':
$ldb2 = new SQLite3(__DIR__ . '/../data/locations.sqlite', SQLITE3_OPEN_READONLY);
$loc2_result = $ldb2->query("SELECT slug, city, hero_lead FROM locations ORDER BY id");
$locs = [];
while ($row = $loc2_result->fetchArray(SQLITE3_ASSOC)) { $locs[] = $row; }
$ldb2->close();
?>
<section class="section section--light">
<div class="container">
<div class="grid grid--3col locations-grid">
<?php foreach ($locs as $loc): ?>
<a href="/locations/<?= htmlspecialchars($loc['slug'], ENT_QUOTES) ?>/" class="card card--location">
<div class="card-body">
<h3><?= htmlspecialchars($loc['city'], ENT_QUOTES) ?>, NY</h3>
<p><?= htmlspecialchars(substr($loc['hero_lead'] ?? '', 0, 120), ENT_QUOTES) ?>...</p>
<span class="card-link">View service area &rarr;</span>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
</section>
<?php
break;
}
}
require __DIR__ . '/../components/_footer.php';
+131
View File
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
$db = new SQLite3(__DIR__ . '/../data/services.sqlite', SQLITE3_OPEN_READONLY);
$db->busyTimeout(3000);
$svc = $db->querySingle("SELECT * FROM services WHERE slug='" . SQLite3::escapeString($slug) . "'", true);
$db->close();
if (!$svc) {
http_response_code(404);
$page_title = '404: Service Not Found | Floor It Hardwood Floors';
$page_meta = '';
require __DIR__ . '/../components/_header.php';
echo '<section class="section section--light"><div class="container"><h1>Service Not Found</h1><p><a href="/services/">View all services</a></p></div></section>';
require __DIR__ . '/../components/_footer.php';
exit;
}
$page_title = $svc['title'];
$page_meta = $svc['meta_description'];
$body = json_decode($svc['body_json'] ?? '{}', true) ?? [];
$faqs = json_decode($svc['faqs_json'] ?? '[]', true) ?? [];
require __DIR__ . '/../components/_header.php';
?>
<section class="page-hero section section--dark">
<div class="container">
<div class="page-hero-content">
<span class="eyebrow"><?= htmlspecialchars($svc['hero_eyebrow'] ?? '', ENT_QUOTES) ?></span>
<h1><?= htmlspecialchars($svc['hero_h1'] ?? '', ENT_QUOTES) ?></h1>
<p class="lead"><?= htmlspecialchars($svc['hero_lead'] ?? '', ENT_QUOTES) ?></p>
<a href="/contact/" class="btn btn--primary btn--lg">Get a Free Estimate</a>
</div>
</div>
</section>
<?php if (!empty($body['intro_h2'])): ?>
<section class="section section--light">
<div class="container service-intro">
<div class="service-intro-text">
<h2><?= htmlspecialchars($body['intro_h2'], ENT_QUOTES) ?></h2>
<?php if (!empty($body['intro_body_1'])): ?><p><?= htmlspecialchars($body['intro_body_1'], ENT_QUOTES) ?></p><?php endif; ?>
<?php if (!empty($body['intro_body_2'])): ?><p><?= htmlspecialchars($body['intro_body_2'], ENT_QUOTES) ?></p><?php endif; ?>
</div>
<?php if (!empty($svc['hero_image'])): ?>
<div class="service-intro-img"><img src="<?= htmlspecialchars($svc['hero_image'], ENT_QUOTES) ?>" alt="<?= htmlspecialchars($svc['service_name'] ?? '', ENT_QUOTES) ?>" loading="lazy"></div>
<?php endif; ?>
</div>
</section>
<?php endif; ?>
<?php if (!empty($body['process_intro'])): ?>
<section class="section section--dark">
<div class="container">
<div class="section-header section-header--center">
<span class="eyebrow">Our Process</span>
<h2>How We Do It</h2>
<p class="lead"><?= htmlspecialchars($body['process_intro'], ENT_QUOTES) ?></p>
</div>
<div class="grid grid--3col process-grid">
<?php for ($i = 1; $i <= 3; $i++): $title_key = "step_{$i}_title"; $body_key = "step_{$i}_body"; ?>
<?php if (!empty($body[$title_key])): ?>
<div class="process-step">
<div class="process-num">0<?= $i ?></div>
<h3><?= htmlspecialchars($body[$title_key], ENT_QUOTES) ?></h3>
<p><?= htmlspecialchars($body[$body_key] ?? '', ENT_QUOTES) ?></p>
</div>
<?php endif; ?>
<?php endfor; ?>
</div>
</div>
</section>
<?php endif; ?>
<?php
$benefits = [];
for ($i = 1; $i <= 4; $i++) {
if (!empty($body["benefit_{$i}_title"])) {
$benefits[] = ['title' => $body["benefit_{$i}_title"], 'body' => $body["benefit_{$i}_body"] ?? ''];
}
}
if ($benefits): ?>
<section class="section section--alt">
<div class="container">
<div class="section-header section-header--center">
<span class="eyebrow">Why Choose Us</span>
<h2>What Sets Us Apart</h2>
</div>
<div class="grid grid--4col benefits-grid">
<?php foreach ($benefits as $b): ?>
<div class="benefit-item">
<h3><?= htmlspecialchars($b['title'], ENT_QUOTES) ?></h3>
<p><?= htmlspecialchars($b['body'], ENT_QUOTES) ?></p>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($faqs): ?>
<section class="section section--light">
<div class="container faq-section">
<div class="section-header">
<span class="eyebrow">Common Questions</span>
<h2>Frequently Asked Questions</h2>
</div>
<div class="faq-list">
<?php foreach ($faqs as $faq): ?>
<details class="faq-item">
<summary><?= htmlspecialchars($faq['q'] ?? '', ENT_QUOTES) ?></summary>
<p><?= htmlspecialchars($faq['a'] ?? '', ENT_QUOTES) ?></p>
</details>
<?php endforeach; ?>
</div>
</div>
</section>
<?php endif; ?>
<section class="section section--amber cta-section">
<div class="container cta-section-inner">
<div class="cta-section-text">
<h2><?= htmlspecialchars($body['form_h2'] ?? 'Get Your Free Estimate', ENT_QUOTES) ?></h2>
<p>Contact our team and we will respond within 24 hours.</p>
</div>
<a href="/contact/" class="btn btn--primary btn--lg"><?= htmlspecialchars($body['form_submit'] ?? 'Request Estimate', ENT_QUOTES) ?></a>
</div>
</section>
<?php require __DIR__ . '/../components/_footer.php'; ?>