Skip to content

Building a Pirate-Themed Barbershop Site, Mostly by Prompting Claude

A barber in Cove, Utah had an Instagram and no website. I gave Claude the right prompt, all the real details, and a styling direction, and it built the three files. The hard part was getting the content, not the code.

· · 13 min read · Updated July 20, 2026

Updated July 2026: the site has since been rebuilt on Astro, and I changed my mind about the framework question I argued the first time. The original build log is below; skip to the rebuild for what changed and why.

Max runs Pirate Barber out of Cove, Utah. I found him the way most people do: his Instagram, @pirate.barber. Good cuts, a real local following, and no website. If you searched his shop, you got a handful of social posts and nothing that told you what he charged, when he was open, or how to book. So I reached out and offered to fix that.

This is the third of these I’ve done now, after a renovation company and a 40-year-old hair salon. The pattern is the same: a small local business that lives on word of mouth and a social profile, and needs one honest page on the internet. What’s different about this one is how little of it I actually typed.

What the thing is

Pirate Barber is a single-page site. A dark hero with the shop name, a short bit about Max, three service cards, and a contact section with location, phone, hours, and an Instagram link. That’s the whole thing. It’s live at piratebarber.vientapps.com.

Under the hood it is three files: one HTML, one CSS, one JS. No framework, no build step, no dependencies. The same call I keep making for sites this size, because it loads instantly and there is nothing to break later.

How I worked with Claude on this one

I’ll be straight about the split: Claude wrote almost all of it. This was not a pair-programming session where I drove and it autocompleted. I gave Claude Code the business details, the page structure I wanted, and a clear styling direction, the dark pirate identity, gold accents, a serif that feels engraved, and let it build the files. Two commits, a couple hours apart, and the site was done.

My actual job was upstream of the code. Get the real information. Decide the look. Write the prompt well enough that Claude had something to aim at. When the prompt and the inputs were good, the output was good. When I was vague, I got vague back. That’s the honest version of “I built this with AI”: the leverage was real, but it lived in the setup, not the typing.

The stack, and why it’s boring on purpose

Vanilla HTML, CSS, and JavaScript. The CSS is one file built around custom properties, so the entire theme lives in a single block at the top. Claude set this up, and it’s the kind of structure that makes the whole palette adjustable from one place:

:root {
  --color-bg: #0a0a0a;
  --color-bg-alt: #1a1a1a;
  --color-gold: #c9a84c;
  --color-gold-light: #ddc06e;
  --color-red: #8b1a1a;
  --color-text: #e8e8e8;
  --font-heading: 'Cinzel', Georgia, serif;
  --font-body: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}

Near-black base, antique gold for highlights, a deep red I barely use, and Cinzel for the headings to get that aged, engraved feel. The body stays on a plain system font so the page weighs almost nothing.

Beyond the three files, the repo carries a robots.txt, a sitemap.xml, and a Netlify _headers file for security headers. Claude added the SEO pieces in the second pass when I asked it to tighten things up.

The hard part wasn’t the code

The code was the easy part. The hard part was content.

An Instagram profile tells you a business exists and roughly what it feels like. It does not tell you that a haircut is $35, a shave is $20, a kids cut is $20, or that the shop is closed Mondays and open till seven Thursday through Saturday. It doesn’t hand you the story of who Max is in a form you can put on an About section. All of that had to come out of conversation and then get confirmed, because the one thing worse than no website is a website with the wrong hours.

This is the same wall I hit on the salon site. The build is a half-day. The content is the part that takes real back-and-forth, and there’s no shortcut Claude can take for you, because Claude does not know what Max charges.

Where Claude surprised me

The accessibility. I did not ask for it.

The mobile nav is 35 lines of JavaScript, and Claude wrote it with the details most people skip on a small marketing site: the toggle updates aria-expanded, the menu closes on the Escape key, and focus returns to the toggle button afterward so a keyboard user isn’t dumped somewhere random.

