Work → Case study
Rustic Cowboy Construction
A residential contractor working across the Dallas–Fort Worth metroplex. Roofing, fences, retaining walls, patios, remodels. The whole site exists to turn a homeowner with hail damage into a phone call.
What it had to do
A contractor's website has one job, and it isn't looking pretty. Someone's roof is leaking, or a storm just came through, and they're standing in the driveway on their phone comparing three companies. The site has to load instantly, prove the company is real and insured, show work they've actually done, and make it take about twenty seconds to ask for an estimate.
DFW adds a specific wrinkle. Hail and wind damage means insurance claims, and a homeowner in the middle of a claim is looking for someone who will meet the adjuster and handle the paperwork. That's a service, and it needed saying plainly rather than being buried in a list.
What I built
An Astro site with Tailwind, every page prerendered to static HTML except the one endpoint that has to run on a server. It builds to Cloudflare and the pages come off the edge as flat files.
All the business content lives in one file, src/data/site.ts. Services, the FAQ, the list of DFW cities, contact details, the before-and-after pairs. Changing what the site says means editing one list, not hunting through markup. That was deliberate: it's the difference between a five-minute change and a billable hour.
The before-and-after slider carries real project photos and stores each pair's pixel dimensions, so the frame sizes itself to a portrait or landscape shot without the page jumping around while images load.
The lead pipeline
The estimate form posts to an endpoint that stores the lead in Supabase, then emails the owner through Resend with the reply-to set to the homeowner's address. He hits reply and he's talking to the customer.
The email carries every field in order, including the submission time formatted in Central Time, because "2:14 PM CT" is useful to a man in Fort Worth and a UTC timestamp isn't. That formatting is wrapped in a try-catch that falls back to UTC, since some runtimes throw on named time zones depending on which ICU data they shipped with. A date-formatting quirk should never be the reason a lead fails.
The same principle runs through the whole endpoint. Storage failing doesn't stop the email. The email failing doesn't stop the storage. Both failing still returns success to the homeowner, logs the problem server-side, and keeps the details in the log rather than dropping them on the floor. Bots get caught by a honeypot field and quietly dropped with a fake success.
The hard parts
Every deploy was quietly deleting the database connection
This one cost me real time and it's the most useful thing on this page.
Leads stopped storing. Not loudly. The form kept working, the owner kept getting emails, and nothing in the logs looked wrong. But the Supabase table stopped filling up.
The cause: Astro's Cloudflare adapter generates its own Worker config at build time, and that generated config had an empty vars block. A Cloudflare deploy treats that block as authoritative, so every single deploy deleted the plaintext variables set in the dashboard. SUPABASE_URL was one of them. The Resend API key survived because encrypted secrets aren't touched by a deploy, which is exactly why email kept working and made the failure look like a database problem instead of a deploy problem.
The fix is a root wrangler.jsonc with two safeguards. keep_vars tells a deploy not to remove anything set in the dashboard, and the non-secret values are pinned in the file so a fresh Worker always has them regardless. Secrets stay encrypted in Cloudflare and never go near the repo.
Why this is on my portfolio
Because it's the kind of failure that makes a site look fine while it quietly loses you business, and the only reason it got caught is that someone was actually checking the database. I now check for it on every build I ship.
Astro 6 moved where environment variables live
Astro 6 removed Astro.locals.runtime.env, and touching it throws. On Cloudflare, environment variables now come from the cloudflare:workers module. That module doesn't exist when astro dev runs on Node, so importing it normally breaks local development. It gets imported dynamically inside a try-catch, falling back to process.env locally, and both environments work off the same code path.
Two version conflicts in the toolchain
The Cloudflare adapter's dev runner doesn't work with Astro 6 on Node 24, so the adapter is applied only for astro build. Local development uses Astro's own Node server, which still serves the lead endpoint fine. Separately, the adapter is incompatible with Vite 8, so Vite is pinned to 7 with an npm override. Neither is elegant. Both are documented in comments right where someone would otherwise waste an afternoon.
Supabase rejecting requests that look like a browser
Supabase's newer secret keys refuse requests that look like they came from a browser, which is a sensible default and briefly confusing when your server is the one calling. Setting a non-browser user agent on the insert fixed it.
Where it stands
Live on rusticcowboycg.com with the lead pipeline running end to end into Supabase and Resend. Deploys run from a Git push through Cloudflare's build integration.
Run a local trade business?
If you're not sure whether the enquiries from your website are actually reaching you, that's worth ten minutes of my time.
Start a project