Client Work · Sole Developer · Requirements to Deployment

Rocky Concrete

I built the website for a Bakersfield concrete and landscaping contractor. It's built around a gallery he keeps current himself, from his phone, in Spanish, without me in the loop.

Behind that simple page: a server-rendered site on a stale-while-revalidate cache, and an image pipeline rebuilt so his updates never cost a deploy.

Astro 5 Sanity Mux Tailwind Netlify

The Brief

A concrete and landscaping contractor in Bakersfield messaged me wanting his first website. For years his work had spread by word of mouth, and he had a phone full of proof: finished patios, driveways, backyards, etc. He just didn't have anywhere to send customers.

Before writing any code, I needed to know what to build, not how. My first questions were too technical and only confused him, so I switched to asking about his work and how he'd use the site, and his answers set the spec.

  • How many photos? About 100
    Categories and subcategories, not one giant pile.
  • How often will you add new ones? After almost every job
    Self-serve. He had to be able to post without help from anyone.
  • Where are they? On my phone
    Mobile-first.
  • Comfortable with tech? Not much, but it's easy to post on Instagram
    An upload flow as easy as posting to Instagram. Reinforced mobile-first.
What I added

Labels in Spanish.

English isn't his first language and he's not great with tech, so every label is in Spanish, telling him exactly what to do.

Free to run.

He's a small business owner, so I kept his costs down. Nothing to pay to keep it online.

Build vs Buy

He was clear from the start: he wanted a site that felt like his, not a template like every other contractor in town. So the look wasn't a nice-to-have, it was the brief.

  1. Off-the-shelf builder

    Wix, Squarespace, CompanyCam, Houzz Pro

    He could edit it himself, but they template the look (the one thing he asked me not to do) and bill monthly, anywhere from $16 to $399. Even the polished ones make him a tenant on someone else's platform.

    So I built custom.
  2. A custom static site

    Build it myself

    Custom met the look and cost the templates couldn't. But he adds a photo after almost every job, from his phone, for as long as the site is up. On a static site every one of those is a code change I make for him, and the first ~100 would've been hand-built markup. He had to be able to publish without me.

    So the site had to be dynamic, with content he could feed himself.
  3. Hand him the CMS's own studio

    A headless CMS as his editor

    A headless CMS lets him post without touching code, and runs on a free tier. But its studio is built for a developer, not for a guy posting one-handed on his phone in his second language. Too clunky to be his.

    So I built one more layer.
  4. A Spanish upload page on top

    What's live

    One screen, a few fields, as easy as posting to Instagram. His own custom look, no monthly subscription.

Picking the CMS took two tries. I built it on Decap (formerly Netlify CMS) first, then tore it down when its git-based model committed every photo he posted into the repo and kicked off a rebuild. I moved to Sanity, which keeps his content in a hosted dataset, off the repo entirely, so posting a photo isn't a code change anymore.

Sunk cost is still a cost, but shipping the wrong tool is worse.

His Upload Page

Rocky Concrete's Spanish upload page on a phone, titled SUBIR FOTO: a photo picker, title field, category dropdown, and a notes field for Jennifer.

What he sees · /upload

I kept Sanity as the backend and built him his own door in front of it. His page only had to do one thing: let him add a photo of a finished job, the one thing he wanted from the site. One screen at /upload, a few fields, submit.

Behind the form, /api/upload converts the iPhone's HEIC straight to JPEG on his phone (browsers won't render HEIC), so he sends a smaller file and my server never touches the original. Then it pushes the image to Sanity's CDN, creates the galleryItem document, and copies his title into the alt text so the site stays accessible.

What I keep · Sanity Studio

Curation. He posts the raw work; the Studio is where I make the site look curated. I set the featured photos, the order each category shows in, and the cover shots, none of it touching his page. When he sends a batch from one job, I group them so they read as one project.

Alt text, automated. Seeding the gallery meant about 100 photos, each one needing alt text for accessibility. I wrote a script that ran them through Claude to generate the alt text and title from the image itself, then wrote both straight to Sanity. I reviewed every one in the Studio, and fixing a label was a click, not a hunt through code.

Maintainability. His work lives in Sanity, not hardcoded in my files, so I'm not the only one who can keep the gallery running. Any developer could take it over, and if he ever needs more than posting a photo, he can hand it to someone who works in Sanity.