// Close mobile menu on Escape key
document.addEventListener('keydown', function (e) {
  if (e.key === 'Escape' && menu && menu.classList.contains('is-open')) {
    menu.classList.remove('is-open');
    toggle.setAttribute('aria-expanded', 'false');
    toggle.focus();
  }
});

That toggle.focus() is the tell. Nobody asked for it, it doesn’t show up in a screenshot, and it’s the right thing to do. Claude treated a three-file barbershop site like it deserved the same care as a real app, which is more than I’d have bothered with by hand on a job this small.

It even paired the JS with a CSS-only hamburger-to-X animation driven entirely off that same aria attribute, so the open state and the icon never drift apart:

.nav__toggle[aria-expanded="true"] .nav__toggle-bar:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}
.nav__toggle[aria-expanded="true"] .nav__toggle-bar:nth-child(2) {
  opacity: 0;
}
.nav__toggle[aria-expanded="true"] .nav__toggle-bar:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

The accessibility state is the single source of truth, and the visual just follows it. No extra JS to keep the icon in sync.

Where Claude fell short

It was generic until I pushed it.

The first pass was competent and forgettable. It looked like a clean template for any local business: fine spacing, sensible sections, zero personality. Nothing about it said pirate, said barber, said this specific shop. Left to its defaults, Claude plays it safe, and safe here meant interchangeable.

What fixed it was giving it something real to react to. I pointed it at an actual high-end barbershop site as a reference (there’s still a WebFetch permission for masterbarbersla.com sitting in the project’s .claude settings from that session), and told it to lean into the name. Once it had a concrete target instead of “make it look nice,” the dark-and-gold identity showed up and the page stopped looking like a template. The lesson I keep relearning: Claude is much better at “make it look like that” than “make it look good.”

Claude also added a Schema.org BarberShop block on its own initiative, which is genuinely useful for local search:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BarberShop",
  "name": "Pirate Barber",
  "telephone": "+14357645900",
  "priceRange": "$$"
}
</script>

Worth knowing it does this, because the JSON-LD has to match reality. It’s another place where Claude will confidently fill in structure, and you’re the one who has to make sure the phone number and hours inside it are actually correct.

What else went wrong

The honest gap is photos. There are none. The hero is a pure CSS gradient because there was nothing to put there, and the Open Graph tags point at an og-image.jpg that doesn’t exist on the live site yet. The gradient carries it, but a real shot of Max in the chair would do more for trust than any amount of styling. That’s on me, not Claude.

Where it stands

It’s live and it does the job. Someone can find Pirate Barber, see the services and prices, check the hours, and tap to call, all from their phone. For a shop that previously existed only as an Instagram handle, that’s the whole point.

What I’d do differently

Get the content first, in one structured pass. Same lesson as last time, still not fully learned. Before I write a prompt or a line of code, I should send a short questionnaire: services with prices, exact hours, the owner’s story, payment methods, and photos. Gather it once instead of pulling it out in pieces while the build waits.

Insist on real photos before launch. The site works without them, but it would be better with them, and “we’ll add photos later” tends to mean never. Next client, photos are a launch requirement, not a nice-to-have.

Hand Claude a reference up front, not after the bland draft. The generic first pass wasn’t Claude’s fault, it was mine for not anchoring the style early. Give it a concrete visual target in the first prompt and skip the round where it plays it safe.

Update, July 2026: the rebuild

The site above ran for a few months and then I rebuilt it from scratch. The screenshot at the top of this post is the new one. Two things changed: the architecture, and what the page is actually about.

I was wrong about the no-framework thing

The original post has a whole section titled “The stack, and why it’s boring on purpose,” and I stand by about half of it. Three hand-written files really are cheap to host and there really is nothing to patch. What I got wrong was measuring the cost at the wrong moment.

