All projects
2025 CLIENT Design + Engineering

PK Training

Marketing site for a firearms-training business, hosted on shared Hostinger with a single PHP contact handler. A one-page site doesn't need a SPA framework — Vite + vanilla + PHP gets a hardened, fast site without the framework tax.

  • Vite
  • PHP
  • JavaScript
  • Cloudflare
View live

Overview

A static single-page marketing site for a firearms-training client, built with Vite and plain ES modules, deployed to shared Hostinger with a Cloudflare front. The only server-side code is one PHP file that handles the contact form. No framework, no SPA router, no test runner — the right tool for a one-page site with a single form.

Goals

  • Match the site to its constraints: shared Hostinger means no Node runtime in production, so a Vite-built static bundle plus a tiny PHP handler is the honest shape.
  • Hardened by default: strict CSP, security headers, and a contact form that survives the real internet (CSRF token, rate limit, honeypot, spam filter).
  • Fast on a mid-range phone over 4G, not just on a desktop fiber connection.
  • Repeatable deploys: push to main, CI builds and rsyncs, Cloudflare purges only the changed paths.

Approach

Small site, small stack

A one-page marketing site doesn’t need component hydration, a router, or a state library. The whole frontend is one index.html, one styles.css, and a handful of ES modules under src/assets/js/ for navigation, scroll behavior, and the contact-form fetch. Vite handles bundling; nothing on the page is reactive that doesn’t need to be. The “framework” decision is mostly the decision not to add one.

Strict CSP, no inline scripts

Content-Security-Policy: script-src 'self' — no inline scripts in index.html, no third-party CDNs, no analytics tag soup. Icons ship as an inline SVG sprite at the top of <body> (Font Awesome Free, SIL OFL 1.1), referenced via <use href="#icon-name">, so there’s no runtime icon-font request and no CDN trust dependency. The full posture — strict CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and SRI on the few external resources — passes all 10 Mozilla HTTP Observatory checks. The same recipe is shared with zachary-price.com — see its Observatory report.

Contact form, defense in depth

contact.php is the only server-side surface, and it’s layered:

  • CSRF token issued on page load, validated on submit
  • Honeypot field to catch the dumbest bots without a captcha
  • Rate limit keyed by IP, file-backed (no Redis, no extra moving part)
  • Spam keyword filter before the email send
  • php -l contact.php is a pre-commit gate; the build refuses to ship if the lint fails

No captcha — the layers above caught essentially all the spam without the UX cost.

Inline-CSS Vite plugin

A custom Vite plugin reads the bundled stylesheet at build time and inlines it into the built HTML’s <head> as a <style> block. Production ships zero render-blocking stylesheet requests; the source index.html stays clean (no inline CSS in the repo). The plugin runs only in the build pipeline, so the dev server keeps HMR.

Selective Cloudflare cache purge

The deploy workflow (GitHub Actions → rsync to Hostinger) diffs the changed paths and purges only those URLs from Cloudflare. A typo fix to one paragraph doesn’t bust the cache for the whole site, but a stylesheet change does. A manual force_cache_clear dispatch flag exists for the rare full-flush case.

Outcomes

  • The same .htaccess recipe is shared with zachary-price.com.
  • Zero third-party JavaScript on the page. No analytics, no chat widget, no font CDN.
  • Deploys are one git push. CI builds, rsyncs, validates headers (grep for Content-Security-Policy, X-Frame-Options, X-Content-Type-Options in .htaccess), lints PHP, and selectively purges Cloudflare.

What’s next

  • Close the SEO 92 → 100 gap on PSI (likely meta description / structured-data tightening).
  • A reciprocal credit link from pk-training.com back to zachary-price.com for design + engineering attribution.