Building lakeforestcomputer.com with Zero Frameworks

Why we chose pure HTML, CSS, and vanilla JavaScript over React for our own website — and what we gained by keeping it simple.

We build complex applications for a living. Our other projects use Next.js, React, PostgreSQL, Redis, TypeScript — the full modern stack. So when it came time to rebuild our own company website, the assumption might be that we'd reach for the same tools. We didn't. lakeforestcomputer.com is 16 pages of hand-written HTML, a single CSS file, and a single JavaScript file. No React. No build tools. No bundlers. No node_modules folder. No npm install.

This wasn't laziness or nostalgia. It was a deliberate architectural decision, and after building the site, we're more convinced than ever that frameworks are dramatically overkill for marketing websites. Here's the full story of what we built and why we built it this way.

The Case Against Frameworks for Marketing Sites

A marketing website has a fundamentally different job than a web application. It doesn't manage state. It doesn't handle complex user interactions. It doesn't need client-side routing. It needs to load fast, render immediately, be indexable by search engines, and look good on every screen size. That's it.

React solves problems that marketing sites don't have. Component reactivity, virtual DOM diffing, state management, hydration — these are solutions to the challenge of building interactive applications where the UI changes in response to data. A services page that describes your hosting offerings doesn't change in response to data. It's static content with a phone number and a contact link.

When you use React for static content, you pay a tax. The browser downloads your JavaScript bundle, parses it, executes it, hydrates the server-rendered HTML, and only then is the page fully interactive. For a marketing site, "fully interactive" means the mobile nav toggle works and the contact form submits. You're shipping 150KB+ of framework code so that a hamburger menu can open.

Our entire JavaScript file — mobile navigation, sticky header, scroll-reveal animations, contact form API submission, testimonial rendering, and star rating interaction — is a single file that weighs a fraction of a React runtime. The browser parses it in milliseconds. There's no hydration step because there's nothing to hydrate.

What We Actually Built

The site has 16 pages: a homepage, 10 dedicated service pages (Hosting, Email, Virtual Machines, Custom Code & API, E-Commerce, Web Development, IT Asset Disposition, AI & Automation, Product Development, and IT Hardware Sales), a Portfolio page, About, Contact, Privacy Policy, Terms of Service, and a custom 404 page.

Frontend Pure HTML, CSS, vanilla JS
CSS Architecture Single file, mobile-first, 4 breakpoints
Web Server Caddy v2.10 (gzip/zstd, security headers)
Backend Python 3 HTTP server (systemd service)
Email MXRoute SMTP (SSL on port 465)
Infrastructure Debian 13 VM on Proxmox (cloud-init)
Analytics Umami (self-hosted, privacy-focused)
Accessibility WCAG 2.1 AA compliant

Every page shares the same header, navigation, and footer structure. Yes, that means changes to the nav require updating 17 HTML files (16 pages plus the 404). That sounds tedious, and it is — occasionally. But it happens rarely, and when it does, a find-and-replace across files takes seconds. The tradeoff is that every page is a complete, self-contained document. There's no build step, no templating engine, no server-side rendering pipeline. You open the HTML file in a browser and it works.

CSS: One File, No Preprocessor