The cost is not hosting. It’s editing. Max’s haircut price appeared in the services section, in the FAQ answer about pricing, in the meta description, and inside the JSON-LD offer catalog. Four copies. Change the price and you get to find all four, and the one you miss is the one in the structured data, which is exactly the copy you can’t see in the browser and Google can. Same for the hours, which existed as prose in the contact section and again as an openingHoursSpecification block.

So the rebuild is Astro with Tailwind. It still ships a static page with zero JavaScript bundles, which was the actual thing worth protecting. But the prices and hours are now one data structure that renders into the visible markup and the schema in the same pass. There is one place to edit and no way for the two to disagree.

The lesson I’d write on the wall: “no dependencies” is a good instinct pointed at the wrong metric. Ask what happens the fourth time the client asks you to change a number, not what happens on first load.

Build the page around the mechanic

The first version was structured like every barbershop site: hero, about, three service cards, contact. It was fine and it buried the point.

The point is that Pirate Barber is one barber and one chair with no front desk. There is nobody to route you, no booking software, no waiting list. You get Max. So the new hero says exactly that, “One barber, one chair, in Cove, Utah,” and the phone number is the only call to action on the screen. The booking section gives you a Call button and a Text button side by side, because a decent share of people would rather text a stranger than call one, and the old site only offered the harder of the two.

Prices came out from behind the phone call too. The original site had service cards with no numbers on them, which meant “how much is a haircut” was a phone call. Now the services section is a flat board:

Haircut $35. Shave $20. Kids cut $20.

That is the single highest-value change on the page and it required no code, just the decision to publish the number.

The hours table got the same treatment. It lists the whole week, and a few lines of inline JavaScript highlight the current day, so the answer to “are they open right now” takes no reading:

const today = new Date().toLocaleDateString('en-US', { weekday: 'long' });
const row = document.querySelector(`tr[data-day="${today}"]`);
if (row) row.classList.add('bg-surface');

The barber pole, unrolled

The first version’s pirate theme was Cinzel headings on near-black with antique gold, which I described at the time as “aged and engraved.” Rereading it, it was a costume. Serif capitals and dark brown is what every themed site reaches for, and it said pirate-restaurant more than it said barber.

The new identity is one idea instead: take a barber pole and unroll it. A striped helix runs the full width above the nav and along the bottom edge of the footer, and it climbs on a slow loop the way a real pole does.

.helix {
  background-image: repeating-linear-gradient(
    -45deg,
    var(--color-flash) 0 7px,
    var(--color-ink) 7px 14px,
    var(--color-bg-2) 14px 21px,
    var(--color-ink) 21px 28px
  );
  background-size: 200% 40px;
  animation: helix-climb 2.6s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .helix { animation: none; }
}

That is the whole theme. Everything else went quiet to let it work: a deep teal-navy base instead of near-black, warm bone text, brass on anything you can act on, and the pole’s red held back so it only ever appears as a flash.

@theme {
  --color-bg: #0c1b22;      /* deep teal-navy, not black */
  --color-bg-2: #102630;
  --color-line: #24475a;
  --color-ink: #f0eadd;     /* warm bone */
  --color-accent: #c9922e;  /* brass, anything actionable */
  --color-flash: #c4392c;   /* the pole's red, used sparingly */
  --font-display: 'Big Shoulders Display', 'Arial Narrow', sans-serif;
  --font-body: 'IBM Plex Sans', system-ui, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, monospace;
}

Big Shoulders Display replaced Cinzel. It’s a tall condensed poster face, so the shop name can run enormous across the hero without needing an image behind it, and IBM Plex Mono handles the small caps labels and prices like a ledger.

Deleting the JavaScript I praised last time

The original post spent a section admiring the mobile nav Claude wrote: 35 lines, aria-expanded toggling, Escape-to-close, focus return. All of that was good work. The rebuild deleted it.

The nav and the FAQ are both native details elements now:

