A practical guide to building a restaurant site with jekcms using what actually ships: standard posts and settings, real schema output, and theme-level template work for anything menu- or reservation-specific — no invented database tables.
Why Restaurant Websites Need More Than a Template
The pattern with small restaurant websites is remarkably consistent: the owner buys a WordPress theme, installs a dozen-plus plugins to handle menus, reservations, and galleries, and ends up with a site that loads slowly and breaks every time one of those plugins pushes an update. A handful of plugins is often all it takes to turn a simple "show a menu, take a booking" site into something fragile and expensive to host.
JekCMS doesn't ship dedicated restaurant tables or a page-builder plugin ecosystem — and that's on purpose. A restaurant site here is built the same way any content-driven site is: standard posts, categories, and post meta, styled by a theme. No plugins to update, no third-party dependencies that break on a core update. This tutorial walks through building one from scratch using only what's actually in the product.
Step 1: Theme Selection and Initial Setup
JekCMS ships with several general-purpose themes you can adapt to a restaurant site; a clean, image-forward business theme is the most natural starting point, since a restaurant site leans heavily on food photography and a simple, scannable layout.
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
Set your site's timezone so reservation dates and times behave correctly, and decide early on your URL structure — flat, human-readable slugs are easier to read on a phone screen than deeply nested paths.
Step 2: Modeling the Menu With Posts, Not New Tables
JekCMS doesn't have a dedicated "menu item" table, and there's no need to invent one — the standard posts/post_meta/categories tables already in the schema handle this well. Create a Menu category (with subcategories for Starters, Main Courses, Desserts, Drinks), and treat each dish as a post in the relevant subcategory:
- Post title = dish name
- Post content or excerpt = dish description
- Featured image = the dish photo
- Custom post meta = price and dietary tags (e.g.,
price,dietary_tagsstored as post meta values)
This keeps every dish inside the normal content workflow — the same admin screens, revisions, and media handling you already use for blog posts, with nothing custom to maintain.
Step 3: Building the Menu Display
The menu page is where most restaurant sites fall apart — a static PDF is terrible for SEO and mobile, and a heavy JavaScript grid takes forever to load. The straightforward approach: a theme template that pulls posts from the Menu category with get_posts() and renders them as server-rendered HTML.
<?php
$categories = get_categories(['parent' => 'menu']);
foreach ($categories as $cat):
$items = get_posts([
'category' => $cat['slug'],
'status' => 'published',
'orderby' => 'menu_order',
]);
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">
<picture>
<?= get_featured_image($item, 'medium') ?>
</picture>
<div class="menu-item-info">
<h3><?= htmlspecialchars($item['title']) ?></h3>
<p><?= htmlspecialchars($item['excerpt']) ?></p>
</div>
<span class="menu-item-price">₺<?= htmlspecialchars(get_post_meta($item['id'], 'price')) ?></span>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
The CSS for this grid is straightforward: .menu-item uses CSS Grid with two columns on desktop, stacking to one column on mobile. No JavaScript framework needed.
Step 4: A Reservation Form That Doesn't Need a New Table
Online reservations are the single most requested feature from restaurant owners. You don't need a bookings database to offer this — a simple form that validates input server-side and emails you the details covers most small restaurants' needs, with the CSRF token JekCMS already gives you via csrf_token():
<form method="POST" action="" id="reservation-form">
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
<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="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>
On submit, validate every field server-side — name length, a plausible phone format, a date that isn't in the past — and if validation passes, email the details to the restaurant's contact address. If you also want a record inside the admin panel rather than relying purely on email, log the request as a private draft post in an internal "Reservations" category; that gives you a searchable history using tools you already have, without a bespoke table.
One detail worth remembering: a redirect() call must happen before any HTML output. That's why the POST handler should sit at the top of the template file, before the header partial is included — put it after HTML has started rendering and you'll get the classic "headers already sent" error.
Step 5: Photos, Without a Separate Gallery System
Restaurant owners want to show off their dishes, their dining room, and their kitchen. JekCMS doesn't need a separate gallery module for this — upload the photos through the regular media library, attach them to posts or pages, and let the platform's built-in image pipeline do the rest: every upload gets converted through an AVIF→WebP→original fallback and pre-generated size variants, automatically, at upload time. No runtime resizing, no separate plugin.
<div class="gallery-grid">
<?php foreach ($gallery_items as $item): ?>
<figure class="gallery-item">
<picture>
<?= get_featured_image($item, 'medium') ?>
</picture>
<figcaption><?= htmlspecialchars($item['title']) ?></figcaption>
</figure>
<?php endforeach; ?>
</div>
For a lightbox effect on click, a small vanilla-JavaScript implementation is enough — there's no need for a jQuery-based plugin.
Step 6: Google Maps Without Killing Page Speed
Embedding Google Maps the standard way (iframe) adds several hundred kilobytes 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.
The lighter approach: show a static map image that lazy-loads, and swap to the interactive embed only when the visitor 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>
A static AVIF placeholder is a fraction of the weight of the live iframe and its JavaScript bundle. Deferring the map until the visitor actually wants it is one of the simplest wins available on a restaurant homepage.
Step 7: Local Business Schema Markup
Search engines rely on structured data to show rich results — star ratings, opening hours, price range, cuisine type. JekCMS's output_schema() automatically emits Organization- and Article-level structured data for your site, but restaurant-specific fields (servesCuisine, priceRange, openingHoursSpecification, acceptsReservations) aren't something the platform generates for you — you add a Restaurant JSON-LD block by hand in your theme's page template:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Restaurant",
"name": "<?= get_setting('site_name') ?>",
"url": "<?= SITE_URL ?>",
"telephone": "<?= get_setting('contact', 'phone') ?>",
"priceRange": "$$",
"servesCuisine": "Seafood",
"address": {
"@type": "PostalAddress",
"streetAddress": "<?= get_setting('contact', 'address') ?>",
"addressLocality": "Izmir",
"addressCountry": "TR"
},
"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 fields Google looks at for restaurant rich results are servesCuisine, priceRange, openingHoursSpecification, and acceptsReservations. Missing any of them means you won't qualify for the enhanced search card.
Step 8: Mobile Optimization
Restaurant websites see a large share of mobile traffic — menus, opening hours, and reservation buttons must be immediately reachable on small screens without zooming or horizontal scrolling.
Key mobile decisions worth making:
- Sticky category navigation: A horizontal scroll bar at the top of the menu page that highlights the current category as you scroll. IntersectionObserver handles this without scroll event listeners.
- Tap-to-call phone number: The phone number in the header should always be a
tel:link, so it triggers the dialer directly on mobile. - Bottom CTA bar: A fixed bar with "Call" and "Reserve" buttons, visible only on small screens.
- Readable type without zooming: Comfortably sized menu item names, descriptions, and prices.
/* 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);
}
Where the Real Performance Gains Come From
Compared to the plugin-heavy WordPress setup this tutorial opened with, the biggest, most reliable wins on a site like this come from three things: dropping the JavaScript weight that a stack of plugins usually drags in, letting JekCMS's AVIF pipeline handle images instead of a bolt-on optimizer, and deferring the Google Maps embed until someone actually clicks it. None of that requires anything beyond what's already described above — no extra plugins, no extra tables.
What's Next
This covers the core setup: menu, reservations, gallery, maps, and schema. If you want to add features on top of this — an online ordering flow through a messaging app, for instance — the same building blocks apply: a post-based content model, a simple validated form, and a webhook or API integration if you need to connect an external service. See our analytics integration guide for how to wire up tracking once the site is live.
If you're building a restaurant site and have questions about any of these steps, reach out through our contact form.