Sanity Studio on desktop showing the custom Content desk I built (Reorder Gallery, Featured Items, Category Covers, Site Images) next to a drill-down from Categories into Concrete, its Walkways & Steps subcategory, and the individual gallery items.

Same database, two doors in. He posts a job; I curate how it shows.

The notes field is doing more than it looks. It's an async channel between us: he leaves a note ("put this one first," or "this is the Hendersons' backyard"), I see it in Sanity, I handle it. No phone call, no scheduling, no "are you free." The tool absorbed the back-and-forth that used to be a conversation.

The result is the thing I set out to build: he posts his own work, from his phone, in his language, and the site updates itself. He doesn't need me to keep his business online.

The System

The gallery is the whole pitch. A contractor charging five figures for a patio can't have a site that looks or loads cheap. Both had to land: the look, and the speed behind it. The catch is that a gallery is the heaviest kind of page there is: dozens of high-res photos plus video, the slowest stuff to load.

It also has the most moving parts, so I built it as a pipeline and designed each layer around who's using it: he posts from the admin side, his customers browse from the public side, and a cache in the middle keeps every visitor's load fast.

PHOTO PATH
His iPhone
HEIC photos, on-site
Custom Upload
Simple form, HEIC→JPEG in-browser, Spanish-friendly
Sanity CMS
Embedded studio, schemas, references
SSR + CDN Cache
Stale-while-revalidate, served instantly from the edge
Visitor's Browser
Instant load from warm cache

No rebuild when he posts. Moving to Sanity took his content off the repo, which was half the rebuild problem. The pages were the other half: while the site was statically generated, every photo he posted meant a production deploy on Netlify, a flat 15 credits each against a 300-credit monthly cap that pauses the site when it runs out. That's about 20 photos a month before the gallery goes dark, on a site he updates after almost every job. So I dropped the static build, replaced getStaticPaths with validation at request time, and moved the pages to server rendering. Now an upload writes straight to the CMS and never triggers a deploy.

On top of that, a cache keeps server rendering fast. The pages sit behind a stale-while-revalidate cache (s-maxage=1, stale-while-revalidate=30d), so visitors load instantly from the CDN while new content re-renders in the background and appears a request later.

The video player never blocks the first load. Each video sits in the grid as a static thumbnail. The actual Mux player is dynamically imported and built only when someone opens the lightbox, so it's never in the initial bundle slowing down render. The hover preview works the same way: it only starts fetching when the thumbnail is about to enter the viewport, not on page load. Before I did this the page crawled on arrival because everything tried to load at once. Now the heavy stuff loads when it's actually needed.

Everything else is keeping bytes off the page until they're needed. Photos come off his phone at full size (~2–5MB each). I serve them through Sanity's image CDN resized to the size they actually display, with auto('format') handing browsers WebP or AVIF, so a multi-megabyte photo lands around 50KB. The first row loads eagerly so something's on screen right away, everything below it is lazy-loaded, and the rest sits behind a "load more" so a visitor isn't downloading a hundred photos to see the first twelve.

The payoff: a site that feels as premium as the work in it.

Lighthouse

99
Performance
98
Accessibility
100
Best Practices
100
SEO
The gallery landing on rockyconcreteinc.com: a Browse by Category grid of six service categories, each with a cover photo, above a curated Featured strip.
The gallery landing page: six categories, each with a cover I set in the Studio, over a curated Featured strip.
The Concrete Work category page on rockyconcreteinc.com: a top nav of service categories, with the Walkways & Steps photos showing 6 of 8 before a load-more.
A category page: his services across the top, the work below.
A category broken into subcategories (Walkways & Steps, Flatwork & Patios, Stone Work), each its own grid of finished jobs with a load-more.
Each category breaks into subcategories, each loading a batch at a time.

Result

Live

rockyconcreteinc.com

His primary digital presence. New customers land here before they call.

Hand-off

Runs without me

He uploads from his phone whenever he finishes a job. The photos appear on the site within a second. No rebuilds, no support tickets.

The Takeaway

The best tool isn't the one with the most features. It's the one that fits the person who has to use it.

Sanity had every feature I wanted; he needed something as simple as Instagram. So I built the simple thing on top of the powerful one, and everything underneath, the rendering, the cache, the image pipeline, exists so he never has to think about it.