A website for a gate and fence contractor

Home page of ajgatesandfences.com: the hero over a photo of a brick and iron driveway gate, with the quote and message buttons

A&J Gates & Fences builds gates, fences and railings around Lakeland, Florida. They had a Facebook page and a phone number, and wanted a site that shows the work, says what they do, and lets someone ask for a quote with photos of their driveway attached, without the request getting lost.

What it is

Four pages. The home page has the services, a strip of featured work, the quote form and the contact details. A gallery page has 27 project photos in a grid with a lightbox. Privacy and terms pages are written for what the site actually does with data, and there is a slot in the footer for the contractor license number, which Florida requires in advertising.

The first version, from February, was plain HTML, CSS and JavaScript on Cloudflare Pages with a Formspree form. In September I rebuilt it with Vite, React, TypeScript and Tailwind v4, starting from shadcn-style components from 21st.dev and adapting them to take the brand, links and images as props. Fonts are self-hosted so the page makes no calls to Google. Photos are 1600 pixel WebP. Security headers and cache rules ship in a _headers file, and hashed assets are cached for a year.

Hosting

Production is a Cloudflare Worker serving the build output as static assets. wrangler deploy runs the build, uploads dist, and owns the two custom domains, so Cloudflare creates the DNS records and certificates itself. The Worker is connected to the GitHub repository, so a merge to main redeploys it. A separate Pages project serves previews for the client to look at before anything goes live.

One thing that went wrong: in wrangler.toml, the routes list has to come before the [vars], [build] and [assets] tables. Put it after and TOML reads it as a key inside the last table, and the domains quietly never attach.

The quote form

The form asks for name, email, phone, the project address, the type of project, rough width and height, a timeline, a description and up to five reference photos. It posts to /api/quote on the same Worker. Any request that does not match a static file falls through to the Worker code, which:

  • drops the submission silently if the hidden honeypot field is filled in;
  • verifies the Cloudflare Turnstile token server-side, and requires the action to be quote and the hostname to be one of the site’s own;
  • caps the number of fields, their length, and the count and size of photos, and refuses anything under photos that is not an image;
  • sends the email through Cloudflare Email Service with the photos attached and the visitor’s address as reply-to.

Turnstile tokens are single-use, so the widget is rendered explicitly and reset after every attempt rather than left to the script’s automatic mode.

If the Worker answers with a 5xx, because the email service is down or the Turnstile secret was never set, the browser submits the same form the ordinary way to Formspree, which redirects back to the page with ?sent=1 and the page shows the same thank-you. The request still arrives; it just arrives by a different road. I would rather have a second path than a form that looks like it worked and did not.

Keeping the copy honest

The first draft of the rebuild had a star rating chip, a location badge and a couple of sections describing services they do not offer. All of it came out. The copy on the site now is the client’s own, with one addition: that they have been licensed and insured since 2016.

Tagged cloudflare, workers, react, vite, tailwind, client work.