A complete walkthrough for building a professional restaurant website — theme selection, dynamic menu management, online reservation forms, gallery setup, Google Maps integration, and local business schema markup.
Why Restaurant Websites Need More Than a Template
I spent three years freelancing for small restaurant owners before we built JekCMS, and the pattern was always the same: someone would buy a WordPress theme, install fifteen plugins to handle menus, reservations, and galleries, then call me six months later because the site loaded in nine seconds and half the plugins had update conflicts. One client in Istanbul had a WordPress site with 23 active plugins just to display a food menu and accept table bookings. The hosting bill alone was $40/month for a shared server that still couldn't keep response times under four seconds.
That experience shaped how we handle restaurant sites in JekCMS. Everything a restaurant needs — menus, reservations, galleries, maps, structured data — is handled through the admin panel and a single theme. No plugins. No third-party dependencies that break after six months.
This tutorial walks through every step of building a restaurant website from scratch. I'll use actual code from a project we delivered last month for a seafood restaurant in Izmir.
Step 1: Theme Selection and Initial Setup
JekCMS ships with several industry-focused themes. For restaurants, the Starter Business theme works best because it includes built-in support for service listings (which we repurpose as menu categories), a gallery module, and a contact form with custom fields.
After installing JekCMS, head to Admin > Settings > General and fill in the basics:
- Site Name: Your restaurant name
- Tagline: A short description (e.g., "Fresh Aegean Seafood Since 1998")
- Logo: Upload your logo in SVG format for crisp rendering at any size
- Favicon: 32x32 PNG or SVG
For the URL structure, I recommend keeping it flat. In config/config.php:
<?php
// Clean URL structure - no /page/ prefix
define('URL_STRUCTURE', 'flat');
// Timezone for reservation handling
date_default_timezone_set('Europe/Istanbul');
Step 2: Database Setup for Menu Items
JekCMS uses the services table for any repeatable content block. For a restaurant, we repurpose this as the menu system. Here's the table structure you already have:
-- services table (used as menu_items)
-- id, title, description, image, price, category_id, sort_order, status
-- We add a custom field for dietary tags
ALTER TABLE services ADD COLUMN tags VARCHAR(255) DEFAULT NULL;
ALTER TABLE services ADD COLUMN price DECIMAL(8,2) DEFAULT NULL;
The tags column stores comma-separated dietary labels: "vegan,gluten-free,spicy". We parse these in the frontend to show small badge icons next to each dish.
For menu categories (Starters, Main Courses, Desserts, Drinks), we use the existing service_categories table:
INSERT INTO service_categories (name, slug, sort_order) VALUES
('Baslangiclar', 'starters', 1),
('Ana Yemekler', 'main-courses', 2),
('Tatlilar', 'desserts', 3),
('Icecekler', 'drinks', 4);
Step 3: Building the Menu Display
The menu page is where most restaurant sites fall apart. They either show a static PDF (terrible for SEO and mobile) or use a bloated JavaScript grid that takes forever to load. Our approach: server-rendered HTML with minimal CSS.
Create pages/menu.php:
<?php
$categories = $db->fetchAll(
"SELECT * FROM service_categories WHERE status = 'active' ORDER BY sort_order"
);
foreach ($categories as $cat):
$items = $db->fetchAll(
"SELECT * FROM services WHERE category_id = ? AND status = 'active' ORDER BY sort_order",
[$cat['id']]
);
if (empty($items)) continue;
?>
<section class="menu-category" id="category-<?= $cat['slug'] ?>">
<h2><?= htmlspecialchars($cat['name']) ?></h2>
<div class="menu-grid">
<?php foreach ($items as $item): ?>
<div class="menu-item">
<?php if ($item['image']): ?>
<picture>
<source srcset="<?= get_featured_image($item, 'thumbnail') ?>" type="image/avif">
<img src="<?= get_featured_image($item, 'medium') ?>"
alt="<?= htmlspecialchars($item['title']) ?>"
width="400" height="300" loading="lazy">
</picture>
<?php endif; ?>
<div class="menu-item-info">
<h3><?= htmlspecialchars($item['title']) ?></h3>
<p><?= htmlspecialchars($item['description']) ?></p>
<?php if ($item['tags']): ?>
<div class="dietary-tags">
<?php foreach (explode(',', $item['tags']) as $tag): ?>
<span class="tag tag--<?= trim($tag) ?>"><?= trim($tag) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<span class="menu-item-price">₺<?= number_format($item['price'], 2) ?></span>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
The CSS for this grid is straightforward. Each .menu-item uses CSS Grid with two columns on desktop and stacks to a single column on mobile. No JavaScript framework needed.
Step 4: Reservation Form with Server-Side Validation
Online reservations are the single most requested feature from restaurant owners. We handle this through the JekCMS contact form system with custom fields. Here's the form markup:
<form method="POST" action="" id="reservation-form">
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
<input type="hidden" name="form_type" value="reservation">
<div class="form-row">
<div class="form-group">
<label for="guest_name">Ad Soyad</label>
<input type="text" id="guest_name" name="guest_name" required>
</div>
<div class="form-group">
<label for="phone">Telefon</label>
<input type="tel" id="phone" name="phone" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="date">Tarih</label>
<input type="date" id="date" name="date"
min="<?= date('Y-m-d') ?>" required>
</div>
<div class="form-group">
<label for="time">Saat</label>
<select id="time" name="time" required>
<option value="12:00">12:00</option>
<option value="12:30">12:30</option>
<!-- generate slots in 30-min intervals -->
<option value="22:00">22:00</option>
</select>
</div>
<div class="form-group">
<label for="guests">Kisi Sayisi</label>
<select id="guests" name="guests" required>
<?php for ($i = 1; $i <= 20; $i++): ?>
<option value="<?= $i ?>"><?= $i ?> Kisi</option>
<?php endfor; ?>
</select>
</div>
</div>
<div class="form-group">
<label for="notes">Ozel Notlar</label>
<textarea id="notes" name="notes" rows="3"></textarea>
</div>
<button type="submit">Rezervasyon Yap</button>
</form>
The server-side handler validates everything before inserting into contact_messages:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['form_type'] === 'reservation') {
verify_csrf_token($_POST['csrf_token'] ?? '');
$name = sanitize_text($_POST['guest_name'] ?? '');
$phone = sanitize_text($_POST['phone'] ?? '');
$date = $_POST['date'] ?? '';
$time = $_POST['time'] ?? '';
$guests = (int)($_POST['guests'] ?? 1);
$notes = sanitize_text($_POST['notes'] ?? '');
$errors = [];
if (strlen($name) < 2) $errors[] = 'Gecerli bir isim girin.';
if (!preg_match('/^[0-9+\-\s]{7,15}$/', $phone)) $errors[] = 'Gecerli bir telefon girin.';
if (strtotime($date) < strtotime('today')) $errors[] = 'Gecmis tarih secilemez.';
if ($guests < 1 || $guests > 20) $errors[] = 'Kisi sayisi 1-20 arasi olmali.';
if (empty($errors)) {
$db->insert('contact_messages', [
'name' => $name,
'phone' => $phone,
'subject' => 'Rezervasyon: ' . $date . ' ' . $time,
'message' => "Tarih: $date
Saat: $time
Kisi: $guests
Not: $notes",
'type' => 'reservation',
'status' => 'unread'
]);
// Send email notification to restaurant
send_notification_email(
get_setting('contact_email'),
'Yeni Rezervasyon: ' . $name . ' - ' . $date,
"Ad: $name
Telefon: $phone
Tarih: $date
Saat: $time
Kisi: $guests
Not: $notes"
);
redirect(SITE_URL . '/reservation?success=1');
}
}
One detail that trips people up: the redirect() call must happen before any HTML output. That's why the POST handler sits at the top of the file, before header.php is included. If you put it after the HTML starts rendering, you'll get the dreaded "Headers already sent" error.
Step 5: Gallery with AVIF/WebP Support
Restaurant owners love showing off their dishes, their dining space, and their kitchen. JekCMS handles gallery images through the gallery table with automatic AVIF and WebP conversion on upload.
When a restaurant owner uploads a 4MB JPEG of their signature dish through the admin panel, JekCMS automatically:
- Creates an AVIF version (typically 200-400KB at 80% quality)
- Creates a WebP fallback (300-500KB at 85% quality)
- Generates three thumbnail sizes: 400x400, 800x800, and 1600x1600
- Deletes the original JPEG to save disk space
The gallery display uses the <picture> element for format negotiation:
<div class="gallery-grid">
<?php foreach ($gallery_items as $item): ?>
<figure class="gallery-item">
<a href="<?= UPLOADS_URL . '/' . $item['image'] ?>" data-lightbox="gallery">
<picture>
<source srcset="<?= get_featured_image($item, 'medium') ?>"
type="image/avif">
<img src="<?= get_featured_image($item, 'medium') ?>"
alt="<?= htmlspecialchars($item['title']) ?>"
width="800" height="600" loading="lazy">
</picture>
</a>
<figcaption><?= htmlspecialchars($item['title']) ?></figcaption>
</figure>
<?php endforeach; ?>
</div>
For the lightbox, we use a vanilla JavaScript implementation — about 3KB minified. No jQuery dependency.
Step 6: Google Maps Without Killing Page Speed
Embedding Google Maps the standard way (iframe) adds 800KB+ to your page weight and blocks rendering. For a restaurant site, the map exists to show your location — it doesn't need to load with the initial page.
Our approach: show a static map image that lazy-loads, and swap to the interactive map only when the user clicks:
<div class="map-container" id="map-placeholder">
<img src="/uploads/general/map-static.avif"
alt="Restoran Konum" width="1200" height="400" loading="lazy">
<button class="map-activate" aria-label="Haritayi Ac">
Interaktif Haritayi Goster
</button>
</div>
<script>
document.getElementById('map-placeholder').addEventListener('click', function() {
this.innerHTML = '<iframe src="https://www.google.com/maps/embed?pb=YOUR_EMBED_URL" ' +
'width="100%" height="400" style="border:0" allowfullscreen loading="lazy" ' +
'referrerpolicy="no-referrer-when-downgrade"></iframe>';
});
</script>
This technique saved 1.2 seconds on First Contentful Paint for our Izmir client. The static image is 45KB in AVIF format versus 800KB+ for the Maps iframe and its JavaScript bundle.
Step 7: Local Business Schema Markup
Search engines need structured data to show rich results for restaurants — star ratings, opening hours, price range, cuisine type. JekCMS outputs this automatically through the output_schema() function, but for restaurants we extend it:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Restaurant",
"name": "<?= get_setting('site_name') ?>",
"image": "<?= SITE_URL ?>/uploads/general/restaurant-hero.avif",
"url": "<?= SITE_URL ?>",
"telephone": "<?= get_setting('contact', 'phone') ?>",
"priceRange": "$$",
"servesCuisine": "Seafood",
"address": {
"@type": "PostalAddress",
"streetAddress": "<?= get_setting('contact', 'address') ?>",
"addressLocality": "Izmir",
"addressCountry": "TR"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 38.4192,
"longitude": 27.1287
},
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "11:00",
"closes": "23:00"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Saturday","Sunday"],
"opens": "10:00",
"closes": "00:00"
}
],
"menu": "<?= SITE_URL ?>/menu",
"acceptsReservations": true
}
</script>
The key fields Google looks at for restaurant rich results are servesCuisine, priceRange, openingHoursSpecification, and acceptsReservations. Missing any of these means you won't qualify for the enhanced search card.
Step 8: Mobile Optimization
72% of restaurant website visitors come from mobile devices (based on analytics across our eight restaurant clients). The menu page especially needs to work flawlessly on small screens.
Key mobile decisions we made:
- Sticky category navigation: A horizontal scroll bar at the top of the menu page that highlights the current category as you scroll. Implemented with IntersectionObserver — no scroll event listeners.
- Tap-to-call phone number: The phone number in the header is always a
tel:link. On mobile, it triggers the dialer directly. - Bottom CTA bar: A fixed bar at the bottom with "Call" and "Reserve" buttons, visible only on mobile screens under 768px.
- Font sizes: Menu item names at 18px, descriptions at 15px, prices at 20px bold. Readable without zooming.
/* Mobile-first menu grid */
.menu-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 768px) {
.menu-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Sticky category nav */
.category-nav {
position: sticky;
top: 0;
z-index: 10;
background: var(--bg-primary);
display: flex;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
border-bottom: 2px solid var(--border-color);
}
.category-nav a {
white-space: nowrap;
padding: 1rem 1.5rem;
text-decoration: none;
font-weight: 600;
}
.category-nav a.active {
border-bottom: 3px solid var(--accent-color);
}
Step 9: Performance Results
After deploying the Izmir seafood restaurant site, we ran PageSpeed Insights on both mobile and desktop:
- Mobile Performance: 91 (was 34 on their old WordPress site)
- Desktop Performance: 97
- First Contentful Paint: 1.1s mobile, 0.6s desktop
- Largest Contentful Paint: 1.8s mobile, 0.9s desktop
- Total page weight (menu page): 380KB (down from 4.2MB on WordPress)
- Requests: 12 (down from 87)
The biggest wins came from three things: eliminating jQuery and plugin JavaScript (saved 1.8MB), converting all images to AVIF (saved 2.1MB), and lazy-loading the Google Maps embed (saved 800KB on initial load).
What's Next
This covers the core setup. In a follow-up post, I'll walk through adding an online ordering system with WhatsApp integration — a feature that three of our restaurant clients requested within the first month of launch. The pattern uses the same contact form infrastructure with a cart-like interface that sends the order summary via the WhatsApp Business API.
If you're building a restaurant site and have questions about any of these steps, open an issue on GitHub or reach out through our contact form. We actively support restaurant-specific implementations in the JekCMS community.