<details class="relative md:hidden" id="mobile-nav">
  <summary aria-label="Menu">...</summary>
  <nav aria-label="Primary">...</nav>
</details>

The browser handles the open state, the keyboard, and the accessibility semantics, and it all works with JavaScript switched off entirely. What’s left is one small inline script that closes the menu on Escape or an outside tap, plus the hours highlight above. No bundles.

This is the part I find slightly humbling in hindsight. The praise in the original post was real, Claude did write careful accessible JavaScript unprompted. But the more accessible version was to not write the JavaScript. Claude answered the question I asked, which was “build a mobile nav,” and built a good one. It did not ask whether the nav needed to be JavaScript in the first place, and neither did I.

The gaps from last time, closed and still open

Two of the complaints at the end of the original post are fixed. The Open Graph image that pointed at a file which didn’t exist now exists. And Pirate Barber has a Google Business profile, which it didn’t before, so the reviews section quotes a real one with the reviewer’s name attached and links out to the rest rather than inventing social proof.

The photo gap is still open. There is still not a single photograph of Max or the shop on the site, and the hero is still carrying that on typography alone. Two rebuilds in, “we’ll get photos later” has meant never, which is more or less what I predicted and did nothing about.

Related: Pirate Barber and more side projects.

Frequently Asked Questions

Can Claude build a full small-business website from one prompt?
Mostly, if you do the setup work. I gave Claude the business details, the structure I wanted, and a clear styling direction, and it produced a coherent three-file site. What it could not do was invent a real brand or know the actual services and hours. The quality of the output tracked the quality of the prompt and the real information I fed it.
What did Claude do well on this build?
Accessibility, without being asked. The mobile nav shipped with aria-expanded toggling, Escape-to-close, and focus returning to the toggle button after closing. Those are the kinds of details people skip on a small marketing site, and Claude wrote them by default. It also added Schema.org structured data and Open Graph tags on its own.
Where did Claude fall short?
The first pass was generic. It looked like a competent template, not a pirate barbershop. The themed identity, the dark background with antique gold and the engraved Cinzel headings, only showed up after I gave it a real reference site to react to. Out of the box it played it safe.
Why use plain HTML and CSS instead of a framework?
For a single-page site it is the right call. The whole thing is three files: one HTML, one CSS, one JS. No npm install, no build step, no dependencies to patch. It loads instantly and there is nothing to maintain. A framework would add weight and a toolchain for zero benefit at this size.
What is the hardest part of building a site for a business that only exists on Instagram?
Getting accurate content. An Instagram profile has a vibe and some photos, but not a services list with prices, not exact hours, not the owner's story written down. You have to extract all of that through conversation and confirm it, which takes more rounds than writing the code does.
Why was the Pirate Barber site rebuilt on Astro after arguing for no framework?
Because the original argument measured the wrong thing. Three hand-written files are genuinely cheaper to run, but the cost that actually bit was editing: every price, every hour, and every FAQ answer lived in three or four places at once, including the JSON-LD, and updating one meant hunting the rest. Astro ships the same zero-JavaScript static page while letting the prices and hours live in one data structure that renders into the markup and the structured data together.
Can a website work with JavaScript disabled?
Yes, if the interactive parts are built from native HTML elements. The Pirate Barber rebuild uses details and summary for both the mobile menu and the FAQ accordion, so both open and close with no script at all. The only JavaScript left is two small inline snippets that close the menu on Escape or an outside tap and highlight today's row in the hours table, and the page is fully usable without either.
Photo of Caden Sorenson
Caden Sorenson

Founder of Vient and senior staff engineer

Caden Sorenson runs Vient, an independent studio building iOS apps, web tools, and client websites, including Travel Vient, a travel research site with everything cited to primary sources. He's a senior staff engineer with 15+ years of experience building iOS apps, web platforms, and developer tools, and a Computer Science graduate from Utah State University. Based in Logan, Utah.

Built as part of

View the project →