DEV Community

Segcam spa
Segcam spa

Posted on

The restaurant website that is one HTML file (with the details everyone gets wrong)

Your client has a restaurant. They need a website. You have two options:

  1. Spin up a Next.js project, install 40 packages, configure Tailwind, deploy to Vercel, and then explain to them why they can't change the phone number themselves.
  2. Send them one HTML file.

I've been doing option 2 for a while now, and I want to show you exactly what it looks like β€” including the parts people get wrong.

The skeleton

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Saffron & Sage β€” Modern Indian Kitchen</title>
  <meta name="description" content="Modern Indian kitchen in downtown. Fresh spices, open fire, no shortcuts.">
  <style>/* all the CSS */</style>
</head>
<body>
  <!-- nav, hero, menu, gallery, reservation, footer -->
  <script>/* all the JS */</script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Everything else is detail. But the details are where it breaks.

Detail 1: the mobile menu that actually works

This is where most single-file attempts fall apart. People hide the nav under 768px and forget to give it a way back.

.nav-links { display: flex; gap: 28px; }
.burger { display: none; background: none; border: 0; cursor: pointer; }

@media (max-width: 768px) {
  .nav-links {
    display: none;
    position: absolute; top: 64px; left: 0; right: 0;
    flex-direction: column; gap: 0;
    background: #fff; border-bottom: 1px solid #e5e7eb;
  }
  .nav-links.open { display: flex; }
  .nav-links a { padding: 14px 20px; }
  .burger { display: block; }
}
Enter fullscreen mode Exit fullscreen mode
const burger = document.querySelector('.burger');
const links = document.querySelector('.nav-links');
burger.addEventListener('click', () => links.classList.toggle('open'));
links.addEventListener('click', e => {
  if (e.target.tagName === 'A') links.classList.remove('open');
});
Enter fullscreen mode Exit fullscreen mode

That last listener is the one everyone forgets: without it, the menu stays open after the user taps a link, and the page scrolls behind an open menu. It looks broken, and the client will notice.

Detail 2: images without images

A restaurant page wants photos. But every external image is a request, a layout shift and a thing that can 404. For the sections that are decoration rather than content, use gradients:

.dish {
  aspect-ratio: 4 / 3;
  border-radius: 12px;
  background: linear-gradient(135deg, #f59e0b 0%, #b45309 55%, #7c2d12 100%);
  display: grid; place-items: center;
  color: #fff8ec; font-weight: 700; letter-spacing: .04em;
}
Enter fullscreen mode Exit fullscreen mode

Real photos of the actual food still go in as real images β€” but the hero background, the section dividers, the card thumbnails in a "coming soon" state: those are gradients. Your page stays fast and nothing breaks while the client is still "getting the photos ready" (they will take three weeks).

Detail 3: an accordion in nine lines

Every business page needs an FAQ. You don't need a library:

<div class="faq">
  <button class="q" aria-expanded="false">Do you take reservations?</button>
  <div class="a" hidden>Yes β€” up to 8 people online, larger groups by phone.</div>
</div>
Enter fullscreen mode Exit fullscreen mode
document.querySelectorAll('.q').forEach(q => {
  q.addEventListener('click', () => {
    const open = q.getAttribute('aria-expanded') === 'true';
    q.setAttribute('aria-expanded', String(!open));
    q.nextElementSibling.hidden = open;
  });
});
Enter fullscreen mode Exit fullscreen mode

Note the aria-expanded and the hidden attribute: that's what makes it work for screen readers, and it costs you nothing.

Detail 4: the reservation form that doesn't need a backend

You don't want to run a server for a form. Two honest options:

<!-- 1. mailto: crude but works everywhere -->
<form action="mailto:hello@restaurant.com" method="post" enctype="text/plain">

<!-- 2. a form endpoint (Formspree, Basin, Web3Forms...) -->
<form action="https://formspree.io/f/xxxxxxx" method="POST">
Enter fullscreen mode Exit fullscreen mode

For a client site, use option 2 and put the endpoint in the file. It's one line, no server, and the emails land in their inbox.

What you end up with

One file. Around 600 lines. It loads in under a second, works offline, scores well on Lighthouse without any tuning, and the client can open it in Notepad to fix a typo.

If you want to see the finished version of the exact restaurant page I described, it's here, running live (not a screenshot):

πŸ‘‰ Restaurant demo

And here's a free one you can download and take apart β€” a coming-soon page with a countdown, no signup required:

πŸ‘‰ Free template

There are 61 of these now, across most of the niches a client will throw at you: segcom.net.

What's the piece you always end up rewriting from scratch?

Top comments (0)