The entire site is styled with a single styles.css file. Mobile-first responsive design with breakpoints at 480px, 768px, 1024px, and 1280px. CSS custom properties handle the color system (dark navy #0A1628, green #00C853, grays, and utility colors). No Sass, no PostCSS, no Tailwind.

Modern CSS is remarkably capable. Custom properties give you variables. clamp() handles fluid typography. Grid and Flexbox handle layout. :focus-visible differentiates keyboard and mouse focus. prefers-reduced-motion lets you respect user accessibility preferences. These are native browser features that work today, and they eliminate entire categories of tooling that existed to work around CSS limitations that no longer exist.

We gated all scroll-reveal animations and hover transforms behind prefers-reduced-motion: no-preference, ensuring users who've indicated sensitivity to motion get a clean, static experience without sacrificing visual polish for everyone else.

The Contact Form: Purpose-Built, Not SaaS

The contact form is where this project gets interesting. We could have embedded a Typeform, used Formspree, or wired up a Netlify form handler. Instead, we built a Python 3 HTTP server that runs as a systemd service on the same VM as the website.

The server listens on 127.0.0.1:8100, and Caddy reverse proxies /api/contact to it. When someone submits the form, the backend validates every field, checks a honeypot field for bot detection, enforces rate limiting (5 submissions per IP per hour), validates the Origin header to prevent cross-site request forgery, and strips CRLF characters from the email subject to prevent header injection attacks.

Every submission is logged to a JSONL file before the email is sent. This is a critical design choice: if the SMTP server is down, if MXRoute has an outage, if there's a transient network error — the submission is never lost. The log file is the source of truth, and email delivery is a best-effort notification layer on top of it.

The email itself goes out via MXRoute over SSL on port 465. It's formatted as both HTML and plain text, sent from web@lakeforestcomputer.com to contact@lakeforestcomputer.com. SPF, DKIM, and DMARC are all configured in DNS, so deliverability is solid.

Why not use a SaaS form handler? Because a $5.99/month form service is $72/year for something that takes 200 lines of Python to build yourself. More importantly, you own the data. There's no third-party processor, no vendor lock-in, and no privacy policy to audit. The submissions live on your server, in a file you control.

Testimonials: Email Verification Without a Database

The homepage features a "What Our Clients Say" section with testimonials. We seeded it with 5 verified Google My Business reviews, but we also built a system for site visitors to submit their own testimonials.

The submission flow works like this: a user fills out a form (name, email, optional company, star rating, and review text), the backend generates a verification token using Python's token_urlsafe, stores the pending testimonial in a JSON file with the token and a 24-hour expiry, and sends a branded verification email. When the user clicks the verification link, the testimonial is moved from the pending file to the approved file, and the email address is stripped from the public data for privacy.

The entire storage layer is two JSON files: testimonials.json for approved reviews and testimonials-pending.json for unverified submissions. No PostgreSQL, no Redis, no MongoDB. For a feature that might see a few submissions per month, file-based storage is not just adequate — it's the right choice. It's human-readable, trivially backed up, and has zero operational overhead.

SEO: 60 FAQs, 6 Schema Types, and Every Meta Tag

Search engine optimization on a static site is both easier and harder than on a framework-rendered site. Easier because every page is exactly what the crawler sees — there's no JavaScript rendering dependency, no hydration race condition, no worry about whether Googlebot will execute your React app correctly. Harder because there's no plugin or library to automate it.

Every page has canonical URLs, Open Graph tags, Twitter Card tags, and JSON-LD structured data. The homepage carries a LocalBusiness schema with aggregate ratings, service area, opening hours, and geo-coordinates. Each service page has Service, BreadcrumbList, and FAQPage schemas. We wrote 60 FAQ entries across the 10 service pages — six per page — with accordion-style details/summary elements and matching JSON-LD markup so they're eligible for Google's rich results.

Meta descriptions were hand-written at 150-160 characters with calls to action, geographic signals ("Lake Forest, CA"), and the company phone number. Every H1 tag includes relevant keywords and the city name. Opening paragraphs on service pages reference nearby cities — Irvine, Mission Viejo, Laguna Hills — for local search relevance.

We also created an llms.txt file for AI discoverability, a sitemap.xml with all 16 URLs, and a robots.txt. These are text files dropped in the web root. No sitemap generator plugin required.

Accessibility: Not an Afterthought

WCAG 2.1 AA compliance was a first-class requirement, not a checklist item addressed after launch. The site includes skip-to-main-content links on every page, :focus-visible styles on all interactive elements, proper ARIA attributes on the mobile menu (aria-hidden, aria-expanded), keyboard-accessible dropdown navigation (Enter, Space, Arrow, and Escape key support), and a full keyboard focus trap in the mobile menu.

All 42 decorative SVG icons carry aria-hidden="true". Star rating buttons track aria-pressed state. Footer headings use role="heading" aria-level="3" since they're styled as span elements. External links include screen-reader-only text indicating they open in a new tab. Footer lists carry role="list" for Safari/VoiceOver compatibility.

Color contrast meets AA requirements across the board. We replaced six opacity-based text dimming rules with explicit hex color values to guarantee contrast ratios. The footer bottom text was bumped from --navy-300 (2.3:1 ratio) to --gray-300 (7.3:1 ratio).

None of this required an accessibility framework or React plugin. It required understanding the WCAG spec and writing correct HTML with the right attributes. Semantic HTML gets you most of the way there. The rest is attention to detail.

Infrastructure: Caddy, Systemd, and Nothing Else

The site runs on a Debian 13 VM provisioned with cloud-init on our Proxmox cluster. Two CPU cores, 2 GB of RAM, 20 GB of disk. Caddy v2.10 serves the static files and reverse proxies the API endpoints to the Python backend.

Caddy's configuration handles gzip and zstd compression, security headers (Content-Security-Policy, X-Frame-Options, Permissions-Policy with a strict allowlist, X-Content-Type-Options), static asset caching with long TTLs, and HTML caching with no-cache, must-revalidate so content updates propagate immediately.

SSL terminates upstream on a separate Apache reverse proxy VM (fog) that handles Let's Encrypt certificates for all our domains behind a shared WAN IP. Caddy on this VM listens on port 80 only. The contact form backend is a systemd service with auto-restart, and the firewall allows only SSH, HTTP, and HTTPS.

Deployment is scp. Or rsync. Or editing files directly over SSH. There's no CI/CD pipeline because there's no build step. The HTML files in the web root are the production artifacts. When we update a page, the change is live the moment the file is saved. There is something genuinely refreshing about a deployment process that's just "put the file on the server."

What We'd Do Differently

The one genuine pain point is shared markup. Updating the navigation, footer, or head tags across 17 files is repetitive. A lightweight templating layer — even just a shell script that concatenates header, content, and footer partials — would eliminate that friction without adding framework complexity. If the site grows beyond 20-25 pages, we'd likely add that.

We'd also consider adding a service worker for offline caching. The site is entirely static content, which makes it an ideal candidate for a cache-first service worker strategy. Visitors who've loaded the site once could browse it offline or on flaky connections.

When Frameworks Are Overkill

This project reinforced a principle we try to apply to all our work: choose the simplest tool that solves the actual problem. React is the right choice for GigDataServ, where we need real-time inventory updates, shopping cart state, authenticated user sessions, and complex interactive UIs. React would be the wrong choice for a 16-page marketing site where the most interactive element is a contact form.

The modern web development ecosystem has a complexity bias. The default starting point for any new project is often npx create-next-app, regardless of whether the project needs what Next.js provides. That default brings with it a node_modules folder with hundreds of dependencies, a build pipeline, a development server, hydration concerns, and a deployment story that usually involves Vercel or a containerized Node.js process.

For a marketing website, the deployment story should be: put HTML files on a web server. That's what the web was designed for, and it still works remarkably well. The files are cacheable, compressible, instantly parseable, and universally compatible. They don't break when a dependency releases a major version. They don't need security patches for transitive dependencies you've never heard of. They'll work the same way in ten years as they do today.

lakeforestcomputer.com loads fast, scores well on Core Web Vitals, is accessible, is thoroughly indexed by search engines, and costs essentially nothing to serve. It was built by the same team that builds complex React applications — we just knew when not to use one.

If you're considering whether your next website project needs a framework, we'd be happy to discuss the tradeoffs. Sometimes the answer is React. Sometimes the answer is HTML.

← All Blog Posts

Need a website that loads fast and just works?

Start a Conversation